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