1 package FS::part_export::shellcommands;
3 use vars qw(@ISA %info @saltset);
5 use String::ShellQuote;
8 @ISA = qw(FS::part_export);
10 tie my %options, 'Tie::IxHash',
11 'user' => { label=>'Remote username', default=>'root' },
12 'useradd' => { label=>'Insert command',
13 default=>'useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username'
14 #default=>'cp -pr /etc/skel $dir; chown -R $uid.$gid $dir'
16 'useradd_stdin' => { label=>'Insert command STDIN',
20 'userdel' => { label=>'Delete command',
21 default=>'userdel -r $username',
22 #default=>'rm -rf $dir',
24 'userdel_stdin' => { label=>'Delete command STDIN',
28 'usermod' => { label=>'Modify command',
29 default=>'usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username',
30 #default=>'[ -d $old_dir ] && mv $old_dir $new_dir || ( '.
31 # 'chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; '.
32 # 'find . -depth -print | cpio -pdm $new_dir; '.
33 # 'chmod u-t $new_dir; chown -R $uid.$gid $new_dir; '.
37 'usermod_stdin' => { label=>'Modify command STDIN',
41 'usermod_pwonly' => { label=>'Disallow username changes',
44 'suspend' => { label=>'Suspension command',
45 default=>'usermod -L $username',
47 'suspend_stdin' => { label=>'Suspension command STDIN',
50 'unsuspend' => { label=>'Unsuspension command',
51 default=>'usermod -U $username',
53 'unsuspend_stdin' => { label=>'Unsuspension command STDIN',
61 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
62 'options' => \%options,
65 Run remote commands via SSH. Usernames are considered unique (also see
66 shellcommands_withdomain). You probably want this if the commands you are
67 running will not accept a domain as a parameter. You will need to
68 <a href="../docs/ssh.html">setup SSH for unattended operation</a>.
70 <BR><BR>Use these buttons for some useful presets:
73 <INPUT TYPE="button" VALUE="Linux" onClick='
74 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
75 this.form.useradd_stdin.value = "";
76 this.form.userdel.value = "userdel -r $username";
77 this.form.userdel_stdin.value="";
78 this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username";
79 this.form.usermod_stdin.value = "";
80 this.form.suspend.value = "usermod -L $username";
81 this.form.suspend_stdin.value="";
82 this.form.unsuspend.value = "usermod -U $username";
83 this.form.unsuspend_stdin.value="";
86 <INPUT TYPE="button" VALUE="FreeBSD before 4.10 / 5.3" onClick='
87 this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
88 this.form.useradd_stdin.value = "$_password\n";
89 this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value="";
90 this.form.usermod.value = "lockf /etc/passwd.lock pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -c $new_finger -h 0";
91 this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username";
92 this.form.suspend_stdin.value="";
93 this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";
95 Note: On FreeBSD versions before 5.3 and 4.10 (4.10 is after 4.9, not
96 4.1!), due to deficient locking in pw(1), you must disable the chpass(1),
97 chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with
98 wrappers that prepend "lockf /etc/passwd.lock". Alternatively, apply the
100 <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
101 and use the "FreeBSD 4.10 / 5.3 or later" button below.
103 <INPUT TYPE="button" VALUE="FreeBSD 4.10 / 5.3 or later" onClick='
104 this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
105 this.form.useradd_stdin.value = "$_password\n";
106 this.form.userdel.value = "pw userdel $username -r";
107 this.form.userdel_stdin.value="";
108 this.form.usermod.value = "pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -c $new_finger -h 0";
109 this.form.usermod_stdin.value = "$new__password\n";
110 this.form.suspend.value = "pw lock $username";
111 this.form.suspend_stdin.value="";
112 this.form.unsuspend.value = "pw unlock $username";
113 this.form.unsuspend_stdin.value="";
116 <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
117 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
118 this.form.useradd_stdin.value = "";
119 this.form.userdel.value = "userdel -r $username";
120 this.form.userdel_stdin.value="";
121 this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username";
122 this.form.usermod_stdin.value = "";
123 this.form.suspend.value = "";
124 this.form.suspend_stdin.value="";
125 this.form.unsuspend.value = "";
126 this.form.unsuspend_stdin.value="";
129 <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
130 this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
131 this.form.usermod.value = "[ -d $old_dir ] && mv $old_dir $new_dir || ( chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; find . -depth -print | cpio -pdm $new_dir; chmod u-t $new_dir; chown -R $new_uid.$new_gid $new_dir; rm -rf $old_dir )";
132 this.form.usermod_stdin.value = "";
133 this.form.userdel.value = "rm -rf $dir";
134 this.form.userdel_stdin.value="";
135 this.form.suspend.value = "";
136 this.form.suspend_stdin.value="";
137 this.form.unsuspend.value = "";
138 this.form.unsuspend_stdin.value="";
142 The following variables are available for interpolation (prefixed with new_ or
143 old_ for replace operations):
145 <LI><code>$username</code>
146 <LI><code>$_password</code>
147 <LI><code>$quoted_password</code> - unencrypted password quoted for the shell
148 <LI><code>$crypt_password</code> - encrypted password
149 <LI><code>$uid</code>
150 <LI><code>$gid</code>
151 <LI><code>$finger</code> - GECOS, already quoted for the shell (do not add additional quotes)
152 <LI><code>$first</code> - First name of GECOS, already quoted for the shell (do not add additional quotes)
153 <LI><code>$last</code> - Last name of GECOS, already quoted for the shell (do not add additional quotes)
154 <LI><code>$dir</code> - home directory
155 <LI><code>$shell</code>
156 <LI><code>$quota</code>
157 <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
162 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
164 sub rebless { shift; }
168 $self->_export_command('useradd', @_);
173 $self->_export_command('userdel', @_);
176 sub _export_suspend {
178 $self->_export_command('suspend', @_);
181 sub _export_unsuspend {
183 $self->_export_command('unsuspend', @_);
186 sub _export_command {
187 my ( $self, $action, $svc_acct) = (shift, shift, shift);
188 my $command = $self->option($action);
189 return '' if $command =~ /^\s*$/;
190 my $stdin = $self->option($action."_stdin");
195 ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
198 foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
199 ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
200 foreach qw( machine username _password );
205 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
207 $email = ( grep { $_ ne 'POST' } $cust_pkg->cust_main->invoicing_list )[0];
212 $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
213 ($first, $last ) = ( $1, $2 );
214 $first = shell_quote $first;
215 $last = shell_quote $last;
216 $finger = shell_quote $finger;
217 $quoted_password = shell_quote $_password;
218 $domain = $svc_acct->domain;
220 #eventually should check a "password-encoding" field
221 if ( length($svc_acct->_password) == 13
222 || $svc_acct->_password =~ /^\$(1|2a?)\$/ ) {
223 $crypt_password = shell_quote $svc_acct->_password;
225 $crypt_password = crypt(
226 $svc_acct->_password,
227 $saltset[int(rand(64))].$saltset[int(rand(64))]
231 $self->shellcommands_queue( $svc_acct->svcnum,
232 user => $self->option('user')||'root',
233 host => $self->machine,
234 command => eval(qq("$command")),
235 stdin_string => eval(qq("$stdin")),
239 sub _export_replace {
240 my($self, $new, $old ) = (shift, shift, shift);
241 my $command = $self->option('usermod');
242 my $stdin = $self->option('usermod_stdin');
246 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
247 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
249 $new_finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
250 ($new_first, $new_last ) = ( $1, $2 );
251 $new_first = shell_quote $new_first;
252 $new_last = shell_quote $new_last;
253 $new_finger = shell_quote $new_finger;
254 $quoted_new__password = shell_quote $new__password; #old, wrong?
255 $new_quoted_password = shell_quote $new__password; #new, better?
256 $old_domain = $old->domain;
257 $new_domain = $new->domain;
259 #eventuall should check a "password-encoding" field
260 if ( length($new->_password) == 13
261 || $new->_password =~ /^\$(1|2a?)\$/ ) {
262 $new_crypt_password = shell_quote $new->_password;
264 $new_crypt_password =
265 crypt( $new->_password, $saltset[int(rand(64))].$saltset[int(rand(64))]
269 if ( $self->option('usermod_pwonly') ) {
271 if ( $old_username ne $new_username ) {
272 $error ||= "can't change username";
274 if ( $old_domain ne $new_domain ) {
275 $error ||= "can't change domain";
277 if ( $old_uid != $new_uid ) {
278 $error ||= "can't change uid";
280 if ( $old_dir ne $new_dir ) {
281 $error ||= "can't change dir";
283 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
286 $self->shellcommands_queue( $new->svcnum,
287 user => $self->option('user')||'root',
288 host => $self->machine,
289 command => eval(qq("$command")),
290 stdin_string => eval(qq("$stdin")),
294 #a good idea to queue anything that could fail or take any time
295 sub shellcommands_queue {
296 my( $self, $svcnum ) = (shift, shift);
297 my $queue = new FS::queue {
299 'job' => "FS::part_export::shellcommands::ssh_cmd",
301 $queue->insert( @_ );
304 sub ssh_cmd { #subroutine, not method
306 &Net::SSH::ssh_cmd( { @_ } );
309 #sub shellcommands_insert { #subroutine, not method
311 #sub shellcommands_replace { #subroutine, not method
313 #sub shellcommands_delete { #subroutine, not method