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>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
183 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
187 map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
190 sub rebless { shift; }
194 $self->_export_command('useradd', @_);
199 $self->_export_command('userdel', @_);
202 sub _export_suspend {
204 $self->_export_command_or_super('suspend', @_);
207 sub _export_unsuspend {
209 $self->_export_command_or_super('unsuspend', @_);
212 sub _export_command_or_super {
213 my($self, $action) = (shift, shift);
214 if ( $self->option($action) =~ /^\s*$/ ) {
215 my $method = "SUPER::_export_$action";
218 $self->_export_command($action, @_);
222 sub _export_command {
223 my ( $self, $action, $svc_acct) = (shift, shift, shift);
224 my $command = $self->option($action);
225 return '' if $command =~ /^\s*$/;
226 my $stdin = $self->option($action."_stdin");
231 ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
233 # snarfs are unused at this point?
235 foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
236 ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
237 foreach qw( machine username _password );
242 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
244 $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
249 $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
250 ($first, $last ) = ( $1, $2 );
251 $domain = $svc_acct->domain;
253 $quoted_password = shell_quote $_password;
255 $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
256 $ldap_password = $svc_acct->ldap_password( $self->option('crypt') );
258 @radius_groups = $svc_acct->radius_groups;
260 my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
261 if ( $cust_pkg && $action eq 'suspend' &&
262 (my $r = $cust_pkg->last_reason('susp')) )
264 $reasonnum = $r->reasonnum;
265 $reasontext = $r->reason;
266 $reasontypenum = $r->reason_type;
267 $reasontypetext = $r->reasontype->type;
269 my %reasonmap = $self->_groups_susp_reason_map;
271 $userspec = $reasonmap{$reasonnum}
272 if exists($reasonmap{$reasonnum});
273 $userspec = $reasonmap{$reasontext}
274 if (!$userspec && exists($reasonmap{$reasontext}));
277 if ( $userspec =~ /^\d+$/ ) {
278 $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
279 } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
280 my ($username,$domain) = split(/\@/, $userspec);
281 for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
282 $suspend_user = $user if $userspec eq $user->email;
284 } elsif ($userspec) {
285 $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
288 @radius_groups = $suspend_user->radius_groups
292 $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
295 my $stdin_string = eval(qq("$stdin"));
297 $first = shell_quote $first;
298 $last = shell_quote $last;
299 $finger = shell_quote $finger;
300 $crypt_password = shell_quote $crypt_password;
301 $ldap_password = shell_quote $ldap_password;
303 my $command_string = eval(qq("$command"));
305 user => $self->option('user') || 'root',
306 host => $self->machine,
307 command => $command_string,
308 stdin_string => $stdin_string,
311 if($self->option('no_queue')) {
312 # discard return value just like freeside-queued.
313 eval { ssh_cmd(@ssh_cmd_args) };
315 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
319 $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
323 sub _export_replace {
324 my($self, $new, $old ) = (shift, shift, shift);
325 my $command = $self->option('usermod');
326 my $stdin = $self->option('usermod_stdin');
330 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
331 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
333 $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
334 ($new_first, $new_last ) = ( $1, $2 );
335 $quoted_new__password = shell_quote $new__password; #old, wrong?
336 $new_quoted_password = shell_quote $new__password; #new, better?
337 $old_domain = $old->domain;
338 $new_domain = $new->domain;
340 $new_crypt_password = $new->crypt_password( $self->option('crypt') );
341 $new_ldap_password = $new->ldap_password( $self->option('crypt') );
343 @old_radius_groups = $old->radius_groups;
344 @new_radius_groups = $new->radius_groups;
347 if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
348 if ( $old_username ne $new_username ) {
349 $error ||= "can't change username";
352 if ( $self->option('usermod_pwonly') ) {
353 if ( $old_domain ne $new_domain ) {
354 $error ||= "can't change domain";
356 if ( $old_uid != $new_uid ) {
357 $error ||= "can't change uid";
359 if ( $old_gid != $new_gid ) {
360 $error ||= "can't change gid";
362 if ( $old_dir ne $new_dir ) {
363 $error ||= "can't change dir";
365 #if ( join("\n", sort @old_radius_groups) ne
366 # join("\n", sort @new_radius_groups) ) {
367 # $error ||= "can't change RADIUS groups";
370 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
373 my $stdin_string = eval(qq("$stdin"));
375 $new_first = shell_quote $new_first;
376 $new_last = shell_quote $new_last;
377 $new_finger = shell_quote $new_finger;
378 $new_crypt_password = shell_quote $new_crypt_password;
379 $new_ldap_password = shell_quote $new_ldap_password;
381 my $command_string = eval(qq("$command"));
384 user => $self->option('user') || 'root',
385 host => $self->machine,
386 command => $command_string,
387 stdin_string => $stdin_string,
390 if($self->option('no_queue')) {
391 # discard return value just like freeside-queued.
392 eval { ssh_cmd(@ssh_cmd_args) };
394 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
398 $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
402 #a good idea to queue anything that could fail or take any time
403 sub shellcommands_queue {
404 my( $self, $svcnum ) = (shift, shift);
405 my $queue = new FS::queue {
407 'job' => "FS::part_export::shellcommands::ssh_cmd",
409 $queue->insert( @_ );
412 sub ssh_cmd { #subroutine, not method
414 &Net::SSH::ssh_cmd( { @_ } );
417 #sub shellcommands_insert { #subroutine, not method
419 #sub shellcommands_replace { #subroutine, not method
421 #sub shellcommands_delete { #subroutine, not method