This commit was generated by cvs2svn to compensate for changes in r4407,
[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
8 @ISA = qw(FS::part_export);
9
10 tie my %options, 'Tie::IxHash',
11   'user' => { label=>'Remote username', default=>'root' },
12   'useradd' => { label=>'Insert command',
13                  default=>'useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username'
14                 #default=>'cp -pr /etc/skel $dir; chown -R $uid.$gid $dir'
15                },
16   'useradd_stdin' => { label=>'Insert command STDIN',
17                        type =>'textarea',
18                        default=>'',
19                      },
20   'userdel' => { label=>'Delete command',
21                  default=>'userdel -r $username',
22                  #default=>'rm -rf $dir',
23                },
24   'userdel_stdin' => { label=>'Delete command STDIN',
25                        type =>'textarea',
26                        default=>'',
27                      },
28   'usermod' => { label=>'Modify command',
29                  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',
30                 #default=>'[ -d $old_dir ] && mv $old_dir $new_dir || ( '.
31                  #  'chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; '.
32                  #  'find . -depth -print | cpio -pdm $new_dir; '.
33                  #  'chmod u-t $new_dir; chown -R $uid.$gid $new_dir; '.
34                  #  'rm -rf $old_dir'.
35                  #')'
36                },
37   'usermod_stdin' => { label=>'Modify command STDIN',
38                        type =>'textarea',
39                        default=>'',
40                      },
41   'usermod_pwonly' => { label=>'Disallow username changes',
42                         type =>'checkbox',
43                       },
44   'suspend' => { label=>'Suspension command',
45                  default=>'usermod -L $username',
46                },
47   'suspend_stdin' => { label=>'Suspension command STDIN',
48                        default=>'',
49                      },
50   'unsuspend' => { label=>'Unsuspension command',
51                    default=>'usermod -U $username',
52                  },
53   'unsuspend_stdin' => { label=>'Unsuspension command STDIN',
54                          default=>'',
55                        },
56   'crypt' => { label   => 'Default password encryption',
57                type=>'select', options=>[qw(crypt md5)],
58                default => 'crypt',
59              },
60 ;
61
62 %info = (
63   'svc'      => 'svc_acct',
64   'desc'     =>
65     'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
66   'options'  => \%options,
67   'nodomain' => 'Y',
68   'notes' => <<'END'
69 Run remote commands via SSH.  Usernames are considered unique (also see
70 shellcommands_withdomain).  You probably want this if the commands you are
71 running will not accept a domain as a parameter.  You will need to
72 <a href="../docs/ssh.html">setup SSH for unattended operation</a>.
73
74 <BR><BR>Use these buttons for some useful presets:
75 <UL>
76   <LI>
77     <INPUT TYPE="button" VALUE="Linux" onClick='
78       this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
79       this.form.useradd_stdin.value = "";
80       this.form.userdel.value = "userdel -r $username";
81       this.form.userdel_stdin.value="";
82       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";
83       this.form.usermod_stdin.value = "";
84       this.form.suspend.value = "usermod -L $username";
85       this.form.suspend_stdin.value="";
86       this.form.unsuspend.value = "usermod -U $username";
87       this.form.unsuspend_stdin.value="";
88     '>
89   <LI>
90     <INPUT TYPE="button" VALUE="FreeBSD before 4.10 / 5.3" onClick='
91       this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -c $finger -h 0";
92       this.form.useradd_stdin.value = "$_password\n";
93       this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value="";
94       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";
95       this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username";
96       this.form.suspend_stdin.value="";
97       this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";
98     '>
99     Note: On FreeBSD versions before 5.3 and 4.10 (4.10 is after 4.9, not
100     4.1!), due to deficient locking in pw(1), you must disable the chpass(1),
101     chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with
102     wrappers that prepend "lockf /etc/passwd.lock".  Alternatively, apply the
103     patch in
104     <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
105     and use the "FreeBSD 4.10 / 5.3 or later" button below.
106   <LI>
107     <INPUT TYPE="button" VALUE="FreeBSD 4.10 / 5.3 or later" onClick='
108       this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
109       this.form.useradd_stdin.value = "$_password\n";
110       this.form.userdel.value = "pw userdel $username -r";
111       this.form.userdel_stdin.value="";
112       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";
113       this.form.usermod_stdin.value = "$new__password\n";
114       this.form.suspend.value = "pw lock $username";
115       this.form.suspend_stdin.value="";
116       this.form.unsuspend.value = "pw unlock $username";
117       this.form.unsuspend_stdin.value="";
118     '>
119   <LI>
120     <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
121       this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
122       this.form.useradd_stdin.value = "";
123       this.form.userdel.value = "userdel -r $username";
124       this.form.userdel_stdin.value="";
125       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";
126       this.form.usermod_stdin.value = "";
127       this.form.suspend.value = "";
128       this.form.suspend_stdin.value="";
129       this.form.unsuspend.value = "";
130       this.form.unsuspend_stdin.value="";
131     '>
132   <LI>
133     <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
134       this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
135       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 )";
136       this.form.usermod_stdin.value = "";
137       this.form.userdel.value = "rm -rf $dir";
138       this.form.userdel_stdin.value="";
139       this.form.suspend.value = "";
140       this.form.suspend_stdin.value="";
141       this.form.unsuspend.value = "";
142       this.form.unsuspend_stdin.value="";
143     '>
144 </UL>
145
146 The following variables are available for interpolation (prefixed with new_ or
147 old_ for replace operations):
148 <UL>
149   <LI><code>$username</code>
150   <LI><code>$_password</code>
151   <LI><code>$quoted_password</code> - unencrypted password quoted for the shell
152   <LI><code>$crypt_password</code> - encrypted password (quoted for the shell)
153   <LI><code>$uid</code>
154   <LI><code>$gid</code>
155   <LI><code>$finger</code> - GECOS, already quoted for the shell (do not add additional quotes)
156   <LI><code>$first</code> - First name of GECOS, already quoted for the shell (do not add additional quotes)
157   <LI><code>$last</code> - Last name of GECOS, already quoted for the shell (do not add additional quotes)
158   <LI><code>$dir</code> - home directory
159   <LI><code>$shell</code>
160   <LI><code>$quota</code>
161   <LI><code>@radius_groups</code>
162   <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
163 </UL>
164 END
165 );
166
167 sub rebless { shift; }
168
169 sub _export_insert {
170   my($self) = shift;
171   $self->_export_command('useradd', @_);
172 }
173
174 sub _export_delete {
175   my($self) = shift;
176   $self->_export_command('userdel', @_);
177 }
178
179 sub _export_suspend {
180   my($self) = shift;
181   $self->_export_command_or_super('suspend', @_);
182 }
183
184 sub _export_unsuspend {
185   my($self) = shift;
186   $self->_export_command_or_super('unsuspend', @_);
187 }
188
189 sub _export_command_or_super {
190   my($self, $action) = (shift, shift);
191   if ( $self->option($action) =~ /^\s*$/ ) {
192     my $method = "SUPER::_export_$action";
193     $self->$method(@_);
194   } else {
195     $self->_export_command($action, @_);
196   }
197 };
198
199
200 sub _export_command {
201   my ( $self, $action, $svc_acct) = (shift, shift, shift);
202   my $command = $self->option($action);
203   return '' if $command =~ /^\s*$/;
204   my $stdin = $self->option($action."_stdin");
205
206   no strict 'vars';
207   {
208     no strict 'refs';
209     ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
210
211     my $count = 1;
212     foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
213       ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
214         foreach qw( machine username _password );
215       $count++;
216     }
217   }
218
219   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
220   if ( $cust_pkg ) {
221     $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
222   } else {
223     $email = '';
224   }
225
226   $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
227   ($first, $last ) = ( $1, $2 );
228   $first = shell_quote $first;
229   $last = shell_quote $last;
230   $finger = shell_quote $finger;
231   $quoted_password = shell_quote $_password;
232   $domain = $svc_acct->domain;
233
234   $crypt_password =
235     shell_quote( $svc_acct->crypt_password( $self->option('crypt') ) );
236
237   @radius_groups = $svc_acct->radius_groups;
238
239   $self->shellcommands_queue( $svc_acct->svcnum,
240     user         => $self->option('user')||'root',
241     host         => $self->machine,
242     command      => eval(qq("$command")),
243     stdin_string => eval(qq("$stdin")),
244   );
245 }
246
247 sub _export_replace {
248   my($self, $new, $old ) = (shift, shift, shift);
249   my $command = $self->option('usermod');
250   my $stdin = $self->option('usermod_stdin');
251   no strict 'vars';
252   {
253     no strict 'refs';
254     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
255     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
256   }
257   $new_finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
258   ($new_first, $new_last ) = ( $1, $2 );
259   $new_first = shell_quote $new_first;
260   $new_last = shell_quote $new_last;
261   $new_finger = shell_quote $new_finger;
262   $quoted_new__password = shell_quote $new__password; #old, wrong?
263   $new_quoted_password = shell_quote $new__password; #new, better?
264   $old_domain = $old->domain;
265   $new_domain = $new->domain;
266
267   $new_crypt_password =
268     shell_quote( $new->crypt_password( $self->option('crypt') ) );
269
270   @old_radius_groups = $old->radius_groups;
271   @new_radius_groups = $new->radius_groups;
272
273   if ( $self->option('usermod_pwonly') ) {
274     my $error = '';
275     if ( $old_username ne $new_username ) {
276       $error ||= "can't change username";
277     }
278     if ( $old_domain ne $new_domain ) {
279       $error ||= "can't change domain";
280     }
281     if ( $old_uid != $new_uid ) {
282       $error ||= "can't change uid";
283     }
284     if ( $old_dir ne $new_dir ) {
285       $error ||= "can't change dir";
286     }
287     if ( join("\n", sort @old_radius_groups) ne
288          join("\n", sort @new_radius_groups)    ) {
289       $error ||= "can't change RADIUS groups";
290     }
291     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
292       if $error;
293   }
294   $self->shellcommands_queue( $new->svcnum,
295     user         => $self->option('user')||'root',
296     host         => $self->machine,
297     command      => eval(qq("$command")),
298     stdin_string => eval(qq("$stdin")),
299   );
300 }
301
302 #a good idea to queue anything that could fail or take any time
303 sub shellcommands_queue {
304   my( $self, $svcnum ) = (shift, shift);
305   my $queue = new FS::queue {
306     'svcnum' => $svcnum,
307     'job'    => "FS::part_export::shellcommands::ssh_cmd",
308   };
309   $queue->insert( @_ );
310 }
311
312 sub ssh_cmd { #subroutine, not method
313   use Net::SSH '0.08';
314   &Net::SSH::ssh_cmd( { @_ } );
315 }
316
317 #sub shellcommands_insert { #subroutine, not method
318 #}
319 #sub shellcommands_replace { #subroutine, not method
320 #}
321 #sub shellcommands_delete { #subroutine, not method
322 #}
323
324 1;
325