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