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