1 # Copyright (c) 2009-2012 Zmanda, Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
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
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
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
20 package Amanda::Changer::rait;
26 @ISA = qw( Amanda::Changer );
28 use File::Glob qw( :glob );
30 use Amanda::Config qw( :getconf );
31 use Amanda::Debug qw( debug warning );
32 use Amanda::Util qw( :alternates );
35 use Amanda::Device qw( :constants );
43 This changer operates several child changers, returning RAIT devices composed of
44 the devices produced by the child changers. It's modeled on the RAIT device.
46 See the amanda-changers(7) manpage for usage information.
52 my ($config, $tpchanger) = @_;
53 my ($kidspecs) = ( $tpchanger =~ /chg-rait:(.*)/ );
55 my @kidspecs = Amanda::Util::expand_braced_alternates($kidspecs);
57 return Amanda::Changer->make_error("fatal", undef,
58 message => "chg-rait needs at least two child changers");
62 ($_ eq "ERROR")? "ERROR" : Amanda::Changer->new($_)
65 if (grep { $_->isa("Amanda::Changer::Error") } @children) {
67 for my $i (0 .. @children-1) {
68 next unless $children[$i]->isa("Amanda::Changer::Error");
69 if ($children[$i]->isa("Amanda::Changer::Error")) {
71 [ $kidspecs[$i], $children[$i] ];
72 } elsif ($children[$i]->isa("Amanda::Changer")) {
73 $children[$i]->quit();
76 return Amanda::Changer->make_combined_error(
77 "fatal", [ @annotated_errs ]);
82 child_names => \@kidspecs,
83 children => \@children,
84 num_children => scalar @children,
86 bless ($self, $class);
94 foreach my $child (@{$self->{'children'}}) {
95 $child->quit() if $child ne "ERROR";
101 # private method to help handle slot input
103 my ($self, $res_cb, $slot, $kid_slots_ref, $err_ref) = @_;
104 @{$kid_slots_ref} = expand_braced_alternates($slot);
105 return 1 if (@{$kid_slots_ref} == $self->{'num_children'});
107 if (@{$kid_slots_ref} == 1) {
108 @{$kid_slots_ref} = ( $slot ) x $self->{'num_children'};
111 ${$err_ref} = $self->make_error("failed", $res_cb,
113 message => "slot string '$slot' does not specify " .
114 "$self->{num_children} child slots");
122 return if $self->check_error($params{'res_cb'});
124 $self->validate_params('load', \%params);
126 my $release_on_error = sub {
127 my ($kid_results) = @_;
129 # an error has occurred, so we have to release all of the *non*-error
130 # reservations (and handle errors in those releases!), then construct
131 # and return a combined error message.
133 my $releases_outstanding = 1; # start at one, in case the releases are immediate
134 my @release_errors = ( undef ) x $self->{'num_children'};
135 my $releases_maybe_done = sub {
136 return if (--$releases_outstanding);
138 # gather up the errors and combine them for return to our caller
140 for my $i (0 .. $self->{'num_children'}-1) {
141 my $child_name = $self->{'child_names'}[$i];
142 if ($kid_results->[$i][0]) {
143 push @annotated_errs,
144 [ "from $child_name", $kid_results->[$i][0] ];
146 if ($release_errors[$i]) {
147 push @annotated_errs,
148 [ "while releasing $child_name reservation",
149 $kid_results->[$i][0] ];
153 return $self->make_combined_error(
154 $params{'res_cb'}, [ @annotated_errs ]);
157 for my $i (0 .. $self->{'num_children'}-1) {
158 next unless (my $res = $kid_results->[$i][1]);
159 $releases_outstanding++;
160 $res->release(finished_cb => sub {
161 $release_errors[$i] = $_[0];
162 $releases_maybe_done->();
166 # we started $releases_outstanding at 1, so decrement it now
167 $releases_maybe_done->();
170 my $all_kids_done_cb = sub {
171 my ($kid_results) = @_;
174 # first, let's see if any changer gave an error
175 if (!grep { defined($_->[0]) } @$kid_results) {
176 # no error .. combine the reservations and return a RAIT reservation
177 return $self->_make_res($params{'res_cb'}, [ map { $_->[1] } @$kid_results ]);
179 return $release_on_error->($kid_results);
183 # make a template for params for the children
184 my %kid_template = %params;
185 delete $kid_template{'res_cb'};
186 delete $kid_template{'slot'};
187 delete $kid_template{'except_slots'};
188 # $kid_template{'label'} is passed directly to children
189 # $kid_template{'relative_slot'} is passed directly to children
190 # $kid_template{'mode'} is passed directly to children
192 # and make a copy for each child
194 for (0 .. $self->{'num_children'}-1) {
195 push @kid_params, { %kid_template };
198 if (exists $params{'slot'}) {
199 my $slot = $params{'slot'};
201 # calculate the slots for each child
202 my (@kid_slots, $err);
203 return $err unless $self->_kid_slots_ok($params{'res_cb'}, $slot, \@kid_slots, \$err);
204 if (@kid_slots != $self->{'num_children'}) {
205 # as a convenience, expand a single slot into the same slot for each child
206 if (@kid_slots == 1) {
207 @kid_slots = ( $slot ) x $self->{'num_children'};
209 return $self->make_error("failed", $params{'res_cb'},
211 message => "slot '$slot' does not specify " .
212 "$self->{num_children} child slots");
215 for (0 .. $self->{'num_children'}-1) {
216 $kid_params[$_]->{'slot'} = $kid_slots[$_];
220 # each slot in except_slots needs to get broken down, and the appropriate slot
221 # given to each child
222 if (exists $params{'except_slots'}) {
223 for (0 .. $self->{'num_children'}-1) {
224 $kid_params[$_]->{'except_slots'} = {};
227 # for each slot, split it up, then apportion the result to each child
228 for my $slot ( keys %{$params{'except_slots'}} ) {
229 my (@kid_slots, $err);
230 return $err unless $self->_kid_slots_ok($params{'res_cb'}, $slot, \@kid_slots, \$err);
231 for (0 .. $self->{'num_children'}-1) {
232 $kid_params[$_]->{'except_slots'}->{$kid_slots[$_]} = 1;
237 $self->_for_each_child(
239 my ($kid_chg, $kid_cb, $kid_params) = @_;
240 $kid_params->{'res_cb'} = $kid_cb;
241 $kid_chg->load(%$kid_params);
244 my ($kid_chg, $kid_cb, $kid_slot) = @_;
245 $kid_cb->(undef, "ERROR");
247 parent_cb => $all_kids_done_cb,
248 args => \@kid_params,
254 my ($res_cb, $kid_reservations) = @_;
255 my @kid_devices = map { ($_ ne "ERROR") ? $_->{'device'} : undef } @$kid_reservations;
257 my $rait_device = Amanda::Device->new_rait_from_children(@kid_devices);
258 if ($rait_device->status() != $DEVICE_STATUS_SUCCESS) {
259 return $self->make_error("failed", $res_cb,
261 message => $rait_device->error_or_status());
264 if (my $err = $self->{'config'}->configure_device($rait_device)) {
265 return $self->make_error("failed", $res_cb,
270 my $combined_res = Amanda::Changer::rait::Reservation->new(
271 $kid_reservations, $rait_device);
272 $rait_device->read_label();
274 $res_cb->(undef, $combined_res);
279 my ($key, %params) = @_;
281 return if $self->check_error($params{'info_cb'});
283 my $check_and_report_errors = sub {
284 my ($kid_results) = @_;
286 if (grep { defined($_->[0]) } @$kid_results) {
287 # we have errors, so collect them and make a "combined" error.
290 for my $i (0 .. $self->{'num_children'}-1) {
291 my $kr = $kid_results->[$i];
292 next unless defined($kr->[0]);
293 push @annotated_errs,
294 [ $self->{'child_names'}[$i], $kr->[0] ];
295 push @err_slots, $kr->[0]->{'slot'}
296 if (defined $kr->[0] and defined $kr->[0]->{'slot'});
300 if (@err_slots == $self->{'num_children'}) {
301 @slotarg = (slot => collapse_braced_alternates([@err_slots]));
304 $self->make_combined_error(
305 $params{'info_cb'}, [ @annotated_errs ],
311 if ($key eq 'num_slots') {
312 my $all_kids_done_cb = sub {
313 my ($kid_results) = @_;
314 return if ($check_and_report_errors->($kid_results));
316 # aggregate the results: the consensus if the children agree,
319 for (@$kid_results) {
320 my ($err, %kid_info) = @$_;
321 next unless exists($kid_info{'num_slots'});
322 my $kid_num_slots = $kid_info{'num_slots'};
323 if (defined $num_slots and $num_slots != $kid_num_slots) {
324 debug("chg-rait: children have different slot counts!");
327 $num_slots = $kid_num_slots;
330 $params{'info_cb'}->(undef, num_slots => $num_slots) if $params{'info_cb'};
333 $self->_for_each_child(
335 my ($kid_chg, $kid_cb) = @_;
336 $kid_chg->info(info => [ 'num_slots' ], info_cb => $kid_cb);
339 parent_cb => $all_kids_done_cb,
341 } elsif ($key eq "vendor_string") {
342 my $all_kids_done_cb = sub {
343 my ($kid_results) = @_;
344 return if ($check_and_report_errors->($kid_results));
348 map { my ($e, %r) = @$_; $r{'vendor_string'} }
352 $vendor_string = collapse_braced_alternates([@kid_vendors]);
353 $params{'info_cb'}->(undef, vendor_string => $vendor_string) if $params{'info_cb'};
355 $params{'info_cb'}->(undef) if $params{'info_cb'};
359 $self->_for_each_child(
361 my ($kid_chg, $kid_cb) = @_;
362 $kid_chg->info(info => [ 'vendor_string' ], info_cb => $kid_cb);
365 parent_cb => $all_kids_done_cb,
367 } elsif ($key eq 'fast_search') {
368 my $all_kids_done_cb = sub {
369 my ($kid_results) = @_;
370 return if ($check_and_report_errors->($kid_results));
374 map { my ($e, %r) = @$_; $r{'fast_search'} }
378 # conduct a logical AND of all child fastnesses
379 for my $f (@kid_fastness) {
380 $fast_search = $fast_search && $f;
382 $params{'info_cb'}->(undef, fast_search => $fast_search) if $params{'info_cb'};
384 $params{'info_cb'}->(undef, fast_search => 0) if $params{'info_cb'};
388 $self->_for_each_child(
390 my ($kid_chg, $kid_cb) = @_;
391 $kid_chg->info(info => [ 'fast_search' ], info_cb => $kid_cb);
394 parent_cb => $all_kids_done_cb,
399 # reset, clean, etc. are all *very* similar to one another, so we create them
402 my ($op, $has_drive) = @_;
407 return if $self->check_error($params{'finished_cb'});
409 my $all_kids_done_cb = sub {
410 my ($kid_results) = @_;
411 if (grep { defined($_->[0]) } @$kid_results) {
412 # we have errors, so collect them and make a "combined" error.
414 for my $i (0 .. $self->{'num_children'}-1) {
415 my $kr = $kid_results->[$i];
416 next unless defined($kr->[0]);
417 push @annotated_errs,
418 [ $self->{'child_names'}[$i], $kr->[0] ];
420 $self->make_combined_error(
421 $params{'finished_cb'}, [ @annotated_errs ]);
424 $params{'finished_cb'}->() if $params{'finished_cb'};
427 # get the drives for the kids, if necessary
429 if ($has_drive and $params{'drive'}) {
430 my $drive = $params{'drive'};
431 my @kid_drives = expand_braced_alternates($drive);
433 if (@kid_drives == 1) {
434 @kid_drives = ( $kid_drives[0] ) x $self->{'num_children'};
437 if (@kid_drives != $self->{'num_children'}) {
438 return $self->make_error("failed", $params{'finished_cb'},
440 message => "drive string '$drive' does not specify " .
441 "$self->{num_children} child drives");
444 @kid_args = map { { drive => $_ } } @kid_drives;
445 delete $params{'drive'};
447 @kid_args = ( {} ) x $self->{'num_children'};
450 $self->_for_each_child(
452 my ($kid_chg, $kid_cb, $args) = @_;
453 $kid_chg->$op(%params, finished_cb => $kid_cb, %$args);
456 parent_cb => $all_kids_done_cb,
463 # perl doesn't like that these symbols are only mentioned once
466 *reset = _mk_simple_op("reset", 0);
467 *update = _mk_simple_op("update", 0);
468 *clean = _mk_simple_op("clean", 1);
469 *eject = _mk_simple_op("eject", 1);
476 return if $self->check_error($params{'inventory_cb'});
478 my $all_kids_done_cb = sub {
479 my ($kid_results) = @_;
480 if (grep { defined($_->[0]) } @$kid_results) {
481 # we have errors, so collect them and make a "combined" error.
483 for my $i (0 .. $self->{'num_children'}-1) {
484 my $kr = $kid_results->[$i];
485 next unless defined($kr->[0]);
486 push @annotated_errs,
487 [ $self->{'child_names'}[$i], $kr->[0] ];
489 return $self->make_combined_error(
490 $params{'inventory_cb'}, [ @annotated_errs ]);
493 my $inv = $self->_merge_inventories($kid_results);
495 return $self->make_error("failed", $params{'inventory_cb'},
497 message => "could not generate consistent inventory from rait child changers");
500 $params{'inventory_cb'}->(undef, $inv);
503 $self->_for_each_child(
505 my ($kid_chg, $kid_cb) = @_;
506 $kid_chg->inventory(inventory_cb => $kid_cb);
509 parent_cb => $all_kids_done_cb,
513 # Takes keyword parameters 'oksub', 'errsub', 'parent_cb', and 'args'. For
514 # each child, runs $oksub (or, if the child is "ERROR", $errsub), passing it
515 # the changer, an aggregating callback, and the corresponding element from
516 # @$args (if specified). The callback combines its results with the results
517 # from other changers, and when all results are available, calls $parent_cb.
519 # This forms a kind of "AND" combinator for a parallel operation on multiple
520 # changers, providing the caller with a simple collection of the results of
521 # the operation. The parent_cb is called as
522 # $parent_cb->([ [ <chg_1_results> ], [ <chg_2_results> ], .. ]).
523 sub _for_each_child {
526 my ($oksub, $errsub, $parent_cb, $args) =
527 ($params{'oksub'}, $params{'errsub'}, $params{'parent_cb'}, $params{'args'});
529 if (defined($args)) {
530 confess "number of args did not match number of children"
531 unless (@$args == $self->{'num_children'});
533 $args = [ ( undef ) x $self->{'num_children'} ];
536 my $remaining = $self->{'num_children'};
537 my @results = ( undef ) x $self->{'num_children'};
538 my $maybe_done = sub {
539 return if (--$remaining);
540 $parent_cb->([ @results ]);
543 for my $i (0 .. $self->{'num_children'}-1) {
544 my $child = $self->{'children'}[$i];
545 my $arg = @$args? $args->[$i] : undef;
548 $results[$i] = [ @_ ];
552 if ($child eq "ERROR") {
553 if (defined $errsub) {
554 $errsub->("ERROR", $child_cb, $arg);
556 # no errsub; just call $child_cb directly
557 $child_cb->(undef) if $child_cb;
560 $oksub->($child, $child_cb, $arg) if $oksub;
565 sub _merge_inventories {
567 my ($kid_results) = @_;
570 for my $kid_result (@$kid_results) {
571 my $kid_inv = $kid_result->[1];
574 for my $x (@$kid_inv) {
576 state => Amanda::Changer::SLOT_FULL,
577 device_status => undef, f_type => undef,
578 label => undef, barcode => [],
579 reserved => 0, slot => [],
580 import_export => 1, loaded_in => [],
585 # if the results have different lengths, then we'll just call it
586 # not implemented; otherwise, we assume that the order of the slots
587 # in each child changer is the same.
588 if (scalar @combined != scalar @$kid_inv) {
589 warning("child changers returned different-length inventories; cannot merge");
594 for ($i = 0; $i < @combined; $i++) {
595 my $c = $combined[$i];
596 my $k = $kid_inv->[$i];
597 # mismatches here are just warnings
598 if (defined $c->{'label'}) {
599 if (defined $k->{'label'} and $c->{'label'} ne $k->{'label'}) {
600 warning("child changers have different labels in slot at index $i");
601 $c->{'label_mismatch'} = 1;
602 $c->{'label'} = undef;
603 } elsif (!defined $k->{'label'}) {
604 $c->{'label_mismatch'} = 1;
605 $c->{'label'} = undef;
608 if (!$c->{'label_mismatch'} && !$c->{'label_set'}) {
609 $c->{'label'} = $k->{'label'};
612 $c->{'label_set'} = 1;
614 $c->{'device_status'} |= $k->{'device_status'}
615 if defined $k->{'device_status'};
617 if (!defined $c->{'f_type'} ||
618 $k->{'f_type'} != $Amanda::Header::F_TAPESTART) {
619 $c->{'f_type'} = $k->{'f_type'};
621 # a slot is empty if any of the child slots are empty
622 $c->{'state'} = Amanda::Changer::SLOT_EMPTY
623 if $k->{'state'} == Amanda::Changer::SLOT_EMPTY;
625 # a slot is reserved if any of the child slots are reserved
626 $c->{'reserved'} = $c->{'reserved'} || $k->{'reserved'};
628 # a slot is import-export if all of the child slots are import_export
629 $c->{'import_export'} = $c->{'import_export'} && $k->{'import_export'};
631 # barcodes, slots, and loaded_in are lists
632 push @{$c->{'slot'}}, $k->{'slot'};
633 push @{$c->{'barcode'}}, $k->{'barcode'};
634 push @{$c->{'loaded_in'}}, $k->{'loaded_in'};
638 # now post-process the slots, barcodes, and loaded_in into braced-alternates notation
640 for ($i = 0; $i < @combined; $i++) {
641 my $c = $combined[$i];
643 delete $c->{'label_mismatch'} if $c->{'label_mismatch'};
644 delete $c->{'label_set'} if $c->{'label_set'};
646 $c->{'slot'} = collapse_braced_alternates([ @{$c->{'slot'}} ]);
648 if (grep { !defined $_ } @{$c->{'barcode'}}) {
649 delete $c->{'barcode'};
651 $c->{'barcode'} = collapse_braced_alternates([ @{$c->{'barcode'}} ]);
654 if (grep { !defined $_ } @{$c->{'loaded_in'}}) {
655 delete $c->{'loaded_in'};
657 $c->{'loaded_in'} = collapse_braced_alternates([ @{$c->{'loaded_in'}} ]);
661 return [ @combined ];
664 package Amanda::Changer::rait::Reservation;
666 use Amanda::Util qw( :alternates );
668 @ISA = qw( Amanda::Changer::Reservation );
670 # utility function to act like 'map', but pass "ERROR" straight through
671 # (this has to appear before it is used, because it has a prototype)
674 return map { ($_ ne "ERROR")? $sub->($_) : "ERROR" } @_;
679 my ($child_reservations, $rait_device) = @_;
680 my $self = Amanda::Changer::Reservation::new($class);
682 # note that $child_reservations may contain "ERROR" in place of a reservation
684 $self->{'child_reservations'} = $child_reservations;
686 $self->{'device'} = $rait_device;
689 @slot_names = errmap { "" . $_->{'this_slot'} } @$child_reservations;
690 $self->{'this_slot'} = collapse_braced_alternates(\@slot_names);
698 my $remaining = @{$self->{'child_reservations'}};
701 my $maybe_finished = sub {
703 push @outer_errors, $err if ($err);
704 return if (--$remaining);
708 $errstr = join("; ", @outer_errors);
711 # unref the device, for good measure
712 $self->{'device'} = undef;
714 $params{'finished_cb'}->($errstr) if $params{'finished_cb'};
717 for my $res (@{$self->{'child_reservations'}}) {
718 # short-circuit an "ERROR" reservation
719 if ($res eq "ERROR") {
720 $maybe_finished->(undef);
723 $res->release(%params, finished_cb => $maybe_finished);