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, 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, already quoted for the shell (do not add additional quotes)
164 <LI><code>$first</code> - First name of GECOS, already quoted for the shell (do not add additional quotes)
165 <LI><code>$last</code> - Last name of GECOS, 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, @_);
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;
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 $first = shell_quote $first;
248 $last = shell_quote $last;
249 $finger = shell_quote $finger;
250 $quoted_password = shell_quote $_password;
251 $domain = $svc_acct->domain;
254 shell_quote( $svc_acct->crypt_password( $self->option('crypt') ) );
256 @radius_groups = $svc_acct->radius_groups;
258 my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
259 if ( $cust_pkg && $action eq 'suspend' && (my $r = $cust_pkg->last_reason)){
260 $reasonnum = $r->reasonnum;
261 $reasontext = $r->reason;
262 $reasontypenum = $r->reason_type;
263 $reasontypetext = $r->reasontype->type;
265 my %reasonmap = $self->_groups_susp_reason_map;
267 $userspec = $reasonmap{$reasonnum}
268 if exists($reasonmap{$reasonnum});
269 $userspec = $reasonmap{$reasontext}
270 if (!$userspec && exists($reasonmap{$reasontext}));
273 if ($userspec =~ /^\d+$/ ){
274 $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
275 }elsif ($userspec =~ /^\S+\@\S+$/){
276 my ($username,$domain) = split(/\@/, $userspec);
277 for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
278 $suspend_user = $user if $userspec eq $user->email;
281 $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
284 @radius_groups = $suspend_user->radius_groups
288 $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
291 $self->shellcommands_queue( $svc_acct->svcnum,
292 user => $self->option('user')||'root',
293 host => $self->machine,
294 command => eval(qq("$command")),
295 stdin_string => eval(qq("$stdin")),
299 sub _export_replace {
300 my($self, $new, $old ) = (shift, shift, shift);
301 my $command = $self->option('usermod');
302 my $stdin = $self->option('usermod_stdin');
306 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
307 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
309 $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
310 ($new_first, $new_last ) = ( $1, $2 );
311 $new_first = shell_quote $new_first;
312 $new_last = shell_quote $new_last;
313 $new_finger = shell_quote $new_finger;
314 $quoted_new__password = shell_quote $new__password; #old, wrong?
315 $new_quoted_password = shell_quote $new__password; #new, better?
316 $old_domain = $old->domain;
317 $new_domain = $new->domain;
319 $new_crypt_password =
320 shell_quote( $new->crypt_password( $self->option('crypt') ) );
322 @old_radius_groups = $old->radius_groups;
323 @new_radius_groups = $new->radius_groups;
326 if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
327 if ( $old_username ne $new_username ) {
328 $error ||= "can't change username";
331 if ( $self->option('usermod_pwonly') ) {
332 if ( $old_domain ne $new_domain ) {
333 $error ||= "can't change domain";
335 if ( $old_uid != $new_uid ) {
336 $error ||= "can't change uid";
338 if ( $old_gid != $new_gid ) {
339 $error ||= "can't change gid";
341 if ( $old_dir ne $new_dir ) {
342 $error ||= "can't change dir";
344 #if ( join("\n", sort @old_radius_groups) ne
345 # join("\n", sort @new_radius_groups) ) {
346 # $error ||= "can't change RADIUS groups";
349 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
352 $self->shellcommands_queue( $new->svcnum,
353 user => $self->option('user')||'root',
354 host => $self->machine,
355 command => eval(qq("$command")),
356 stdin_string => eval(qq("$stdin")),
360 #a good idea to queue anything that could fail or take any time
361 sub shellcommands_queue {
362 my( $self, $svcnum ) = (shift, shift);
363 my $queue = new FS::queue {
365 'job' => "FS::part_export::shellcommands::ssh_cmd",
367 $queue->insert( @_ );
370 sub ssh_cmd { #subroutine, not method
372 &Net::SSH::ssh_cmd( { @_ } );
375 #sub shellcommands_insert { #subroutine, not method
377 #sub shellcommands_replace { #subroutine, not method
379 #sub shellcommands_delete { #subroutine, not method