bad tyops
[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   my @ssh_cmd_args = (
305     user          => $self->option('user') || 'root',
306     host          => $self->machine,
307     command       => $command_string,
308     stdin_string  => $stdin_string,
309   );
310
311   if($self->option('no_queue')) {
312     # discard return value just like freeside-queued.
313     eval { ssh_cmd(@ssh_cmd_args) };
314     $error = $@;
315     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
316       if $error;
317   }
318   else {
319     $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
320   }
321 }
322
323 sub _export_replace {
324   my($self, $new, $old ) = (shift, shift, shift);
325   my $command = $self->option('usermod');
326   my $stdin = $self->option('usermod_stdin');
327   no strict 'vars';
328   {
329     no strict 'refs';
330     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
331     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
332   }
333   $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
334   ($new_first, $new_last ) = ( $1, $2 );
335   $quoted_new__password = shell_quote $new__password; #old, wrong?
336   $new_quoted_password = shell_quote $new__password; #new, better?
337   $old_domain = $old->domain;
338   $new_domain = $new->domain;
339
340   $new_crypt_password = $new->crypt_password( $self->option('crypt') );
341   $new_ldap_password  = $new->ldap_password(  $self->option('crypt') );
342
343   @old_radius_groups = $old->radius_groups;
344   @new_radius_groups = $new->radius_groups;
345
346   my $error = '';
347   if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
348     if ( $old_username ne $new_username ) {
349       $error ||= "can't change username";
350     }
351   }
352   if ( $self->option('usermod_pwonly') ) {
353     if ( $old_domain ne $new_domain ) {
354       $error ||= "can't change domain";
355     }
356     if ( $old_uid != $new_uid ) {
357       $error ||= "can't change uid";
358     }
359     if ( $old_gid != $new_gid ) {
360       $error ||= "can't change gid";
361     }
362     if ( $old_dir ne $new_dir ) {
363       $error ||= "can't change dir";
364     }
365     #if ( join("\n", sort @old_radius_groups) ne
366     #     join("\n", sort @new_radius_groups)    ) {
367     #  $error ||= "can't change RADIUS groups";
368     #}
369   }
370   return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
371     if $error;
372
373   my $stdin_string = eval(qq("$stdin"));
374
375   $new_first = shell_quote $new_first;
376   $new_last = shell_quote $new_last;
377   $new_finger = shell_quote $new_finger;
378   $new_crypt_password = shell_quote $new_crypt_password;
379   $new_ldap_password  = shell_quote $new_ldap_password;
380
381   my $command_string = eval(qq("$command"));
382
383   my @ssh_cmd_args = (
384     user          => $self->option('user') || 'root',
385     host          => $self->machine,
386     command       => $command_string,
387     stdin_string  => $stdin_string,
388   );
389
390   if($self->option('no_queue')) {
391     # discard return value just like freeside-queued.
392     eval { ssh_cmd(@ssh_cmd_args) };
393     $error = $@;
394     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
395       if $error;
396   }
397   else {
398     $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
399   }
400 }
401
402 #a good idea to queue anything that could fail or take any time
403 sub shellcommands_queue {
404   my( $self, $svcnum ) = (shift, shift);
405   my $queue = new FS::queue {
406     'svcnum' => $svcnum,
407     'job'    => "FS::part_export::shellcommands::ssh_cmd",
408   };
409   $queue->insert( @_ );
410 }
411
412 sub ssh_cmd { #subroutine, not method
413   use Net::SSH '0.08';
414   &Net::SSH::ssh_cmd( { @_ } );
415 }
416
417 #sub shellcommands_insert { #subroutine, not method
418 #}
419 #sub shellcommands_replace { #subroutine, not method
420 #}
421 #sub shellcommands_delete { #subroutine, not method
422 #}
423
424 1;
425