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