#! @PERL@
# Copyright (c) 2007-2012 Zmanda, Inc. All Rights Reserved.
# Copyright (c) 2013-2016 Carbonite, Inc. All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Contact information: Carbonite Inc., 756 N Pastoria Ave
# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
use lib '@amperldir@';
use strict;
use warnings;
use File::Basename;
use Getopt::Long;
use IPC::Open3;
use Symbol;
use Amanda::Device qw( :constants );
use Amanda::Debug qw( :logging );
use Amanda::Config qw( :init :getconf config_dir_relative );
use Amanda::Tapelist;
use Amanda::Logfile;
use Amanda::Util qw( :constants );
use Amanda::Storage;
use Amanda::Changer;
use Amanda::Recovery::Clerk;
use Amanda::Recovery::Scan;
use Amanda::Recovery::Planner;
use Amanda::Constants;
use Amanda::DB::Catalog;
use Amanda::Cmdline;
use Amanda::MainLoop;
use Amanda::Xfer qw( :constants );
use XML::Simple;
use MIME::Base64 ();
sub usage {
print <<EOF;
USAGE: amreindex [ --timestamp|-t timestamp ] [-o configoption]* <conf>
USAGE: amreindex [-o configoption]* [--exact-match] <conf> [hostname [diskname [datestamp [hostname [diskname [datestamp ... ]]]]]]
amreindex re-index Amanda dump images by reading them from storage volume(s)
Arguments:
config - The Amanda configuration name to use.
-t timestamp - The run of amdump or amflush to check. By default, check
the most recent dump; if this parameter is specified,
check the most recent dump matching the given
date or timestamp.
--exact_match - host, disk and datestamp must match exactly.
-o configoption - see the CONFIGURATION OVERRIDE section of amanda(8)
EOF
exit(1);
}
## Application initialization
Amanda::Util::setup_application("amreindex", "server", $CONTEXT_CMDLINE, "amanda", "amanda");
my $exit_code = 0;
my $opt_timestamp;
my $opt_verbose = 0;
my $opt_exact_match = 0;
my @opt_dumpspecs;
my $config_overrides = new_config_overrides($#ARGV+1);
debug("Arguments: " . join(' ', @ARGV));
Getopt::Long::Configure(qw(bundling));
GetOptions(
'version' => \&Amanda::Util::version_opt,
'timestamp|t=s' => \$opt_timestamp,
'verbose|v' => \$opt_verbose,
'help|usage|?' => \&usage,
'exact-match' => \$opt_exact_match,
'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
) or usage();
usage() if (@ARGV < 1);
my $config_name = shift @ARGV;
if ($opt_timestamp and @ARGV) {
print STDERR "Can't specify both a timestamp and a host\n";
usage();
}
#my $cmd_flags = $Amanda::Cmdline::CMDLINE_PARSE_DATESTAMP |
# $Amanda::Cmdline::CMDLINE_PARSE_LEVEL;
my $cmd_flags = $Amanda::Cmdline::CMDLINE_PARSE_DATESTAMP;
$cmd_flags |= $Amanda::Cmdline::CMDLINE_EXACT_MATCH if $opt_exact_match;
@opt_dumpspecs = Amanda::Cmdline::parse_dumpspecs([@ARGV], $cmd_flags);
my $timestamp = $opt_timestamp;
set_config_overrides($config_overrides);
config_init_with_global($CONFIG_INIT_EXPLICIT_NAME, $config_name);
my ($cfgerr_level, @cfgerr_errors) = config_errors();
if ($cfgerr_level >= $CFGERR_WARNINGS) {
config_print_errors();
if ($cfgerr_level >= $CFGERR_ERRORS) {
die("errors processing config file");
}
}
Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
# Interactivity package
package Amanda::Interactivity::amreindex;
use POSIX qw( :errno_h );
use Amanda::MainLoop qw( :GIOCondition );
use vars qw( @ISA );
@ISA = qw( Amanda::Interactivity );
sub new {
my $class = shift;
my $self = {
input_src => undef};
return bless ($self, $class);
}
sub abort() {
my $self = shift;
if ($self->{'input_src'}) {
$self->{'input_src'}->remove();
$self->{'input_src'} = undef;
}
}
sub user_request {
my $self = shift;
my %params = @_;
my $buffer = "";
my $message = $params{'message'};
my $label = $params{'label'};
my $err = $params{'err'};
my $chg_name = $params{'chg_name'};
my $data_in = sub {
my $b;
my $n_read = POSIX::read(0, $b, 1);
if (!defined $n_read) {
return if ($! == EINTR);
$self->abort();
return $params{'request_cb'}->(
Amanda::Changer::Error->new('fatal',
message => "Fail to read from stdin"));
} elsif ($n_read == 0) {
$self->abort();
return $params{'request_cb'}->(
Amanda::Changer::Error->new('fatal',
message => "Aborted by user"));
} else {
$buffer .= $b;
if ($b eq "\n") {
my $line = $buffer;
chomp $line;
$buffer = "";
$self->abort();
return $params{'request_cb'}->(undef, $line);
}
}
};
print STDERR "$err\n";
print STDERR "Insert volume labeled '$label' in $chg_name\n";
print STDERR "and press enter, or ^D to abort.\n";
$self->{'input_src'} = Amanda::MainLoop::fd_source(0, $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
$self->{'input_src'}->set_callback($data_in);
return;
};
package main::Feedback;
use Amanda::Recovery::Clerk;
use parent -norequire, 'Amanda::Recovery::Clerk::Feedback';
use Amanda::MainLoop;
sub new {
my $class = shift;
my ($chg, $dev_name) = @_;
return bless {
chg => $chg,
dev_name => $dev_name,
}, $class;
}
sub clerk_notif_part {
my $self = shift;
my ($label, $filenum, $header) = @_;
print STDERR "Reading volume $label file $filenum\n";
}
sub clerk_notif_holding {
my $self = shift;
my ($filename, $header) = @_;
print STDERR "Reading '$filename'\n";
}
package main;
use Amanda::MainLoop qw( :GIOCondition );
# Given a dumpfile_t, figure out the command line to reindex, specified
# as an argv array
sub find_index_command {
my ($header) = @_;
my @result = ();
# We base the actual archiver on our own table
my $program = uc(basename($header->{program}));
my $validation_program;
if ($program ne "APPLICATION") {
debug("Can't reindex '$program'");
return undef;
} else {
if (!defined $header->{application}) {
warning("Application not set");
return undef;
}
my $program_path = $Amanda::Paths::APPLICATION_DIR . "/" .
$header->{application};
if (!-x $program_path) {
debug("Application '" . $header->{application}.
"($program_path)' not available on the server");
return undef;
} else {
my $dle_str = $header->{'dle_str'};
my $p1 = XML::Simple->new();
my $dle;
if (defined $dle_str) {
eval {$dle = $p1->XMLin($dle_str); };
if ($@) {
print "ERROR: XML error\n";
debug("XML Error: $@\n$dle_str");
}
if (defined $dle->{'diskdevice'} and UNIVERSAL::isa( $dle->{'diskdevice'}, "HASH" )) {
$dle->{'diskdevice'} = MIME::Base64::decode($dle->{'diskdevice'}->{'raw'});
}
}
my @argv;
push @argv, $program_path, "index",
"--config", get_config_name(),
"--host" , $header->{'name'},
"--disk" , $header->{'disk'},
"--level", $header->{'dumplevel'};
if ($dle) {
push @argv, "--device", $dle->{'diskdevice'} if defined $dle->{'diskdevice'};
}
return [ @argv ];
}
}
}
sub main {
my ($finished_cb) = @_;
my $tapelist;
my $interactivity;
my %scan;
my $clerk;
my %clerk;
my %storage;
my $plan;
my $timestamp;
my $all_success = 1;
my @xfer_errs;
my %all_filter;
my $current_dump;
my $recovery_done;
my %recovery_params;
my $xfer_src;
my $xfer_dest;
my $xfer_dest_native;
my $hdr;
my $source_crc;
my $dest_crc;
my $dest_native_crc;
my $steps = define_steps
cb_ref => \$finished_cb,
finalize => sub { foreach my $name (keys %storage) {
$storage{$name}->quit();
}
};
step start => sub {
# set up the tapelist
my $tapelist_file = config_dir_relative(getconf($CNF_TAPELIST));
($tapelist, my $message) = Amanda::Tapelist->new($tapelist_file);
return $steps->{'quit'}->($message) if defined $message;
# get the timestamp
$timestamp = $opt_timestamp;
$timestamp = Amanda::DB::Catalog::get_latest_write_timestamp()
if !defined $opt_timestamp and @opt_dumpspecs == 0;
# make an interactivity plugin
$interactivity = Amanda::Interactivity::amreindex->new();
# make a changer
my ($storage) = Amanda::Storage->new(tapelist => $tapelist);
return $steps->{'quit'}->($storage) if $storage->isa("Amanda::Message");
$storage{$storage->{"storage_name"}} = $storage;
my $chg = $storage->{'chg'};
return $steps->{'quit'}->($chg) if $chg->isa("Amanda::Message");
# make a scan
my $scan = Amanda::Recovery::Scan->new(
chg => $chg,
interactivity => $interactivity);
return $steps->{'quit'}->($scan)
if $scan->isa("Amanda::Changer::Error");
$scan{$storage->{"storage_name"}} = $scan;
# make a clerk
my $clerk = Amanda::Recovery::Clerk->new(
feedback => main::Feedback->new($chg),
scan => $scan{$storage->{"storage_name"}});
$clerk{$storage->{"storage_name"}} = $clerk;
# make a plan
my @spec;
if ($timestamp) {
@spec = [ Amanda::Cmdline::dumpspec_t->new(undef, undef, undef, undef, $timestamp) ];
} else {
@spec = [ @opt_dumpspecs ];
}
my $storage_list;
my $only_in_storage = 0;
if (getconf_linenum($CNF_STORAGE) == -2) {
$storage_list = getconf($CNF_STORAGE);
$only_in_storage = 1;
}
Amanda::Recovery::Planner::make_plan(
dumpspecs => @spec,
storage_list => $storage_list,
only_in_storage => $only_in_storage,
changer => $chg,
all_copy => 1,
status => 'OK',
plan_cb => $steps->{'plan_cb'});
};
step plan_cb => sub {
(my $err, $plan) = @_;
$steps->{'quit'}->($err) if $err;
my @tapes = $plan->get_volume_list();
my @holding = $plan->get_holding_file_list();
if (!@tapes && !@holding) {
print "Could not find any matching dumps.\n";
return $steps->{'quit'}->();
}
if (@tapes) {
printf("You will need the following volume%s: %s\n", (@tapes > 1) ? "s" : "",
join(", ", map { $_->{'label'} } @tapes));
}
if (@holding) {
printf("You will need the following holding file%s: %s\n", (@tapes > 1) ? "s" : "",
join(", ", @holding));
}
# nothing else is going on right now, so a blocking "Press enter.." is OK
print "Press enter when ready\n";
<STDIN>;
my $dump = shift @{$plan->{'dumps'}};
if (!$dump) {
return $steps->{'quit'}->("No backup written on timestamp $timestamp.");
}
$steps->{'check_dumpfile'}->($dump);
};
step check_dumpfile => sub {
my ($dump) = @_;
$current_dump = $dump;
$recovery_done = 0;
%recovery_params = ();
print "Re-indexing image " . $dump->{hostname} . ":" .
$dump->{diskname} . " dumped " . $dump->{dump_timestamp} . " level ".
$dump->{level};
if ($dump->{'nparts'} > 1) {
print " ($dump->{nparts} parts)";
}
print "\n";
my $storage_name = $dump->{'storage'};
if (!$storage{$storage_name}) {
my ($storage) = Amanda::Storage->new(storage_name => $storage_name,
tapelist => $tapelist);
#return $steps->{'quit'}->($storage) if $storage->isa("Amanda::Changer::Error");
$storage{$storage_name} = $storage;
};
my $chg = $storage{$storage_name}->{'chg'};
if (!$scan{$storage_name}) {
my $scan = Amanda::Recovery::Scan->new(
chg => $chg,
interactivity => $interactivity);
#return $steps->{'quit'}->($scan)
# if $scan{$storage->{"storage_name"}}->isa("Amanda::Changer::Error");
$scan{$storage_name} = $scan;
};
if (!$clerk{$storage_name}) {
my $clerk = Amanda::Recovery::Clerk->new(
feedback => main::Feedback->new($chg),
scan => $scan{$storage_name});
$clerk{$storage_name} = $clerk;
};
$clerk = $clerk{$storage_name};
@xfer_errs = ();
$clerk->get_xfer_src(
dump => $dump,
xfer_src_cb => $steps->{'xfer_src_cb'});
};
step xfer_src_cb => sub {
(my $errs, $hdr, $xfer_src, my $directtcp_supported) = @_;
return $steps->{'quit'}->(join("; ", @$errs)) if $errs;
# Write the header in the index directory
my $header_fname = Amanda::Logfile::getheaderfname(
$current_dump->{hostname}, $current_dump->{diskname},
$current_dump->{dump_timestamp}, $current_dump->{level});
my $header_file;
open($header_file, ">", $header_fname);
print {$header_file} $hdr->to_string(32, 65536);
close($header_file);
# set up any filters that need to be applied; decryption first
my @filters;
if ($hdr->{'encrypted'}) {
if ($hdr->{'srv_encrypt'}) {
push @filters,
Amanda::Xfer::Filter::Process->new(
[ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0, 0, 0, 0);
} elsif ($hdr->{'clnt_encrypt'}) {
push @filters,
Amanda::Xfer::Filter::Process->new(
[ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0, 0, 0, 0);
} else {
return $steps->quit("could not decrypt encrypted dump: no program specified");
}
$hdr->{'encrypted'} = 0;
$hdr->{'srv_encrypt'} = '';
$hdr->{'srv_decrypt_opt'} = '';
$hdr->{'clnt_encrypt'} = '';
$hdr->{'clnt_decrypt_opt'} = '';
$hdr->{'encrypt_suffix'} = 'N';
}
if ($hdr->{'compressed'}) {
# need to uncompress this file
if ($hdr->{'srvcompprog'}) {
# TODO: this assumes that srvcompprog takes "-d" to decrypt
push @filters,
Amanda::Xfer::Filter::Process->new(
[ $hdr->{'srvcompprog'}, "-d" ], 0, 0, 0, 0);
} elsif ($hdr->{'clntcompprog'}) {
# TODO: this assumes that clntcompprog takes "-d" to decrypt
push @filters,
Amanda::Xfer::Filter::Process->new(
[ $hdr->{'clntcompprog'}, "-d" ], 0, 0, 0, 0);
} else {
push @filters,
Amanda::Xfer::Filter::Process->new(
[ $Amanda::Constants::UNCOMPRESS_PATH,
$Amanda::Constants::UNCOMPRESS_OPT ], 0, 0, 0, 0);
}
# adjust the header
$hdr->{'compressed'} = 0;
$hdr->{'uncompress_cmd'} = '';
}
# set up a CRC filter to compute the native CRC
$xfer_dest_native = Amanda::Xfer::Filter::Crc->new();
push @filters, $xfer_dest_native;
# and set up the validation command as a filter element,
# its stdout is the index stream.
my $argv = find_index_command($hdr);
if (defined $argv) {
push @filters, Amanda::Xfer::Filter::Process->new($argv, 0, 1, 0, 1);
}
# unlink all possible index files for this dump.
my $index_filename_gz = Amanda::Logfile::getindex_unsorted_gz_fname(
$current_dump->{hostname}, $current_dump->{diskname},
$current_dump->{dump_timestamp}, $current_dump->{level});
unlink $index_filename_gz;
my $index_filename = Amanda::Logfile::getindex_unsorted_fname(
$current_dump->{hostname}, $current_dump->{diskname},
$current_dump->{dump_timestamp}, $current_dump->{level});
unlink $index_filename;
unlink Amanda::Logfile::getindex_sorted_gz_fname(
$current_dump->{hostname}, $current_dump->{diskname},
$current_dump->{dump_timestamp}, $current_dump->{level});
unlink Amanda::Logfile::getindex_sorted_fname(
$current_dump->{hostname}, $current_dump->{diskname},
$current_dump->{dump_timestamp}, $current_dump->{level});
# send stdout to the index file
if (getconf($CNF_COMPRESS_INDEX)) {
my $index_file;
open($index_file, ">", $index_filename_gz);
push @filters,
Amanda::Xfer::Filter::Process->new(
[ $Amanda::Constants::COMPRESS_PATH,
$Amanda::Constants::COMPRESS_BEST_OPT ], 0, 0, 0, 0);
$xfer_dest = Amanda::Xfer::Dest::Fd->new(fileno($index_file));
close($index_file);
} else {
my $index_file;
open($index_file, ">", $index_filename);
$xfer_dest = Amanda::Xfer::Dest::Fd->new(fileno($index_file));
close($index_file);
}
# start reading all filter stderr
foreach my $filter (@filters) {
next if !$filter->can("get_stderr_fd");
my $fd = $filter->get_stderr_fd();
$fd.="";
$fd = int($fd);
my $src = Amanda::MainLoop::fd_source($fd,
$G_IO_IN|$G_IO_HUP|$G_IO_ERR);
my $buffer = "";
$all_filter{$src} = 1;
$src->set_callback( sub {
my $b;
my $n_read = POSIX::read($fd, $b, 1);
if (!defined $n_read) {
return;
} elsif ($n_read == 0) {
delete $all_filter{$src};
$src->remove();
POSIX::close($fd);
if (!%all_filter and $recovery_done) {
$steps->{'filter_done'}->();
}
} else {
$buffer .= $b;
if ($b eq "\n") {
my $line = $buffer;
print STDERR "filter stderr: $line";
chomp $line;
debug("filter stderr: $line");
$buffer = "";
}
}
});
}
my $xfer = Amanda::Xfer->new([ $xfer_src, @filters, $xfer_dest ]);
$xfer->start($steps->{'handle_xmsg'}, 0, $current_dump->{'bytes'});
$clerk->start_recovery(
xfer => $xfer,
recovery_cb => $steps->{'recovery_cb'});
};
step handle_xmsg => sub {
my ($src, $msg, $xfer) = @_;
if ($msg->{'type'} == $XMSG_CRC) {
if ($msg->{'elt'} == $xfer_src) {
$source_crc = $msg->{'crc'}.":".$msg->{'size'};
debug("source_crc: $source_crc");
} elsif ($msg->{'elt'} == $xfer_dest_native) {
$dest_native_crc = $msg->{'crc'}.":".$msg->{'size'};
debug("dest_native_crc: $dest_native_crc");
} elsif ($msg->{'elt'} == $xfer_dest) {
$dest_crc = $msg->{'crc'}.":".$msg->{'size'};
debug("dest_crc: $dest_crc");
} else {
debug("unhandled XMSG_CRC $msg->{'elt'} $msg->{'crc'}:$msg->{'size'}")
}
} else {
$clerk->handle_xmsg($src, $msg, $xfer);
if ($msg->{'type'} == $XMSG_INFO) {
Amanda::Debug::info($msg->{'message'});
} elsif ($msg->{'type'} == $XMSG_ERROR) {
push @xfer_errs, $msg->{'message'};
}
}
};
step recovery_cb => sub {
%recovery_params = @_;
$recovery_done = 1;
$steps->{'filter_done'}->() if !%all_filter;
};
step filter_done => sub {
# distinguish device errors from validation errors
if (@{$recovery_params{'errors'}}) {
print STDERR "While reading from volumes:\n";
print STDERR "$_\n" for @{$recovery_params{'errors'}};
return $steps->{'quit'}->("validation aborted");
}
if (@xfer_errs) {
print STDERR "Validation errors:\n";
print STDERR "$_\n" for @xfer_errs;
$all_success = 0;
}
my $msg;
if (defined $hdr->{'native_crc'} and $hdr->{'native_crc'} !~ /^00000000:/ and
defined $current_dump->{'native_crc'} and $current_dump->{'native_crc'} !~ /^00000000:/ and
$hdr->{'native_crc'} ne $current_dump->{'native_crc'}) {
$msg = "recovery failed: native-crc in header ($hdr->{'native_crc'}) and native-crc in log ($current_dump->{'native_crc'}) differ";
print STDERR "$msg\n";
debug($msg);
}
if (defined $hdr->{'client_crc'} and $hdr->{'client_crc'} !~ /^00000000:/ and
defined $current_dump->{'client_crc'} and $current_dump->{'client_crc'} !~ /^00000000:/ and
$hdr->{'client_crc'} ne $current_dump->{'client_crc'}) {
$msg = "recovery failed: client-crc in header ($hdr->{'client_crc'}) and client-crc in log ($current_dump->{'client_crc'}) differ";
print STDERR "$msg\n";
debug($msg);
}
my $hdr_server_crc_size;
my $current_dump_server_crc_size;
my $source_crc_size;
if (defined $hdr->{'server_crc'}) {
$hdr->{'server_crc'} =~ /[^:]*:(.*)/;
$hdr_server_crc_size = $1;
}
if (defined $current_dump->{'server_crc'}) {
$current_dump->{'server_crc'} =~ /[^:]*:(.*)/;
$current_dump_server_crc_size = $1;
}
if (defined $source_crc) {
$source_crc =~ /[^:]*:(.*)/;
$source_crc_size = $1;
}
if (defined $hdr->{'server_crc'} and $hdr->{'server_crc'} !~ /^00000000:/ and
defined $current_dump->{'server_crc'} and $current_dump->{'server_crc'} !~ /^00000000:/ and
$hdr_server_crc_size == $current_dump_server_crc_size and
$hdr->{'server_crc'} ne $current_dump->{'server_crc'}) {
$msg = "recovery failed: server-crc in header ($hdr->{'server_crc'}) and server-crc in log ($current_dump->{'server_crc'}) differ";
print STDERR "$msg\n";
debug($msg);
}
if (defined $current_dump->{'server_crc'} and $current_dump->{'server_crc'} !~ /^00000000:/ and
$current_dump_server_crc_size == $source_crc_size and
$current_dump->{'server_crc'} ne $source_crc) {
$msg = "recovery failed: server-crc ($current_dump->{'server_crc'}) and source_crc ($source_crc) differ",
print STDERR "$msg\n";
debug($msg);
}
if (defined $current_dump->{'native_crc'} and $current_dump->{'native_crc'} !~ /^00000000:/ and
defined $dest_native_crc and $current_dump->{'native_crc'} ne $dest_native_crc) {
$msg = "recovery failed: native-crc ($current_dump->{'native_crc'}) and dest-native-crc ($dest_native_crc) differ";
print STDERR "$msg\n";
debug($msg);
}
my $dump = shift @{$plan->{'dumps'}};
if (!$dump) {
return $steps->{'quit'}->();
}
$steps->{'check_dumpfile'}->($dump);
};
step quit => sub {
my ($err) = @_;
if ($err) {
$exit_code = 1;
print STDERR $err, "\n";
} elsif ($all_success) {
print "All images successfully reindexed\n";
} else {
print "Some images failed to be correctly reindexed.\n";
$exit_code = 1;
}
return $steps->{'quit2'}->();
};
step quit2 => sub {
my ($storage_name) = keys %clerk;
if ($storage_name) {
my $clerk = $clerk{$storage_name};
delete $clerk{$storage_name};
return $clerk->quit(finished_cb => $steps->{'quit2'});
}
return $finished_cb->();
};
}
main(sub { Amanda::MainLoop::quit(); });
Amanda::MainLoop::run();
Amanda::Util::finish_application();
exit($exit_code);