1#!@PERL@
2# Copyright (c) 2009-2013 Zmanda, Inc.  All Rights Reserved.
3#
4# This program is free software; you can redistribute it and/or
5# modify it under the terms of the GNU General Public License
6# as published by the Free Software Foundation; either version 2
7# of the License, or (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12# for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program; if not, write to the Free Software Foundation, Inc.,
16# 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
17#
18# Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
19# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20
21use lib '@amperldir@';
22use strict;
23use warnings;
24use Getopt::Long;
25
26package Amanda::Application::Amsuntar;
27use base qw(Amanda::Application);
28use File::Copy;
29use File::Temp qw( tempfile );
30use File::Path;
31use IPC::Open2;
32use IPC::Open3;
33use Sys::Hostname;
34use Symbol;
35use Amanda::Constants;
36use Amanda::Config qw( :init :getconf  config_dir_relative );
37use Amanda::Debug qw( :logging );
38use Amanda::Paths;
39use Amanda::Util qw( :constants quote_string );
40
41sub new {
42    my $class = shift;
43    my ($config, $host, $disk, $device, $level, $index, $message, $collection, $record, $exclude_list, $exclude_optional,  $include_list, $include_optional, $bsize, $ext_attrib, $ext_header, $ignore, $normal, $strange, $error_exp, $directory, $suntar_path) = @_;
44    my $self = $class->SUPER::new($config);
45
46    $self->{suntar}            = $Amanda::Constants::SUNTAR;
47    if (defined $suntar_path) {
48	$self->{suntar}        = $suntar_path;
49    }
50    $self->{pfexec}            = "/usr/bin/pfexec";
51    $self->{gnutar}            = $Amanda::Constants::GNUTAR;
52    $self->{teecount}          = $Amanda::Paths::amlibexecdir."/teecount";
53
54    $self->{config}            = $config;
55    $self->{host}              = $host;
56    if (defined $disk) {
57	$self->{disk}          = $disk;
58    } else {
59	$self->{disk}          = $device;
60    }
61    if (defined $device) {
62	$self->{device}        = $device;
63    } else {
64	$self->{device}        = $disk;
65    }
66    $self->{level}             = $level;
67    $self->{index}             = $index;
68    $self->{message}           = $message;
69    $self->{collection}        = $collection;
70    $self->{record}            = $record;
71    $self->{exclude_list}      = [ @{$exclude_list} ];
72    $self->{exclude_optional}  = $exclude_optional;
73    $self->{include_list}      = [ @{$include_list} ];
74    $self->{include_optional}  = $include_optional;
75    $self->{block_size}        = $bsize;
76    $self->{extended_header}   = $ext_header;
77    $self->{extended_attrib}   = $ext_attrib;
78    $self->{directory}         = $directory;
79
80    $self->{regex} = ();
81    my $regex;
82    for $regex (@{$ignore}) {
83	my $a = { regex => $regex, type => "IGNORE" };
84	push @{$self->{regex}}, $a;
85    }
86
87    for $regex (@{$normal}) {
88	my $a = { regex => $regex, type => "NORMAL" };
89	push @{$self->{regex}}, $a;
90    }
91
92    for $regex (@{$strange}) {
93	my $a = { regex => $regex, type => "STRANGE" };
94	push @{$self->{regex}}, $a;
95    }
96
97    for $regex (@{$error_exp}) {
98	my $a = { regex => $regex, type => "ERROR" };
99	push @{$self->{regex}}, $a;
100    }
101
102    #type can be IGNORE/NORMAL/STRANGE/ERROR
103    push @{$self->{regex}}, { regex => "is not a file. Not dumped\$",
104			      type  => "NORMAL" };
105    push @{$self->{regex}}, { regex => "same as archive file\$",
106			      type  => "NORMAL" };
107    push @{$self->{regex}}, { regex => ": invalid character in UTF-8 conversion of ",
108			      type  => "STRANGE" };
109    push @{$self->{regex}}, { regex => ": UTF-8 conversion failed.\$",
110			      type  => "STRANGE" };
111    push @{$self->{regex}}, { regex => ": Permission denied\$",
112			      type  => "ERROR" };
113
114    for $regex (@{$self->{regex}}) {
115	debug ($regex->{type} . ": " . $regex->{regex});
116    }
117
118    return $self;
119}
120
121sub command_support {
122   my $self = shift;
123
124   print "CONFIG YES\n";
125   print "HOST YES\n";
126   print "DISK YES\n";
127   print "MAX-LEVEL 0\n";
128   print "INDEX-LINE YES\n";
129   print "INDEX-XML NO\n";
130   print "MESSAGE-LINE YES\n";
131   print "MESSAGE-XML NO\n";
132   print "RECORD YES\n";
133   print "EXCLUDE-FILE NO\n";
134   print "EXCLUDE-LIST YES\n";
135   print "EXCLUDE-OPTIONAL YES\n";
136   print "INCLUDE-FILE NO\n";
137   print "INCLUDE-LIST YES\n";
138   print "INCLUDE-OPTIONAL YES\n";
139   print "COLLECTION NO\n";
140   print "MULTI-ESTIMATE NO\n";
141   print "CALCSIZE NO\n";
142   print "CLIENT-ESTIMATE YES\n";
143}
144
145sub command_selfcheck {
146   my $self = shift;
147
148   $self->print_to_server("disk " . quote_string($self->{disk}));
149
150   $self->print_to_server("amsuntar version " . $Amanda::Constants::VERSION,
151			  $Amanda::Script_App::GOOD);
152
153   if (!-e $self->{suntar}) {
154      $self->print_to_server_and_die(
155		       "application binary $self->{suntar} doesn't exist",
156                       $Amanda::Script_App::ERROR);
157   }
158   if (!-x $self->{suntar}) {
159      $self->print_to_server_and_die(
160                       "application binary $self->{suntar} is not a executable",
161                       $Amanda::Script_App::ERROR);
162   }
163   if (!defined $self->{disk} || !defined $self->{device}) {
164      return;
165   }
166   print "OK " . $self->{device} . "\n";
167   print "OK " . $self->{directory} . "\n" if defined $self->{directory};
168   $self->validate_inexclude();
169}
170
171sub command_estimate() {
172    my $self = shift;
173    my $size = "-1";
174    my $level = $self->{level};
175
176    $self->{index} = undef;	#remove verbose flag to suntar.
177    my(@cmd) = $self->build_command();
178    my(@cmdwc) = ("/usr/bin/wc", "-c");
179
180    debug("cmd:" . join(" ", @cmd) . " | " . join(" ", @cmdwc));
181    my($wtr, $rdr, $err, $pid, $rdrwc, $pidwc);
182    $err = Symbol::gensym;
183    $pid = open3($wtr, \*DATA, $err, @cmd);
184    $pidwc = open2($rdrwc, '>&DATA', @cmdwc);
185    close $wtr;
186
187    my $errmsg;
188    my $result = 0;
189    while (<$err>) {
190	my $matched = 0;
191	for my $regex (@{$self->{regex}}) {
192	    my $regex1 = $regex->{regex};
193	    if (/$regex1/) {
194		$result = 1 if ($regex->{type} eq "ERROR");
195		$matched = 1;
196		last;
197	    }
198	}
199	$result = 1 if ($matched == 0);
200	$errmsg = $_ if (!defined $errmsg);
201    }
202    my ($msgsize) = <$rdrwc>;
203    waitpid $pid, 0;
204    close $rdrwc;
205    close $err;
206    if ($result ==  1) {
207        if (defined $errmsg) {
208            $self->print_to_server_and_die($errmsg, $Amanda::Script_App::ERROR);
209        } else {
210                $self->print_to_server_and_die(
211			"cannot estimate archive size': unknown reason",
212			$Amanda::Script_App::ERROR);
213        }
214    }
215    output_size($level, $msgsize);
216    exit 0;
217}
218
219
220sub output_size {
221   my($level) = shift;
222   my($size) = shift;
223   if($size == -1) {
224      print "$level -1 -1\n";
225      #exit 2;
226   }
227   else {
228      my($ksize) = int $size / (1024);
229      $ksize=32 if ($ksize<32);
230      print "$level $ksize 1\n";
231   }
232}
233
234sub command_backup {
235   my $self = shift;
236
237   $self->validate_inexclude();
238
239   my(@cmd) = $self->build_command();
240   my(@cmdtc) = $self->{teecount};
241
242   debug("cmd:" . join(" ", @cmd) . " | " . join(" ", @cmdtc));
243
244   my($wtr, $pid, $rdrtc, $errtc, $pidtc);
245   my $index_fd = Symbol::gensym;
246   $errtc = Symbol::gensym;
247
248   $pid = open3($wtr, \*DATA, $index_fd, @cmd) ||
249      $self->print_to_server_and_die("Can't run $cmd[0]: $!",
250				     $Amanda::Script_App::ERROR);
251   $pidtc = open3('<&DATA', '>&STDOUT', $errtc, @cmdtc) ||
252      $self->print_to_server_and_die("Can't run $cmdtc[0]: $!",
253				     $Amanda::Script_App::ERROR);
254   close($wtr);
255
256   unlink($self->{include_tmp}) if defined $self->{include_tmp} and -e $self->{include_tmp};
257   unlink($self->{exclude_tmp}) if defined $self->{exclude_tmp} and -e $self->{exclude_tmp};
258
259   my $result;
260   if(defined($self->{index})) {
261      my $indexout_fd;
262      open($indexout_fd, '>&=4') ||
263      $self->print_to_server_and_die("Can't open indexout_fd: $!",
264				     $Amanda::Script_App::ERROR);
265      $result = $self->parse_backup($index_fd, $self->{mesgout}, $indexout_fd);
266      close($indexout_fd);
267   }
268   else {
269      $result = $self->parse_backup($index_fd, $self->{mesgout}, undef);
270   }
271   close($index_fd);
272   my $size = <$errtc>;
273
274   waitpid $pid, 0;
275
276   my $status = $?;
277   if( $status != 0 ){
278       debug("exit status $status ?" );
279   }
280
281   if ($result == 1) {
282       debug("$self->{suntar} returned error" );
283       $self->print_to_server("$self->{suntar} returned error",
284			      $Amanda::Script_App::ERROR);
285   }
286
287   my($ksize) = int ($size/1024);
288   print {$self->{mesgout}} "sendbackup: size $ksize\n";
289   print {$self->{mesgout}} "sendbackup: end\n";
290   debug("sendbackup: size $ksize ");
291
292   exit 0;
293}
294
295sub parse_backup {
296   my $self = shift;
297   my($fhin, $fhout, $indexout) = @_;
298   my $size  = -1;
299   my $result = 0;
300   while(<$fhin>) {
301      if ( /^ ?a\s+(\.\/.*) \d*K/ ||
302	   /^a\s+(\.\/.*) symbolic link to/ ||
303	   /^a\s+(\.\/.*) link to/ ) {
304	 my $name = $1;
305         if(defined($indexout)) {
306	    if(defined($self->{index})) {
307               $name =~ s/^\.//;
308               print $indexout $name, "\n";
309	    }
310         }
311      }
312      else {
313	 my $matched = 0;
314	 for my $regex (@{$self->{regex}}) {
315	    my $regex1 = $regex->{regex};
316	    if (/$regex1/) {
317	       $result = 1 if ($regex->{type} eq "ERROR");
318	       if (defined($fhout)) {
319	          if ($regex->{type} eq "IGNORE") {
320	          } elsif ($regex->{type} eq "NORMAL") {
321		     print $fhout "| $_";
322	          } elsif ($regex->{type} eq "STRANGE") {
323		     print $fhout "? $_";
324	          } else {
325		     print $fhout "? $_";
326	          }
327	       }
328	       $matched = 1;
329	       last;
330	    }
331	 }
332	 if ($matched == 0) {
333	    $result = 1;
334	    if (defined($fhout)) {
335               print $fhout "? $_";
336	    }
337	 }
338      }
339   }
340   return $result;
341}
342
343sub validate_inexclude {
344   my $self = shift;
345   my $fh;
346   my @tmp;
347
348   if ($#{$self->{exclude_list}} >= 0 && $#{$self->{include_list}} >= 0 )  {
349      $self->print_to_server_and_die("Can't have both include and exclude",
350                                     $Amanda::Script_App::ERROR);
351   }
352
353   foreach my $file (@{$self->{exclude_list}}){
354      if (!open($fh, $file)) {
355          if ($self->{action} eq "check" && !$self->{exclude_optional}) {
356                $self->print_to_server("Open of '$file' failed: $!",
357                                       $Amanda::Script_App::ERROR);
358          }
359          next;
360      }
361      while (<$fh>) {
362          push @tmp, $_;
363      }
364      close($fh);
365   }
366
367   #Merging list into a single file
368   if($self->{action} eq 'backup' && $#{$self->{exculde_list}} >= 0) {
369      ($fh, $self->{exclude_tmp}) = tempfile(DIR => $Amanda::paths::AMANDA_TMPDIR);
370      unless($fh) {
371                $self->print_to_server_and_die(
372                          "Open of tmp file '$self->{exclude_tmp}' failed: $!",
373                          $Amanda::Script_App::ERROR);
374      }
375      print $fh @tmp;
376      close $fh;
377      undef (@tmp);
378   }
379
380   foreach my $file (@{$self->{include_list}}) {
381      if (!open($fh, $file)) {
382         if ($self->{action} eq "check" && !$self->{include_optional}) {
383                $self->print_to_server("Open of '$file' failed: $!",
384                                       $Amanda::Script_App::ERROR);
385         }
386         next;
387      }
388      while (<$fh>) {
389         push @tmp, $_;
390      }
391      close($fh);
392   }
393
394   if($self->{action} eq 'backup' && $#{$self->{include_list}} >= 0) {
395      ($fh, $self->{include_tmp}) = tempfile(DIR => $Amanda::paths::AMANDA_TMPDIR);
396      unless($fh) {
397                $self->print_to_server_and_die(
398                          "Open of tmp file '$self->{include_tmp}' failed: $!",
399                          $Amanda::Script_App::ERROR);
400      }
401      print $fh @tmp;
402      close $fh;
403      undef (@tmp);
404   }
405}
406
407sub command_index_from_output {
408   index_from_output(0, 1);
409   exit 0;
410}
411
412sub index_from_output {
413   my($fhin, $fhout) = @_;
414   my($size) = -1;
415   while(<$fhin>) {
416      next if /^Total bytes written:/;
417      next if !/^\.\//;
418      s/^\.//;
419      print $fhout $_;
420   }
421}
422
423sub command_index_from_image {
424   my $self = shift;
425   my $index_fd;
426   open($index_fd, "$self->{suntar} -tf - |") ||
427      $self->print_to_server_and_die("Can't run $self->{suntar}: $!",
428				     $Amanda::Script_App::ERROR);
429   index_from_output($index_fd, 1);
430}
431
432sub command_restore {
433   my $self = shift;
434
435   chdir(Amanda::Util::get_original_cwd());
436   if (defined $self->{directory}) {
437      if (!-d $self->{directory}) {
438         $self->print_to_server_and_die("Directory $self->{directory}: $!",
439				        $Amanda::Script_App::ERROR);
440      }
441      if (!-w $self->{directory}) {
442         $self->print_to_server_and_die("Directory $self->{directory}: $!",
443				        $Amanda::Script_App::ERROR);
444      }
445      chdir($self->{directory});
446   }
447
448   my $cmd = "-xpv";
449
450   if($self->{extended_header} eq "YES") {
451      $cmd .= "E";
452   }
453   if($self->{extended_attrib} eq "YES") {
454      $cmd .= "\@";
455   }
456
457   $cmd .= "f";
458
459   if (defined($self->{exclude_list}) && defined($self->{exclude_list}[0]) && (-e $self->{exclude_list}[0])) {
460      $cmd .= "X";
461   }
462
463   my(@cmd) = ($self->{pfexec},$self->{suntar}, $cmd);
464
465   push @cmd, "-";  # for f argument
466   if (defined($self->{exclude_list}) && defined($self->{exclude_list}[0]) && (-e $self->{exclude_list}[0])) {
467      push @cmd, $self->{exclude_list}[0]; # for X argument
468   }
469
470   if(defined($self->{include_list}) && defined($self->{include_list}[0]) && (-e $self->{include_list}[0]))  {
471      push @cmd, "-I", $self->{include_list}[0];
472   }
473
474   for(my $i=1;defined $ARGV[$i]; $i++) {
475      my $param = $ARGV[$i];
476      $param =~ /^(.*)$/;
477      push @cmd, $1;
478   }
479   debug("cmd:" . join(" ", @cmd));
480   exec { $cmd[0] } @cmd;
481   die("Can't exec '", $cmd[0], "': $!");
482}
483
484sub command_validate {
485   my $self = shift;
486   my @cmd;
487   my $program;
488
489   if (-e $self->{suntar}) {
490      $program = $self->{suntar};
491   } elsif (-e $self->{gnutar}) {
492      $program = $self->{gnutar};
493   } else {
494      return $self->default_validate();
495   }
496   @cmd = ($program, "-tf", "-");
497   debug("cmd:" . join(" ", @cmd));
498   my $pid = open3('>&STDIN', '>&STDOUT', '>&STDERR', @cmd) ||
499      $self->print_to_server_and_die("Unable to run @cmd",
500				     $Amanda::Script_App::ERROR);
501   waitpid $pid, 0;
502   if( $? != 0 ){
503	$self->print_to_server_and_die("$program returned error",
504				       $Amanda::Script_App::ERROR);
505   }
506   exit(0);
507}
508
509sub build_command {
510  my $self = shift;
511
512   #Careful sun tar options and ordering is very very tricky
513
514   my($cmd) = "-cp";
515   my(@optparams) = ();
516
517   $self->validate_inexclude();
518
519   if($self->{extended_header} =~ /^YES$/i) {
520      $cmd .= "E";
521   }
522   if($self->{extended_attrib} =~ /^YES$/i) {
523      $cmd .= "\@";
524   }
525   if(defined($self->{index})) {
526      $cmd .= "v";
527   }
528
529   if(defined($self->{block_size})) {
530      $cmd .= "b";
531      push @optparams, $self->{block_size};
532   }
533
534   if (defined($self->{exclude_tmp})) {
535      $cmd .= "fX";
536      push @optparams,"-",$self->{exclude_tmp};
537   } else {
538      $cmd .= "f";
539      push @optparams,"-";
540   }
541   if ($self->{directory}) {
542      push @optparams, "-C", $self->{directory};
543   } else {
544      push @optparams, "-C", $self->{device};
545   }
546
547   if(defined($self->{include_tmp}))  {
548      push @optparams,"-I", $self->{include_tmp};
549   } else {
550      push @optparams,".";
551   }
552
553   my(@cmd) = ($self->{pfexec}, $self->{suntar}, $cmd, @optparams);
554   return (@cmd);
555}
556
557package main;
558
559sub usage {
560    print <<EOF;
561Usage: Amsuntar <command> --config=<config> --host=<host> --disk=<disk> --device=<device> --level=<level> --index=<yes|no> --message=<text> --collection=<no> --record=<yes|no> --exclude-list=<fileList> --include-list=<fileList> --block-size=<size> --extended_attributes=<yes|no> --extended_headers<yes|no> --ignore=<regex> --normal=<regex> --strange=<regex> --error=<regex> --lang=<lang>.
562EOF
563    exit(1);
564}
565
566my $opt_config;
567my $opt_host;
568my $opt_disk;
569my $opt_device;
570my $opt_level;
571my $opt_index;
572my $opt_message;
573my $opt_collection;
574my $opt_record;
575my @opt_exclude_list;
576my $opt_exclude_optional;
577my @opt_include_list;
578my $opt_include_optional;
579my $opt_bsize = 256;
580my $opt_ext_attrib = "YES";
581my $opt_ext_head   = "YES";
582my @opt_ignore;
583my @opt_normal;
584my @opt_strange;
585my @opt_error;
586my $opt_lang;
587my $opt_directory;
588my $opt_suntar_path;
589
590my @orig_argv = @ARGV;
591
592Getopt::Long::Configure(qw{bundling});
593GetOptions(
594    'config=s'     	  => \$opt_config,
595    'host=s'       	  => \$opt_host,
596    'disk=s'       	  => \$opt_disk,
597    'device=s'     	  => \$opt_device,
598    'level=s'      	  => \$opt_level,
599    'index=s'      	  => \$opt_index,
600    'message=s'    	  => \$opt_message,
601    'collection=s' 	  => \$opt_collection,
602    'exclude-list=s'      => \@opt_exclude_list,
603    'exclude-optional=s'  => \$opt_exclude_optional,
604    'include-list=s'      => \@opt_include_list,
605    'include-optional=s'  => \$opt_include_optional,
606    'record'       	  => \$opt_record,
607    'block-size=s'        => \$opt_bsize,
608    'extended-attributes=s'  => \$opt_ext_attrib,
609    'extended-headers=s'     => \$opt_ext_head,
610    'ignore=s'               => \@opt_ignore,
611    'normal=s'               => \@opt_normal,
612    'strange=s'              => \@opt_strange,
613    'error=s'                => \@opt_error,
614    'lang=s'                 => \$opt_lang,
615    'directory=s'            => \$opt_directory,
616    'suntar-path=s'          => \$opt_suntar_path,
617) or usage();
618
619if (defined $opt_lang) {
620    $ENV{LANG} = $opt_lang;
621}
622
623my $application = Amanda::Application::Amsuntar->new($opt_config, $opt_host, $opt_disk, $opt_device, $opt_level, $opt_index, $opt_message, $opt_collection, $opt_record, \@opt_exclude_list, $opt_exclude_optional, \@opt_include_list, $opt_include_optional,$opt_bsize,$opt_ext_attrib,$opt_ext_head, \@opt_ignore, \@opt_normal, \@opt_strange, \@opt_error, $opt_directory, $opt_suntar_path);
624
625Amanda::Debug::debug("Arguments: " . join(' ', @orig_argv));
626
627$application->do($ARGV[0]);
628