Add no_queue option to shellcommands exports
[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>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
179 </UL>
180 END
181 );
182
183 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
184
185 sub _map {
186   my $self = shift;
187   map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
188 }
189
190 sub rebless { shift; }
191
192 sub _export_insert {
193   my($self) = shift;
194   $self->_export_command('useradd', @_);
195 }
196
197 sub _export_delete {
198   my($self) = shift;
199   $self->_export_command('userdel', @_);
200 }
201
202 sub _export_suspend {
203   my($self) = shift;
204   $self->_export_command_or_super('suspend', @_);
205 }
206
207 sub _export_unsuspend {
208   my($self) = shift;
209   $self->_export_command_or_super('unsuspend', @_);
210 }
211
212 sub _export_command_or_super {
213   my($self, $action) = (shift, shift);
214   if ( $self->option($action) =~ /^\s*$/ ) {
215     my $method = "SUPER::_export_$action";
216     $self->$method(@_);
217   } else {
218     $self->_export_command($action, @_);
219   }
220 };
221
222 sub _export_command {
223   my ( $self, $action, $svc_acct) = (shift, shift, shift);
224   my $command = $self->option($action);
225   return '' if $command =~ /^\s*$/;
226   my $stdin = $self->option($action."_stdin");
227
228   no strict 'vars';
229   {
230     no strict 'refs';
231     ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
232
233     # snarfs are unused at this point?
234     my $count = 1;
235     foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
236       ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
237         foreach qw( machine username _password );
238       $count++;
239     }
240   }
241
242   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
243   if ( $cust_pkg ) {
244     $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
245   } else {
246     $email = '';
247   }
248
249   $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
250   ($first, $last ) = ( $1, $2 );
251   $domain = $svc_acct->domain;
252
253   $quoted_password = shell_quote $_password;
254
255   $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
256   $ldap_password  = $svc_acct->ldap_password(  $self->option('crypt') );
257
258   @radius_groups = $svc_acct->radius_groups;
259
260   my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
261   if ( $cust_pkg && $action eq 'suspend' &&
262        (my $r = $cust_pkg->last_reason('susp')) )
263   {
264     $reasonnum = $r->reasonnum;
265     $reasontext = $r->reason;
266     $reasontypenum = $r->reason_type;
267     $reasontypetext = $r->reasontype->type;
268
269     my %reasonmap = $self->_groups_susp_reason_map;
270     my $userspec = '';
271     $userspec = $reasonmap{$reasonnum}
272       if exists($reasonmap{$reasonnum});
273     $userspec = $reasonmap{$reasontext}
274       if (!$userspec && exists($reasonmap{$reasontext}));
275
276     my $suspend_user;
277     if ( $userspec =~ /^\d+$/ ) {
278       $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
279     } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
280       my ($username,$domain) = split(/\@/, $userspec);
281       for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
282         $suspend_user = $user if $userspec eq $user->email;
283       }
284     } elsif ($userspec) {
285       $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
286     }
287
288     @radius_groups = $suspend_user->radius_groups
289       if $suspend_user;  
290
291   } else {
292     $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
293   }
294
295   my $stdin_string = eval(qq("$stdin"));
296
297   $first = shell_quote $first;
298   $last = shell_quote $last;
299   $finger = shell_quote $finger;
300   $crypt_password = shell_quote $crypt_password;
301   $ldap_password  = shell_quote $ldap_password;
302
303   my $command_string = eval(qq("$command"));
304
305   $self->shellcommands_queue( $svc_acct->svcnum,
306     user         => $self->option('user')||'root',
307     host         => $self->machine,
308     command      => $command_string,
309     stdin_string => $stdin_string,
310   );
311 }
312
313 sub _export_replace {
314   my($self, $new, $old ) = (shift, shift, shift);
315   my $command = $self->option('usermod');
316   my $stdin = $self->option('usermod_stdin');
317   no strict 'vars';
318   {
319     no strict 'refs';
320     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
321     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
322   }
323   $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
324   ($new_first, $new_last ) = ( $1, $2 );
325   $quoted_new__password = shell_quote $new__password; #old, wrong?
326   $new_quoted_password = shell_quote $new__password; #new, better?
327   $old_domain = $old->domain;
328   $new_domain = $new->domain;
329
330   $new_crypt_password = $new->crypt_password( $self->option('crypt') );
331   $new_ldap_password  = $new->ldap_password(  $self->option('crypt') );
332
333   @old_radius_groups = $old->radius_groups;
334   @new_radius_groups = $new->radius_groups;
335
336   my $error = '';
337   if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
338     if ( $old_username ne $new_username ) {
339       $error ||= "can't change username";
340     }
341   }
342   if ( $self->option('usermod_pwonly') ) {
343     if ( $old_domain ne $new_domain ) {
344       $error ||= "can't change domain";
345     }
346     if ( $old_uid != $new_uid ) {
347       $error ||= "can't change uid";
348     }
349     if ( $old_gid != $new_gid ) {
350       $error ||= "can't change gid";
351     }
352     if ( $old_dir ne $new_dir ) {
353       $error ||= "can't change dir";
354     }
355     #if ( join("\n", sort @old_radius_groups) ne
356     #     join("\n", sort @new_radius_groups)    ) {
357     #  $error ||= "can't change RADIUS groups";
358     #}
359   }
360   return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
361     if $error;
362
363   my $stdin_string = eval(qq("$stdin"));
364
365   $new_first = shell_quote $new_first;
366   $new_last = shell_quote $new_last;
367   $new_finger = shell_quote $new_finger;
368   $new_crypt_password = shell_quote $new_crypt_password;
369   $new_ldap_password  = shell_quote $new_ldap_password;
370
371   my $command_string = eval(qq("$command"));
372
373   my @ssh_cmd_args = (
374     user          => $self->option('user') || 'root',
375     host          => $self->machine,
376     command       => $command_string,
377     stdin_string  => $stdin_string,
378   );
379
380   if($self->options('no_queue')) {
381     # discard return value just like freeside-queued.
382     eval { ssh_cmd(@ssh_cmd_args) };
383     $error = $@;
384     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
385       if $error;
386   }
387   else {
388     $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
389   }
390 }
391
392 #a good idea to queue anything that could fail or take any time
393 sub shellcommands_queue {
394   my( $self, $svcnum ) = (shift, shift);
395   my $queue = new FS::queue {
396     'svcnum' => $svcnum,
397     'job'    => "FS::part_export::shellcommands::ssh_cmd",
398   };
399   $queue->insert( @_ );
400 }
401
402 sub ssh_cmd { #subroutine, not method
403   use Net::SSH '0.08';
404   &Net::SSH::ssh_cmd( { @_ } );
405 }
406
407 #sub shellcommands_insert { #subroutine, not method
408 #}
409 #sub shellcommands_replace { #subroutine, not method
410 #}
411 #sub shellcommands_delete { #subroutine, not method
412 #}
413
414 1;
415