fix legacy link (fallout from RT#22596)
[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 expire contract_end );
288
289   no strict 'vars';
290   {
291     no strict 'refs';
292     foreach (@fields) {
293       ${"old_$_"} = $old_cust_pkg ? $old_cust_pkg->getfield($_) : '';
294       ${"new_$_"} = $new_cust_pkg->getfield($_);
295     }
296     foreach (@date_fields) {
297       ${"old_$_"} = $old_cust_pkg
298                       ? time2str('%Y-%m-%d', $old_cust_pkg->getfield($_))
299                       : '';
300       ${"new_$_"} = time2str('%Y-%m-%d', $new_cust_pkg->getfield($_));
301     }
302   }
303
304   $self->_export_command('pkg_change', $svc_acct);
305 }
306
307 sub _export_command_or_super {
308   my($self, $action) = (shift, shift);
309   if ( $self->option($action) =~ /^\s*$/ ) {
310     my $method = "SUPER::_export_$action";
311     $self->$method(@_);
312   } else {
313     $self->_export_command($action, @_);
314   }
315 };
316
317 sub _export_command {
318   my ( $self, $action, $svc_acct) = (shift, shift, shift);
319   my $command = $self->option($action);
320
321   return '' if $command =~ /^\s*$/;
322   my $stdin = $self->option($action."_stdin");
323
324   no strict 'vars';
325   {
326     no strict 'refs';
327     ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
328
329     # snarfs are unused at this point?
330     # my $count = 1;
331     # foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
332     #   ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
333     #     foreach qw( machine username _password );
334     #   $count++;
335     # }
336   }
337
338   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
339   if ( $cust_pkg ) {
340     no strict 'vars';
341     {
342       no strict 'refs';
343       foreach my $custf (qw( company address1 address2 city state zip country
344                              daytime night fax otaker agent_custid locale
345                         ))
346       {
347         ${$custf} = $cust_pkg->cust_main->$custf();
348       }
349     }
350     $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0];
351   } else {
352     $email = '';
353   }
354
355   $finger =~ /^(.*)\s+(\S+)$/ or $finger =~ /^((.*))$/;
356   ($first, $last ) = ( $1, $2 );
357   $domain = $svc_acct->domain;
358
359   $quoted_password = shell_quote $_password;
360
361   $crypt_password = $svc_acct->crypt_password( $self->option('crypt') );
362   $ldap_password  = $svc_acct->ldap_password(  $self->option('crypt') );
363
364   @radius_groups = $svc_acct->radius_groups;
365
366   my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext);
367   if ( $cust_pkg && $action eq 'suspend' &&
368        (my $r = $cust_pkg->last_reason('susp')) )
369   {
370     $reasonnum = $r->reasonnum;
371     $reasontext = $r->reason;
372     $reasontypenum = $r->reason_type;
373     $reasontypetext = $r->reasontype->type;
374
375     my %reasonmap = $self->_groups_susp_reason_map;
376     my $userspec = '';
377     $userspec = $reasonmap{$reasonnum}
378       if exists($reasonmap{$reasonnum});
379     $userspec = $reasonmap{$reasontext}
380       if (!$userspec && exists($reasonmap{$reasontext}));
381
382     my $suspend_user;
383     if ( $userspec =~ /^\d+$/ ) {
384       $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } );
385     } elsif ( $userspec =~ /^\S+\@\S+$/ ) {
386       my ($username,$domain) = split(/\@/, $userspec);
387       for my $user (qsearch( 'svc_acct', { 'username' => $username } )){
388         $suspend_user = $user if $userspec eq $user->email;
389       }
390     } elsif ($userspec) {
391       $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } );
392     }
393
394     @radius_groups = $suspend_user->radius_groups
395       if $suspend_user;  
396
397   } else {
398     $reasonnum = $reasontext = $reasontypenum = $reasontypetext = '';
399   }
400
401   $pkgnum = $cust_pkg ? $cust_pkg->pkgnum : '';
402   $custnum = $cust_pkg ? $cust_pkg->custnum : '';
403
404   my $stdin_string = eval(qq("$stdin"));
405   return "error filling in STDIN: $@" if $@;
406
407   $first = shell_quote $first;
408   $last = shell_quote $last;
409   $finger = shell_quote $finger;
410   $crypt_password = shell_quote $crypt_password;
411   $ldap_password  = shell_quote $ldap_password;
412
413   $company = shell_quote $company;
414   $address1 = shell_quote $address1;
415   $address2 = shell_quote $address2;
416   $city = shell_quote $city;
417   $state = shell_quote $state;
418   $zip = shell_quote $zip;
419   $country = shell_quote $country;
420   $daytime = shell_quote $daytime;
421   $night = shell_quote $night;
422   $fax = shell_quote $fax;
423   $otaker = shell_quote $otaker; 
424   $agent_custid = shell_quote $agent_custid;
425   $locale = shell_quote $locale;
426
427   my $command_string = eval(qq("$command"));
428   return "error filling in command: $@" if $@;
429
430   my @ssh_cmd_args = (
431     user          => $self->option('user') || 'root',
432     host          => $self->machine,
433     command       => $command_string,
434     stdin_string  => $stdin_string,
435     ignored_errors    => $self->option('ignored_errors') || '',
436     ignore_all_errors => $self->option('ignore_all_errors'),
437     fail_on_output    => $self->option('fail_on_output'),
438  );
439
440   if ( $self->option($action. '_no_queue') ) {
441     # discard return value just like freeside-queued.
442     eval { ssh_cmd(@ssh_cmd_args) };
443     $error = $@;
444     $error = $error->full_message if ref $error; # Exception::Class::Base
445     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
446       if $error;
447   } else {
448     $self->shellcommands_queue( $svc_acct->svcnum, @ssh_cmd_args );
449   }
450 }
451
452 sub _export_replace {
453   my($self, $new, $old ) = (shift, shift, shift);
454   my $command = $self->option('usermod');
455   return '' if $command =~ /^\s*$/;
456   my $stdin = $self->option('usermod_stdin');
457   no strict 'vars';
458   {
459     no strict 'refs';
460     ${"old_$_"} = $old->getfield($_) foreach $old->fields;
461     ${"new_$_"} = $new->getfield($_) foreach $new->fields;
462   }
463   my $old_cust_pkg = $old->cust_svc->cust_pkg;
464   my $new_cust_pkg = $new->cust_svc->cust_pkg;
465   my $new_cust_main = $new_cust_pkg ? $new_cust_pkg->cust_main : '';
466
467   $new_finger =~ /^(.*)\s+(\S+)$/ or $new_finger =~ /^((.*))$/;
468   ($new_first, $new_last ) = ( $1, $2 );
469   $quoted_new__password = shell_quote $new__password; #old, wrong?
470   $new_quoted_password = shell_quote $new__password; #new, better?
471   $old_domain = $old->domain;
472   $new_domain = $new->domain;
473
474   $new_crypt_password = $new->crypt_password( $self->option('crypt') );
475   $new_ldap_password  = $new->ldap_password(  $self->option('crypt') );
476
477   @old_radius_groups = $old->radius_groups;
478   @new_radius_groups = $new->radius_groups;
479
480   my $error = '';
481   if ( $self->option('usermod_pwonly') || $self->option('usermod_nousername') ){
482     if ( $old_username ne $new_username ) {
483       $error ||= "can't change username";
484     }
485   }
486   if ( $self->option('usermod_pwonly') ) {
487     if ( $old_domain ne $new_domain ) {
488       $error ||= "can't change domain";
489     }
490     if ( $old_uid != $new_uid ) {
491       $error ||= "can't change uid";
492     }
493     if ( $old_gid != $new_gid ) {
494       $error ||= "can't change gid";
495     }
496     if ( $old_dir ne $new_dir ) {
497       $error ||= "can't change dir";
498     }
499     #if ( join("\n", sort @old_radius_groups) ne
500     #     join("\n", sort @new_radius_groups)    ) {
501     #  $error ||= "can't change RADIUS groups";
502     #}
503   }
504   return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
505     if $error;
506
507   $new_agent_custid = $new_cust_main ? $new_cust_main->agent_custid : '';
508   $new_locale = $new_cust_main ? $new_cust_main->locale : '';
509   $old_pkgnum = $old_cust_pkg ? $old_cust_pkg->pkgnum : '';
510   $old_custnum = $old_cust_pkg ? $old_cust_pkg->custnum : '';
511   $new_pkgnum = $new_cust_pkg ? $new_cust_pkg->pkgnum : '';
512   $new_custnum = $new_cust_pkg ? $new_cust_pkg->custnum : '';
513
514   my $stdin_string = eval(qq("$stdin"));
515
516   $new_first = shell_quote $new_first;
517   $new_last = shell_quote $new_last;
518   $new_finger = shell_quote $new_finger;
519   $new_crypt_password = shell_quote $new_crypt_password;
520   $new_ldap_password  = shell_quote $new_ldap_password;
521   $new_agent_custid = shell_quote $new_agent_custid;
522   $new_locale = shell_quote $new_locale;
523
524   my $command_string = eval(qq("$command"));
525
526   my @ssh_cmd_args = (
527     user          => $self->option('user') || 'root',
528     host          => $self->machine,
529     command       => $command_string,
530     stdin_string  => $stdin_string,
531     ignored_errors    => $self->option('ignored_errors') || '',
532     ignore_all_errors => $self->option('ignore_all_errors'),
533     fail_on_output    => $self->option('fail_on_output'),
534   );
535
536   if($self->option('usermod_no_queue')) {
537     # discard return value just like freeside-queued.
538     eval { ssh_cmd(@ssh_cmd_args) };
539     $error = $@;
540     $error = $error->full_message if ref $error; # Exception::Class::Base
541     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
542       if $error;
543   }
544   else {
545     $self->shellcommands_queue( $new->svcnum, @ssh_cmd_args );
546   }
547 }
548
549 #a good idea to queue anything that could fail or take any time
550 sub shellcommands_queue {
551   my( $self, $svcnum ) = (shift, shift);
552   my $queue = new FS::queue {
553     'svcnum' => $svcnum,
554     'job'    => "FS::part_export::shellcommands::ssh_cmd",
555   };
556   $queue->insert( @_ );
557 }
558
559 sub ssh_cmd { #subroutine, not method
560   use Net::OpenSSH;
561   my $opt = { @_ };
562   open my $def_in, '<', '/dev/null' or die "unable to open /dev/null\n";
563   my $ssh = Net::OpenSSH->new(
564     $opt->{'user'}.'@'.$opt->{'host'},
565     'default_stdin_fh' => $def_in
566   );
567   # ignore_all_errors doesn't override SSH connection/auth errors--
568   # probably correct
569   die "Couldn't establish SSH connection: ". $ssh->error if $ssh->error;
570
571   my $ssh_opt = {};
572   $ssh_opt->{'stdin_data'} = $opt->{'stdin_string'}
573     if exists($opt->{'stdin_string'}) and length($opt->{'stdin_string'});
574
575   my ($output, $errput) = $ssh->capture2($ssh_opt, $opt->{'command'});
576
577   return if $opt->{'ignore_all_errors'};
578   die "Error running SSH command: ". $ssh->error if $ssh->error;
579
580   if ( ($output || $errput)
581        && $opt->{'ignored_errors'} && length($opt->{'ignored_errors'})
582   ) {
583     my @ignored_errors = split('\n',$opt->{'ignored_errors'});
584     foreach my $ignored_error ( @ignored_errors ) {
585         $output =~ s/$ignored_error//g;
586         $errput =~ s/$ignored_error//g;
587     }
588     $output =~ s/[\s\n]//g;
589     $errput =~ s/[\s\n]//g;
590   }
591
592   die "$errput\n" if $errput;
593   die "$output\n" if $output and $opt->{'fail_on_output'};
594   '';
595 }
596
597 #sub shellcommands_insert { #subroutine, not method
598 #}
599 #sub shellcommands_replace { #subroutine, not method
600 #}
601 #sub shellcommands_delete { #subroutine, not method
602 #}
603
604 sub _upgrade_exporttype {
605   my $class = shift;
606   $class =~ /^FS::part_export::(\w+)$/;
607   foreach my $self ( qsearch('part_export', { 'exporttype' => $1 }) ) {
608     my %options = $self->options;
609     my $changed = 0;
610     # 2011-12-13 - 2012-02-16: ignore_all_output option
611     if ( $options{'ignore_all_output'} ) {
612       # ignoring STDOUT is now the default
613       $options{'ignore_all_errors'} = 1;
614       delete $options{'ignore_all_output'};
615       $changed++;
616     }
617     my $error = $self->replace(%options) if $changed;
618     die $error if $error;
619   }
620 }
621
622 1;
623