freebsd fix will be in 5.3 and later only so far, still waiting to hear about 4.10
[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 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, due to deficient locking in pw(1),
96     you must disable the chpass(1), chsh(1), chfn(1), passwd(1), and vipw(1)
97     commands, or replace them with wrappers that prepend
98     "lockf /etc/passwd.lock".  Alternatively, apply the patch in
99     <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
100     and use the "FreeBSD 5.3 or later" button below.
101   <LI>
102     <INPUT TYPE="button" VALUE="FreeBSD 5.3 or later" onClick='
103       this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
104       this.form.useradd_stdin.value = "$_password\n";
105       this.form.userdel.value = "pw userdel $username -r";
106       this.form.userdel_stdin.value="";
107       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";
108       this.form.usermod_stdin.value = "$new__password\n";
109       this.form.suspend.value = "pw lock $username";
110       this.form.suspend_stdin.value="";
111       this.form.unsuspend.value = "pw unlock $username";
112       this.form.unsuspend_stdin.value="";
113     '>
114   <LI>
115     <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
116       this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
117       this.form.useradd_stdin.value = "";
118       this.form.userdel.value = "userdel -r $username";
119       this.form.userdel_stdin.value="";
120       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";
121       this.form.usermod_stdin.value = "";
122       this.form.suspend.value = "";
123       this.form.suspend_stdin.value="";
124       this.form.unsuspend.value = "";
125       this.form.unsuspend_stdin.value="";
126     '>
127   <LI>
128     <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
129       this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
130       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 )";
131       this.form.usermod_stdin.value = "";
132       this.form.userdel.value = "rm -rf $dir";
133       this.form.userdel_stdin.value="";
134       this.form.suspend.value = "";
135       this.form.suspend_stdin.value="";
136       this.form.unsuspend.value = "";
137       this.form.unsuspend_stdin.value="";
138     '>
139 </UL>
140
141 The following variables are available for interpolation (prefixed with new_ or
142 old_ for replace operations):
143 <UL>
144   <LI><code>$username</code>
145   <LI><code>$_password</code>
146   <LI><code>$quoted_password</code> - unencrypted password quoted for the shell
147   <LI><code>$crypt_password</code> - encrypted password
148   <LI><code>$uid</code>
149   <LI><code>$gid</code>
150   <LI><code>$finger</code> - GECOS, already quoted for the shell (do not add additional quotes)
151   <LI><code>$dir</code> - home directory
152   <LI><code>$shell</code>
153   <LI><code>$quota</code>
154   <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
155 </UL>
156 END
157 );
158
159 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
160
161 sub rebless { shift; }
162
163 sub _export_insert {
164   my($self) = shift;
165   $self->_export_command('useradd', @_);
166 }
167
168 sub _export_delete {
169   my($self) = shift;
170   $self->_export_command('userdel', @_);
171 }
172
173 sub _export_suspend {
174   my($self) = shift;
175   $self->_export_command('suspend', @_);
176 }
177
178 sub _export_unsuspend {
179   my($self) = shift;
180   $self->_export_command('unsuspend', @_);
181 }
182
183 sub _export_command {
184   my ( $self, $action, $svc_acct) = (shift, shift, shift);
185   my $command = $self->option($action);
186   return '' if $command =~ /^\s*$/;
187   my $stdin = $self->option($action."_stdin");
188
189   no strict 'vars';
190   {
191     no strict 'refs';
192     ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
193
194     my $count = 1;
195     foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
196       ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
197         foreach qw( machine username _password );
198       $count++;
199     }
200   }
201
202   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
203   if ( $cust_pkg ) {
204     $email = ( grep { $_ ne 'POST' } $cust_pkg->cust_main->invoicing_list )[0];
205   } else {
206     $email = '';
207   }
208
209   $finger = shell_quote $finger;
210   $quoted_password = shell_quote $_password;
211   $domain = $svc_acct->domain;
212
213   #eventually should check a "password-encoding" field
214   if ( length($svc_acct->_password) == 13
215        || $svc_acct->_password =~ /^\$(1|2a?)\$/ ) {
216     $crypt_password = shell_quote $svc_acct->_password;
217   } else {
218     $crypt_password = crypt(
219       $svc_acct->_password,
220       $saltset[int(rand(64))].$saltset[int(rand(64))]
221     );
222   }
223
224   $self->shellcommands_queue( $svc_acct->svcnum,
225     user         => $self->option('user')||'root',
226     host         => $self->machine,
227     command      => eval(qq("$command")),
228     stdin_string => eval(qq("$stdin")),
229   );
230 }
231
232 sub _export_replace {
233   my($self, $new, $old ) = (shift, shift, shift);
234   my $command = $self->option('usermod');
235   my $stdin = $self->option('usermod_stdin');
236   no strict 'vars';
237   {
238     no strict 'refs';
239     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
240     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
241   }
242   $new_finger = shell_quote $new_finger;
243   $quoted_new__password = shell_quote $new__password; #old, wrong?
244   $new_quoted_password = shell_quote $new__password; #new, better?
245   $old_domain = $old->domain;
246   $new_domain = $new->domain;
247
248   #eventuall should check a "password-encoding" field
249   if ( length($new->_password) == 13
250        || $new->_password =~ /^\$(1|2a?)\$/ ) {
251     $new_crypt_password = shell_quote $new->_password;
252   } else {
253     $new_crypt_password =
254       crypt( $new->_password, $saltset[int(rand(64))].$saltset[int(rand(64))]
255     );
256   }
257
258   if ( $self->option('usermod_pwonly') ) {
259     my $error = '';
260     if ( $old_username ne $new_username ) {
261       $error ||= "can't change username";
262     }
263     if ( $old_domain ne $new_domain ) {
264       $error ||= "can't change domain";
265     }
266     if ( $old_uid != $new_uid ) {
267       $error ||= "can't change uid";
268     }
269     if ( $old_dir ne $new_dir ) {
270       $error ||= "can't change dir";
271     }
272     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
273       if $error;
274   }
275   $self->shellcommands_queue( $new->svcnum,
276     user         => $self->option('user')||'root',
277     host         => $self->machine,
278     command      => eval(qq("$command")),
279     stdin_string => eval(qq("$stdin")),
280   );
281 }
282
283 #a good idea to queue anything that could fail or take any time
284 sub shellcommands_queue {
285   my( $self, $svcnum ) = (shift, shift);
286   my $queue = new FS::queue {
287     'svcnum' => $svcnum,
288     'job'    => "FS::part_export::shellcommands::ssh_cmd",
289   };
290   $queue->insert( @_ );
291 }
292
293 sub ssh_cmd { #subroutine, not method
294   use Net::SSH '0.08';
295   &Net::SSH::ssh_cmd( { @_ } );
296 }
297
298 #sub shellcommands_insert { #subroutine, not method
299 #}
300 #sub shellcommands_replace { #subroutine, not method
301 #}
302 #sub shellcommands_delete { #subroutine, not method
303 #}
304
305 1;
306