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