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 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, due to deficient locking in pw(1),
96 you must disable the chpass(1), chsh(1), chfn(1), passwd(1), and vipw(1)
97 commands, or replace them with wrappers that prepend
98 "lockf /etc/passwd.lock". Alternatively, apply the patch in
99 <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
100 and use the "FreeBSD 5.3 or later" button below.
102 <INPUT TYPE="button" VALUE="FreeBSD 5.3 or later" onClick='
103 this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
104 this.form.useradd_stdin.value = "$_password\n";
105 this.form.userdel.value = "pw userdel $username -r";
106 this.form.userdel_stdin.value="";
107 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";
108 this.form.usermod_stdin.value = "$new__password\n";
109 this.form.suspend.value = "pw lock $username";
110 this.form.suspend_stdin.value="";
111 this.form.unsuspend.value = "pw unlock $username";
112 this.form.unsuspend_stdin.value="";
115 <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
116 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
117 this.form.useradd_stdin.value = "";
118 this.form.userdel.value = "userdel -r $username";
119 this.form.userdel_stdin.value="";
120 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";
121 this.form.usermod_stdin.value = "";
122 this.form.suspend.value = "";
123 this.form.suspend_stdin.value="";
124 this.form.unsuspend.value = "";
125 this.form.unsuspend_stdin.value="";
128 <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
129 this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
130 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 )";
131 this.form.usermod_stdin.value = "";
132 this.form.userdel.value = "rm -rf $dir";
133 this.form.userdel_stdin.value="";
134 this.form.suspend.value = "";
135 this.form.suspend_stdin.value="";
136 this.form.unsuspend.value = "";
137 this.form.unsuspend_stdin.value="";
141 The following variables are available for interpolation (prefixed with new_ or
142 old_ for replace operations):
144 <LI><code>$username</code>
145 <LI><code>$_password</code>
146 <LI><code>$quoted_password</code> - unencrypted password quoted for the shell
147 <LI><code>$crypt_password</code> - encrypted password
148 <LI><code>$uid</code>
149 <LI><code>$gid</code>
150 <LI><code>$finger</code> - GECOS, already quoted for the shell (do not add additional quotes)
151 <LI><code>$first</code> - First name of GECOS, already quoted for the shell (do not add additional quotes)
152 <LI><code>$last</code> - Last name of GECOS, already quoted for the shell (do not add additional quotes)
153 <LI><code>$dir</code> - home directory
154 <LI><code>$shell</code>
155 <LI><code>$quota</code>
156 <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
161 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
163 sub rebless { shift; }
167 $self->_export_command('useradd', @_);
172 $self->_export_command('userdel', @_);
175 sub _export_suspend {
177 $self->_export_command('suspend', @_);
180 sub _export_unsuspend {
182 $self->_export_command('unsuspend', @_);
185 sub _export_command {
186 my ( $self, $action, $svc_acct) = (shift, shift, shift);
187 my $command = $self->option($action);
188 return '' if $command =~ /^\s*$/;
189 my $stdin = $self->option($action."_stdin");
194 ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
197 foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
198 ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
199 foreach qw( machine username _password );
204 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
206 $email = ( grep { $_ ne 'POST' } $cust_pkg->cust_main->invoicing_list )[0];
211 $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
212 ($first, $last ) = ( $1, $2 );
213 $first = shell_quote $first;
214 $last = shell_quote $last;
215 $finger = shell_quote $finger;
216 $quoted_password = shell_quote $_password;
217 $domain = $svc_acct->domain;
219 #eventually should check a "password-encoding" field
220 if ( length($svc_acct->_password) == 13
221 || $svc_acct->_password =~ /^\$(1|2a?)\$/ ) {
222 $crypt_password = shell_quote $svc_acct->_password;
224 $crypt_password = crypt(
225 $svc_acct->_password,
226 $saltset[int(rand(64))].$saltset[int(rand(64))]
230 $self->shellcommands_queue( $svc_acct->svcnum,
231 user => $self->option('user')||'root',
232 host => $self->machine,
233 command => eval(qq("$command")),
234 stdin_string => eval(qq("$stdin")),
238 sub _export_replace {
239 my($self, $new, $old ) = (shift, shift, shift);
240 my $command = $self->option('usermod');
241 my $stdin = $self->option('usermod_stdin');
245 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
246 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
248 $new_finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
249 ($new_first, $new_last ) = ( $1, $2 );
250 $new_first = shell_quote $new_first;
251 $new_last = shell_quote $new_last;
252 $new_finger = shell_quote $new_finger;
253 $quoted_new__password = shell_quote $new__password; #old, wrong?
254 $new_quoted_password = shell_quote $new__password; #new, better?
255 $old_domain = $old->domain;
256 $new_domain = $new->domain;
258 #eventuall should check a "password-encoding" field
259 if ( length($new->_password) == 13
260 || $new->_password =~ /^\$(1|2a?)\$/ ) {
261 $new_crypt_password = shell_quote $new->_password;
263 $new_crypt_password =
264 crypt( $new->_password, $saltset[int(rand(64))].$saltset[int(rand(64))]
268 if ( $self->option('usermod_pwonly') ) {
270 if ( $old_username ne $new_username ) {
271 $error ||= "can't change username";
273 if ( $old_domain ne $new_domain ) {
274 $error ||= "can't change domain";
276 if ( $old_uid != $new_uid ) {
277 $error ||= "can't change uid";
279 if ( $old_dir ne $new_dir ) {
280 $error ||= "can't change dir";
282 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
285 $self->shellcommands_queue( $new->svcnum,
286 user => $self->option('user')||'root',
287 host => $self->machine,
288 command => eval(qq("$command")),
289 stdin_string => eval(qq("$stdin")),
293 #a good idea to queue anything that could fail or take any time
294 sub shellcommands_queue {
295 my( $self, $svcnum ) = (shift, shift);
296 my $queue = new FS::queue {
298 'job' => "FS::part_export::shellcommands::ssh_cmd",
300 $queue->insert( @_ );
303 sub ssh_cmd { #subroutine, not method
305 &Net::SSH::ssh_cmd( { @_ } );
308 #sub shellcommands_insert { #subroutine, not method
310 #sub shellcommands_replace { #subroutine, not method
312 #sub shellcommands_delete { #subroutine, not method