15a4d5f44243bb4b7e38efaadb6c7ef1a652ff67
[freeside.git] / FS / FS / part_export / shellcommands.pm
1 package FS::part_export::shellcommands;
2
3 use vars qw(@ISA %info);
4 use Tie::IxHash;
5 use Date::Format;
6 use String::ShellQuote;
7 use FS::part_export;
8 use FS::Record qw( qsearch qsearchs );
9
10 @ISA = qw(FS::part_export);
11
12 tie my %options, 'Tie::IxHash',
13
14   'user' => { label=>'Remote username', default=>'root' },
15
16   'useradd' => { label=>'Insert command',
17                  default=>'useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username'
18                 #default=>'cp -pr /etc/skel $dir; chown -R $uid.$gid $dir'
19                },
20   'useradd_no_queue' => { label=>'Run immediately',
21                           type => 'checkbox',
22                         },
23   'useradd_stdin' => { label=>'Insert command STDIN',
24                        type =>'textarea',
25                        default=>'',
26                      },
27
28   'userdel' => { label=>'Delete command',
29                  default=>'userdel -r $username',
30                  #default=>'rm -rf $dir',
31                },
32   'userdel_no_queue' => { label=>'Run immediately',
33                           type =>'checkbox',
34                         },
35   'userdel_stdin' => { label=>'Delete command STDIN',
36                        type =>'textarea',
37                        default=>'',
38                      },
39
40   'usermod' => { label=>'Modify command',
41                  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',
42                 #default=>'[ -d $old_dir ] && mv $old_dir $new_dir || ( '.
43                  #  'chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; '.
44                  #  'find . -depth -print | cpio -pdm $new_dir; '.
45                  #  'chmod u-t $new_dir; chown -R $uid.$gid $new_dir; '.
46                  #  'rm -rf $old_dir'.
47                  #')'
48                },
49   'usermod_no_queue' => { label=>'Run immediately',
50                           type =>'checkbox',
51                         },
52   'usermod_stdin' => { label=>'Modify command STDIN',
53                        type =>'textarea',
54                        default=>'',
55                      },
56   'usermod_pwonly' => { label=>'Disallow username, domain, uid, gid, and dir changes', #and RADIUS group changes',
57                         type =>'checkbox',
58                       },
59   'usermod_nousername' => { label=>'Disallow just username changes',
60                             type =>'checkbox',
61                           },
62
63   'suspend' => { label=>'Suspension command',
64                  default=>'usermod -L $username',
65                },
66   'suspend_no_queue' => { label=>'Run immediately',
67                           type =>'checkbox',
68                         },
69   'suspend_stdin' => { label=>'Suspension command STDIN',
70                        default=>'',
71                      },
72
73   'unsuspend' => { label=>'Unsuspension command',
74                    default=>'usermod -U $username',
75                  },
76   'unsuspend_no_queue' => { label=>'Run immediately',
77                             type =>'checkbox',
78                           },
79   'unsuspend_stdin' => { label=>'Unsuspension command STDIN',
80                          default=>'',
81                        },
82
83   'pkg_change' => { label=>'Package changed command',
84                     default=>'',
85                   },
86
87   # run commands on package change for multiple services and roll back the
88   #  package change transaciton if one fails?  yuck. no.
89   #  if this was really needed, would need to restrict to a single service with
90   #  this kind of export configured.
91   #'pkg_change_no_queue' => { label=>'Run immediately',
92   #                           type =>'checkbox',
93   #                         },
94   'pkg_change_stdin' => { label=>'Package changed command STDIN',
95                           default=>'',
96                         },
97
98   'crypt' => { label   => 'Default password encryption',
99                type=>'select', options=>[qw(crypt md5)],
100                default => 'crypt',
101              },
102   'groups_susp_reason' => { label =>
103                              'Radius group mapping to reason (via template user)',
104                              type  => 'textarea',
105                           },
106   'fail_on_output' => {
107       label => 'Treat any output from the command as an error',
108       type  => 'checkbox',
109   },
110   'ignore_all_errors' => {
111       label => 'Ignore all errors from the command',
112       type  => 'checkbox',
113   },
114   'ignored_errors' => { label   => 'Regexes of specific errors to ignore, separated by newlines',
115                         type    => 'textarea'
116                       },
117 #  'no_queue' => { label => 'Run command immediately',
118 #                   type  => 'checkbox',
119 #                },
120 ;
121
122 %info = (
123   'svc'      => 'svc_acct',
124   'desc'     =>
125     'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
126   'options'  => \%options,
127   'nodomain' => 'Y',
128   'notes' => <<'END'
129 Run remote commands via SSH.  Usernames are considered unique (also see
130 shellcommands_withdomain).  You probably want this if the commands you are
131 running will not accept a domain as a parameter.  You will need to
132 <a href="http://www.freeside.biz/mediawiki/index.php/Freeside:1.9:Documentation:Administration:SSH_Keys">setup SSH for unattended operation</a>.
133
134 <BR><BR>Use these buttons for some useful presets:
135 <UL>
136   <LI>
137     <INPUT TYPE="button" VALUE="Linux" onClick='
138       this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
139       this.form.useradd_stdin.value = "";
140       this.form.userdel.value = "userdel -r $username";
141       this.form.userdel_stdin.value="";
142       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";
143       this.form.usermod_stdin.value = "";
144       this.form.suspend.value = "usermod -L $username";
145       this.form.suspend_stdin.value="";
146       this.form.unsuspend.value = "usermod -U $username";
147       this.form.unsuspend_stdin.value="";
148     '>
149   <LI>
150     <INPUT TYPE="button" VALUE="FreeBSD before 4.10 / 5.3" onClick='
151       this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -c $finger -h 0";
152       this.form.useradd_stdin.value = "$_password\n";
153       this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value="";
154       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";
155       this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username";
156       this.form.suspend_stdin.value="";
157       this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";
158     '>
159     Note: On FreeBSD versions before 5.3 and 4.10 (4.10 is after 4.9, not
160     4.1!), due to deficient locking in pw(1), you must disable the chpass(1),
161     chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with
162     wrappers that prepend "lockf /etc/passwd.lock".  Alternatively, apply the
163     patch in
164     <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A>
165     and use the "FreeBSD 4.10 / 5.3 or later" button below.
166   <LI>
167     <INPUT TYPE="button" VALUE="FreeBSD 4.10 / 5.3 or later" onClick='
168       this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0";
169       this.form.useradd_stdin.value = "$_password\n";
170       this.form.userdel.value = "pw userdel $username -r";
171       this.form.userdel_stdin.value="";
172       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";
173       this.form.usermod_stdin.value = "$new__password\n";
174       this.form.suspend.value = "pw lock $username";
175       this.form.suspend_stdin.value="";
176       this.form.unsuspend.value = "pw unlock $username";
177       this.form.unsuspend_stdin.value="";
178     '>
179   <LI>
180     <INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick='
181       this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username";
182       this.form.useradd_stdin.value = "";
183       this.form.userdel.value = "userdel -r $username";
184       this.form.userdel_stdin.value="";
185       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";
186       this.form.usermod_stdin.value = "";
187       this.form.suspend.value = "";
188       this.form.suspend_stdin.value="";
189       this.form.unsuspend.value = "";
190       this.form.unsuspend_stdin.value="";
191     '>
192   <LI>
193     <INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick='
194       this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = "";
195       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 )";
196       this.form.usermod_stdin.value = "";
197       this.form.userdel.value = "rm -rf $dir";
198       this.form.userdel_stdin.value="";
199       this.form.suspend.value = "";
200       this.form.suspend_stdin.value="";
201       this.form.unsuspend.value = "";
202       this.form.unsuspend_stdin.value="";
203     '>
204 </UL>
205
206 The following variables are available for interpolation (prefixed with new_ or
207 old_ for replace operations):
208 <UL>
209   <LI><code>$username</code>
210   <LI><code>$_password</code>
211   <LI><code>$quoted_password</code> - unencrypted password, already quoted for the shell (do not add additional quotes).
212   <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).
213   <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).
214   <LI><code>$uid</code>
215   <LI><code>$gid</code>
216   <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).
217   <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).
218   <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).
219   <LI><code>$dir</code> - home directory
220   <LI><code>$shell</code>
221   <LI><code>$quota</code>
222   <LI><code>@radius_groups</code>
223   <LI><code>$reasonnum (when suspending)</code>
224   <LI><code>$reasontext (when suspending)</code>
225   <LI><code>$reasontypenum (when suspending)</code>
226   <LI><code>$reasontypetext (when suspending)</code>
227   <LI><code>$pkgnum</code>
228   <LI><code>$custnum</code>
229   <LI>All other fields in <b>svc_acct</b> are also available.
230   <LI>The following fields from <b>cust_main</b> are also available (except during replace): company, address1, address2, city, state, zip, county, daytime, night, fax, otaker, agent_custid, locale.  When used on the command line (rather than STDIN), they will be quoted for the shell already (do not add additional quotes).
231 </UL>
232 For the package changed command only, the following fields are also available:
233 <UL>
234   <LI>$old_pkgnum and $new_pkgnum
235   <LI>$old_pkgpart and $new_pkgpart
236   <LI>$old_agent_pkgid and $new_agent_pkgid
237   <LI>$old_order_date and $new_order_date
238   <LI>$old_start_date and $new_start_date
239   <LI>$old_setup and $new_setup
240   <LI>$old_bill and $new_bill
241   <LI>$old_last_bill and $new_last_bill
242   <LI>$old_susp and $new_susp
243   <LI>$old_adjourn and $new_adjourn
244   <LI>$old_resume and $new_resume
245   <LI>$old_cancel and $new_cancel
246   <LI>$old_unancel and $new_unancel
247   <LI>$old_expire and $new_expire
248   <LI>$old_contract_end and $new_contract_end
249 </UL>
250 END
251 );
252
253 sub _groups_susp_reason_map { shift->_map('groups_susp_reason'); }
254
255 sub _map {
256   my $self = shift;
257   map { reverse(/^\s*(\S+)\s*(.*)\s*$/) } split("\n", $self->option(shift) );
258 }
259
260 sub rebless { shift; }
261
262 sub _export_insert {
263   my $self = shift;
264   $self->_export_command('useradd', @_);
265 }
266
267 sub _export_delete {
268   my $self = shift;
269   $self->_export_command('userdel', @_);
270 }
271
272 sub _export_suspend {
273   my $self = shift;
274   $self->_export_command_or_super('suspend', @_);
275 }
276
277 sub _export_unsuspend {
278   my $self = shift;
279   $self->_export_command_or_super('unsuspend', @_);
280 }
281
282 sub export_pkg_change {
283   my( $self, $svc_acct, $new_cust_pkg, $old_cust_pkg ) = @_;
284
285   my @fields = qw( pkgnum pkgpart agent_pkgid ); #others?
286   my @date_fields = qw( order_date start_date setup bill last_bill susp adjourn
287                         resume cancel uncancel expore contract_end );
288
289   no strict 'vars';
290   {
291     no strict 'refs';
292     foreach (@fields) {
293       ${"old_$_"} = $old_cust_pkg->getfield($_);
294       ${"new_$_"} = $new_cust_pkg->getfield($_);
295     }
296     foreach (@date_fields) {
297       ${"old_$_"} = time2str('%Y-%m-%d', $old_cust_pkg->getfield($_));
298       ${"new_$_"} = time2str('%Y-%m-%d', $new_cust_pkg->getfield($_));
299     }
300   }
301
302   $self->_export_command('pkg_change', $svc_acct);
303 }
304
305 sub _export_command_or_super {
306   my($self, $action) = (shift, shift);
307   if ( $self->option($action) =~ /^\s*$/ ) {
308     my $method = "SUPER::_export_$action";
309     $self->$method(@_);
310   } else {
311     $self->_export_command($action, @_);
312   }
313 };
314
315 sub _export_command {
316   my ( $self, $action, $svc_acct) = (shift, shift, shift);
317   my $command = $self->option($action);
318
319   return '' if $command =~ /^\s*$/;
320   my $stdin = $self->option($action."_stdin");
321
322   no strict 'vars';
323   {
324     no strict 'refs';
325     ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
326
327     # snarfs are unused at this point?
328     # my $count = 1;
329     # foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
330     #   ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
331     #     foreach qw( machine username _password );
332     #   $count++;
333     # }
334   }
335
336   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
337   if ( $cust_pkg ) {
338     no strict 'vars';
339     {
340       no strict 'refs';
341       foreach my $custf (qw( company address1 address2 city state zip country
342                              daytime night fax otaker agent_custid locale
343                         ))
344       {
345         ${$custf} = $cust_pkg->cust_main->$custf();
346       }
347     }
348     $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
349   } else {
350     $email = '';
351   }
352
353   $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
354   ($first, $last ) = ( $1, $2 );
355   $domain = $svc_acct->domain;
356
357   $quoted_password = shell_quote $_password;
358
359   $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
360   $ldap_password  = $svc_acct->ldap_password(  $self->option('crypt') );
361
362   @radius_groups = $svc_acct->radius_groups;
363
364   my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
365   if ( $cust_pkg && $action eq 'suspend' &&
366        (my $r = $cust_pkg->last_reason('susp')) )
367   {
368     $reasonnum = $r->reasonnum;
369     $reasontext = $r->reason;
370     $reasontypenum = $r->reason_type;
371     $reasontypetext = $r->reasontype->type;
372
373     my %reasonmap = $self->_groups_susp_reason_map;
374     my $userspec = '';
375     $userspec = $reasonmap{$reasonnum}
376       if exists($reasonmap{$reasonnum});
377     $userspec = $reasonmap{$reasontext}
378       if (!$userspec && exists($reasonmap{$reasontext}));
379
380     my $suspend_user;
381     if ( $userspec =~ /^\d+$/ ) {
382       $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
383     } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
384       my ($username,$domain) = split(/\@/, $userspec);
385       for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
386         $suspend_user = $user if $userspec eq $user->email;
387       }
388     } elsif ($userspec) {
389       $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
390     }
391
392     @radius_groups = $suspend_user->radius_groups
393       if $suspend_user;  
394
395   } else {
396     $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
397   }
398
399   $pkgnum = $cust_pkg ? $cust_pkg->pkgnum : '';
400   $custnum = $cust_pkg ? $cust_pkg->custnum : '';
401
402   my $stdin_string = eval(qq("$stdin"));
403   return "error filling in STDIN: $@" if $@;
404
405   $first = shell_quote $first;
406   $last = shell_quote $last;
407   $finger = shell_quote $finger;
408   $crypt_password = shell_quote $crypt_password;
409   $ldap_password  = shell_quote $ldap_password;
410
411   $company = shell_quote $company;
412   $address1 = shell_quote $address1;
413   $address2 = shell_quote $address2;
414   $city = shell_quote $city;
415   $state = shell_quote $state;
416   $zip = shell_quote $zip;
417   $country = shell_quote $country;
418   $daytime = shell_quote $daytime;
419   $night = shell_quote $night;
420   $fax = shell_quote $fax;
421   $otaker = shell_quote $otaker; 
422   $agent_custid = shell_quote $agent_custid;
423   $locale = shell_quote $locale;
424
425   my $command_string = eval(qq("$command"));
426   return "error filling in command: $@" if $@;
427
428   my @ssh_cmd_args = (
429     user          => $self->option('user') || 'root',
430     host          => $self->machine,
431     command       => $command_string,
432     stdin_string  => $stdin_string,
433     ignored_errors    => $self->option('ignored_errors') || '',
434     ignore_all_errors => $self->option('ignore_all_errors'),
435     fail_on_output    => $self->option('fail_on_output'),
436  );
437
438   if ( $self->option($action. '_no_queue') ) {
439     # discard return value just like freeside-queued.
440     eval { ssh_cmd(@ssh_cmd_args) };
441     $error = $@;
442     $error = $error->full_message if ref $error; # Exception::Class::Base
443     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
444       if $error;
445   } else {
446     $self->shellcommands_queue( $svc_acct->svcnum, @ssh_cmd_args );
447   }
448 }
449
450 sub _export_replace {
451   my($self, $new, $old ) = (shift, shift, shift);
452   my $command = $self->option('usermod');
453   return '' if $command =~ /^\s*$/;
454   my $stdin = $self->option('usermod_stdin');
455   no strict 'vars';
456   {
457     no strict 'refs';
458     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
459     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
460   }
461   my $old_cust_pkg = $old->cust_svc->cust_pkg;
462   my $new_cust_pkg = $new->cust_svc->cust_pkg;
463   my $new_cust_main = $new_cust_pkg ? $new_cust_pkg->cust_main : '';
464
465   $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
466   ($new_first, $new_last ) = ( $1, $2 );
467   $quoted_new__password = shell_quote $new__password; #old, wrong?
468   $new_quoted_password = shell_quote $new__password; #new, better?
469   $old_domain = $old->domain;
470   $new_domain = $new->domain;
471
472   $new_crypt_password = $new->crypt_password( $self->option('crypt') );
473   $new_ldap_password  = $new->ldap_password(  $self->option('crypt') );
474
475   @old_radius_groups = $old->radius_groups;
476   @new_radius_groups = $new->radius_groups;
477
478   my $error = '';
479   if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
480     if ( $old_username ne $new_username ) {
481       $error ||= "can't change username";
482     }
483   }
484   if ( $self->option('usermod_pwonly') ) {
485     if ( $old_domain ne $new_domain ) {
486       $error ||= "can't change domain";
487     }
488     if ( $old_uid != $new_uid ) {
489       $error ||= "can't change uid";
490     }
491     if ( $old_gid != $new_gid ) {
492       $error ||= "can't change gid";
493     }
494     if ( $old_dir ne $new_dir ) {
495       $error ||= "can't change dir";
496     }
497     #if ( join("\n", sort @old_radius_groups) ne
498     #     join("\n", sort @new_radius_groups)    ) {
499     #  $error ||= "can't change RADIUS groups";
500     #}
501   }
502   return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
503     if $error;
504
505   $new_agent_custid = $new_cust_main ? $new_cust_main->agent_custid : '';
506   $new_locale = $new_cust_main ? $new_cust_main->locale : '';
507   $old_pkgnum = $old_cust_pkg ? $old_cust_pkg->pkgnum : '';
508   $old_custnum = $old_cust_pkg ? $old_cust_pkg->custnum : '';
509   $new_pkgnum = $new_cust_pkg ? $new_cust_pkg->pkgnum : '';
510   $new_custnum = $new_cust_pkg ? $new_cust_pkg->custnum : '';
511
512   my $stdin_string = eval(qq("$stdin"));
513
514   $new_first = shell_quote $new_first;
515   $new_last = shell_quote $new_last;
516   $new_finger = shell_quote $new_finger;
517   $new_crypt_password = shell_quote $new_crypt_password;
518   $new_ldap_password  = shell_quote $new_ldap_password;
519   $new_agent_custid = shell_quote $new_agent_custid;
520   $new_locale = shell_quote $new_locale;
521
522   my $command_string = eval(qq("$command"));
523
524   my @ssh_cmd_args = (
525     user          => $self->option('user') || 'root',
526     host          => $self->machine,
527     command       => $command_string,
528     stdin_string  => $stdin_string,
529     ignored_errors    => $self->option('ignored_errors') || '',
530     ignore_all_errors => $self->option('ignore_all_errors'),
531     fail_on_output    => $self->option('fail_on_output'),
532   );
533
534   if($self->option('usermod_no_queue')) {
535     # discard return value just like freeside-queued.
536     eval { ssh_cmd(@ssh_cmd_args) };
537     $error = $@;
538     $error = $error->full_message if ref $error; # Exception::Class::Base
539     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
540       if $error;
541   }
542   else {
543     $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
544   }
545 }
546
547 #a good idea to queue anything that could fail or take any time
548 sub shellcommands_queue {
549   my( $self, $svcnum ) = (shift, shift);
550   my $queue = new FS::queue {
551     'svcnum' => $svcnum,
552     'job'    => "FS::part_export::shellcommands::ssh_cmd",
553   };
554   $queue->insert( @_ );
555 }
556
557 sub ssh_cmd { #subroutine, not method
558   use Net::OpenSSH;
559   my $opt = { @_ };
560   open my $def_in, '<', '/dev/null' or die "unable to open /dev/null\n";
561   my $ssh = Net::OpenSSH->new(
562     $opt->{'user'}.'@'.$opt->{'host'},
563     'default_stdin_fh' => $def_in
564   );
565   # ignore_all_errors doesn't override SSH connection/auth errors--
566   # probably correct
567   die "Couldn't establish SSH connection: ". $ssh->error if $ssh->error;
568
569   my $ssh_opt = {};
570   $ssh_opt->{'stdin_data'} = $opt->{'stdin_string'}
571     if exists($opt->{'stdin_string'}) and length($opt->{'stdin_string'});
572
573   my ($output, $errput) = $ssh->capture2($ssh_opt, $opt->{'command'});
574
575   return if $opt->{'ignore_all_errors'};
576   die "Error running SSH command: ". $ssh->error if $ssh->error;
577
578   if ( ($output || $errput)
579        && $opt->{'ignored_errors'} && length($opt->{'ignored_errors'})
580   ) {
581     my @ignored_errors = split('\n',$opt->{'ignored_errors'});
582     foreach my $ignored_error ( @ignored_errors ) {
583         $output =~ s/$ignored_error//g;
584         $errput =~ s/$ignored_error//g;
585     }
586     $output =~ s/[\s\n]//g;
587     $errput =~ s/[\s\n]//g;
588   }
589
590   die "$errput\n" if $errput;
591   die "$output\n" if $output and $opt->{'fail_on_output'};
592   '';
593 }
594
595 #sub shellcommands_insert { #subroutine, not method
596 #}
597 #sub shellcommands_replace { #subroutine, not method
598 #}
599 #sub shellcommands_delete { #subroutine, not method
600 #}
601
602 sub _upgrade_exporttype {
603   my $class = shift;
604   $class =~ /^FS::part_export::(\w+)$/;
605   foreach my $self ( qsearch('part_export', { 'exporttype' => $1 }) ) {
606     my %options = $self->options;
607     my $changed = 0;
608     # 2011-12-13 - 2012-02-16: ignore_all_output option
609     if ( $options{'ignore_all_output'} ) {
610       # ignoring STDOUT is now the default
611       $options{'ignore_all_errors'} = 1;
612       delete $options{'ignore_all_output'};
613       $changed++;
614     }
615     my $error = $self->replace(%options) if $changed;
616     die $error if $error;
617   }
618 }
619
620 1;
621