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 # 'no_queue' => { label => 'Run command immediately',
91 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
92 'options' => \%options,
95 Run remote commands via SSH. Usernames are considered unique (also see
96 shellcommands_withdomain). You probably want this if the commands you are
97 running will not accept a domain as a parameter. You will need to
98 <a href="http://www.freeside.biz/mediawiki/index.php/Freeside:1.9:Documentation:Administration:SSH_Keys">setup SSH for unattended operation</a>.
100 <BR><BR>Use these buttons for some useful presets:
103 <INPUT TYPE="button" VALUE="Linux" onClick='
104 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
105 this.form.useradd_stdin.value = "";
106 this.form.userdel.value = "userdel -r $username";
107 this.form.userdel_stdin.value="";
108 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";
109 this.form.usermod_stdin.value = "";
110 this.form.suspend.value = "usermod -L $username";
111 this.form.suspend_stdin.value="";
112 this.form.unsuspend.value = "usermod -U $username";
113 this.form.unsuspend_stdin.value="";
116 <INPUT TYPE="button" VALUE="FreeBSD before 4.10 / 5.3" onClick='
117 this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -c $finger -h 0";
118 this.form.useradd_stdin.value = "$_password\n";
119 this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value="";
120 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";
121 this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username";
122 this.form.suspend_stdin.value="";
123 this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";
125 Note: On FreeBSD versions before 5.3 and 4.10 (4.10 is after 4.9, not
126 4.1!), due to deficient locking in pw(1), you must disable the chpass(1),
127 chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with
128 wrappers that prepend "lockf /etc/passwd.lock". Alternatively, apply the
130 <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
131 and use the "FreeBSD 4.10 / 5.3 or later" button below.
133 <INPUT TYPE="button" VALUE="FreeBSD 4.10 / 5.3 or later" onClick='
134 this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
135 this.form.useradd_stdin.value = "$_password\n";
136 this.form.userdel.value = "pw userdel $username -r";
137 this.form.userdel_stdin.value="";
138 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";
139 this.form.usermod_stdin.value = "$new__password\n";
140 this.form.suspend.value = "pw lock $username";
141 this.form.suspend_stdin.value="";
142 this.form.unsuspend.value = "pw unlock $username";
143 this.form.unsuspend_stdin.value="";
146 <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
147 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
148 this.form.useradd_stdin.value = "";
149 this.form.userdel.value = "userdel -r $username";
150 this.form.userdel_stdin.value="";
151 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";
152 this.form.usermod_stdin.value = "";
153 this.form.suspend.value = "";
154 this.form.suspend_stdin.value="";
155 this.form.unsuspend.value = "";
156 this.form.unsuspend_stdin.value="";
159 <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
160 this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
161 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 )";
162 this.form.usermod_stdin.value = "";
163 this.form.userdel.value = "rm -rf $dir";
164 this.form.userdel_stdin.value="";
165 this.form.suspend.value = "";
166 this.form.suspend_stdin.value="";
167 this.form.unsuspend.value = "";
168 this.form.unsuspend_stdin.value="";
172 The following variables are available for interpolation (prefixed with new_ or
173 old_ for replace operations):
175 <LI><code>$username</code>
176 <LI><code>$_password</code>
177 <LI><code>$quoted_password</code> - unencrypted password, already quoted for the shell (do not add additional quotes).
178 <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).
179 <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).
180 <LI><code>$uid</code>
181 <LI><code>$gid</code>
182 <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).
183 <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).
184 <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).
185 <LI><code>$dir</code> - home directory
186 <LI><code>$shell</code>
187 <LI><code>$quota</code>
188 <LI><code>@radius_groups</code>
189 <LI><code>$reasonnum (when suspending)</code>
190 <LI><code>$reasontext (when suspending)</code>
191 <LI><code>$reasontypenum (when suspending)</code>
192 <LI><code>$reasontypetext (when suspending)</code>
193 <LI><code>$pkgnum</code>
194 <LI><code>$custnum</code>
195 <LI>All other fields in <b>svc_acct</b> are also available.
196 <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).
201 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
205 map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
208 sub rebless { shift; }
212 $self->_export_command('useradd', @_);
217 $self->_export_command('userdel', @_);
220 sub _export_suspend {
222 $self->_export_command_or_super('suspend', @_);
225 sub _export_unsuspend {
227 $self->_export_command_or_super('unsuspend', @_);
230 sub _export_command_or_super {
231 my($self, $action) = (shift, shift);
232 if ( $self->option($action) =~ /^\s*$/ ) {
233 my $method = "SUPER::_export_$action";
236 $self->_export_command($action, @_);
240 sub _export_command {
241 my ( $self, $action, $svc_acct) = (shift, shift, shift);
242 my $command = $self->option($action);
243 return '' if $command =~ /^\s*$/;
244 my $stdin = $self->option($action."_stdin");
249 ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
251 # snarfs are unused at this point?
253 foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
254 ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
255 foreach qw( machine username _password );
260 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
265 foreach my $custf (qw( company address1 address2 city state zip country
266 daytime night fax otaker agent_custid locale
269 ${$custf} = $cust_pkg->cust_main->$custf();
272 $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
277 $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
278 ($first, $last ) = ( $1, $2 );
279 $domain = $svc_acct->domain;
281 $quoted_password = shell_quote $_password;
283 $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
284 $ldap_password = $svc_acct->ldap_password( $self->option('crypt') );
286 @radius_groups = $svc_acct->radius_groups;
288 my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
289 if ( $cust_pkg && $action eq 'suspend' &&
290 (my $r = $cust_pkg->last_reason('susp')) )
292 $reasonnum = $r->reasonnum;
293 $reasontext = $r->reason;
294 $reasontypenum = $r->reason_type;
295 $reasontypetext = $r->reasontype->type;
297 my %reasonmap = $self->_groups_susp_reason_map;
299 $userspec = $reasonmap{$reasonnum}
300 if exists($reasonmap{$reasonnum});
301 $userspec = $reasonmap{$reasontext}
302 if (!$userspec && exists($reasonmap{$reasontext}));
305 if ( $userspec =~ /^\d+$/ ) {
306 $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
307 } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
308 my ($username,$domain) = split(/\@/, $userspec);
309 for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
310 $suspend_user = $user if $userspec eq $user->email;
312 } elsif ($userspec) {
313 $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
316 @radius_groups = $suspend_user->radius_groups
320 $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
323 $pkgnum = $cust_pkg ? $cust_pkg->pkgnum : '';
324 $custnum = $cust_pkg ? $cust_pkg->custnum : '';
326 my $stdin_string = eval(qq("$stdin"));
328 $first = shell_quote $first;
329 $last = shell_quote $last;
330 $finger = shell_quote $finger;
331 $crypt_password = shell_quote $crypt_password;
332 $ldap_password = shell_quote $ldap_password;
334 $company = shell_quote $company;
335 $address1 = shell_quote $address1;
336 $address2 = shell_quote $address2;
337 $city = shell_quote $city;
338 $state = shell_quote $state;
339 $zip = shell_quote $zip;
340 $country = shell_quote $country;
341 $daytime = shell_quote $daytime;
342 $night = shell_quote $night;
343 $fax = shell_quote $fax;
344 $otaker = shell_quote $otaker;
345 $agent_custid = shell_quote $agent_custid;
346 $locale = shell_quote $locale;
348 my $command_string = eval(qq("$command"));
350 user => $self->option('user') || 'root',
351 host => $self->machine,
352 command => $command_string,
353 stdin_string => $stdin_string,
356 if($self->option($action . '_no_queue')) {
357 # discard return value just like freeside-queued.
358 eval { ssh_cmd(@ssh_cmd_args) };
360 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
364 $self->shellcommands_queue( $svc_acct->svcnum, @ssh_cmd_args );
368 sub _export_replace {
369 my($self, $new, $old ) = (shift, shift, shift);
370 my $command = $self->option('usermod');
371 my $stdin = $self->option('usermod_stdin');
375 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
376 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
378 my $old_cust_pkg = $old->cust_svc->cust_pkg;
379 my $new_cust_pkg = $new->cust_svc->cust_pkg;
380 my $new_cust_main = $new_cust_pkg ? $new_cust_pkg->cust_main : '';
382 $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
383 ($new_first, $new_last ) = ( $1, $2 );
384 $quoted_new__password = shell_quote $new__password; #old, wrong?
385 $new_quoted_password = shell_quote $new__password; #new, better?
386 $old_domain = $old->domain;
387 $new_domain = $new->domain;
389 $new_crypt_password = $new->crypt_password( $self->option('crypt') );
390 $new_ldap_password = $new->ldap_password( $self->option('crypt') );
392 @old_radius_groups = $old->radius_groups;
393 @new_radius_groups = $new->radius_groups;
396 if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
397 if ( $old_username ne $new_username ) {
398 $error ||= "can't change username";
401 if ( $self->option('usermod_pwonly') ) {
402 if ( $old_domain ne $new_domain ) {
403 $error ||= "can't change domain";
405 if ( $old_uid != $new_uid ) {
406 $error ||= "can't change uid";
408 if ( $old_gid != $new_gid ) {
409 $error ||= "can't change gid";
411 if ( $old_dir ne $new_dir ) {
412 $error ||= "can't change dir";
414 #if ( join("\n", sort @old_radius_groups) ne
415 # join("\n", sort @new_radius_groups) ) {
416 # $error ||= "can't change RADIUS groups";
419 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
422 $new_agent_custid = $new_cust_main ? $new_cust_main->agent_custid : '';
423 $new_locale = $new_cust_main ? $new_cust_main->locale : '';
424 $old_pkgnum = $old_cust_pkg ? $old_cust_pkg->pkgnum : '';
425 $old_custnum = $old_cust_pkg ? $old_cust_pkg->custnum : '';
426 $new_pkgnum = $new_cust_pkg ? $new_cust_pkg->pkgnum : '';
427 $new_custnum = $new_cust_pkg ? $new_cust_pkg->custnum : '';
429 my $stdin_string = eval(qq("$stdin"));
431 $new_first = shell_quote $new_first;
432 $new_last = shell_quote $new_last;
433 $new_finger = shell_quote $new_finger;
434 $new_crypt_password = shell_quote $new_crypt_password;
435 $new_ldap_password = shell_quote $new_ldap_password;
436 $new_agent_custid = shell_quote $new_agent_custid;
437 $new_locale = shell_quote $new_locale;
439 my $command_string = eval(qq("$command"));
442 user => $self->option('user') || 'root',
443 host => $self->machine,
444 command => $command_string,
445 stdin_string => $stdin_string,
448 if($self->option('usermod_no_queue')) {
449 # discard return value just like freeside-queued.
450 eval { ssh_cmd(@ssh_cmd_args) };
452 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
456 $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
460 #a good idea to queue anything that could fail or take any time
461 sub shellcommands_queue {
462 my( $self, $svcnum ) = (shift, shift);
463 my $queue = new FS::queue {
465 'job' => "FS::part_export::shellcommands::ssh_cmd",
467 $queue->insert( @_ );
470 sub ssh_cmd { #subroutine, not method
472 &Net::SSH::ssh_cmd( { @_ } );
475 #sub shellcommands_insert { #subroutine, not method
477 #sub shellcommands_replace { #subroutine, not method
479 #sub shellcommands_delete { #subroutine, not method