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 already quoted for the shell (do not add additional quotes).
161 <LI><code>$uid</code>
162 <LI><code>$gid</code>
163 <LI><code>$finger</code> - GECOS. When used on the command line (rather than STDIN), it will be already quoted for the shell (do not add additional quotes).
164 <LI><code>$first</code> - First name of GECOS. When used on the command line (rather than STDIN), it will be already quoted for the shell (do not add additional quotes).
165 <LI><code>$last</code> - Last name of GECOS. When used on the command line (rather than STDIN), it will be already quoted for the shell (do not add additional quotes).
166 <LI><code>$dir</code> - home directory
167 <LI><code>$shell</code>
168 <LI><code>$quota</code>
169 <LI><code>@radius_groups</code>
170 <LI><code>$reasonnum (when suspending)</code>
171 <LI><code>$reasontext (when suspending)</code>
172 <LI><code>$reasontypenum (when suspending)</code>
173 <LI><code>$reasontypetext (when suspending)</code>
174 <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
179 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
183 map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
186 sub rebless { shift; }
190 $self->_export_command('useradd', @_);
195 $self->_export_command('userdel', @_);
198 sub _export_suspend {
200 $self->_export_command_or_super('suspend', @_);
203 sub _export_unsuspend {
205 $self->_export_command_or_super('unsuspend', @_);
208 sub _export_command_or_super {
209 my($self, $action) = (shift, shift);
210 if ( $self->option($action) =~ /^\s*$/ ) {
211 my $method = "SUPER::_export_$action";
214 $self->_export_command($action, @_);
218 sub _export_command {
219 my ( $self, $action, $svc_acct) = (shift, shift, shift);
220 my $command = $self->option($action);
221 return '' if $command =~ /^\s*$/;
222 my $stdin = $self->option($action."_stdin");
227 ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
229 # snarfs are unused at this point?
231 foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
232 ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
233 foreach qw( machine username _password );
238 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
240 $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
245 $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
246 ($first, $last ) = ( $1, $2 );
247 $domain = $svc_acct->domain;
249 $quoted_password = shell_quote $_password;
251 $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
253 @radius_groups = $svc_acct->radius_groups;
255 my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
256 if ( $cust_pkg && $action eq 'suspend' && (my $r = $cust_pkg->last_reason) ) {
257 $reasonnum = $r->reasonnum;
258 $reasontext = $r->reason;
259 $reasontypenum = $r->reason_type;
260 $reasontypetext = $r->reasontype->type;
262 my %reasonmap = $self->_groups_susp_reason_map;
264 $userspec = $reasonmap{$reasonnum}
265 if exists($reasonmap{$reasonnum});
266 $userspec = $reasonmap{$reasontext}
267 if (!$userspec && exists($reasonmap{$reasontext}));
270 if ( $userspec =~ /^\d+$/ ) {
271 $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
272 } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
273 my ($username,$domain) = split(/\@/, $userspec);
274 for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
275 $suspend_user = $user if $userspec eq $user->email;
277 } elsif ($userspec) {
278 $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
281 @radius_groups = $suspend_user->radius_groups
285 $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
288 my $stdin_string = eval(qq("$stdin"));
290 $first = shell_quote $first;
291 $last = shell_quote $last;
292 $finger = shell_quote $finger;
293 $crypt_password = shell_quote $crypt_password;
295 my $command_string = eval(qq("$command"));
297 $self->shellcommands_queue( $svc_acct->svcnum,
298 user => $self->option('user')||'root',
299 host => $self->machine,
300 command => $command_string,
301 stdin_string => $stdin_string,
305 sub _export_replace {
306 my($self, $new, $old ) = (shift, shift, shift);
307 my $command = $self->option('usermod');
308 my $stdin = $self->option('usermod_stdin');
312 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
313 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
315 $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
316 ($new_first, $new_last ) = ( $1, $2 );
317 $quoted_new__password = shell_quote $new__password; #old, wrong?
318 $new_quoted_password = shell_quote $new__password; #new, better?
319 $old_domain = $old->domain;
320 $new_domain = $new->domain;
322 $new_crypt_password = $new->crypt_password( $self->option('crypt') );
324 @old_radius_groups = $old->radius_groups;
325 @new_radius_groups = $new->radius_groups;
328 if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
329 if ( $old_username ne $new_username ) {
330 $error ||= "can't change username";
333 if ( $self->option('usermod_pwonly') ) {
334 if ( $old_domain ne $new_domain ) {
335 $error ||= "can't change domain";
337 if ( $old_uid != $new_uid ) {
338 $error ||= "can't change uid";
340 if ( $old_gid != $new_gid ) {
341 $error ||= "can't change gid";
343 if ( $old_dir ne $new_dir ) {
344 $error ||= "can't change dir";
346 #if ( join("\n", sort @old_radius_groups) ne
347 # join("\n", sort @new_radius_groups) ) {
348 # $error ||= "can't change RADIUS groups";
351 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
354 my $stdin_string = eval(qq("$stdin"));
356 $new_first = shell_quote $new_first;
357 $new_last = shell_quote $new_last;
358 $new_finger = shell_quote $new_finger;
359 $new_crypt_password = shell_quote $crypt_password;
361 my $command_string = eval(qq("$command"));
363 $self->shellcommands_queue( $new->svcnum,
364 user => $self->option('user')||'root',
365 host => $self->machine,
366 command => $command_string,
367 stdin_string => $stdin_string,
371 #a good idea to queue anything that could fail or take any time
372 sub shellcommands_queue {
373 my( $self, $svcnum ) = (shift, shift);
374 my $queue = new FS::queue {
376 'job' => "FS::part_export::shellcommands::ssh_cmd",
378 $queue->insert( @_ );
381 sub ssh_cmd { #subroutine, not method
383 &Net::SSH::ssh_cmd( { @_ } );
386 #sub shellcommands_insert { #subroutine, not method
388 #sub shellcommands_replace { #subroutine, not method
390 #sub shellcommands_delete { #subroutine, not method