# Copyright (c) 2009-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 94085, or: http://www.zmanda.com
package Amanda::Changer::rait;
use strict;
use warnings;
use Carp;
use vars qw( @ISA );
@ISA = qw( Amanda::Changer );
use File::Glob qw( :glob );
use File::Path;
use Amanda::Config qw( :init :getconf );
use Amanda::Debug qw( debug warning );
use Amanda::Util qw( :alternates );
use Amanda::Changer;
use Amanda::MainLoop;
use Amanda::Device qw( :constants );
=head1 NAME
Amanda::Changer::rait
=head1 DESCRIPTION
This changer operates several child changers, returning RAIT devices composed of
the devices produced by the child changers. It's modeled on the RAIT device.
See the amanda-changers(7) manpage for usage information.
=cut
sub new {
my $class = shift;
my ($config, $tpchanger) = @_;
my ($kidspecs) = ( $tpchanger =~ /chg-rait:(.*)/ );
my @kidspecs = Amanda::Util::expand_braced_alternates($kidspecs);
if (@kidspecs < 2) {
return Amanda::Changer->make_error("fatal", undef,
message => "chg-rait needs at least two child changers");
}
my @children = map {
($_ eq "ERROR")? "ERROR" : Amanda::Changer->new($_)
} @kidspecs;
if (grep { $_->isa("Amanda::Changer::Error") } @children) {
my @annotated_errs;
for my $i (0 .. @children-1) {
next unless $children[$i]->isa("Amanda::Changer::Error");
if ($children[$i]->isa("Amanda::Changer::Error")) {
push @annotated_errs,
[ $kidspecs[$i], $children[$i] ];
} elsif ($children[$i]->isa("Amanda::Changer")) {
$children[$i]->quit();
}
}
return Amanda::Changer->make_combined_error(
"fatal", [ @annotated_errs ]);
}
my $self = {
config => $config,
child_names => \@kidspecs,
children => \@children,
num_children => scalar @children,
};
bless ($self, $class);
return $self;
}
sub quit {
my $self = shift;
# quit each child
foreach my $child (@{$self->{'children'}}) {
$child->quit() if $child ne "ERROR";
}
$self->SUPER::quit();
}
# private method to help handle slot input
sub _kid_slots_ok {
my ($self, $res_cb, $slot, $kid_slots_ref, $err_ref) = @_;
@{$kid_slots_ref} = expand_braced_alternates($slot);
return 1 if (@{$kid_slots_ref} == $self->{'num_children'});
if (@{$kid_slots_ref} == 1) {
@{$kid_slots_ref} = ( $slot ) x $self->{'num_children'};
return 1;
}
${$err_ref} = $self->make_error("failed", $res_cb,
reason => "invalid",
message => "slot string '$slot' does not specify " .
"$self->{num_children} child slots");
return 0;
}
sub load {
my $self = shift;
my %params = @_;
return if $self->check_error($params{'res_cb'});
$self->validate_params('load', \%params);
my $release_on_error = sub {
my ($kid_results) = @_;
# an error has occurred, so we have to release all of the *non*-error
# reservations (and handle errors in those releases!), then construct
# and return a combined error message.
my $releases_outstanding = 1; # start at one, in case the releases are immediate
my @release_errors = ( undef ) x $self->{'num_children'};
my $releases_maybe_done = sub {
return if (--$releases_outstanding);
# gather up the errors and combine them for return to our caller
my @annotated_errs;
for my $i (0 .. $self->{'num_children'}-1) {
my $child_name = $self->{'child_names'}[$i];
if ($kid_results->[$i][0]) {
push @annotated_errs,
[ "from $child_name", $kid_results->[$i][0] ];
}
if ($release_errors[$i]) {
push @annotated_errs,
[ "while releasing $child_name reservation",
$kid_results->[$i][0] ];
}
}
return $self->make_combined_error(
$params{'res_cb'}, [ @annotated_errs ]);
};
for my $i (0 .. $self->{'num_children'}-1) {
next unless (my $res = $kid_results->[$i][1]);
$releases_outstanding++;
$res->release(finished_cb => sub {
$release_errors[$i] = $_[0];
$releases_maybe_done->();
});
}
# we started $releases_outstanding at 1, so decrement it now
$releases_maybe_done->();
};
my $all_kids_done_cb = sub {
my ($kid_results) = @_;
my $result;
# first, let's see if any changer gave an error
if (!grep { defined($_->[0]) } @$kid_results) {
# no error .. combine the reservations and return a RAIT reservation
return $self->_make_res($params{'res_cb'}, [ map { $_->[1] } @$kid_results ]);
} else {
return $release_on_error->($kid_results);
}
};
# make a template for params for the children
my %kid_template = %params;
delete $kid_template{'res_cb'};
delete $kid_template{'slot'};
delete $kid_template{'except_slots'};
# $kid_template{'label'} is passed directly to children
# $kid_template{'relative_slot'} is passed directly to children
# $kid_template{'mode'} is passed directly to children
# and make a copy for each child
my @kid_params;
for (0 .. $self->{'num_children'}-1) {
push @kid_params, { %kid_template };
}
if (exists $params{'slot'}) {
my $slot = $params{'slot'};
# calculate the slots for each child
my (@kid_slots, $err);
return $err unless $self->_kid_slots_ok($params{'res_cb'}, $slot, \@kid_slots, \$err);
if (@kid_slots != $self->{'num_children'}) {
# as a convenience, expand a single slot into the same slot for each child
if (@kid_slots == 1) {
@kid_slots = ( $slot ) x $self->{'num_children'};
} else {
return $self->make_error("failed", $params{'res_cb'},
reason => "invalid",
message => "slot '$slot' does not specify " .
"$self->{num_children} child slots");
}
}
for (0 .. $self->{'num_children'}-1) {
$kid_params[$_]->{'slot'} = $kid_slots[$_];
}
}
# each slot in except_slots needs to get broken down, and the appropriate slot
# given to each child
if (exists $params{'except_slots'}) {
for (0 .. $self->{'num_children'}-1) {
$kid_params[$_]->{'except_slots'} = {};
}
# for each slot, split it up, then apportion the result to each child
for my $slot ( keys %{$params{'except_slots'}} ) {
my (@kid_slots, $err);
return $err unless $self->_kid_slots_ok($params{'res_cb'}, $slot, \@kid_slots, \$err);
for (0 .. $self->{'num_children'}-1) {
$kid_params[$_]->{'except_slots'}->{$kid_slots[$_]} = 1;
}
}
}
$self->_for_each_child(
oksub => sub {
my ($kid_chg, $kid_cb, $kid_params) = @_;
$kid_params->{'res_cb'} = $kid_cb;
$kid_chg->load(%$kid_params);
},
errsub => sub {
my ($kid_chg, $kid_cb, $kid_slot) = @_;
$kid_cb->(undef, "ERROR");
},
parent_cb => $all_kids_done_cb,
args => \@kid_params,
);
}
sub _make_res {
my $self = shift;
my ($res_cb, $kid_reservations) = @_;
my @kid_devices = map { ($_ ne "ERROR") ? $_->{'device'} : undef } @$kid_reservations;
my $rait_device = Amanda::Device->new_rait_from_children(@kid_devices);
if ($rait_device->status() != $DEVICE_STATUS_SUCCESS) {
return $self->make_error("failed", $res_cb,
reason => "device",
message => $rait_device->error_or_status());
}
if (my $err = $self->{'config'}->configure_device($rait_device, $self->{'storage'})) {
return $self->make_error("failed", $res_cb,
reason => "device",
message => $err);
}
my $combined_res = Amanda::Changer::rait::Reservation->new(
$self,
$kid_reservations, $rait_device);
$rait_device->read_label();
$res_cb->(undef, $combined_res);
}
sub info_key {
my $self = shift;
my ($key, %params) = @_;
return if $self->check_error($params{'info_cb'});
my $check_and_report_errors = sub {
my ($kid_results) = @_;
if (grep { defined($_->[0]) } @$kid_results) {
# we have errors, so collect them and make a "combined" error.
my @annotated_errs;
my @err_slots;
for my $i (0 .. $self->{'num_children'}-1) {
my $kr = $kid_results->[$i];
next unless defined($kr->[0]);
push @annotated_errs,
[ $self->{'child_names'}[$i], $kr->[0] ];
push @err_slots, $kr->[0]->{'slot'}
if (defined $kr->[0] and defined $kr->[0]->{'slot'});
}
my @slotarg;
if (@err_slots == $self->{'num_children'}) {
@slotarg = (slot => collapse_braced_alternates([@err_slots]));
}
$self->make_combined_error(
$params{'info_cb'}, [ @annotated_errs ],
@slotarg);
return 1;
}
};
if ($key eq 'num_slots') {
my $all_kids_done_cb = sub {
my ($kid_results) = @_;
return if ($check_and_report_errors->($kid_results));
# aggregate the results: the consensus if the children agree,
# otherwise -1
my $num_slots;
for (@$kid_results) {
my ($err, %kid_info) = @$_;
next unless exists($kid_info{'num_slots'});
my $kid_num_slots = $kid_info{'num_slots'};
if (defined $num_slots and $num_slots != $kid_num_slots) {
debug("chg-rait: children have different slot counts!");
$num_slots = -1;
} else {
$num_slots = $kid_num_slots;
}
}
$params{'info_cb'}->(undef, num_slots => $num_slots) if $params{'info_cb'};
};
$self->_for_each_child(
oksub => sub {
my ($kid_chg, $kid_cb) = @_;
$kid_chg->info(info => [ 'num_slots' ], info_cb => $kid_cb);
},
errsub => undef,
parent_cb => $all_kids_done_cb,
);
} elsif ($key eq 'slots') {
my $all_kids_done_cb = sub {
my ($kid_results) = @_;
return if ($check_and_report_errors->($kid_results));
my @slots;
my $a = 0;
my $nb_kid = @$kid_results;
while (@{@{$kid_results}[0]->[2]}) {
my @name;
for (my $k = 0; $k < $nb_kid; $k++) {
push @name, shift @{@{$kid_results}[$k]->[2]};
}
my $name = '{' . join(',', @name) . '}';
push @slots, $name;
}
$params{'info_cb'}->(undef, slots => \@slots) if $params{'info_cb'};
};
$self->_for_each_child(
oksub => sub {
my ($kid_chg, $kid_cb) = @_;
$kid_chg->info(info => [ 'slots' ], info_cb => $kid_cb);
},
errsub => undef,
parent_cb => $all_kids_done_cb,
);
} elsif ($key eq "vendor_string") {
my $all_kids_done_cb = sub {
my ($kid_results) = @_;
return if ($check_and_report_errors->($kid_results));
my @kid_vendors =
grep { defined($_) }
map { my ($e, %r) = @$_; $r{'vendor_string'} }
@$kid_results;
my $vendor_string;
if (@kid_vendors) {
$vendor_string = collapse_braced_alternates([@kid_vendors]);
$params{'info_cb'}->(undef, vendor_string => $vendor_string) if $params{'info_cb'};
} else {
$params{'info_cb'}->(undef) if $params{'info_cb'};
}
};
$self->_for_each_child(
oksub => sub {
my ($kid_chg, $kid_cb) = @_;
$kid_chg->info(info => [ 'vendor_string' ], info_cb => $kid_cb);
},
errsub => undef,
parent_cb => $all_kids_done_cb,
);
} elsif ($key eq 'fast_search') {
my $all_kids_done_cb = sub {
my ($kid_results) = @_;
return if ($check_and_report_errors->($kid_results));
my @kid_fastness =
grep { defined($_) }
map { my ($e, %r) = @$_; $r{'fast_search'} }
@$kid_results;
if (@kid_fastness) {
my $fast_search = 1;
# conduct a logical AND of all child fastnesses
for my $f (@kid_fastness) {
$fast_search = $fast_search && $f;
}
$params{'info_cb'}->(undef, fast_search => $fast_search) if $params{'info_cb'};
} else {
$params{'info_cb'}->(undef, fast_search => 0) if $params{'info_cb'};
}
};
$self->_for_each_child(
oksub => sub {
my ($kid_chg, $kid_cb) = @_;
$kid_chg->info(info => [ 'fast_search' ], info_cb => $kid_cb);
},
errsub => undef,
parent_cb => $all_kids_done_cb,
);
}
}
# reset, clean, etc. are all *very* similar to one another, so we create them
# generically
sub _mk_simple_op {
my ($op, $has_drive) = @_;
sub {
my $self = shift;
my %params = @_;
return if $self->check_error($params{'finished_cb'});
my $all_kids_done_cb = sub {
my ($kid_results) = @_;
if (grep { defined($_->[0]) } @$kid_results) {
# we have errors, so collect them and make a "combined" error.
my @annotated_errs;
for my $i (0 .. $self->{'num_children'}-1) {
my $kr = $kid_results->[$i];
next unless defined($kr->[0]);
push @annotated_errs,
[ $self->{'child_names'}[$i], $kr->[0] ];
}
$self->make_combined_error(
$params{'finished_cb'}, [ @annotated_errs ]);
return 1;
}
$params{'finished_cb'}->() if $params{'finished_cb'};
};
# get the drives for the kids, if necessary
my @kid_args;
if ($has_drive and $params{'drive'}) {
my $drive = $params{'drive'};
my @kid_drives = expand_braced_alternates($drive);
if (@kid_drives == 1) {
@kid_drives = ( $kid_drives[0] ) x $self->{'num_children'};
}
if (@kid_drives != $self->{'num_children'}) {
return $self->make_error("failed", $params{'finished_cb'},
reason => "invalid",
message => "drive string '$drive' does not specify " .
"$self->{num_children} child drives");
}
@kid_args = map { { drive => $_ } } @kid_drives;
delete $params{'drive'};
} else {
@kid_args = ( {} ) x $self->{'num_children'};
}
$self->_for_each_child(
oksub => sub {
my ($kid_chg, $kid_cb, $args) = @_;
$kid_chg->$op(%params, finished_cb => $kid_cb, %$args);
},
errsub => undef,
parent_cb => $all_kids_done_cb,
args => \@kid_args,
);
};
}
{
# perl doesn't like that these symbols are only mentioned once
no warnings;
*reset = _mk_simple_op("reset", 0);
*update = _mk_simple_op("update", 0);
*clean = _mk_simple_op("clean", 1);
*eject = _mk_simple_op("eject", 1);
}
sub inventory {
my $self = shift;
my %params = @_;
return if $self->check_error($params{'inventory_cb'});
my $all_kids_done_cb = sub {
my ($kid_results) = @_;
if (grep { defined($_->[0]) } @$kid_results) {
# we have errors, so collect them and make a "combined" error.
my @annotated_errs;
for my $i (0 .. $self->{'num_children'}-1) {
my $kr = $kid_results->[$i];
next unless defined($kr->[0]);
push @annotated_errs,
[ $self->{'child_names'}[$i], $kr->[0] ];
}
return $self->make_combined_error(
$params{'inventory_cb'}, [ @annotated_errs ]);
}
my $inv = $self->_merge_inventories($kid_results);
if (!defined $inv) {
return $self->make_error("failed", $params{'inventory_cb'},
reason => "notimpl",
message => "could not generate consistent inventory from rait child changers");
}
$params{'inventory_cb'}->(undef, $inv);
};
$self->_for_each_child(
oksub => sub {
my ($kid_chg, $kid_cb) = @_;
$kid_chg->inventory(inventory_cb => $kid_cb);
},
errsub => undef,
parent_cb => $all_kids_done_cb,
);
}
# Takes keyword parameters 'oksub', 'errsub', 'parent_cb', and 'args'. For
# each child, runs $oksub (or, if the child is "ERROR", $errsub), passing it
# the changer, an aggregating callback, and the corresponding element from
# @$args (if specified). The callback combines its results with the results
# from other changers, and when all results are available, calls $parent_cb.
#
# This forms a kind of "AND" combinator for a parallel operation on multiple
# changers, providing the caller with a simple collection of the results of
# the operation. The parent_cb is called as
# $parent_cb->([ [ <chg_1_results> ], [ <chg_2_results> ], .. ]).
sub _for_each_child {
my $self = shift;
my %params = @_;
my ($oksub, $errsub, $parent_cb, $args) =
($params{'oksub'}, $params{'errsub'}, $params{'parent_cb'}, $params{'args'});
if (defined($args)) {
confess "number of args did not match number of children"
unless (@$args == $self->{'num_children'});
} else {
$args = [ ( undef ) x $self->{'num_children'} ];
}
my $remaining = $self->{'num_children'};
my @results = ( undef ) x $self->{'num_children'};
my $maybe_done = sub {
return if (--$remaining);
$parent_cb->([ @results ]);
};
for my $i (0 .. $self->{'num_children'}-1) {
my $child = $self->{'children'}[$i];
my $arg = @$args? $args->[$i] : undef;
my $child_cb = sub {
$results[$i] = [ @_ ];
$maybe_done->();
};
if ($child eq "ERROR") {
if (defined $errsub) {
$errsub->("ERROR", $child_cb, $arg);
} else {
# no errsub; just call $child_cb directly
$child_cb->(undef) if $child_cb;
}
} else {
$oksub->($child, $child_cb, $arg) if $oksub;
}
}
}
sub _merge_inventories {
my $self = shift;
my ($kid_results) = @_;
my @combined;
for my $kid_result (@$kid_results) {
my $kid_inv = $kid_result->[1];
if (!@combined) {
for my $x (@$kid_inv) {
push @combined, {
state => Amanda::Changer::SLOT_FULL,
device_status => undef, f_type => undef,
label => undef, barcode => [],
reserved => 0, slot => [],
import_export => 1, loaded_in => [],
};
}
}
# if the results have different lengths, then we'll just call it
# not implemented; otherwise, we assume that the order of the slots
# in each child changer is the same.
if (scalar @combined != scalar @$kid_inv) {
warning("child changers returned different-length inventories; cannot merge");
return undef;
}
my $i;
for ($i = 0; $i < @combined; $i++) {
my $c = $combined[$i];
my $k = $kid_inv->[$i];
# mismatches here are just warnings
if (defined $c->{'label'}) {
if (defined $k->{'label'} and $c->{'label'} ne $k->{'label'}) {
warning("child changers have different labels in slot at index $i");
$c->{'label_mismatch'} = 1;
$c->{'label'} = undef;
} elsif (!defined $k->{'label'}) {
$c->{'label_mismatch'} = 1;
$c->{'label'} = undef;
}
} else {
if (!$c->{'label_mismatch'} && !$c->{'label_set'}) {
$c->{'label'} = $k->{'label'};
}
}
$c->{'label_set'} = 1;
$c->{'device_status'} |= $k->{'device_status'}
if defined $k->{'device_status'};
if (!defined $c->{'f_type'} ||
$k->{'f_type'} != $Amanda::Header::F_TAPESTART) {
$c->{'f_type'} = $k->{'f_type'};
}
# a slot is empty if any of the child slots are empty
$c->{'state'} = Amanda::Changer::SLOT_EMPTY
if $k->{'state'} == Amanda::Changer::SLOT_EMPTY;
# a slot is reserved if any of the child slots are reserved
$c->{'reserved'} = $c->{'reserved'} || $k->{'reserved'};
# a slot is import-export if all of the child slots are import_export
$c->{'import_export'} = $c->{'import_export'} && $k->{'import_export'};
# barcodes, slots, and loaded_in are lists
push @{$c->{'slot'}}, $k->{'slot'};
push @{$c->{'barcode'}}, $k->{'barcode'};
push @{$c->{'loaded_in'}}, $k->{'loaded_in'};
}
}
# now post-process the slots, barcodes, and loaded_in into braced-alternates notation
my $i;
for ($i = 0; $i < @combined; $i++) {
my $c = $combined[$i];
delete $c->{'label_mismatch'} if $c->{'label_mismatch'};
delete $c->{'label_set'} if $c->{'label_set'};
$c->{'slot'} = collapse_braced_alternates([ @{$c->{'slot'}} ]);
if (grep { !defined $_ } @{$c->{'barcode'}}) {
delete $c->{'barcode'};
} else {
$c->{'barcode'} = collapse_braced_alternates([ @{$c->{'barcode'}} ]);
}
if (grep { !defined $_ } @{$c->{'loaded_in'}}) {
delete $c->{'loaded_in'};
} else {
$c->{'loaded_in'} = collapse_braced_alternates([ @{$c->{'loaded_in'}} ]);
}
}
return [ @combined ];
}
package Amanda::Changer::rait::Reservation;
use Amanda::Util qw( :alternates );
use vars qw( @ISA );
@ISA = qw( Amanda::Changer::Reservation );
# utility function to act like 'map', but pass "ERROR" straight through
# (this has to appear before it is used, because it has a prototype)
sub errmap (&@) {
my $sub = shift;
return map { ($_ ne "ERROR")? $sub->($_) : "ERROR" } @_;
}
sub new {
my $class = shift;
my ($chg, $child_reservations, $rait_device) = @_;
my $self = Amanda::Changer::Reservation::new($class);
$self->{'chg'} = $chg;
# note that $child_reservations may contain "ERROR" in place of a reservation
$self->{'child_reservations'} = $child_reservations;
$self->{'device'} = $rait_device;
my @barcodes = errmap { defined($_->{'barcode'}) ? "" . $_->{'barcode'} : "" } @$child_reservations;
$self->{'barcode'} = collapse_braced_alternates(\@barcodes);
my @slot_names;
@slot_names = errmap { "" . $_->{'this_slot'} } @$child_reservations;
$self->{'this_slot'} = collapse_braced_alternates(\@slot_names);
return $self;
}
sub do_release {
my $self = shift;
my %params = @_;
my $remaining = @{$self->{'child_reservations'}};
my @outer_errors;
my $maybe_finished = sub {
my ($err) = @_;
push @outer_errors, $err if ($err);
return if (--$remaining);
my $errstr;
if (@outer_errors) {
$errstr = join("; ", @outer_errors);
}
# unref the device, for good measure
$self->{'device'} = undef;
$params{'finished_cb'}->($errstr) if $params{'finished_cb'};
};
for my $res (@{$self->{'child_reservations'}}) {
# short-circuit an "ERROR" reservation
if ($res eq "ERROR") {
$maybe_finished->(undef);
next;
}
$res->release(%params, finished_cb => $maybe_finished);
}
}