Add more fine-grained queue options
[freeside.git] / FS / FS / part_export / shellcommands.pm
1 package FS::part_export::shellcommands;
2
3 use vars qw(@ISA %info);
4 use Tie::IxHash;
5 use String::ShellQuote;
6 use FS::part_export;
7 use FS::Record qw( qsearch qsearchs );
8
9 @ISA = qw(FS::part_export);
10
11 tie my %options, 'Tie::IxHash',
12   'user' => { label=>'Remote username', default=>'root' },
13   'useradd' => { label=>'Insert command',
14                  default=>'useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username'
15                 #default=>'cp -pr /etc/skel $dir; chown -R $uid.$gid $dir'
16                },
17   'useradd_no_queue' => { label=>'Run immediately',
18                           type => 'checkbox',
19                         },
20   'useradd_stdin' => { label=>'Insert command STDIN',
21                        type =>'textarea',
22                        default=>'',
23                      },
24   'userdel' => { label=>'Delete command',
25                  default=>'userdel -r $username',
26                  #default=>'rm -rf $dir',
27                },
28   'userdel_no_queue' => { label=>'Run immediately',
29                           type =>'checkbox',
30                         },
31   'userdel_stdin' => { label=>'Delete command STDIN',
32                        type =>'textarea',
33                        default=>'',
34                      },
35   'usermod' => { label=>'Modify command',
36                  default=>'usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -p $new_crypt_password $old_username',
37                 #default=>'[ -d $old_dir ] && mv $old_dir $new_dir || ( '.
38                  #  'chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; '.
39                  #  'find . -depth -print | cpio -pdm $new_dir; '.
40                  #  'chmod u-t $new_dir; chown -R $uid.$gid $new_dir; '.
41                  #  'rm -rf $old_dir'.
42                  #')'
43                },
44   'usermod_no_queue' => { label=>'Run immediately',
45                           type =>'checkbox',
46                         },
47   'usermod_stdin' => { label=>'Modify command STDIN',
48                        type =>'textarea',
49                        default=>'',
50                      },
51   'usermod_pwonly' => { label=>'Disallow username, domain, uid, gid, and dir changes', #and RADIUS group changes',
52                         type =>'checkbox',
53                       },
54   'usermod_nousername' => { label=>'Disallow just username changes',
55                             type =>'checkbox',
56                           },
57   'suspend' => { label=>'Suspension command',
58                  default=>'usermod -L $username',
59                },
60   'suspend_no_queue' => { label=>'Run immediately',
61                           type =>'checkbox',
62                         },
63   'suspend_stdin' => { label=>'Suspension command STDIN',
64                        default=>'',
65                      },
66   'unsuspend' => { label=>'Unsuspension command',
67                    default=>'usermod -U $username',
68                  },
69   'unsuspend_no_queue' => { label=>'Run immediately',
70                             type =>'checkbox',
71                           },
72   'unsuspend_stdin' => { label=>'Unsuspension command STDIN',
73                          default=>'',
74                        },
75   'crypt' => { label   => 'Default password encryption',
76                type=>'select', options=>[qw(crypt md5)],
77                default => 'crypt',
78              },
79   'groups_susp_reason' => { label =>
80                              'Radius group mapping to reason (via template user)',
81                             type  => 'textarea',
82                           },
83 #  'no_queue' => { label => 'Run command immediately',
84 #                   type  => 'checkbox',
85 #                },
86 ;
87
88 %info = (
89   'svc'      => 'svc_acct',
90   'desc'     =>
91     'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
92   'options'  => \%options,
93   'nodomain' => 'Y',
94   'notes' => <<'END'
95 Run remote commands via SSH.  Usernames are considered unique (also see
96 shellcommands_withdomain).  You probably want this if the commands you are
97 running will not accept a domain as a parameter.  You will need to
98 <a href="../docs/ssh.html">setup SSH for unattended operation</a>.
99
100 <BR><BR>Use these buttons for some useful presets:
101 <UL>
102   <LI>
103     <INPUT TYPE="button" VALUE="Linux" onClick='
104       this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
105       this.form.useradd_stdin.value = "";
106       this.form.userdel.value = "userdel -r $username";
107       this.form.userdel_stdin.value="";
108       this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -p $new_crypt_password $old_username";
109       this.form.usermod_stdin.value = "";
110       this.form.suspend.value = "usermod -L $username";
111       this.form.suspend_stdin.value="";
112       this.form.unsuspend.value = "usermod -U $username";
113       this.form.unsuspend_stdin.value="";
114     '>
115   <LI>
116     <INPUT TYPE="button" VALUE="FreeBSD before 4.10 / 5.3" onClick='
117       this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -c $finger -h 0";
118       this.form.useradd_stdin.value = "$_password\n";
119       this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value="";
120       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 -g $new_gid -c $new_finger -h 0";
121       this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username";
122       this.form.suspend_stdin.value="";
123       this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";
124     '>
125     Note: On FreeBSD versions before 5.3 and 4.10 (4.10 is after 4.9, not
126     4.1!), due to deficient locking in pw(1), you must disable the chpass(1),
127     chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with
128     wrappers that prepend "lockf /etc/passwd.lock".  Alternatively, apply the
129     patch in
130     <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
131     and use the "FreeBSD 4.10 / 5.3 or later" button below.
132   <LI>
133     <INPUT TYPE="button" VALUE="FreeBSD 4.10 / 5.3 or later" onClick='
134       this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
135       this.form.useradd_stdin.value = "$_password\n";
136       this.form.userdel.value = "pw userdel $username -r";
137       this.form.userdel_stdin.value="";
138       this.form.usermod.value = "pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -c $new_finger -h 0";
139       this.form.usermod_stdin.value = "$new__password\n";
140       this.form.suspend.value = "pw lock $username";
141       this.form.suspend_stdin.value="";
142       this.form.unsuspend.value = "pw unlock $username";
143       this.form.unsuspend_stdin.value="";
144     '>
145   <LI>
146     <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
147       this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
148       this.form.useradd_stdin.value = "";
149       this.form.userdel.value = "userdel -r $username";
150       this.form.userdel_stdin.value="";
151       this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -p $new_crypt_password $old_username";
152       this.form.usermod_stdin.value = "";
153       this.form.suspend.value = "";
154       this.form.suspend_stdin.value="";
155       this.form.unsuspend.value = "";
156       this.form.unsuspend_stdin.value="";
157     '>
158   <LI>
159     <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
160       this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
161       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 )";
162       this.form.usermod_stdin.value = "";
163       this.form.userdel.value = "rm -rf $dir";
164       this.form.userdel_stdin.value="";
165       this.form.suspend.value = "";
166       this.form.suspend_stdin.value="";
167       this.form.unsuspend.value = "";
168       this.form.unsuspend_stdin.value="";
169     '>
170 </UL>
171
172 The following variables are available for interpolation (prefixed with new_ or
173 old_ for replace operations):
174 <UL>
175   <LI><code>$username</code>
176   <LI><code>$_password</code>
177   <LI><code>$quoted_password</code> - unencrypted password, already quoted for the shell (do not add additional quotes).
178   <LI><code>$crypt_password</code> - encrypted password.  When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
179   <LI><code>$ldap_password</code> - Password in LDAP/RFC2307 format (for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or "{MD5}5426824942db4253f87a1009fd5d2d4").  When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
180   <LI><code>$uid</code>
181   <LI><code>$gid</code>
182   <LI><code>$finger</code> - GECOS.  When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
183   <LI><code>$first</code> - First name of GECOS.  When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
184   <LI><code>$last</code> - Last name of GECOS.  When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
185   <LI><code>$dir</code> - home directory
186   <LI><code>$shell</code>
187   <LI><code>$quota</code>
188   <LI><code>@radius_groups</code>
189   <LI><code>$reasonnum (when suspending)</code>
190   <LI><code>$reasontext (when suspending)</code>
191   <LI><code>$reasontypenum (when suspending)</code>
192   <LI><code>$reasontypetext (when suspending)</code>
193   <LI><code>$pkgnum</code>
194   <LI><code>$custnum</code>
195   <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
196 </UL>
197 END
198 );
199
200 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
201
202 sub _map {
203   my $self = shift;
204   map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
205 }
206
207 sub rebless { shift; }
208
209 sub _export_insert {
210   my($self) = shift;
211   $self->_export_command('useradd', @_);
212 }
213
214 sub _export_delete {
215   my($self) = shift;
216   $self->_export_command('userdel', @_);
217 }
218
219 sub _export_suspend {
220   my($self) = shift;
221   $self->_export_command_or_super('suspend', @_);
222 }
223
224 sub _export_unsuspend {
225   my($self) = shift;
226   $self->_export_command_or_super('unsuspend', @_);
227 }
228
229 sub _export_command_or_super {
230   my($self, $action) = (shift, shift);
231   if ( $self->option($action) =~ /^\s*$/ ) {
232     my $method = "SUPER::_export_$action";
233     $self->$method(@_);
234   } else {
235     $self->_export_command($action, @_);
236   }
237 };
238
239 sub _export_command {
240   my ( $self, $action, $svc_acct) = (shift, shift, shift);
241   my $command = $self->option($action);
242   return '' if $command =~ /^\s*$/;
243   my $stdin = $self->option($action."_stdin");
244
245   no strict 'vars';
246   {
247     no strict 'refs';
248     ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
249
250     # snarfs are unused at this point?
251     my $count = 1;
252     foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
253       ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
254         foreach qw( machine username _password );
255       $count++;
256     }
257   }
258
259   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
260   if ( $cust_pkg ) {
261     $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
262   } else {
263     $email = '';
264   }
265
266   $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
267   ($first, $last ) = ( $1, $2 );
268   $domain = $svc_acct->domain;
269
270   $quoted_password = shell_quote $_password;
271
272   $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
273   $ldap_password  = $svc_acct->ldap_password(  $self->option('crypt') );
274
275   @radius_groups = $svc_acct->radius_groups;
276
277   my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
278   if ( $cust_pkg && $action eq 'suspend' &&
279        (my $r = $cust_pkg->last_reason('susp')) )
280   {
281     $reasonnum = $r->reasonnum;
282     $reasontext = $r->reason;
283     $reasontypenum = $r->reason_type;
284     $reasontypetext = $r->reasontype->type;
285
286     my %reasonmap = $self->_groups_susp_reason_map;
287     my $userspec = '';
288     $userspec = $reasonmap{$reasonnum}
289       if exists($reasonmap{$reasonnum});
290     $userspec = $reasonmap{$reasontext}
291       if (!$userspec && exists($reasonmap{$reasontext}));
292
293     my $suspend_user;
294     if ( $userspec =~ /^\d+$/ ) {
295       $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
296     } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
297       my ($username,$domain) = split(/\@/, $userspec);
298       for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
299         $suspend_user = $user if $userspec eq $user->email;
300       }
301     } elsif ($userspec) {
302       $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
303     }
304
305     @radius_groups = $suspend_user->radius_groups
306       if $suspend_user;  
307
308   } else {
309     $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
310   }
311
312   my $stdin_string = eval(qq("$stdin"));
313
314   $first = shell_quote $first;
315   $last = shell_quote $last;
316   $finger = shell_quote $finger;
317   $crypt_password = shell_quote $crypt_password;
318   $ldap_password  = shell_quote $ldap_password;
319   $pkgnum = $cust_pkg ? $cust_pkg->pkgnum : '';
320   $custnum = $cust_pkg ? $cust_pkg->custnum : '';
321
322   my $command_string = eval(qq("$command"));
323   my @ssh_cmd_args = (
324     user          => $self->option('user') || 'root',
325     host          => $self->machine,
326     command       => $command_string,
327     stdin_string  => $stdin_string,
328   );
329
330   if($self->option($action . '_no_queue')) {
331     # discard return value just like freeside-queued.
332     eval { ssh_cmd(@ssh_cmd_args) };
333     $error = $@;
334     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
335       if $error;
336   }
337   else {
338     $self->shellcommands_queue( $svc_acct->svcnum, @ssh_cmd_args );
339   }
340 }
341
342 sub _export_replace {
343   my($self, $new, $old ) = (shift, shift, shift);
344   my $command = $self->option('usermod');
345   my $stdin = $self->option('usermod_stdin');
346   no strict 'vars';
347   {
348     no strict 'refs';
349     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
350     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
351   }
352   my $old_cust_pkg = $old->cust_svc->cust_pkg;
353   my $new_cust_pkg = $new->cust_svc->cust_pkg;
354   $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
355   ($new_first, $new_last ) = ( $1, $2 );
356   $quoted_new__password = shell_quote $new__password; #old, wrong?
357   $new_quoted_password = shell_quote $new__password; #new, better?
358   $old_domain = $old->domain;
359   $new_domain = $new->domain;
360
361   $new_crypt_password = $new->crypt_password( $self->option('crypt') );
362   $new_ldap_password  = $new->ldap_password(  $self->option('crypt') );
363
364   @old_radius_groups = $old->radius_groups;
365   @new_radius_groups = $new->radius_groups;
366
367   my $error = '';
368   if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
369     if ( $old_username ne $new_username ) {
370       $error ||= "can't change username";
371     }
372   }
373   if ( $self->option('usermod_pwonly') ) {
374     if ( $old_domain ne $new_domain ) {
375       $error ||= "can't change domain";
376     }
377     if ( $old_uid != $new_uid ) {
378       $error ||= "can't change uid";
379     }
380     if ( $old_gid != $new_gid ) {
381       $error ||= "can't change gid";
382     }
383     if ( $old_dir ne $new_dir ) {
384       $error ||= "can't change dir";
385     }
386     #if ( join("\n", sort @old_radius_groups) ne
387     #     join("\n", sort @new_radius_groups)    ) {
388     #  $error ||= "can't change RADIUS groups";
389     #}
390   }
391   return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
392     if $error;
393
394   my $stdin_string = eval(qq("$stdin"));
395
396   $new_first = shell_quote $new_first;
397   $new_last = shell_quote $new_last;
398   $new_finger = shell_quote $new_finger;
399   $new_crypt_password = shell_quote $new_crypt_password;
400   $new_ldap_password  = shell_quote $new_ldap_password;
401   $old_pkgnum = $old_cust_pkg ? $old_cust_pkg->pkgnum : '';
402   $old_custnum = $old_cust_pkg ? $old_cust_pkg->custnum : '';
403   $new_pkgnum = $new_cust_pkg ? $new_cust_pkg->pkgnum : '';
404   $new_custnum = $new_cust_pkg ? $new_cust_pkg->custnum : '';
405
406   my $command_string = eval(qq("$command"));
407
408   my @ssh_cmd_args = (
409     user          => $self->option('user') || 'root',
410     host          => $self->machine,
411     command       => $command_string,
412     stdin_string  => $stdin_string,
413   );
414
415   if($self->option('usermod_no_queue')) {
416     # discard return value just like freeside-queued.
417     eval { ssh_cmd(@ssh_cmd_args) };
418     $error = $@;
419     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
420       if $error;
421   }
422   else {
423     $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
424   }
425 }
426
427 #a good idea to queue anything that could fail or take any time
428 sub shellcommands_queue {
429   my( $self, $svcnum ) = (shift, shift);
430   my $queue = new FS::queue {
431     'svcnum' => $svcnum,
432     'job'    => "FS::part_export::shellcommands::ssh_cmd",
433   };
434   $queue->insert( @_ );
435 }
436
437 sub ssh_cmd { #subroutine, not method
438   use Net::SSH '0.08';
439   &Net::SSH::ssh_cmd( { @_ } );
440 }
441
442 #sub shellcommands_insert { #subroutine, not method
443 #}
444 #sub shellcommands_replace { #subroutine, not method
445 #}
446 #sub shellcommands_delete { #subroutine, not method
447 #}
448
449 1;
450