1 package FS::part_export::shellcommands;
3 use vars qw(@ISA %info @saltset);
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 -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 changes',
44 'suspend' => { label=>'Suspension command',
45 default=>'usermod -L $username',
47 'suspend_stdin' => { label=>'Suspension command STDIN',
50 'unsuspend' => { label=>'Unsuspension command',
51 default=>'usermod -U $username',
53 'unsuspend_stdin' => { label=>'Unsuspension command STDIN',
61 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
62 'options' => \%options,
65 Run remote commands via SSH. Usernames are considered unique (also see
66 shellcommands_withdomain). You probably want this if the commands you are
67 running will not accept a domain as a parameter. You will need to
68 <a href="../docs/ssh.html">setup SSH for unattended operation</a>.
70 <BR><BR>Use these buttons for some useful presets:
73 <INPUT TYPE="button" VALUE="Linux" onClick='
74 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
75 this.form.useradd_stdin.value = "";
76 this.form.userdel.value = "userdel -r $username";
77 this.form.userdel_stdin.value="";
78 this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username";
79 this.form.usermod_stdin.value = "";
80 this.form.suspend.value = "usermod -L $username";
81 this.form.suspend_stdin.value="";
82 this.form.unsuspend.value = "usermod -U $username";
83 this.form.unsuspend_stdin.value="";
86 <INPUT TYPE="button" VALUE="FreeBSD before 5.3" onClick='
87 this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
88 this.form.useradd_stdin.value = "$_password\n";
89 this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value="";
90 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 -c $new_finger -h 0";
91 this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username";
92 this.form.suspend_stdin.value="";
93 this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";
95 Note: On FreeBSD versions before 5.3, due to deficient locking in pw(1),
96 you must disable the chpass(1), chsh(1), chfn(1), passwd(1), and vipw(1)
97 commands, or replace them with wrappers that prepend
98 "lockf /etc/passwd.lock". Alternatively, apply the patch in
99 <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
100 and use the "FreeBSD 5.3 or later" button below.
102 <INPUT TYPE="button" VALUE="FreeBSD 5.3 or later" onClick='
103 this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
104 this.form.useradd_stdin.value = "$_password\n";
105 this.form.userdel.value = "pw userdel $username -r";
106 this.form.userdel_stdin.value="";
107 this.form.usermod.value = "pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -c $new_finger -h 0";
108 this.form.usermod_stdin.value = "$new__password\n";
109 this.form.suspend.value = "pw lock $username";
110 this.form.suspend_stdin.value="";
111 this.form.unsuspend.value = "pw unlock $username";
112 this.form.unsuspend_stdin.value="";
115 <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
116 this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
117 this.form.useradd_stdin.value = "";
118 this.form.userdel.value = "userdel -r $username";
119 this.form.userdel_stdin.value="";
120 this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username";
121 this.form.usermod_stdin.value = "";
122 this.form.suspend.value = "";
123 this.form.suspend_stdin.value="";
124 this.form.unsuspend.value = "";
125 this.form.unsuspend_stdin.value="";
128 <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
129 this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
130 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 )";
131 this.form.usermod_stdin.value = "";
132 this.form.userdel.value = "rm -rf $dir";
133 this.form.userdel_stdin.value="";
134 this.form.suspend.value = "";
135 this.form.suspend_stdin.value="";
136 this.form.unsuspend.value = "";
137 this.form.unsuspend_stdin.value="";
141 The following variables are available for interpolation (prefixed with new_ or
142 old_ for replace operations):
144 <LI><code>$username</code>
145 <LI><code>$_password</code>
146 <LI><code>$quoted_password</code> - unencrypted password quoted for the shell
147 <LI><code>$crypt_password</code> - encrypted password
148 <LI><code>$uid</code>
149 <LI><code>$gid</code>
150 <LI><code>$finger</code> - GECOS, already quoted for the shell (do not add additional quotes)
151 <LI><code>$dir</code> - home directory
152 <LI><code>$shell</code>
153 <LI><code>$quota</code>
154 <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.
159 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
161 sub rebless { shift; }
165 $self->_export_command('useradd', @_);
170 $self->_export_command('userdel', @_);
173 sub _export_suspend {
175 $self->_export_command('suspend', @_);
178 sub _export_unsuspend {
180 $self->_export_command('unsuspend', @_);
183 sub _export_command {
184 my ( $self, $action, $svc_acct) = (shift, shift, shift);
185 my $command = $self->option($action);
186 return '' if $command =~ /^\s*$/;
187 my $stdin = $self->option($action."_stdin");
192 ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
195 foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
196 ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
197 foreach qw( machine username _password );
202 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
204 $email = ( grep { $_ ne 'POST' } $cust_pkg->cust_main->invoicing_list )[0];
209 $finger = shell_quote $finger;
210 $quoted_password = shell_quote $_password;
211 $domain = $svc_acct->domain;
213 #eventually should check a "password-encoding" field
214 if ( length($svc_acct->_password) == 13
215 || $svc_acct->_password =~ /^\$(1|2a?)\$/ ) {
216 $crypt_password = shell_quote $svc_acct->_password;
218 $crypt_password = crypt(
219 $svc_acct->_password,
220 $saltset[int(rand(64))].$saltset[int(rand(64))]
224 $self->shellcommands_queue( $svc_acct->svcnum,
225 user => $self->option('user')||'root',
226 host => $self->machine,
227 command => eval(qq("$command")),
228 stdin_string => eval(qq("$stdin")),
232 sub _export_replace {
233 my($self, $new, $old ) = (shift, shift, shift);
234 my $command = $self->option('usermod');
235 my $stdin = $self->option('usermod_stdin');
239 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
240 ${"new_$_"} = $new->getfield($_) foreach $new->fields;
242 $new_finger = shell_quote $new_finger;
243 $quoted_new__password = shell_quote $new__password; #old, wrong?
244 $new_quoted_password = shell_quote $new__password; #new, better?
245 $old_domain = $old->domain;
246 $new_domain = $new->domain;
248 #eventuall should check a "password-encoding" field
249 if ( length($new->_password) == 13
250 || $new->_password =~ /^\$(1|2a?)\$/ ) {
251 $new_crypt_password = shell_quote $new->_password;
253 $new_crypt_password =
254 crypt( $new->_password, $saltset[int(rand(64))].$saltset[int(rand(64))]
258 if ( $self->option('usermod_pwonly') ) {
260 if ( $old_username ne $new_username ) {
261 $error ||= "can't change username";
263 if ( $old_domain ne $new_domain ) {
264 $error ||= "can't change domain";
266 if ( $old_uid != $new_uid ) {
267 $error ||= "can't change uid";
269 if ( $old_dir ne $new_dir ) {
270 $error ||= "can't change dir";
272 return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
275 $self->shellcommands_queue( $new->svcnum,
276 user => $self->option('user')||'root',
277 host => $self->machine,
278 command => eval(qq("$command")),
279 stdin_string => eval(qq("$stdin")),
283 #a good idea to queue anything that could fail or take any time
284 sub shellcommands_queue {
285 my( $self, $svcnum ) = (shift, shift);
286 my $queue = new FS::queue {
288 'job' => "FS::part_export::shellcommands::ssh_cmd",
290 $queue->insert( @_ );
293 sub ssh_cmd { #subroutine, not method
295 &Net::SSH::ssh_cmd( { @_ } );
298 #sub shellcommands_insert { #subroutine, not method
300 #sub shellcommands_replace { #subroutine, not method
302 #sub shellcommands_delete { #subroutine, not method