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 'ignored_errors' => { label => 'Regexes of errors to ignore, separated by newlines',
86 # 'no_queue' => { label => 'Run command immediately',
94 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
95 'options' => \%options,
98 Run remote commands via SSH. Usernames are considered unique (also see
99 shellcommands_withdomain). You probably want this if the commands you are
100 running will not accept a domain as a parameter. You will need to
101 <a href="http://www.freeside.biz/mediawiki/index.php/Freeside:1.9:Documentation:Administration:SSH_Keys">setup SSH for unattended operation</a>.
103 <BR><BR>Use these buttons for some useful presets:
106 <INPUT TYPE="button" VALUE="Linux" onClick='
107 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
108 this.form.useradd_stdin.value = "";
109 this.form.userdel.value = "userdel -r $username";
110 this.form.userdel_stdin.value="";
111 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";
112 this.form.usermod_stdin.value = "";
113 this.form.suspend.value = "usermod -L $username";
114 this.form.suspend_stdin.value="";
115 this.form.unsuspend.value = "usermod -U $username";
116 this.form.unsuspend_stdin.value="";
119 <INPUT TYPE="button" VALUE="FreeBSD before 4.10 / 5.3" onClick='
120 this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -c $finger -h 0";
121 this.form.useradd_stdin.value = "$_password\n";
122 this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value="";
123 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";
124 this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username";
125 this.form.suspend_stdin.value="";
126 this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";
128 Note: On FreeBSD versions before 5.3 and 4.10 (4.10 is after 4.9, not
129 4.1!), due to deficient locking in pw(1), you must disable the chpass(1),
130 chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with
131 wrappers that prepend "lockf /etc/passwd.lock". Alternatively, apply the
133 <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
134 and use the "FreeBSD 4.10 / 5.3 or later" button below.
136 <INPUT TYPE="button" VALUE="FreeBSD 4.10 / 5.3 or later" onClick='
137 this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
138 this.form.useradd_stdin.value = "$_password\n";
139 this.form.userdel.value = "pw userdel $username -r";
140 this.form.userdel_stdin.value="";
141 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";
142 this.form.usermod_stdin.value = "$new__password\n";
143 this.form.suspend.value = "pw lock $username";
144 this.form.suspend_stdin.value="";
145 this.form.unsuspend.value = "pw unlock $username";
146 this.form.unsuspend_stdin.value="";
149 <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
150 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
151 this.form.useradd_stdin.value = "";
152 this.form.userdel.value = "userdel -r $username";
153 this.form.userdel_stdin.value="";
154 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";
155 this.form.usermod_stdin.value = "";
156 this.form.suspend.value = "";
157 this.form.suspend_stdin.value="";
158 this.form.unsuspend.value = "";
159 this.form.unsuspend_stdin.value="";
162 <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
163 this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
164 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 )";
165 this.form.usermod_stdin.value = "";
166 this.form.userdel.value = "rm -rf $dir";
167 this.form.userdel_stdin.value="";
168 this.form.suspend.value = "";
169 this.form.suspend_stdin.value="";
170 this.form.unsuspend.value = "";
171 this.form.unsuspend_stdin.value="";
175 The following variables are available for interpolation (prefixed with new_ or
176 old_ for replace operations):
178 <LI><code>$username</code>
179 <LI><code>$_password</code>
180 <LI><code>$quoted_password</code> - unencrypted password, already quoted for the shell (do not add additional quotes).
181 <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).
182 <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).
183 <LI><code>$uid</code>
184 <LI><code>$gid</code>
185 <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).
186 <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).
187 <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).
188 <LI><code>$dir</code> - home directory
189 <LI><code>$shell</code>
190 <LI><code>$quota</code>
191 <LI><code>@radius_groups</code>
192 <LI><code>$reasonnum (when suspending)</code>
193 <LI><code>$reasontext (when suspending)</code>
194 <LI><code>$reasontypenum (when suspending)</code>
195 <LI><code>$reasontypetext (when suspending)</code>
196 <LI><code>$pkgnum</code>
197 <LI><code>$custnum</code>
198 <LI>All other fields in <b>svc_acct</b> are also available.
199 <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).
204 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
208 map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
211 sub rebless { shift; }
215 $self->_export_command('useradd', @_);
220 $self->_export_command('userdel', @_);
223 sub _export_suspend {
225 $self->_export_command_or_super('suspend', @_);
228 sub _export_unsuspend {
230 $self->_export_command_or_super('unsuspend', @_);
233 sub _export_command_or_super {
234 my($self, $action) = (shift, shift);
235 if ( $self->option($action) =~ /^\s*$/ ) {
236 my $method = "SUPER::_export_$action";
239 $self->_export_command($action, @_);
243 sub _export_command {
244 my ( $self, $action, $svc_acct) = (shift, shift, shift);
245 my $command = $self->option($action);
246 return '' if $command =~ /^\s*$/;
247 my $stdin = $self->option($action."_stdin");
252 ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
254 # snarfs are unused at this point?
256 foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
257 ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
258 foreach qw( machine username _password );
263 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
268 foreach my $custf (qw( company address1 address2 city state zip country
269 daytime night fax otaker agent_custid locale
272 ${$custf} = $cust_pkg->cust_main->$custf();
275 $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
280 $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
281 ($first, $last ) = ( $1, $2 );
282 $domain = $svc_acct->domain;
284 $quoted_password = shell_quote $_password;
286 $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
287 $ldap_password = $svc_acct->ldap_password( $self->option('crypt') );
289 @radius_groups = $svc_acct->radius_groups;
291 my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
292 if ( $cust_pkg && $action eq 'suspend' &&
293 (my $r = $cust_pkg->last_reason('susp')) )
295 $reasonnum = $r->reasonnum;
296 $reasontext = $r->reason;
297 $reasontypenum = $r->reason_type;
298 $reasontypetext = $r->reasontype->type;
300 my %reasonmap = $self->_groups_susp_reason_map;
302 $userspec = $reasonmap{$reasonnum}
303 if exists($reasonmap{$reasonnum});
304 $userspec = $reasonmap{$reasontext}
305 if (!$userspec && exists($reasonmap{$reasontext}));
308 if ( $userspec =~ /^\d+$/ ) {
309 $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
310 } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
311 my ($username,$domain) = split(/\@/, $userspec);
312 for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
313 $suspend_user = $user if $userspec eq $user->email;
315 } elsif ($userspec) {
316 $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
319 @radius_groups = $suspend_user->radius_groups
323 $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
326 $pkgnum = $cust_pkg ? $cust_pkg->pkgnum : '';
327 $custnum = $cust_pkg ? $cust_pkg->custnum : '';
329 my $stdin_string = eval(qq("$stdin"));
331 $first = shell_quote $first;
332 $last = shell_quote $last;
333 $finger = shell_quote $finger;
334 $crypt_password = shell_quote $crypt_password;
335 $ldap_password = shell_quote $ldap_password;
337 $company = shell_quote $company;
338 $address1 = shell_quote $address1;
339 $address2 = shell_quote $address2;
340 $city = shell_quote $city;
341 $state = shell_quote $state;
342 $zip = shell_quote $zip;
343 $country = shell_quote $country;
344 $daytime = shell_quote $daytime;
345 $night = shell_quote $night;
346 $fax = shell_quote $fax;
347 $otaker = shell_quote $otaker;
348 $agent_custid = shell_quote $agent_custid;
349 $locale = shell_quote $locale;
351 my $command_string = eval(qq("$command"));
353 user => $self->option('user') || 'root',
354 host => $self->machine,
355 command => $command_string,
356 stdin_string => $stdin_string,
357 ignored_errors => $self->option('ignored_errors') || '',
360 if($self->option($action . '_no_queue')) {
361 # discard return value just like freeside-queued.
362 eval { ssh_cmd(@ssh_cmd_args) };
364 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
368 $self->shellcommands_queue( $svc_acct->svcnum, @ssh_cmd_args );
372 sub _export_replace {
373 my($self, $new, $old ) = (shift, shift, shift);
374 my $command = $self->option('usermod');
375 my $stdin = $self->option('usermod_stdin');
379 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
380 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
382 my $old_cust_pkg = $old->cust_svc->cust_pkg;
383 my $new_cust_pkg = $new->cust_svc->cust_pkg;
384 my $new_cust_main = $new_cust_pkg ? $new_cust_pkg->cust_main : '';
386 $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
387 ($new_first, $new_last ) = ( $1, $2 );
388 $quoted_new__password = shell_quote $new__password; #old, wrong?
389 $new_quoted_password = shell_quote $new__password; #new, better?
390 $old_domain = $old->domain;
391 $new_domain = $new->domain;
393 $new_crypt_password = $new->crypt_password( $self->option('crypt') );
394 $new_ldap_password = $new->ldap_password( $self->option('crypt') );
396 @old_radius_groups = $old->radius_groups;
397 @new_radius_groups = $new->radius_groups;
400 if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
401 if ( $old_username ne $new_username ) {
402 $error ||= "can't change username";
405 if ( $self->option('usermod_pwonly') ) {
406 if ( $old_domain ne $new_domain ) {
407 $error ||= "can't change domain";
409 if ( $old_uid != $new_uid ) {
410 $error ||= "can't change uid";
412 if ( $old_gid != $new_gid ) {
413 $error ||= "can't change gid";
415 if ( $old_dir ne $new_dir ) {
416 $error ||= "can't change dir";
418 #if ( join("\n", sort @old_radius_groups) ne
419 # join("\n", sort @new_radius_groups) ) {
420 # $error ||= "can't change RADIUS groups";
423 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
426 $new_agent_custid = $new_cust_main ? $new_cust_main->agent_custid : '';
427 $new_locale = $new_cust_main ? $new_cust_main->locale : '';
428 $old_pkgnum = $old_cust_pkg ? $old_cust_pkg->pkgnum : '';
429 $old_custnum = $old_cust_pkg ? $old_cust_pkg->custnum : '';
430 $new_pkgnum = $new_cust_pkg ? $new_cust_pkg->pkgnum : '';
431 $new_custnum = $new_cust_pkg ? $new_cust_pkg->custnum : '';
433 my $stdin_string = eval(qq("$stdin"));
435 $new_first = shell_quote $new_first;
436 $new_last = shell_quote $new_last;
437 $new_finger = shell_quote $new_finger;
438 $new_crypt_password = shell_quote $new_crypt_password;
439 $new_ldap_password = shell_quote $new_ldap_password;
440 $new_agent_custid = shell_quote $new_agent_custid;
441 $new_locale = shell_quote $new_locale;
443 my $command_string = eval(qq("$command"));
446 user => $self->option('user') || 'root',
447 host => $self->machine,
448 command => $command_string,
449 stdin_string => $stdin_string,
450 ignored_errors => $self->option('ignored_errors') || '',
453 if($self->option('usermod_no_queue')) {
454 # discard return value just like freeside-queued.
455 eval { ssh_cmd(@ssh_cmd_args) };
457 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
461 $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
465 #a good idea to queue anything that could fail or take any time
466 sub shellcommands_queue {
467 my( $self, $svcnum ) = (shift, shift);
468 my $queue = new FS::queue {
470 'job' => "FS::part_export::shellcommands::ssh_cmd",
472 $queue->insert( @_ );
475 sub ssh_cmd { #subroutine, not method
478 my $ssh = Net::OpenSSH->new($opt->{'user'}.'@'.$opt->{'host'});
479 die "Couldn't establish SSH connection: ". $ssh->error if $ssh->error;
482 $ssh_opt->{'stdin_data'} = $opt->{'stdin_string'}
483 if exists($opt->{'stdin_string'});
484 my ($output, $errput) = $ssh->capture2($ssh_opt, $opt->{'command'});
485 die "Error running SSH command: ". $ssh->error if $ssh->error;
487 if ($errput && $opt->{'ignored_errors'} && length($opt->{'ignored_errors'})) {
488 my @ignored_errors = split('\n',$opt->{'ignored_errors'});
489 foreach my $ignored_error ( @ignored_errors ) {
490 $errput =~ s/$ignored_error//g;
494 die $errput if $errput;
495 die $output if $output;
499 #sub shellcommands_insert { #subroutine, not method
501 #sub shellcommands_replace { #subroutine, not method
503 #sub shellcommands_delete { #subroutine, not method