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