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