Imported Upstream version 3.3.3
[debian/amanda] / application-src / amsuntar.pl
1 #!@PERL@
2 # Copyright (c) 2009-2012 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
21 use lib '@amperldir@';
22 use strict;
23 use warnings;
24 use Getopt::Long;
25
26 package Amanda::Application::Amsuntar;
27 use base qw(Amanda::Application);
28 use File::Copy;
29 use File::Temp qw( tempfile );
30 use File::Path;
31 use IPC::Open2;
32 use IPC::Open3;
33 use Sys::Hostname;
34 use Symbol;
35 use Amanda::Constants;
36 use Amanda::Config qw( :init :getconf  config_dir_relative );
37 use Amanda::Debug qw( :logging );
38 use Amanda::Paths;
39 use Amanda::Util qw( :constants quote_string );
40
41 sub 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
121 sub 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
145 sub 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
171 sub 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 ($msgsize) = <$rdrwc>;
188     my $errmsg;
189     my $result;
190     while (<$err>) {
191         my $matched = 0;
192         for my $regex (@{$self->{regex}}) {
193             my $regex1 = $regex->{regex};
194             if (/$regex1/) {
195                 $result = 1 if ($regex->{type} eq "ERROR");
196                 $matched = 1;
197                 last;
198             }
199         }
200         $result = 1 if ($matched == 0);
201         $errmsg = $_ if (!defined $errmsg);
202     }
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
220 sub 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
234 sub 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(-e $self->{include_tmp});
257    unlink($self->{exclude_tmp}) if(-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
295 sub 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
343 sub 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
407 sub command_index_from_output {
408    index_from_output(0, 1);
409    exit 0;
410 }
411
412 sub 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
423 sub 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
432 sub 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}) && (-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}) && (-e $self->{exclude_list}[0])) {
467       push @cmd, $self->{exclude_list}[0]; # for X argument
468    }
469
470    if(defined($self->{include_list}) && (-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
484 sub 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
509 sub 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
557 package main;
558
559 sub usage {
560     print <<EOF;
561 Usage: 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>.
562 EOF
563     exit(1);
564 }
565
566 my $opt_config;
567 my $opt_host;
568 my $opt_disk;
569 my $opt_device;
570 my $opt_level;
571 my $opt_index;
572 my $opt_message;
573 my $opt_collection;
574 my $opt_record;
575 my @opt_exclude_list;
576 my $opt_exclude_optional;
577 my @opt_include_list;
578 my $opt_include_optional;
579 my $opt_bsize = 256;
580 my $opt_ext_attrib = "YES";
581 my $opt_ext_head   = "YES";
582 my @opt_ignore;
583 my @opt_normal;
584 my @opt_strange;
585 my @opt_error;
586 my $opt_lang;
587 my $opt_directory;
588 my $opt_suntar_path;
589
590 Getopt::Long::Configure(qw{bundling});
591 GetOptions(
592     'config=s'            => \$opt_config,
593     'host=s'              => \$opt_host,
594     'disk=s'              => \$opt_disk,
595     'device=s'            => \$opt_device,
596     'level=s'             => \$opt_level,
597     'index=s'             => \$opt_index,
598     'message=s'           => \$opt_message,
599     'collection=s'        => \$opt_collection,
600     'exclude-list=s'      => \@opt_exclude_list,
601     'exclude-optional=s'  => \$opt_exclude_optional,
602     'include-list=s'      => \@opt_include_list,
603     'include-optional=s'  => \$opt_include_optional,
604     'record'              => \$opt_record,
605     'block-size=s'        => \$opt_bsize,
606     'extended-attributes=s'  => \$opt_ext_attrib,
607     'extended-headers=s'     => \$opt_ext_head,
608     'ignore=s'               => \@opt_ignore,
609     'normal=s'               => \@opt_normal,
610     'strange=s'              => \@opt_strange,
611     'error=s'                => \@opt_error,
612     'lang=s'                 => \$opt_lang,
613     'directory=s'            => \$opt_directory,
614     'suntar-path=s'          => \$opt_suntar_path,
615 ) or usage();
616
617 if (defined $opt_lang) {
618     $ENV{LANG} = $opt_lang;
619 }
620
621 my $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);
622
623 $application->do($ARGV[0]);