export enhancements for suspend reasons
[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, already quoted for the shell (do not add additional quotes)
161   <LI><code>$uid</code>
162   <LI><code>$gid</code>
163   <LI><code>$finger</code> - GECOS, already quoted for the shell (do not add additional quotes)
164   <LI><code>$first</code> - First name of GECOS, already quoted for the shell (do not add additional quotes)
165   <LI><code>$last</code> - Last name of GECOS, already quoted for the shell (do not add additional quotes)
166   <LI><code>$dir</code> - home directory
167   <LI><code>$shell</code>
168   <LI><code>$quota</code>
169   <LI><code>@radius_groups</code>
170   <LI><code>$reasonnum (when suspending)</code>
171   <LI><code>$reasontext (when suspending)</code>
172   <LI><code>$reasontypenum (when suspending)</code>
173   <LI><code>$reasontypetext (when suspending)</code>
174   <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
175 </UL>
176 END
177 );
178
179 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
180
181 sub _map {
182   my $self = shift;
183   map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
184 }
185
186 sub rebless { shift; }
187
188 sub _export_insert {
189   my($self) = shift;
190   $self->_export_command('useradd', @_);
191 }
192
193 sub _export_delete {
194   my($self) = shift;
195   $self->_export_command('userdel', @_);
196 }
197
198 sub _export_suspend {
199   my($self) = shift;
200   $self->_export_command_or_super('suspend', @_);
201 }
202
203 sub _export_unsuspend {
204   my($self) = shift;
205   $self->_export_command_or_super('unsuspend', @_);
206 }
207
208 sub _export_command_or_super {
209   my($self, $action) = (shift, shift);
210   if ( $self->option($action) =~ /^\s*$/ ) {
211     my $method = "SUPER::_export_$action";
212     $self->$method(@_);
213   } else {
214     $self->_export_command($action, @_);
215   }
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     my $count = 1;
231     foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
232       ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
233         foreach qw( machine username _password );
234       $count++;
235     }
236   }
237
238   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
239   if ( $cust_pkg ) {
240     $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
241   } else {
242     $email = '';
243   }
244
245   $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
246   ($first, $last ) = ( $1, $2 );
247   $first = shell_quote $first;
248   $last = shell_quote $last;
249   $finger = shell_quote $finger;
250   $quoted_password = shell_quote $_password;
251   $domain = $svc_acct->domain;
252
253   $crypt_password =
254     shell_quote( $svc_acct->crypt_password( $self->option('crypt') ) );
255
256   @radius_groups = $svc_acct->radius_groups;
257
258   my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
259   if ( $cust_pkg && $action eq 'suspend' && (my $r = $cust_pkg->last_reason)){
260     $reasonnum = $r->reasonnum;
261     $reasontext = $r->reason;
262     $reasontypenum = $r->reason_type;
263     $reasontypetext = $r->reasontype->type;
264
265     my %reasonmap = $self->_groups_susp_reason_map;
266     my $userspec = '';
267     $userspec = $reasonmap{$reasonnum}
268       if exists($reasonmap{$reasonnum});
269     $userspec = $reasonmap{$reasontext}
270       if (!$userspec && exists($reasonmap{$reasontext}));
271
272     my $suspend_user;
273     if ($userspec =~ /^\d+$/ ){
274       $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
275     }elsif ($userspec =~ /^\S+\@\S+$/){
276       my ($username,$domain) = split(/\@/, $userspec);
277       for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
278         $suspend_user = $user if $userspec eq $user->email;
279       }
280     }elsif ($userspec){
281       $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
282     }
283
284     @radius_groups = $suspend_user->radius_groups
285       if $suspend_user;  
286
287   }else{
288     $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
289   }
290
291   $self->shellcommands_queue( $svc_acct->svcnum,
292     user         => $self->option('user')||'root',
293     host         => $self->machine,
294     command      => eval(qq("$command")),
295     stdin_string => eval(qq("$stdin")),
296   );
297 }
298
299 sub _export_replace {
300   my($self, $new, $old ) = (shift, shift, shift);
301   my $command = $self->option('usermod');
302   my $stdin = $self->option('usermod_stdin');
303   no strict 'vars';
304   {
305     no strict 'refs';
306     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
307     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
308   }
309   $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
310   ($new_first, $new_last ) = ( $1, $2 );
311   $new_first = shell_quote $new_first;
312   $new_last = shell_quote $new_last;
313   $new_finger = shell_quote $new_finger;
314   $quoted_new__password = shell_quote $new__password; #old, wrong?
315   $new_quoted_password = shell_quote $new__password; #new, better?
316   $old_domain = $old->domain;
317   $new_domain = $new->domain;
318
319   $new_crypt_password =
320     shell_quote( $new->crypt_password( $self->option('crypt') ) );
321
322   @old_radius_groups = $old->radius_groups;
323   @new_radius_groups = $new->radius_groups;
324
325   my $error = '';
326   if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
327     if ( $old_username ne $new_username ) {
328       $error ||= "can't change username";
329     }
330   }
331   if ( $self->option('usermod_pwonly') ) {
332     if ( $old_domain ne $new_domain ) {
333       $error ||= "can't change domain";
334     }
335     if ( $old_uid != $new_uid ) {
336       $error ||= "can't change uid";
337     }
338     if ( $old_gid != $new_gid ) {
339       $error ||= "can't change gid";
340     }
341     if ( $old_dir ne $new_dir ) {
342       $error ||= "can't change dir";
343     }
344     #if ( join("\n", sort @old_radius_groups) ne
345     #     join("\n", sort @new_radius_groups)    ) {
346     #  $error ||= "can't change RADIUS groups";
347     #}
348   }
349   return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
350     if $error;
351
352   $self->shellcommands_queue( $new->svcnum,
353     user         => $self->option('user')||'root',
354     host         => $self->machine,
355     command      => eval(qq("$command")),
356     stdin_string => eval(qq("$stdin")),
357   );
358 }
359
360 #a good idea to queue anything that could fail or take any time
361 sub shellcommands_queue {
362   my( $self, $svcnum ) = (shift, shift);
363   my $queue = new FS::queue {
364     'svcnum' => $svcnum,
365     'job'    => "FS::part_export::shellcommands::ssh_cmd",
366   };
367   $queue->insert( @_ );
368 }
369
370 sub ssh_cmd { #subroutine, not method
371   use Net::SSH '0.08';
372   &Net::SSH::ssh_cmd( { @_ } );
373 }
374
375 #sub shellcommands_insert { #subroutine, not method
376 #}
377 #sub shellcommands_replace { #subroutine, not method
378 #}
379 #sub shellcommands_delete { #subroutine, not method
380 #}
381
382 1;
383