1 package FS::part_export::shellcommands;
3 use vars qw(@ISA %info);
5 use String::ShellQuote;
8 @ISA = qw(FS::part_export);
10 tie my %options, 'Tie::IxHash',
11 'user' => { label=>'Remote username', default=>'root' },
12 'useradd' => { label=>'Insert command',
13 default=>'useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username'
14 #default=>'cp -pr /etc/skel $dir; chown -R $uid.$gid $dir'
16 'useradd_stdin' => { label=>'Insert command STDIN',
20 'userdel' => { label=>'Delete command',
21 default=>'userdel -r $username',
22 #default=>'rm -rf $dir',
24 'userdel_stdin' => { label=>'Delete command STDIN',
28 'usermod' => { label=>'Modify command',
29 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',
30 #default=>'[ -d $old_dir ] && mv $old_dir $new_dir || ( '.
31 # 'chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; '.
32 # 'find . -depth -print | cpio -pdm $new_dir; '.
33 # 'chmod u-t $new_dir; chown -R $uid.$gid $new_dir; '.
37 'usermod_stdin' => { label=>'Modify command STDIN',
41 'usermod_pwonly' => { label=>'Disallow username, domain, uid, gid, dir and RADIUS group changes',
44 'usermod_nousername' => { label=>'Disallow just username changes',
47 'suspend' => { label=>'Suspension command',
48 default=>'usermod -L $username',
50 'suspend_stdin' => { label=>'Suspension command STDIN',
53 'unsuspend' => { label=>'Unsuspension command',
54 default=>'usermod -U $username',
56 'unsuspend_stdin' => { label=>'Unsuspension command STDIN',
59 'crypt' => { label => 'Default password encryption',
60 type=>'select', options=>[qw(crypt md5)],
68 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
69 'options' => \%options,
72 Run remote commands via SSH. Usernames are considered unique (also see
73 shellcommands_withdomain). You probably want this if the commands you are
74 running will not accept a domain as a parameter. You will need to
75 <a href="../docs/ssh.html">setup SSH for unattended operation</a>.
77 <BR><BR>Use these buttons for some useful presets:
80 <INPUT TYPE="button" VALUE="Linux" onClick='
81 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
82 this.form.useradd_stdin.value = "";
83 this.form.userdel.value = "userdel -r $username";
84 this.form.userdel_stdin.value="";
85 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";
86 this.form.usermod_stdin.value = "";
87 this.form.suspend.value = "usermod -L $username";
88 this.form.suspend_stdin.value="";
89 this.form.unsuspend.value = "usermod -U $username";
90 this.form.unsuspend_stdin.value="";
93 <INPUT TYPE="button" VALUE="FreeBSD before 4.10 / 5.3" onClick='
94 this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -c $finger -h 0";
95 this.form.useradd_stdin.value = "$_password\n";
96 this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value="";
97 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";
98 this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username";
99 this.form.suspend_stdin.value="";
100 this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";
102 Note: On FreeBSD versions before 5.3 and 4.10 (4.10 is after 4.9, not
103 4.1!), due to deficient locking in pw(1), you must disable the chpass(1),
104 chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with
105 wrappers that prepend "lockf /etc/passwd.lock". Alternatively, apply the
107 <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
108 and use the "FreeBSD 4.10 / 5.3 or later" button below.
110 <INPUT TYPE="button" VALUE="FreeBSD 4.10 / 5.3 or later" onClick='
111 this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
112 this.form.useradd_stdin.value = "$_password\n";
113 this.form.userdel.value = "pw userdel $username -r";
114 this.form.userdel_stdin.value="";
115 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";
116 this.form.usermod_stdin.value = "$new__password\n";
117 this.form.suspend.value = "pw lock $username";
118 this.form.suspend_stdin.value="";
119 this.form.unsuspend.value = "pw unlock $username";
120 this.form.unsuspend_stdin.value="";
123 <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
124 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
125 this.form.useradd_stdin.value = "";
126 this.form.userdel.value = "userdel -r $username";
127 this.form.userdel_stdin.value="";
128 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";
129 this.form.usermod_stdin.value = "";
130 this.form.suspend.value = "";
131 this.form.suspend_stdin.value="";
132 this.form.unsuspend.value = "";
133 this.form.unsuspend_stdin.value="";
136 <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
137 this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
138 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 )";
139 this.form.usermod_stdin.value = "";
140 this.form.userdel.value = "rm -rf $dir";
141 this.form.userdel_stdin.value="";
142 this.form.suspend.value = "";
143 this.form.suspend_stdin.value="";
144 this.form.unsuspend.value = "";
145 this.form.unsuspend_stdin.value="";
149 The following variables are available for interpolation (prefixed with new_ or
150 old_ for replace operations):
152 <LI><code>$username</code>
153 <LI><code>$_password</code>
154 <LI><code>$quoted_password</code> - unencrypted password, already quoted for the shell (do not add additional quotes)
155 <LI><code>$crypt_password</code> - encrypted password, already quoted for the shell (do not add additional quotes)
156 <LI><code>$uid</code>
157 <LI><code>$gid</code>
158 <LI><code>$finger</code> - GECOS, already quoted for the shell (do not add additional quotes)
159 <LI><code>$first</code> - First name of GECOS, already quoted for the shell (do not add additional quotes)
160 <LI><code>$last</code> - Last name of GECOS, already quoted for the shell (do not add additional quotes)
161 <LI><code>$dir</code> - home directory
162 <LI><code>$shell</code>
163 <LI><code>$quota</code>
164 <LI><code>@radius_groups</code>
165 <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
170 sub rebless { shift; }
174 $self->_export_command('useradd', @_);
179 $self->_export_command('userdel', @_);
182 sub _export_suspend {
184 $self->_export_command_or_super('suspend', @_);
187 sub _export_unsuspend {
189 $self->_export_command_or_super('unsuspend', @_);
192 sub _export_command_or_super {
193 my($self, $action) = (shift, shift);
194 if ( $self->option($action) =~ /^\s*$/ ) {
195 my $method = "SUPER::_export_$action";
198 $self->_export_command($action, @_);
203 sub _export_command {
204 my ( $self, $action, $svc_acct) = (shift, shift, shift);
205 my $command = $self->option($action);
206 return '' if $command =~ /^\s*$/;
207 my $stdin = $self->option($action."_stdin");
212 ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
215 foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
216 ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
217 foreach qw( machine username _password );
222 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
224 $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
229 $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
230 ($first, $last ) = ( $1, $2 );
231 $first = shell_quote $first;
232 $last = shell_quote $last;
233 $finger = shell_quote $finger;
234 $quoted_password = shell_quote $_password;
235 $domain = $svc_acct->domain;
238 shell_quote( $svc_acct->crypt_password( $self->option('crypt') ) );
240 @radius_groups = $svc_acct->radius_groups;
242 $self->shellcommands_queue( $svc_acct->svcnum,
243 user => $self->option('user')||'root',
244 host => $self->machine,
245 command => eval(qq("$command")),
246 stdin_string => eval(qq("$stdin")),
250 sub _export_replace {
251 my($self, $new, $old ) = (shift, shift, shift);
252 my $command = $self->option('usermod');
253 my $stdin = $self->option('usermod_stdin');
257 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
258 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
260 $new_finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
261 ($new_first, $new_last ) = ( $1, $2 );
262 $new_first = shell_quote $new_first;
263 $new_last = shell_quote $new_last;
264 $new_finger = shell_quote $new_finger;
265 $quoted_new__password = shell_quote $new__password; #old, wrong?
266 $new_quoted_password = shell_quote $new__password; #new, better?
267 $old_domain = $old->domain;
268 $new_domain = $new->domain;
270 $new_crypt_password =
271 shell_quote( $new->crypt_password( $self->option('crypt') ) );
273 @old_radius_groups = $old->radius_groups;
274 @new_radius_groups = $new->radius_groups;
277 if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
278 if ( $old_username ne $new_username ) {
279 $error ||= "can't change username";
282 if ( $self->option('usermod_pwonly') ) {
283 if ( $old_domain ne $new_domain ) {
284 $error ||= "can't change domain";
286 if ( $old_uid != $new_uid ) {
287 $error ||= "can't change uid";
289 if ( $old_gid != $new_gid ) {
290 $error ||= "can't change gid";
292 if ( $old_dir ne $new_dir ) {
293 $error ||= "can't change dir";
295 if ( join("\n", sort @old_radius_groups) ne
296 join("\n", sort @new_radius_groups) ) {
297 $error ||= "can't change RADIUS groups";
300 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
303 $self->shellcommands_queue( $new->svcnum,
304 user => $self->option('user')||'root',
305 host => $self->machine,
306 command => eval(qq("$command")),
307 stdin_string => eval(qq("$stdin")),
311 #a good idea to queue anything that could fail or take any time
312 sub shellcommands_queue {
313 my( $self, $svcnum ) = (shift, shift);
314 my $queue = new FS::queue {
316 'job' => "FS::part_export::shellcommands::ssh_cmd",
318 $queue->insert( @_ );
321 sub ssh_cmd { #subroutine, not method
323 &Net::SSH::ssh_cmd( { @_ } );
326 #sub shellcommands_insert { #subroutine, not method
328 #sub shellcommands_replace { #subroutine, not method
330 #sub shellcommands_delete { #subroutine, not method