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_stdin' => { label=>'Insert command STDIN',
21 'userdel' => { label=>'Delete command',
22 default=>'userdel -r $username',
23 #default=>'rm -rf $dir',
25 'userdel_stdin' => { label=>'Delete command STDIN',
29 'usermod' => { label=>'Modify command',
30 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',
31 #default=>'[ -d $old_dir ] && mv $old_dir $new_dir || ( '.
32 # 'chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; '.
33 # 'find . -depth -print | cpio -pdm $new_dir; '.
34 # 'chmod u-t $new_dir; chown -R $uid.$gid $new_dir; '.
38 'usermod_stdin' => { label=>'Modify command STDIN',
42 'usermod_pwonly' => { label=>'Disallow username, domain, uid, gid, and dir changes', #and RADIUS group changes',
45 'usermod_nousername' => { label=>'Disallow just username changes',
48 'suspend' => { label=>'Suspension command',
49 default=>'usermod -L $username',
51 'suspend_stdin' => { label=>'Suspension command STDIN',
54 'unsuspend' => { label=>'Unsuspension command',
55 default=>'usermod -U $username',
57 'unsuspend_stdin' => { label=>'Unsuspension command STDIN',
60 'crypt' => { label => 'Default password encryption',
61 type=>'select', options=>[qw(crypt md5)],
64 'groups_susp_reason' => { label =>
65 'Radius group mapping to reason (via template user)',
68 'no_queue' => { label => 'Run command immediately',
76 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
77 'options' => \%options,
80 Run remote commands via SSH. Usernames are considered unique (also see
81 shellcommands_withdomain). You probably want this if the commands you are
82 running will not accept a domain as a parameter. You will need to
83 <a href="../docs/ssh.html">setup SSH for unattended operation</a>.
85 <BR><BR>Use these buttons for some useful presets:
88 <INPUT TYPE="button" VALUE="Linux" onClick='
89 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
90 this.form.useradd_stdin.value = "";
91 this.form.userdel.value = "userdel -r $username";
92 this.form.userdel_stdin.value="";
93 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";
94 this.form.usermod_stdin.value = "";
95 this.form.suspend.value = "usermod -L $username";
96 this.form.suspend_stdin.value="";
97 this.form.unsuspend.value = "usermod -U $username";
98 this.form.unsuspend_stdin.value="";
101 <INPUT TYPE="button" VALUE="FreeBSD before 4.10 / 5.3" onClick='
102 this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -c $finger -h 0";
103 this.form.useradd_stdin.value = "$_password\n";
104 this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value="";
105 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";
106 this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username";
107 this.form.suspend_stdin.value="";
108 this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";
110 Note: On FreeBSD versions before 5.3 and 4.10 (4.10 is after 4.9, not
111 4.1!), due to deficient locking in pw(1), you must disable the chpass(1),
112 chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with
113 wrappers that prepend "lockf /etc/passwd.lock". Alternatively, apply the
115 <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
116 and use the "FreeBSD 4.10 / 5.3 or later" button below.
118 <INPUT TYPE="button" VALUE="FreeBSD 4.10 / 5.3 or later" onClick='
119 this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
120 this.form.useradd_stdin.value = "$_password\n";
121 this.form.userdel.value = "pw userdel $username -r";
122 this.form.userdel_stdin.value="";
123 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";
124 this.form.usermod_stdin.value = "$new__password\n";
125 this.form.suspend.value = "pw lock $username";
126 this.form.suspend_stdin.value="";
127 this.form.unsuspend.value = "pw unlock $username";
128 this.form.unsuspend_stdin.value="";
131 <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
132 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
133 this.form.useradd_stdin.value = "";
134 this.form.userdel.value = "userdel -r $username";
135 this.form.userdel_stdin.value="";
136 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";
137 this.form.usermod_stdin.value = "";
138 this.form.suspend.value = "";
139 this.form.suspend_stdin.value="";
140 this.form.unsuspend.value = "";
141 this.form.unsuspend_stdin.value="";
144 <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
145 this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
146 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 )";
147 this.form.usermod_stdin.value = "";
148 this.form.userdel.value = "rm -rf $dir";
149 this.form.userdel_stdin.value="";
150 this.form.suspend.value = "";
151 this.form.suspend_stdin.value="";
152 this.form.unsuspend.value = "";
153 this.form.unsuspend_stdin.value="";
157 The following variables are available for interpolation (prefixed with new_ or
158 old_ for replace operations):
160 <LI><code>$username</code>
161 <LI><code>$_password</code>
162 <LI><code>$quoted_password</code> - unencrypted password, already quoted for the shell (do not add additional quotes).
163 <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).
164 <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).
165 <LI><code>$uid</code>
166 <LI><code>$gid</code>
167 <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).
168 <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).
169 <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).
170 <LI><code>$dir</code> - home directory
171 <LI><code>$shell</code>
172 <LI><code>$quota</code>
173 <LI><code>@radius_groups</code>
174 <LI><code>$reasonnum (when suspending)</code>
175 <LI><code>$reasontext (when suspending)</code>
176 <LI><code>$reasontypenum (when suspending)</code>
177 <LI><code>$reasontypetext (when suspending)</code>
178 <LI><code>$pkgnum</code>
179 <LI><code>$custnum</code>
180 <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
185 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
189 map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
192 sub rebless { shift; }
196 $self->_export_command('useradd', @_);
201 $self->_export_command('userdel', @_);
204 sub _export_suspend {
206 $self->_export_command_or_super('suspend', @_);
209 sub _export_unsuspend {
211 $self->_export_command_or_super('unsuspend', @_);
214 sub _export_command_or_super {
215 my($self, $action) = (shift, shift);
216 if ( $self->option($action) =~ /^\s*$/ ) {
217 my $method = "SUPER::_export_$action";
220 $self->_export_command($action, @_);
224 sub _export_command {
225 my ( $self, $action, $svc_acct) = (shift, shift, shift);
226 my $command = $self->option($action);
227 return '' if $command =~ /^\s*$/;
228 my $stdin = $self->option($action."_stdin");
233 ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
235 # snarfs are unused at this point?
237 foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
238 ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
239 foreach qw( machine username _password );
244 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
246 $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
251 $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
252 ($first, $last ) = ( $1, $2 );
253 $domain = $svc_acct->domain;
255 $quoted_password = shell_quote $_password;
257 $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
258 $ldap_password = $svc_acct->ldap_password( $self->option('crypt') );
260 @radius_groups = $svc_acct->radius_groups;
262 my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
263 if ( $cust_pkg && $action eq 'suspend' &&
264 (my $r = $cust_pkg->last_reason('susp')) )
266 $reasonnum = $r->reasonnum;
267 $reasontext = $r->reason;
268 $reasontypenum = $r->reason_type;
269 $reasontypetext = $r->reasontype->type;
271 my %reasonmap = $self->_groups_susp_reason_map;
273 $userspec = $reasonmap{$reasonnum}
274 if exists($reasonmap{$reasonnum});
275 $userspec = $reasonmap{$reasontext}
276 if (!$userspec && exists($reasonmap{$reasontext}));
279 if ( $userspec =~ /^\d+$/ ) {
280 $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
281 } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
282 my ($username,$domain) = split(/\@/, $userspec);
283 for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
284 $suspend_user = $user if $userspec eq $user->email;
286 } elsif ($userspec) {
287 $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
290 @radius_groups = $suspend_user->radius_groups
294 $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
297 my $stdin_string = eval(qq("$stdin"));
299 $first = shell_quote $first;
300 $last = shell_quote $last;
301 $finger = shell_quote $finger;
302 $crypt_password = shell_quote $crypt_password;
303 $ldap_password = shell_quote $ldap_password;
304 $pkgnum = $cust_pkg ? $cust_pkg->pkgnum : '';
305 $custnum = $cust_pkg ? $cust_pkg->custnum : '';
307 my $command_string = eval(qq("$command"));
309 user => $self->option('user') || 'root',
310 host => $self->machine,
311 command => $command_string,
312 stdin_string => $stdin_string,
315 if($self->option('no_queue')) {
316 # discard return value just like freeside-queued.
317 eval { ssh_cmd(@ssh_cmd_args) };
319 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
323 $self->shellcommands_queue( $svc_acct->svcnum, @ssh_cmd_args );
327 sub _export_replace {
328 my($self, $new, $old ) = (shift, shift, shift);
329 my $command = $self->option('usermod');
330 my $stdin = $self->option('usermod_stdin');
334 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
335 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
337 my $old_cust_pkg = $old->cust_svc->cust_pkg;
338 my $new_cust_pkg = $new->cust_svc->cust_pkg;
339 $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
340 ($new_first, $new_last ) = ( $1, $2 );
341 $quoted_new__password = shell_quote $new__password; #old, wrong?
342 $new_quoted_password = shell_quote $new__password; #new, better?
343 $old_domain = $old->domain;
344 $new_domain = $new->domain;
346 $new_crypt_password = $new->crypt_password( $self->option('crypt') );
347 $new_ldap_password = $new->ldap_password( $self->option('crypt') );
349 @old_radius_groups = $old->radius_groups;
350 @new_radius_groups = $new->radius_groups;
353 if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
354 if ( $old_username ne $new_username ) {
355 $error ||= "can't change username";
358 if ( $self->option('usermod_pwonly') ) {
359 if ( $old_domain ne $new_domain ) {
360 $error ||= "can't change domain";
362 if ( $old_uid != $new_uid ) {
363 $error ||= "can't change uid";
365 if ( $old_gid != $new_gid ) {
366 $error ||= "can't change gid";
368 if ( $old_dir ne $new_dir ) {
369 $error ||= "can't change dir";
371 #if ( join("\n", sort @old_radius_groups) ne
372 # join("\n", sort @new_radius_groups) ) {
373 # $error ||= "can't change RADIUS groups";
376 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
379 my $stdin_string = eval(qq("$stdin"));
381 $new_first = shell_quote $new_first;
382 $new_last = shell_quote $new_last;
383 $new_finger = shell_quote $new_finger;
384 $new_crypt_password = shell_quote $new_crypt_password;
385 $new_ldap_password = shell_quote $new_ldap_password;
386 $old_pkgnum = $old_cust_pkg ? $old_cust_pkg->pkgnum : '';
387 $old_custnum = $old_cust_pkg ? $old_cust_pkg->custnum : '';
388 $new_pkgnum = $new_cust_pkg ? $new_cust_pkg->pkgnum : '';
389 $new_custnum = $new_cust_pkg ? $new_cust_pkg->custnum : '';
391 my $command_string = eval(qq("$command"));
394 user => $self->option('user') || 'root',
395 host => $self->machine,
396 command => $command_string,
397 stdin_string => $stdin_string,
400 if($self->option('no_queue')) {
401 # discard return value just like freeside-queued.
402 eval { ssh_cmd(@ssh_cmd_args) };
404 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
408 $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
412 #a good idea to queue anything that could fail or take any time
413 sub shellcommands_queue {
414 my( $self, $svcnum ) = (shift, shift);
415 my $queue = new FS::queue {
417 'job' => "FS::part_export::shellcommands::ssh_cmd",
419 $queue->insert( @_ );
422 sub ssh_cmd { #subroutine, not method
424 &Net::SSH::ssh_cmd( { @_ } );
427 #sub shellcommands_insert { #subroutine, not method
429 #sub shellcommands_replace { #subroutine, not method
431 #sub shellcommands_delete { #subroutine, not method