1af5bb348b3e4a5f15055bb4202da735391c34c4
[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>$first</code> - First name of GECOS, already quoted for the shell (do not add additional quotes)
152   <LI><code>$last</code> - Last name of GECOS, already quoted for the shell (do not add additional quotes)
153   <LI><code>$dir</code> - home directory
154   <LI><code>$shell</code>
155   <LI><code>$quota</code>
156   <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
157 </UL>
158 END
159 );
160
161 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
162
163 sub rebless { shift; }
164
165 sub _export_insert {
166   my($self) = shift;
167   $self->_export_command('useradd', @_);
168 }
169
170 sub _export_delete {
171   my($self) = shift;
172   $self->_export_command('userdel', @_);
173 }
174
175 sub _export_suspend {
176   my($self) = shift;
177   $self->_export_command('suspend', @_);
178 }
179
180 sub _export_unsuspend {
181   my($self) = shift;
182   $self->_export_command('unsuspend', @_);
183 }
184
185 sub _export_command {
186   my ( $self, $action, $svc_acct) = (shift, shift, shift);
187   my $command = $self->option($action);
188   return '' if $command =~ /^\s*$/;
189   my $stdin = $self->option($action."_stdin");
190
191   no strict 'vars';
192   {
193     no strict 'refs';
194     ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
195
196     my $count = 1;
197     foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
198       ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
199         foreach qw( machine username _password );
200       $count++;
201     }
202   }
203
204   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
205   if ( $cust_pkg ) {
206     $email = ( grep { $_ ne 'POST' } $cust_pkg->cust_main->invoicing_list )[0];
207   } else {
208     $email = '';
209   }
210
211   $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
212   ($first, $last ) = ( $1, $2 );
213   $first = shell_quote $first;
214   $last = shell_quote $last;
215   $finger = shell_quote $finger;
216   $quoted_password = shell_quote $_password;
217   $domain = $svc_acct->domain;
218
219   #eventually should check a "password-encoding" field
220   if ( length($svc_acct->_password) == 13
221        || $svc_acct->_password =~ /^\$(1|2a?)\$/ ) {
222     $crypt_password = shell_quote $svc_acct->_password;
223   } else {
224     $crypt_password = crypt(
225       $svc_acct->_password,
226       $saltset[int(rand(64))].$saltset[int(rand(64))]
227     );
228   }
229
230   $self->shellcommands_queue( $svc_acct->svcnum,
231     user         => $self->option('user')||'root',
232     host         => $self->machine,
233     command      => eval(qq("$command")),
234     stdin_string => eval(qq("$stdin")),
235   );
236 }
237
238 sub _export_replace {
239   my($self, $new, $old ) = (shift, shift, shift);
240   my $command = $self->option('usermod');
241   my $stdin = $self->option('usermod_stdin');
242   no strict 'vars';
243   {
244     no strict 'refs';
245     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
246     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
247   }
248   $new_finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
249   ($new_first, $new_last ) = ( $1, $2 );
250   $new_first = shell_quote $new_first;
251   $new_last = shell_quote $new_last;
252   $new_finger = shell_quote $new_finger;
253   $quoted_new__password = shell_quote $new__password; #old, wrong?
254   $new_quoted_password = shell_quote $new__password; #new, better?
255   $old_domain = $old->domain;
256   $new_domain = $new->domain;
257
258   #eventuall should check a "password-encoding" field
259   if ( length($new->_password) == 13
260        || $new->_password =~ /^\$(1|2a?)\$/ ) {
261     $new_crypt_password = shell_quote $new->_password;
262   } else {
263     $new_crypt_password =
264       crypt( $new->_password, $saltset[int(rand(64))].$saltset[int(rand(64))]
265     );
266   }
267
268   if ( $self->option('usermod_pwonly') ) {
269     my $error = '';
270     if ( $old_username ne $new_username ) {
271       $error ||= "can't change username";
272     }
273     if ( $old_domain ne $new_domain ) {
274       $error ||= "can't change domain";
275     }
276     if ( $old_uid != $new_uid ) {
277       $error ||= "can't change uid";
278     }
279     if ( $old_dir ne $new_dir ) {
280       $error ||= "can't change dir";
281     }
282     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
283       if $error;
284   }
285   $self->shellcommands_queue( $new->svcnum,
286     user         => $self->option('user')||'root',
287     host         => $self->machine,
288     command      => eval(qq("$command")),
289     stdin_string => eval(qq("$stdin")),
290   );
291 }
292
293 #a good idea to queue anything that could fail or take any time
294 sub shellcommands_queue {
295   my( $self, $svcnum ) = (shift, shift);
296   my $queue = new FS::queue {
297     'svcnum' => $svcnum,
298     'job'    => "FS::part_export::shellcommands::ssh_cmd",
299   };
300   $queue->insert( @_ );
301 }
302
303 sub ssh_cmd { #subroutine, not method
304   use Net::SSH '0.08';
305   &Net::SSH::ssh_cmd( { @_ } );
306 }
307
308 #sub shellcommands_insert { #subroutine, not method
309 #}
310 #sub shellcommands_replace { #subroutine, not method
311 #}
312 #sub shellcommands_delete { #subroutine, not method
313 #}
314
315 1;
316