0b9e475dbd8a2a961ee65622ae72e36814ab9614
[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 <b>svc_acct</b> are also available.
196   <LI>The following fields from <b>cust_main</b> are also available (except during replace): company, address1, address2, city, state, zip, county, daytime, night, fax, otaker.  When used on the command line (rather than STDIN), they will be quoted for the shell already (do not add additional quotes).
197 </UL>
198 END
199 );
200
201 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
202
203 sub _map {
204   my $self = shift;
205   map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
206 }
207
208 sub rebless { shift; }
209
210 sub _export_insert {
211   my($self) = shift;
212   $self->_export_command('useradd', @_);
213 }
214
215 sub _export_delete {
216   my($self) = shift;
217   $self->_export_command('userdel', @_);
218 }
219
220 sub _export_suspend {
221   my($self) = shift;
222   $self->_export_command_or_super('suspend', @_);
223 }
224
225 sub _export_unsuspend {
226   my($self) = shift;
227   $self->_export_command_or_super('unsuspend', @_);
228 }
229
230 sub _export_command_or_super {
231   my($self, $action) = (shift, shift);
232   if ( $self->option($action) =~ /^\s*$/ ) {
233     my $method = "SUPER::_export_$action";
234     $self->$method(@_);
235   } else {
236     $self->_export_command($action, @_);
237   }
238 };
239
240 sub _export_command {
241   my ( $self, $action, $svc_acct) = (shift, shift, shift);
242   my $command = $self->option($action);
243   return '' if $command =~ /^\s*$/;
244   my $stdin = $self->option($action."_stdin");
245
246   no strict 'vars';
247   {
248     no strict 'refs';
249     ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
250
251     # snarfs are unused at this point?
252     my $count = 1;
253     foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
254       ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
255         foreach qw( machine username _password );
256       $count++;
257     }
258   }
259
260   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
261   if ( $cust_pkg ) {
262     no strict 'vars';
263     {
264       no strict 'refs';
265       foreach my $custf (qw( company address1 address2 city state zip country
266                              daytime night fax otaker
267                         ))
268       {
269         ${$custf} = $cust_pkg->cust_main->$custf();
270       }
271     }
272     $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
273   } else {
274     $email = '';
275   }
276
277   $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
278   ($first, $last ) = ( $1, $2 );
279   $domain = $svc_acct->domain;
280
281   $quoted_password = shell_quote $_password;
282
283   $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
284   $ldap_password  = $svc_acct->ldap_password(  $self->option('crypt') );
285
286   @radius_groups = $svc_acct->radius_groups;
287
288   my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
289   if ( $cust_pkg && $action eq 'suspend' &&
290        (my $r = $cust_pkg->last_reason('susp')) )
291   {
292     $reasonnum = $r->reasonnum;
293     $reasontext = $r->reason;
294     $reasontypenum = $r->reason_type;
295     $reasontypetext = $r->reasontype->type;
296
297     my %reasonmap = $self->_groups_susp_reason_map;
298     my $userspec = '';
299     $userspec = $reasonmap{$reasonnum}
300       if exists($reasonmap{$reasonnum});
301     $userspec = $reasonmap{$reasontext}
302       if (!$userspec && exists($reasonmap{$reasontext}));
303
304     my $suspend_user;
305     if ( $userspec =~ /^\d+$/ ) {
306       $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
307     } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
308       my ($username,$domain) = split(/\@/, $userspec);
309       for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
310         $suspend_user = $user if $userspec eq $user->email;
311       }
312     } elsif ($userspec) {
313       $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
314     }
315
316     @radius_groups = $suspend_user->radius_groups
317       if $suspend_user;  
318
319   } else {
320     $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
321   }
322
323   $pkgnum = $cust_pkg ? $cust_pkg->pkgnum : '';
324   $custnum = $cust_pkg ? $cust_pkg->custnum : '';
325
326   my $stdin_string = eval(qq("$stdin"));
327
328   $first = shell_quote $first;
329   $last = shell_quote $last;
330   $finger = shell_quote $finger;
331   $crypt_password = shell_quote $crypt_password;
332   $ldap_password  = shell_quote $ldap_password;
333
334   $company = shell_quote $company;
335   $address1 = shell_quote $address1;
336   $address2 = shell_quote $address2;
337   $city = shell_quote $city;
338   $state = shell_quote $state;
339   $zip = shell_quote $zip;
340   $country = shell_quote $country;
341   $daytime = shell_quote $daytime;
342   $night = shell_quote $night;
343   $fax = shell_quote $fax;
344   $otaker = shell_quote $otaker; 
345
346   my $command_string = eval(qq("$command"));
347   my @ssh_cmd_args = (
348     user          => $self->option('user') || 'root',
349     host          => $self->machine,
350     command       => $command_string,
351     stdin_string  => $stdin_string,
352   );
353
354   if($self->option($action . '_no_queue')) {
355     # discard return value just like freeside-queued.
356     eval { ssh_cmd(@ssh_cmd_args) };
357     $error = $@;
358     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
359       if $error;
360   }
361   else {
362     $self->shellcommands_queue( $svc_acct->svcnum, @ssh_cmd_args );
363   }
364 }
365
366 sub _export_replace {
367   my($self, $new, $old ) = (shift, shift, shift);
368   my $command = $self->option('usermod');
369   my $stdin = $self->option('usermod_stdin');
370   no strict 'vars';
371   {
372     no strict 'refs';
373     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
374     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
375   }
376   my $old_cust_pkg = $old->cust_svc->cust_pkg;
377   my $new_cust_pkg = $new->cust_svc->cust_pkg;
378   $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
379   ($new_first, $new_last ) = ( $1, $2 );
380   $quoted_new__password = shell_quote $new__password; #old, wrong?
381   $new_quoted_password = shell_quote $new__password; #new, better?
382   $old_domain = $old->domain;
383   $new_domain = $new->domain;
384
385   $new_crypt_password = $new->crypt_password( $self->option('crypt') );
386   $new_ldap_password  = $new->ldap_password(  $self->option('crypt') );
387
388   @old_radius_groups = $old->radius_groups;
389   @new_radius_groups = $new->radius_groups;
390
391   my $error = '';
392   if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
393     if ( $old_username ne $new_username ) {
394       $error ||= "can't change username";
395     }
396   }
397   if ( $self->option('usermod_pwonly') ) {
398     if ( $old_domain ne $new_domain ) {
399       $error ||= "can't change domain";
400     }
401     if ( $old_uid != $new_uid ) {
402       $error ||= "can't change uid";
403     }
404     if ( $old_gid != $new_gid ) {
405       $error ||= "can't change gid";
406     }
407     if ( $old_dir ne $new_dir ) {
408       $error ||= "can't change dir";
409     }
410     #if ( join("\n", sort @old_radius_groups) ne
411     #     join("\n", sort @new_radius_groups)    ) {
412     #  $error ||= "can't change RADIUS groups";
413     #}
414   }
415   return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
416     if $error;
417
418   my $stdin_string = eval(qq("$stdin"));
419
420   $new_first = shell_quote $new_first;
421   $new_last = shell_quote $new_last;
422   $new_finger = shell_quote $new_finger;
423   $new_crypt_password = shell_quote $new_crypt_password;
424   $new_ldap_password  = shell_quote $new_ldap_password;
425   $old_pkgnum = $old_cust_pkg ? $old_cust_pkg->pkgnum : '';
426   $old_custnum = $old_cust_pkg ? $old_cust_pkg->custnum : '';
427   $new_pkgnum = $new_cust_pkg ? $new_cust_pkg->pkgnum : '';
428   $new_custnum = $new_cust_pkg ? $new_cust_pkg->custnum : '';
429
430   my $command_string = eval(qq("$command"));
431
432   my @ssh_cmd_args = (
433     user          => $self->option('user') || 'root',
434     host          => $self->machine,
435     command       => $command_string,
436     stdin_string  => $stdin_string,
437   );
438
439   if($self->option('usermod_no_queue')) {
440     # discard return value just like freeside-queued.
441     eval { ssh_cmd(@ssh_cmd_args) };
442     $error = $@;
443     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
444       if $error;
445   }
446   else {
447     $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
448   }
449 }
450
451 #a good idea to queue anything that could fail or take any time
452 sub shellcommands_queue {
453   my( $self, $svcnum ) = (shift, shift);
454   my $queue = new FS::queue {
455     'svcnum' => $svcnum,
456     'job'    => "FS::part_export::shellcommands::ssh_cmd",
457   };
458   $queue->insert( @_ );
459 }
460
461 sub ssh_cmd { #subroutine, not method
462   use Net::SSH '0.08';
463   &Net::SSH::ssh_cmd( { @_ } );
464 }
465
466 #sub shellcommands_insert { #subroutine, not method
467 #}
468 #sub shellcommands_replace { #subroutine, not method
469 #}
470 #sub shellcommands_delete { #subroutine, not method
471 #}
472
473 1;
474