1 package FS::part_export::shellcommands;
3 use vars qw(@ISA %info @saltset);
5 use String::ShellQuote;
8 @ISA = qw(FS::part_export);
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'
16 'useradd_stdin' => { label=>'Insert command STDIN',
20 'userdel' => { label=>'Delete command',
21 default=>'userdel -r $username',
22 #default=>'rm -rf $dir',
24 'userdel_stdin' => { label=>'Delete command STDIN',
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; '.
37 'usermod_stdin' => { label=>'Modify command STDIN',
41 'usermod_pwonly' => { label=>'Disallow username changes',
44 'suspend' => { label=>'Suspension command',
45 default=>'usermod -L $username',
47 'suspend_stdin' => { label=>'Suspension command STDIN',
50 'unsuspend' => { label=>'Unsuspension command',
51 default=>'usermod -U $username',
53 'unsuspend_stdin' => { label=>'Unsuspension command STDIN',
61 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
62 'options' => \%options,
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>.
70 <BR><BR>Use these buttons for some useful presets:
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="";
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="";
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
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.
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="";
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="";
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="";
142 The following variables are available for interpolation (prefixed with new_ or
143 old_ for replace operations):
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.
163 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
165 sub rebless { shift; }
169 $self->_export_command('useradd', @_);
174 $self->_export_command('userdel', @_);
177 sub _export_suspend {
179 $self->_export_command_or_super('suspend', @_);
182 sub _export_unsuspend {
184 $self->_export_command_or_super('unsuspend', @_);
187 sub _export_command_or_super {
188 my($self, $action) = (shift, shift);
189 if ( $self->option($action) =~ /^\s*$/ ) {
190 my $method = "SUPER::_export_$action";
193 $self->_export_command($action, @_);
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");
207 ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
210 foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
211 ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
212 foreach qw( machine username _password );
217 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
219 $email = ( grep { $_ ne 'POST' } $cust_pkg->cust_main->invoicing_list )[0];
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;
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;
237 $crypt_password = crypt(
238 $svc_acct->_password,
239 $saltset[int(rand(64))].$saltset[int(rand(64))]
243 @radius_groups = $svc_acct->radius_groups;
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")),
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');
260 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
261 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
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;
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;
278 $new_crypt_password =
279 crypt( $new->_password, $saltset[int(rand(64))].$saltset[int(rand(64))]
283 @old_radius_groups = $old->radius_groups;
284 @new_radius_groups = $new->radius_groups;
286 if ( $self->option('usermod_pwonly') ) {
288 if ( $old_username ne $new_username ) {
289 $error ||= "can't change username";
291 if ( $old_domain ne $new_domain ) {
292 $error ||= "can't change domain";
294 if ( $old_uid != $new_uid ) {
295 $error ||= "can't change uid";
297 if ( $old_dir ne $new_dir ) {
298 $error ||= "can't change dir";
300 if ( join("\n", sort @old_radius_groups) ne
301 join("\n", sort @new_radius_groups) ) {
302 $error ||= "can't change RADIUS groups";
304 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
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")),
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 {
320 'job' => "FS::part_export::shellcommands::ssh_cmd",
322 $queue->insert( @_ );
325 sub ssh_cmd { #subroutine, not method
327 &Net::SSH::ssh_cmd( { @_ } );
330 #sub shellcommands_insert { #subroutine, not method
332 #sub shellcommands_replace { #subroutine, not method
334 #sub shellcommands_delete { #subroutine, not method