1 package FS::part_export::shellcommands;
3 use vars qw(@ISA %info);
5 use String::ShellQuote;
7 use FS::Record qw( qsearch qsearchs );
9 @ISA = qw(FS::part_export);
11 tie my %options, 'Tie::IxHash',
12 'user' => { label=>'Remote username', default=>'root' },
13 'useradd' => { label=>'Insert command',
14 default=>'useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username'
15 #default=>'cp -pr /etc/skel $dir; chown -R $uid.$gid $dir'
17 'useradd_no_queue' => { label=>'Run immediately',
20 'useradd_stdin' => { label=>'Insert command STDIN',
24 'userdel' => { label=>'Delete command',
25 default=>'userdel -r $username',
26 #default=>'rm -rf $dir',
28 'userdel_no_queue' => { label=>'Run immediately',
31 'userdel_stdin' => { label=>'Delete command STDIN',
35 'usermod' => { label=>'Modify command',
36 default=>'usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -p $new_crypt_password $old_username',
37 #default=>'[ -d $old_dir ] && mv $old_dir $new_dir || ( '.
38 # 'chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; '.
39 # 'find . -depth -print | cpio -pdm $new_dir; '.
40 # 'chmod u-t $new_dir; chown -R $uid.$gid $new_dir; '.
44 'usermod_no_queue' => { label=>'Run immediately',
47 'usermod_stdin' => { label=>'Modify command STDIN',
51 'usermod_pwonly' => { label=>'Disallow username, domain, uid, gid, and dir changes', #and RADIUS group changes',
54 'usermod_nousername' => { label=>'Disallow just username changes',
57 'suspend' => { label=>'Suspension command',
58 default=>'usermod -L $username',
60 'suspend_no_queue' => { label=>'Run immediately',
63 'suspend_stdin' => { label=>'Suspension command STDIN',
66 'unsuspend' => { label=>'Unsuspension command',
67 default=>'usermod -U $username',
69 'unsuspend_no_queue' => { label=>'Run immediately',
72 'unsuspend_stdin' => { label=>'Unsuspension command STDIN',
75 'crypt' => { label => 'Default password encryption',
76 type=>'select', options=>[qw(crypt md5)],
79 'groups_susp_reason' => { label =>
80 'Radius group mapping to reason (via template user)',
83 'ignore_all_output' => {
84 label => 'Ignore all output and errors from the command',
87 'ignored_errors' => { label => 'Regexes of specific errors to ignore, separated by newlines',
90 # 'no_queue' => { label => 'Run command immediately',
98 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
99 'options' => \%options,
102 Run remote commands via SSH. Usernames are considered unique (also see
103 shellcommands_withdomain). You probably want this if the commands you are
104 running will not accept a domain as a parameter. You will need to
105 <a href="http://www.freeside.biz/mediawiki/index.php/Freeside:1.9:Documentation:Administration:SSH_Keys">setup SSH for unattended operation</a>.
107 <BR><BR>Use these buttons for some useful presets:
110 <INPUT TYPE="button" VALUE="Linux" onClick='
111 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
112 this.form.useradd_stdin.value = "";
113 this.form.userdel.value = "userdel -r $username";
114 this.form.userdel_stdin.value="";
115 this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -p $new_crypt_password $old_username";
116 this.form.usermod_stdin.value = "";
117 this.form.suspend.value = "usermod -L $username";
118 this.form.suspend_stdin.value="";
119 this.form.unsuspend.value = "usermod -U $username";
120 this.form.unsuspend_stdin.value="";
123 <INPUT TYPE="button" VALUE="FreeBSD before 4.10 / 5.3" onClick='
124 this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -c $finger -h 0";
125 this.form.useradd_stdin.value = "$_password\n";
126 this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value="";
127 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 -g $new_gid -c $new_finger -h 0";
128 this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username";
129 this.form.suspend_stdin.value="";
130 this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";
132 Note: On FreeBSD versions before 5.3 and 4.10 (4.10 is after 4.9, not
133 4.1!), due to deficient locking in pw(1), you must disable the chpass(1),
134 chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with
135 wrappers that prepend "lockf /etc/passwd.lock". Alternatively, apply the
137 <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
138 and use the "FreeBSD 4.10 / 5.3 or later" button below.
140 <INPUT TYPE="button" VALUE="FreeBSD 4.10 / 5.3 or later" onClick='
141 this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
142 this.form.useradd_stdin.value = "$_password\n";
143 this.form.userdel.value = "pw userdel $username -r";
144 this.form.userdel_stdin.value="";
145 this.form.usermod.value = "pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -c $new_finger -h 0";
146 this.form.usermod_stdin.value = "$new__password\n";
147 this.form.suspend.value = "pw lock $username";
148 this.form.suspend_stdin.value="";
149 this.form.unsuspend.value = "pw unlock $username";
150 this.form.unsuspend_stdin.value="";
153 <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
154 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
155 this.form.useradd_stdin.value = "";
156 this.form.userdel.value = "userdel -r $username";
157 this.form.userdel_stdin.value="";
158 this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -g $new_gid -p $new_crypt_password $old_username";
159 this.form.usermod_stdin.value = "";
160 this.form.suspend.value = "";
161 this.form.suspend_stdin.value="";
162 this.form.unsuspend.value = "";
163 this.form.unsuspend_stdin.value="";
166 <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
167 this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
168 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 )";
169 this.form.usermod_stdin.value = "";
170 this.form.userdel.value = "rm -rf $dir";
171 this.form.userdel_stdin.value="";
172 this.form.suspend.value = "";
173 this.form.suspend_stdin.value="";
174 this.form.unsuspend.value = "";
175 this.form.unsuspend_stdin.value="";
179 The following variables are available for interpolation (prefixed with new_ or
180 old_ for replace operations):
182 <LI><code>$username</code>
183 <LI><code>$_password</code>
184 <LI><code>$quoted_password</code> - unencrypted password, already quoted for the shell (do not add additional quotes).
185 <LI><code>$crypt_password</code> - encrypted password. When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
186 <LI><code>$ldap_password</code> - Password in LDAP/RFC2307 format (for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or "{MD5}5426824942db4253f87a1009fd5d2d4"). When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
187 <LI><code>$uid</code>
188 <LI><code>$gid</code>
189 <LI><code>$finger</code> - GECOS. When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
190 <LI><code>$first</code> - First name of GECOS. When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
191 <LI><code>$last</code> - Last name of GECOS. When used on the command line (rather than STDIN), it will be quoted for the shell already (do not add additional quotes).
192 <LI><code>$dir</code> - home directory
193 <LI><code>$shell</code>
194 <LI><code>$quota</code>
195 <LI><code>@radius_groups</code>
196 <LI><code>$reasonnum (when suspending)</code>
197 <LI><code>$reasontext (when suspending)</code>
198 <LI><code>$reasontypenum (when suspending)</code>
199 <LI><code>$reasontypetext (when suspending)</code>
200 <LI><code>$pkgnum</code>
201 <LI><code>$custnum</code>
202 <LI>All other fields in <b>svc_acct</b> are also available.
203 <LI>The following fields from <b>cust_main</b> are also available (except during replace): company, address1, address2, city, state, zip, county, daytime, night, fax, otaker, agent_custid, locale. When used on the command line (rather than STDIN), they will be quoted for the shell already (do not add additional quotes).
208 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
212 map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
215 sub rebless { shift; }
219 $self->_export_command('useradd', @_);
224 $self->_export_command('userdel', @_);
227 sub _export_suspend {
229 $self->_export_command_or_super('suspend', @_);
232 sub _export_unsuspend {
234 $self->_export_command_or_super('unsuspend', @_);
237 sub _export_command_or_super {
238 my($self, $action) = (shift, shift);
239 if ( $self->option($action) =~ /^\s*$/ ) {
240 my $method = "SUPER::_export_$action";
243 $self->_export_command($action, @_);
247 sub _export_command {
248 my ( $self, $action, $svc_acct) = (shift, shift, shift);
249 my $command = $self->option($action);
250 return '' if $command =~ /^\s*$/;
251 my $stdin = $self->option($action."_stdin");
256 ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
258 # snarfs are unused at this point?
260 foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
261 ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
262 foreach qw( machine username _password );
267 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
272 foreach my $custf (qw( company address1 address2 city state zip country
273 daytime night fax otaker agent_custid locale
276 ${$custf} = $cust_pkg->cust_main->$custf();
279 $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
284 $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
285 ($first, $last ) = ( $1, $2 );
286 $domain = $svc_acct->domain;
288 $quoted_password = shell_quote $_password;
290 $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
291 $ldap_password = $svc_acct->ldap_password( $self->option('crypt') );
293 @radius_groups = $svc_acct->radius_groups;
295 my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
296 if ( $cust_pkg && $action eq 'suspend' &&
297 (my $r = $cust_pkg->last_reason('susp')) )
299 $reasonnum = $r->reasonnum;
300 $reasontext = $r->reason;
301 $reasontypenum = $r->reason_type;
302 $reasontypetext = $r->reasontype->type;
304 my %reasonmap = $self->_groups_susp_reason_map;
306 $userspec = $reasonmap{$reasonnum}
307 if exists($reasonmap{$reasonnum});
308 $userspec = $reasonmap{$reasontext}
309 if (!$userspec && exists($reasonmap{$reasontext}));
312 if ( $userspec =~ /^\d+$/ ) {
313 $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
314 } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
315 my ($username,$domain) = split(/\@/, $userspec);
316 for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
317 $suspend_user = $user if $userspec eq $user->email;
319 } elsif ($userspec) {
320 $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
323 @radius_groups = $suspend_user->radius_groups
327 $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
330 $pkgnum = $cust_pkg ? $cust_pkg->pkgnum : '';
331 $custnum = $cust_pkg ? $cust_pkg->custnum : '';
333 my $stdin_string = eval(qq("$stdin"));
335 $first = shell_quote $first;
336 $last = shell_quote $last;
337 $finger = shell_quote $finger;
338 $crypt_password = shell_quote $crypt_password;
339 $ldap_password = shell_quote $ldap_password;
341 $company = shell_quote $company;
342 $address1 = shell_quote $address1;
343 $address2 = shell_quote $address2;
344 $city = shell_quote $city;
345 $state = shell_quote $state;
346 $zip = shell_quote $zip;
347 $country = shell_quote $country;
348 $daytime = shell_quote $daytime;
349 $night = shell_quote $night;
350 $fax = shell_quote $fax;
351 $otaker = shell_quote $otaker;
352 $agent_custid = shell_quote $agent_custid;
353 $locale = shell_quote $locale;
355 my $command_string = eval(qq("$command"));
358 user => $self->option('user') || 'root',
359 host => $self->machine,
360 command => $command_string,
361 stdin_string => $stdin_string,
362 ignore_all_output => $self->option('ignore_all_output'),
363 ignored_errors => $self->option('ignored_errors') || '',
366 if($self->option($action . '_no_queue')) {
367 # discard return value just like freeside-queued.
368 eval { ssh_cmd(@ssh_cmd_args) };
370 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
374 $self->shellcommands_queue( $svc_acct->svcnum, @ssh_cmd_args );
378 sub _export_replace {
379 my($self, $new, $old ) = (shift, shift, shift);
380 my $command = $self->option('usermod');
381 return '' if $command =~ /^\s*$/;
382 my $stdin = $self->option('usermod_stdin');
386 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
387 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
389 my $old_cust_pkg = $old->cust_svc->cust_pkg;
390 my $new_cust_pkg = $new->cust_svc->cust_pkg;
391 my $new_cust_main = $new_cust_pkg ? $new_cust_pkg->cust_main : '';
393 $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
394 ($new_first, $new_last ) = ( $1, $2 );
395 $quoted_new__password = shell_quote $new__password; #old, wrong?
396 $new_quoted_password = shell_quote $new__password; #new, better?
397 $old_domain = $old->domain;
398 $new_domain = $new->domain;
400 $new_crypt_password = $new->crypt_password( $self->option('crypt') );
401 $new_ldap_password = $new->ldap_password( $self->option('crypt') );
403 @old_radius_groups = $old->radius_groups;
404 @new_radius_groups = $new->radius_groups;
407 if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
408 if ( $old_username ne $new_username ) {
409 $error ||= "can't change username";
412 if ( $self->option('usermod_pwonly') ) {
413 if ( $old_domain ne $new_domain ) {
414 $error ||= "can't change domain";
416 if ( $old_uid != $new_uid ) {
417 $error ||= "can't change uid";
419 if ( $old_gid != $new_gid ) {
420 $error ||= "can't change gid";
422 if ( $old_dir ne $new_dir ) {
423 $error ||= "can't change dir";
425 #if ( join("\n", sort @old_radius_groups) ne
426 # join("\n", sort @new_radius_groups) ) {
427 # $error ||= "can't change RADIUS groups";
430 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
433 $new_agent_custid = $new_cust_main ? $new_cust_main->agent_custid : '';
434 $new_locale = $new_cust_main ? $new_cust_main->locale : '';
435 $old_pkgnum = $old_cust_pkg ? $old_cust_pkg->pkgnum : '';
436 $old_custnum = $old_cust_pkg ? $old_cust_pkg->custnum : '';
437 $new_pkgnum = $new_cust_pkg ? $new_cust_pkg->pkgnum : '';
438 $new_custnum = $new_cust_pkg ? $new_cust_pkg->custnum : '';
440 my $stdin_string = eval(qq("$stdin"));
442 $new_first = shell_quote $new_first;
443 $new_last = shell_quote $new_last;
444 $new_finger = shell_quote $new_finger;
445 $new_crypt_password = shell_quote $new_crypt_password;
446 $new_ldap_password = shell_quote $new_ldap_password;
447 $new_agent_custid = shell_quote $new_agent_custid;
448 $new_locale = shell_quote $new_locale;
450 my $command_string = eval(qq("$command"));
453 user => $self->option('user') || 'root',
454 host => $self->machine,
455 command => $command_string,
456 stdin_string => $stdin_string,
457 ignore_all_output => $self->option('ignore_all_output'),
458 ignored_errors => $self->option('ignored_errors') || '',
461 if($self->option('usermod_no_queue')) {
462 # discard return value just like freeside-queued.
463 eval { ssh_cmd(@ssh_cmd_args) };
465 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
469 $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
473 #a good idea to queue anything that could fail or take any time
474 sub shellcommands_queue {
475 my( $self, $svcnum ) = (shift, shift);
476 my $queue = new FS::queue {
478 'job' => "FS::part_export::shellcommands::ssh_cmd",
480 $queue->insert( @_ );
483 sub ssh_cmd { #subroutine, not method
486 open my $def_in, '<', '/dev/null' or die "unable to open /dev/null\n";
487 my $ssh = Net::OpenSSH->new(
488 $opt->{'user'}.'@'.$opt->{'host'},
489 'default_stdin_fh' => $def_in
491 # ignore_all_output doesn't override this
492 die "Couldn't establish SSH connection: ". $ssh->error if $ssh->error;
495 $ssh_opt->{'stdin_data'} = $opt->{'stdin_string'}
496 if exists($opt->{'stdin_string'}) and length($opt->{'stdin_string'});
497 my ($output, $errput) = $ssh->capture2($ssh_opt, $opt->{'command'});
498 return if $opt->{'ignore_all_output'};
499 die "Error running SSH command: ". $ssh->error if $ssh->error;
501 if ( ($output || $errput)
502 && $opt->{'ignored_errors'} && length($opt->{'ignored_errors'})
504 my @ignored_errors = split('\n',$opt->{'ignored_errors'});
505 foreach my $ignored_error ( @ignored_errors ) {
506 $output =~ s/$ignored_error//g;
507 $errput =~ s/$ignored_error//g;
513 die $errput if $errput;
514 die $output if $output;
518 #sub shellcommands_insert { #subroutine, not method
520 #sub shellcommands_replace { #subroutine, not method
522 #sub shellcommands_delete { #subroutine, not method