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)',
73 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
74 'options' => \%options,
77 Run remote commands via SSH. Usernames are considered unique (also see
78 shellcommands_withdomain). You probably want this if the commands you are
79 running will not accept a domain as a parameter. You will need to
80 <a href="../docs/ssh.html">setup SSH for unattended operation</a>.
82 <BR><BR>Use these buttons for some useful presets:
85 <INPUT TYPE="button" VALUE="Linux" onClick='
86 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
87 this.form.useradd_stdin.value = "";
88 this.form.userdel.value = "userdel -r $username";
89 this.form.userdel_stdin.value="";
90 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";
91 this.form.usermod_stdin.value = "";
92 this.form.suspend.value = "usermod -L $username";
93 this.form.suspend_stdin.value="";
94 this.form.unsuspend.value = "usermod -U $username";
95 this.form.unsuspend_stdin.value="";
98 <INPUT TYPE="button" VALUE="FreeBSD before 4.10 / 5.3" onClick='
99 this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -c $finger -h 0";
100 this.form.useradd_stdin.value = "$_password\n";
101 this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value="";
102 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";
103 this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username";
104 this.form.suspend_stdin.value="";
105 this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";
107 Note: On FreeBSD versions before 5.3 and 4.10 (4.10 is after 4.9, not
108 4.1!), due to deficient locking in pw(1), you must disable the chpass(1),
109 chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with
110 wrappers that prepend "lockf /etc/passwd.lock". Alternatively, apply the
112 <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
113 and use the "FreeBSD 4.10 / 5.3 or later" button below.
115 <INPUT TYPE="button" VALUE="FreeBSD 4.10 / 5.3 or later" onClick='
116 this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
117 this.form.useradd_stdin.value = "$_password\n";
118 this.form.userdel.value = "pw userdel $username -r";
119 this.form.userdel_stdin.value="";
120 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";
121 this.form.usermod_stdin.value = "$new__password\n";
122 this.form.suspend.value = "pw lock $username";
123 this.form.suspend_stdin.value="";
124 this.form.unsuspend.value = "pw unlock $username";
125 this.form.unsuspend_stdin.value="";
128 <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
129 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
130 this.form.useradd_stdin.value = "";
131 this.form.userdel.value = "userdel -r $username";
132 this.form.userdel_stdin.value="";
133 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";
134 this.form.usermod_stdin.value = "";
135 this.form.suspend.value = "";
136 this.form.suspend_stdin.value="";
137 this.form.unsuspend.value = "";
138 this.form.unsuspend_stdin.value="";
141 <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
142 this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
143 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 )";
144 this.form.usermod_stdin.value = "";
145 this.form.userdel.value = "rm -rf $dir";
146 this.form.userdel_stdin.value="";
147 this.form.suspend.value = "";
148 this.form.suspend_stdin.value="";
149 this.form.unsuspend.value = "";
150 this.form.unsuspend_stdin.value="";
154 The following variables are available for interpolation (prefixed with new_ or
155 old_ for replace operations):
157 <LI><code>$username</code>
158 <LI><code>$_password</code>
159 <LI><code>$quoted_password</code> - unencrypted password, already quoted for the shell (do not add additional quotes).
160 <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).
161 <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).
162 <LI><code>$uid</code>
163 <LI><code>$gid</code>
164 <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).
165 <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).
166 <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).
167 <LI><code>$dir</code> - home directory
168 <LI><code>$shell</code>
169 <LI><code>$quota</code>
170 <LI><code>@radius_groups</code>
171 <LI><code>$reasonnum (when suspending)</code>
172 <LI><code>$reasontext (when suspending)</code>
173 <LI><code>$reasontypenum (when suspending)</code>
174 <LI><code>$reasontypetext (when suspending)</code>
175 <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
180 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
184 map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
187 sub rebless { shift; }
191 $self->_export_command('useradd', @_);
196 $self->_export_command('userdel', @_);
199 sub _export_suspend {
201 $self->_export_command_or_super('suspend', @_);
204 sub _export_unsuspend {
206 $self->_export_command_or_super('unsuspend', @_);
209 sub _export_command_or_super {
210 my($self, $action) = (shift, shift);
211 if ( $self->option($action) =~ /^\s*$/ ) {
212 my $method = "SUPER::_export_$action";
215 $self->_export_command($action, @_);
219 sub _export_command {
220 my ( $self, $action, $svc_acct) = (shift, shift, shift);
221 my $command = $self->option($action);
222 return '' if $command =~ /^\s*$/;
223 my $stdin = $self->option($action."_stdin");
228 ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
230 # snarfs are unused at this point?
232 foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
233 ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
234 foreach qw( machine username _password );
239 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
241 $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
246 $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
247 ($first, $last ) = ( $1, $2 );
248 $domain = $svc_acct->domain;
250 $quoted_password = shell_quote $_password;
252 $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
253 $ldap_password = $svc_acct->ldap_password( $self->option('crypt') );
255 @radius_groups = $svc_acct->radius_groups;
257 my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
258 if ( $cust_pkg && $action eq 'suspend' &&
259 (my $r = $cust_pkg->last_reason('susp')) )
261 $reasonnum = $r->reasonnum;
262 $reasontext = $r->reason;
263 $reasontypenum = $r->reason_type;
264 $reasontypetext = $r->reasontype->type;
266 my %reasonmap = $self->_groups_susp_reason_map;
268 $userspec = $reasonmap{$reasonnum}
269 if exists($reasonmap{$reasonnum});
270 $userspec = $reasonmap{$reasontext}
271 if (!$userspec && exists($reasonmap{$reasontext}));
274 if ( $userspec =~ /^\d+$/ ) {
275 $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
276 } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
277 my ($username,$domain) = split(/\@/, $userspec);
278 for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
279 $suspend_user = $user if $userspec eq $user->email;
281 } elsif ($userspec) {
282 $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
285 @radius_groups = $suspend_user->radius_groups
289 $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
292 my $stdin_string = eval(qq("$stdin"));
294 $first = shell_quote $first;
295 $last = shell_quote $last;
296 $finger = shell_quote $finger;
297 $crypt_password = shell_quote $crypt_password;
298 $ldap_password = shell_quote $ldap_password;
300 my $command_string = eval(qq("$command"));
302 $self->shellcommands_queue( $svc_acct->svcnum,
303 user => $self->option('user')||'root',
304 host => $self->machine,
305 command => $command_string,
306 stdin_string => $stdin_string,
310 sub _export_replace {
311 my($self, $new, $old ) = (shift, shift, shift);
312 my $command = $self->option('usermod');
313 my $stdin = $self->option('usermod_stdin');
317 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
318 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
320 $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
321 ($new_first, $new_last ) = ( $1, $2 );
322 $quoted_new__password = shell_quote $new__password; #old, wrong?
323 $new_quoted_password = shell_quote $new__password; #new, better?
324 $old_domain = $old->domain;
325 $new_domain = $new->domain;
327 $new_crypt_password = $new->crypt_password( $self->option('crypt') );
328 $new_ldap_password = $new->ldap_password( $self->option('crypt') );
330 @old_radius_groups = $old->radius_groups;
331 @new_radius_groups = $new->radius_groups;
334 if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
335 if ( $old_username ne $new_username ) {
336 $error ||= "can't change username";
339 if ( $self->option('usermod_pwonly') ) {
340 if ( $old_domain ne $new_domain ) {
341 $error ||= "can't change domain";
343 if ( $old_uid != $new_uid ) {
344 $error ||= "can't change uid";
346 if ( $old_gid != $new_gid ) {
347 $error ||= "can't change gid";
349 if ( $old_dir ne $new_dir ) {
350 $error ||= "can't change dir";
352 #if ( join("\n", sort @old_radius_groups) ne
353 # join("\n", sort @new_radius_groups) ) {
354 # $error ||= "can't change RADIUS groups";
357 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
360 my $stdin_string = eval(qq("$stdin"));
362 $new_first = shell_quote $new_first;
363 $new_last = shell_quote $new_last;
364 $new_finger = shell_quote $new_finger;
365 $new_crypt_password = shell_quote $new_crypt_password;
366 $new_ldap_password = shell_quote $new_ldap_password;
368 my $command_string = eval(qq("$command"));
370 $self->shellcommands_queue( $new->svcnum,
371 user => $self->option('user')||'root',
372 host => $self->machine,
373 command => $command_string,
374 stdin_string => $stdin_string,
378 #a good idea to queue anything that could fail or take any time
379 sub shellcommands_queue {
380 my( $self, $svcnum ) = (shift, shift);
381 my $queue = new FS::queue {
383 'job' => "FS::part_export::shellcommands::ssh_cmd",
385 $queue->insert( @_ );
388 sub ssh_cmd { #subroutine, not method
390 &Net::SSH::ssh_cmd( { @_ } );
393 #sub shellcommands_insert { #subroutine, not method
395 #sub shellcommands_replace { #subroutine, not method
397 #sub shellcommands_delete { #subroutine, not method