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