import torrus 1.0.9
[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="http://www.freeside.biz/mediawiki/index.php/Freeside:1.9:Documentation:Administration:SSH_Keys">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, agent_custid.  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 agent_custid
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   $agent_custid = shell_quote $agent_custid;
346
347   my $command_string = eval(qq("$command"));
348   my @ssh_cmd_args = (
349     user          => $self->option('user') || 'root',
350     host          => $self->machine,
351     command       => $command_string,
352     stdin_string  => $stdin_string,
353   );
354
355   if($self->option($action . '_no_queue')) {
356     # discard return value just like freeside-queued.
357     eval { ssh_cmd(@ssh_cmd_args) };
358     $error = $@;
359     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
360       if $error;
361   }
362   else {
363     $self->shellcommands_queue( $svc_acct->svcnum, @ssh_cmd_args );
364   }
365 }
366
367 sub _export_replace {
368   my($self, $new, $old ) = (shift, shift, shift);
369   my $command = $self->option('usermod');
370   my $stdin = $self->option('usermod_stdin');
371   no strict 'vars';
372   {
373     no strict 'refs';
374     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
375     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
376   }
377   my $old_cust_pkg = $old->cust_svc->cust_pkg;
378   my $new_cust_pkg = $new->cust_svc->cust_pkg;
379   my $new_cust_main = $new_cust_pkg ? $new_cust_pkg->cust_main : '';
380
381   $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
382   ($new_first, $new_last ) = ( $1, $2 );
383   $quoted_new__password = shell_quote $new__password; #old, wrong?
384   $new_quoted_password = shell_quote $new__password; #new, better?
385   $old_domain = $old->domain;
386   $new_domain = $new->domain;
387
388   $new_crypt_password = $new->crypt_password( $self->option('crypt') );
389   $new_ldap_password  = $new->ldap_password(  $self->option('crypt') );
390
391   @old_radius_groups = $old->radius_groups;
392   @new_radius_groups = $new->radius_groups;
393
394   my $error = '';
395   if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
396     if ( $old_username ne $new_username ) {
397       $error ||= "can't change username";
398     }
399   }
400   if ( $self->option('usermod_pwonly') ) {
401     if ( $old_domain ne $new_domain ) {
402       $error ||= "can't change domain";
403     }
404     if ( $old_uid != $new_uid ) {
405       $error ||= "can't change uid";
406     }
407     if ( $old_gid != $new_gid ) {
408       $error ||= "can't change gid";
409     }
410     if ( $old_dir ne $new_dir ) {
411       $error ||= "can't change dir";
412     }
413     #if ( join("\n", sort @old_radius_groups) ne
414     #     join("\n", sort @new_radius_groups)    ) {
415     #  $error ||= "can't change RADIUS groups";
416     #}
417   }
418   return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
419     if $error;
420
421   $new_agent_custid = $new_cust_main ? $new_cust_main->agent_custid : '';
422   $old_pkgnum = $old_cust_pkg ? $old_cust_pkg->pkgnum : '';
423   $old_custnum = $old_cust_pkg ? $old_cust_pkg->custnum : '';
424   $new_pkgnum = $new_cust_pkg ? $new_cust_pkg->pkgnum : '';
425   $new_custnum = $new_cust_pkg ? $new_cust_pkg->custnum : '';
426
427   my $stdin_string = eval(qq("$stdin"));
428
429   $new_first = shell_quote $new_first;
430   $new_last = shell_quote $new_last;
431   $new_finger = shell_quote $new_finger;
432   $new_crypt_password = shell_quote $new_crypt_password;
433   $new_ldap_password  = shell_quote $new_ldap_password;
434   $new_agent_custid = shell_quote $new_agent_custid;
435
436   my $command_string = eval(qq("$command"));
437
438   my @ssh_cmd_args = (
439     user          => $self->option('user') || 'root',
440     host          => $self->machine,
441     command       => $command_string,
442     stdin_string  => $stdin_string,
443   );
444
445   if($self->option('usermod_no_queue')) {
446     # discard return value just like freeside-queued.
447     eval { ssh_cmd(@ssh_cmd_args) };
448     $error = $@;
449     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
450       if $error;
451   }
452   else {
453     $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
454   }
455 }
456
457 #a good idea to queue anything that could fail or take any time
458 sub shellcommands_queue {
459   my( $self, $svcnum ) = (shift, shift);
460   my $queue = new FS::queue {
461     'svcnum' => $svcnum,
462     'job'    => "FS::part_export::shellcommands::ssh_cmd",
463   };
464   $queue->insert( @_ );
465 }
466
467 sub ssh_cmd { #subroutine, not method
468   use Net::SSH '0.08';
469   &Net::SSH::ssh_cmd( { @_ } );
470 }
471
472 #sub shellcommands_insert { #subroutine, not method
473 #}
474 #sub shellcommands_replace { #subroutine, not method
475 #}
476 #sub shellcommands_delete { #subroutine, not method
477 #}
478
479 1;
480