4431cc0c4da5c17cd5942e301f98b67d8383143e
[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('suspend', @_);
180 }
181
182 sub _export_unsuspend {
183   my($self) = shift;
184   $self->_export_command('unsuspend', @_);
185 }
186
187 sub _export_command {
188   my ( $self, $action, $svc_acct) = (shift, shift, shift);
189   my $command = $self->option($action);
190   return '' if $command =~ /^\s*$/;
191   my $stdin = $self->option($action."_stdin");
192
193   no strict 'vars';
194   {
195     no strict 'refs';
196     ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
197
198     my $count = 1;
199     foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
200       ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
201         foreach qw( machine username _password );
202       $count++;
203     }
204   }
205
206   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
207   if ( $cust_pkg ) {
208     $email = ( grep { $_ ne 'POST' } $cust_pkg->cust_main->invoicing_list )[0];
209   } else {
210     $email = '';
211   }
212
213   $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
214   ($first, $last ) = ( $1, $2 );
215   $first = shell_quote $first;
216   $last = shell_quote $last;
217   $finger = shell_quote $finger;
218   $quoted_password = shell_quote $_password;
219   $domain = $svc_acct->domain;
220
221   #eventually should check a "password-encoding" field
222   if ( length($svc_acct->_password) == 13
223        || $svc_acct->_password =~ /^\$(1|2a?)\$/ ) {
224     $crypt_password = shell_quote $svc_acct->_password;
225   } else {
226     $crypt_password = crypt(
227       $svc_acct->_password,
228       $saltset[int(rand(64))].$saltset[int(rand(64))]
229     );
230   }
231
232   @radius_groups = $svc_acct->radius_groups;
233
234   $self->shellcommands_queue( $svc_acct->svcnum,
235     user         => $self->option('user')||'root',
236     host         => $self->machine,
237     command      => eval(qq("$command")),
238     stdin_string => eval(qq("$stdin")),
239   );
240 }
241
242 sub _export_replace {
243   my($self, $new, $old ) = (shift, shift, shift);
244   my $command = $self->option('usermod');
245   my $stdin = $self->option('usermod_stdin');
246   no strict 'vars';
247   {
248     no strict 'refs';
249     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
250     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
251   }
252   $new_finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
253   ($new_first, $new_last ) = ( $1, $2 );
254   $new_first = shell_quote $new_first;
255   $new_last = shell_quote $new_last;
256   $new_finger = shell_quote $new_finger;
257   $quoted_new__password = shell_quote $new__password; #old, wrong?
258   $new_quoted_password = shell_quote $new__password; #new, better?
259   $old_domain = $old->domain;
260   $new_domain = $new->domain;
261
262   #eventuall should check a "password-encoding" field
263   if ( length($new->_password) == 13
264        || $new->_password =~ /^\$(1|2a?)\$/ ) {
265     $new_crypt_password = shell_quote $new->_password;
266   } else {
267     $new_crypt_password =
268       crypt( $new->_password, $saltset[int(rand(64))].$saltset[int(rand(64))]
269     );
270   }
271
272   @old_radius_groups = $old->radius_groups;
273   @new_radius_groups = $new->radius_groups;
274
275   if ( $self->option('usermod_pwonly') ) {
276     my $error = '';
277     if ( $old_username ne $new_username ) {
278       $error ||= "can't change username";
279     }
280     if ( $old_domain ne $new_domain ) {
281       $error ||= "can't change domain";
282     }
283     if ( $old_uid != $new_uid ) {
284       $error ||= "can't change uid";
285     }
286     if ( $old_dir ne $new_dir ) {
287       $error ||= "can't change dir";
288     }
289     if ( join("\n", sort @old_radius_groups) ne
290          join("\n", sort @new_radius_groups)    ) {
291       $error ||= "can't change RADIUS groups";
292     }
293     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
294       if $error;
295   }
296   $self->shellcommands_queue( $new->svcnum,
297     user         => $self->option('user')||'root',
298     host         => $self->machine,
299     command      => eval(qq("$command")),
300     stdin_string => eval(qq("$stdin")),
301   );
302 }
303
304 #a good idea to queue anything that could fail or take any time
305 sub shellcommands_queue {
306   my( $self, $svcnum ) = (shift, shift);
307   my $queue = new FS::queue {
308     'svcnum' => $svcnum,
309     'job'    => "FS::part_export::shellcommands::ssh_cmd",
310   };
311   $queue->insert( @_ );
312 }
313
314 sub ssh_cmd { #subroutine, not method
315   use Net::SSH '0.08';
316   &Net::SSH::ssh_cmd( { @_ } );
317 }
318
319 #sub shellcommands_insert { #subroutine, not method
320 #}
321 #sub shellcommands_replace { #subroutine, not method
322 #}
323 #sub shellcommands_delete { #subroutine, not method
324 #}
325
326 1;
327