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