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' && (my $r = $cust_pkg->last_reason) ) {
259 $reasonnum = $r->reasonnum;
260 $reasontext = $r->reason;
261 $reasontypenum = $r->reason_type;
262 $reasontypetext = $r->reasontype->type;
264 my %reasonmap = $self->_groups_susp_reason_map;
266 $userspec = $reasonmap{$reasonnum}
267 if exists($reasonmap{$reasonnum});
268 $userspec = $reasonmap{$reasontext}
269 if (!$userspec && exists($reasonmap{$reasontext}));
272 if ( $userspec =~ /^\d+$/ ) {
273 $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
274 } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
275 my ($username,$domain) = split(/\@/, $userspec);
276 for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
277 $suspend_user = $user if $userspec eq $user->email;
279 } elsif ($userspec) {
280 $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
283 @radius_groups = $suspend_user->radius_groups
287 $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
290 my $stdin_string = eval(qq("$stdin"));
292 $first = shell_quote $first;
293 $last = shell_quote $last;
294 $finger = shell_quote $finger;
295 $crypt_password = shell_quote $crypt_password;
296 $ldap_password = shell_quote $ldap_password;
298 my $command_string = eval(qq("$command"));
300 $self->shellcommands_queue( $svc_acct->svcnum,
301 user => $self->option('user')||'root',
302 host => $self->machine,
303 command => $command_string,
304 stdin_string => $stdin_string,
308 sub _export_replace {
309 my($self, $new, $old ) = (shift, shift, shift);
310 my $command = $self->option('usermod');
311 my $stdin = $self->option('usermod_stdin');
315 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
316 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
318 $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
319 ($new_first, $new_last ) = ( $1, $2 );
320 $quoted_new__password = shell_quote $new__password; #old, wrong?
321 $new_quoted_password = shell_quote $new__password; #new, better?
322 $old_domain = $old->domain;
323 $new_domain = $new->domain;
325 $new_crypt_password = $new->crypt_password( $self->option('crypt') );
326 $new_ldap_password = $new->ldap_password( $self->option('crypt') );
328 @old_radius_groups = $old->radius_groups;
329 @new_radius_groups = $new->radius_groups;
332 if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
333 if ( $old_username ne $new_username ) {
334 $error ||= "can't change username";
337 if ( $self->option('usermod_pwonly') ) {
338 if ( $old_domain ne $new_domain ) {
339 $error ||= "can't change domain";
341 if ( $old_uid != $new_uid ) {
342 $error ||= "can't change uid";
344 if ( $old_gid != $new_gid ) {
345 $error ||= "can't change gid";
347 if ( $old_dir ne $new_dir ) {
348 $error ||= "can't change dir";
350 #if ( join("\n", sort @old_radius_groups) ne
351 # join("\n", sort @new_radius_groups) ) {
352 # $error ||= "can't change RADIUS groups";
355 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
358 my $stdin_string = eval(qq("$stdin"));
360 $new_first = shell_quote $new_first;
361 $new_last = shell_quote $new_last;
362 $new_finger = shell_quote $new_finger;
363 $new_crypt_password = shell_quote $new_crypt_password;
364 $new_ldap_password = shell_quote $new_ldap_password;
366 my $command_string = eval(qq("$command"));
368 $self->shellcommands_queue( $new->svcnum,
369 user => $self->option('user')||'root',
370 host => $self->machine,
371 command => $command_string,
372 stdin_string => $stdin_string,
376 #a good idea to queue anything that could fail or take any time
377 sub shellcommands_queue {
378 my( $self, $svcnum ) = (shift, shift);
379 my $queue = new FS::queue {
381 'job' => "FS::part_export::shellcommands::ssh_cmd",
383 $queue->insert( @_ );
386 sub ssh_cmd { #subroutine, not method
388 &Net::SSH::ssh_cmd( { @_ } );
391 #sub shellcommands_insert { #subroutine, not method
393 #sub shellcommands_replace { #subroutine, not method
395 #sub shellcommands_delete { #subroutine, not method