1 # Copyright (c) 2009-2012 Zmanda, Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or modify it
4 # under the terms of the GNU General Public License version 2 as published
5 # by the Free Software Foundation.
7 # This program is distributed in the hope that it will be useful, but
8 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
9 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 # You should have received a copy of the GNU General Public License along
13 # with this program; if not, write to the Free Software Foundation, Inc.,
14 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
19 package Amanda::Changer::aggregate;
25 @ISA = qw( Amanda::Changer );
27 use File::Glob qw( :glob );
29 use Amanda::Config qw( :getconf );
31 use Amanda::Debug qw( debug warning );
32 use Amanda::Util qw( :alternates );
35 use Amanda::Device qw( :constants );
39 Amanda::Changer::aggregate
43 This changer operates several child changers.
50 See the amanda-changers(7) manpage for usage information.
56 my ($config, $tpchanger) = @_;
57 my ($kidspecs) = ( $tpchanger =~ /chg-aggregate:(.*)/ );
59 my @kidspecs = Amanda::Util::expand_braced_alternates($kidspecs);
61 return Amanda::Changer->make_error("fatal", undef,
62 message => "chg-aggregate needs at least two child changers");
66 ($_ eq "ERROR")? "ERROR" : Amanda::Changer->new($_)
70 if (defined $config->{'properties'}->{'fail-on-error'}) {
71 $fail_on_error = string_to_boolean($config->{'properties'}->{'fail-on-error'}->{'values'}[0]);
76 if (grep { $_->isa("Amanda::Changer::Error") } @children) {
79 for my $i (0 .. @children-1) {
80 if ($children[$i]->isa("Amanda::Changer::Error")) {
82 [ $kidspecs[$i], $children[$i] ];
87 if ($valid == 0 || $fail_on_error) {
88 return Amanda::Changer->make_combined_error(
89 "fatal", [ @annotated_errs ]);
94 my $state_filename_prop = $config->{'properties'}->{'state_filename'};
96 if (defined $state_filename_prop) {
97 $state_filename = $state_filename_prop->{'values'}[0];
99 if (!defined $state_filename) {
100 $state_filename = $Amanda::Paths::CONFIG_DIR . '/' . $config->{'name'} . ".state";
105 child_names => \@kidspecs,
106 children => \@children,
107 num_children => scalar @children,
108 current_slot => undef,
109 state_filename => $state_filename,
111 bless ($self, $class);
119 foreach my $child (@{$self->{'children'}}) {
123 $self->SUPER::quit();
126 sub _get_current_slot
131 $self->with_locked_state($self->{'state_filename'}, $cb, sub {
132 my ($state, $cb) = @_;
133 $self->{'current_slot'} = $state->{'current_slot'};
134 $self->{'current_slot'} = "0:first" if !defined $self->{'current_slot'};
139 sub _set_current_slot
144 $self->with_locked_state($self->{'state_filename'}, $cb, sub {
145 my ($state, $cb) = @_;
146 $state->{'current_slot'} = $self->{'current_slot'};
156 return if $self->check_error($params{'res_cb'});
158 my $res_cb = $params{'res_cb'};
159 my $orig_slot = $params{'slot'};
160 $self->validate_params('load', \%params);
162 my $steps = define_steps
165 step which_slot => sub {
166 if (exists $params{'relative_slot'} &&
167 $params{'relative_slot'} eq "current") {
168 if (defined $self->{'current_slot'}) {
169 return $steps->{'set_from_current'}->();
171 return $self->_get_current_slot($steps->{'set_from_current'});
173 } elsif (exists $params{'relative_slot'} &&
174 $params{'relative_slot'} eq "next") {
175 if (defined $self->{'current_slot'}) {
176 return $steps->{'get_inventory_next'}->();
178 return $self->_get_current_slot($steps->{'get_inventory_next'});
180 } elsif (exists $params{'label'}) {
181 return $self->inventory(inventory_cb => $steps->{'got_inventory_label'});
183 return $steps->{'slot_set'}->();
186 step get_inventory_next => sub {
187 return $self->inventory(inventory_cb => $steps->{'got_inventory_next'})
190 step got_inventory_next => sub {
191 my ($err, $inv) = @_;
197 for my $i (0.. scalar(@$inv)-1) {
198 $slot = @$inv[$i]->{'slot'};
199 if ($slot eq $self->{'current_slot'}) {
201 } elsif ($found >= 0 && (!exists $params{'except_slots'} ||
202 !exists $params{'except_slots'}->{$slot})) {
204 return $steps->{'slot_set'}->();
208 for my $i (0..($found-1)) {
209 $slot = @$inv[$i]->{'slot'};
210 if (!exists($params{'except_slots'}) ||
211 !exists($params{'except_slots'}->{$slot})) {
213 return $steps->{'slot_set'}->();
217 return $self->make_error("failed", $res_cb,
218 reason => "notfound",
219 message => "all slots have been loaded");
222 step got_inventory_label => sub {
223 my ($err, $inv) = @_;
228 for my $i (0.. scalar(@$inv)-1) {
229 my $slot = @$inv[$i]->{'slot'};
230 my $label = @$inv[$i]->{'label'};
231 if ($label eq $params{'label'}) {
233 return $steps->{'slot_set'}->();
236 return $self->make_error("failed", $res_cb,
237 reason => "notfound",
238 message => "label $params{'label'} not found");
241 step set_from_current => sub {
242 $orig_slot = $self->{'current_slot'};
243 return $steps->{'slot_set'}->();
246 step slot_set => sub {
247 my ($kid, $slot) = split(':', $orig_slot, 2);
248 my $child = $self->{'children'}[$kid];
249 if (!defined $child) {
250 return $self->make_error("failed", $res_cb,
252 message => "no changer $kid");
254 delete $params{'relative_slot'};
255 $params{'slot'} = $slot;
256 $params{'res_cb'} = sub {
257 my ($err, $res) = @_;
259 if ($slot ne "first" && $res->{'this_slot'} != $slot) {
260 return $self->make_error("failed", $res_cb,
262 message => "slot doesn't match: $res->{'this_slot'} != $slot");
264 $self->{'current_slot'} = "$kid:$res->{'this_slot'}";
265 $aggregate_res = Amanda::Changer::aggregate::Reservation->new($self, $res, $self->{'current_slot'});
266 return $self->_set_current_slot($steps->{'done'});
269 return $res_cb->($err, undef);
271 return $child->load(%params);
275 $res_cb->(undef, $aggregate_res);
281 my ($key, %params) = @_;
283 return if $self->check_error($params{'info_cb'});
285 my $check_and_report_errors = sub {
286 my ($kid_results) = @_;
288 if (grep { defined($_->[0]) } @$kid_results) {
289 # we have errors, so collect them and make a "combined" error.
292 for my $i (0 .. $self->{'num_children'}-1) {
293 my $kr = $kid_results->[$i];
294 next unless defined($kr->[0]);
295 push @annotated_errs,
296 [ $self->{'child_names'}[$i], $kr->[0] ];
297 push @err_slots, $kr->[0]->{'slot'}
298 if (defined $kr->[0] and defined $kr->[0]->{'slot'});
302 if (@err_slots == $self->{'num_children'}) {
303 @slotarg = (slot => collapse_braced_alternates([@err_slots]));
306 $self->make_combined_error(
307 $params{'info_cb'}, [ @annotated_errs ],
313 if ($key eq 'num_slots') {
314 my $all_kids_done_cb = sub {
315 my ($kid_results) = @_;
316 return if ($check_and_report_errors->($kid_results));
320 for (@$kid_results) {
321 my ($err, %kid_info) = @$_;
322 next unless exists($kid_info{'num_slots'});
323 my $kid_num_slots = $kid_info{'num_slots'};
324 $num_slots += $kid_num_slots;
326 $params{'info_cb'}->(undef, num_slots => $num_slots) if $params{'info_cb'};
329 $self->_for_each_child(
331 my ($kid_chg, $kid_cb) = @_;
332 $kid_chg->info(info => [ 'num_slots' ], info_cb => $kid_cb);
335 parent_cb => $all_kids_done_cb,
337 } elsif ($key eq "vendor_string") {
338 my $all_kids_done_cb = sub {
339 my ($kid_results) = @_;
340 return if ($check_and_report_errors->($kid_results));
344 map { my ($e, %r) = @$_; $r{'vendor_string'} }
348 $vendor_string = collapse_braced_alternates([@kid_vendors]);
349 $params{'info_cb'}->(undef, vendor_string => $vendor_string) if $params{'info_cb'};
351 $params{'info_cb'}->(undef) if $params{'info_cb'};
355 $self->_for_each_child(
357 my ($kid_chg, $kid_cb) = @_;
358 $kid_chg->info(info => [ 'vendor_string' ], info_cb => $kid_cb);
361 parent_cb => $all_kids_done_cb,
363 } elsif ($key eq 'fast_search') {
364 my $all_kids_done_cb = sub {
365 my ($kid_results) = @_;
366 return if ($check_and_report_errors->($kid_results));
370 map { my ($e, %r) = @$_; $r{'fast_search'} }
374 # conduct a logical AND of all child fastnesses
375 for my $f (@kid_fastness) {
376 $fast_search = $fast_search && $f;
378 $params{'info_cb'}->(undef, fast_search => $fast_search) if $params{'info_cb'};
380 $params{'info_cb'}->(undef, fast_search => 0) if $params{'info_cb'};
384 $self->_for_each_child(
386 my ($kid_chg, $kid_cb) = @_;
387 $kid_chg->info(info => [ 'fast_search' ], info_cb => $kid_cb);
390 parent_cb => $all_kids_done_cb,
395 # reset, clean, etc. are all *very* similar to one another, so we create them
398 my ($op, $has_drive) = @_;
403 return if $self->check_error($params{'finished_cb'});
405 if (exists $params{'drive'}) {
406 return $self->make_error("failed", $params{'finished_cb'},
408 message => "Can't specify drive fo $op command");
411 my $all_kids_done_cb = sub {
412 my ($kid_results) = @_;
413 if (grep { defined($_->[0]) } @$kid_results) {
414 # we have errors, so collect them and make a "combined" error.
416 for my $i (0 .. $self->{'num_children'}-1) {
417 my $kr = $kid_results->[$i];
418 next unless defined($kr->[0]);
419 push @annotated_errs,
420 [ $self->{'child_names'}[$i], $kr->[0] ];
422 $self->make_combined_error(
423 $params{'finished_cb'}, [ @annotated_errs ]);
426 $params{'finished_cb'}->() if $params{'finished_cb'};
429 $self->_for_each_child(
431 my ($kid_chg, $kid_cb) = @_;
432 $kid_chg->$op(%params, finished_cb => $kid_cb);
435 parent_cb => $all_kids_done_cb,
441 # perl doesn't like that these symbols are only mentioned once
444 *reset = _mk_simple_op("reset");
445 *clean = _mk_simple_op("clean");
446 *eject = _mk_simple_op("eject");
455 my $user_msg_fn = $params{'user_msg_fn'};
456 $user_msg_fn ||= sub { Amanda::Debug::info("chg-aggregate: " . $_[0]); };
458 if (exists $params{'changed'}) {
459 for my $range (split ',', $params{'changed'}) {
460 my ($first, $last) = ($range =~ /([:\d]+)(?:-([:\d]+))?/);
461 $last = $first unless defined($last);
463 my ($f_kid, $f_slot) = split(':', $first, 2);
464 my ($l_kid, $l_slot) = split(':', $last, 2);
465 if ($f_kid != $l_kid) {
468 if ($changed{$f_kid} != 1) {
469 for my $slot ($f_slot..$l_slot) {
470 $changed{$f_kid}{$slot} = 1;
474 for my $kid ($first..$last) {
479 for my $kid (0..$self->{'num_children'}-1) {
480 if ($changed{$kid} == 1) {
481 $kid_args[$kid] = "ALL";
482 } elsif (keys %{$changed{$kid}} > 0) {
483 $kid_args[$kid] = { changed => join(',',sort(keys %{$changed{$kid}})) };
485 $kid_args[$kid] = "NONE";
489 for my $kid (0..$self->{'num_children'}-1) {
490 $kid_args[$kid] = "ALL";
494 my $all_kids_done_cb = sub {
495 my ($kid_results) = @_;
496 if (grep { defined($_->[0]) } @$kid_results) {
497 # we have errors, so collect them and make a "combined" error.
499 for my $i (0 .. $self->{'num_children'}-1) {
500 my $kr = $kid_results->[$i];
501 next unless defined($kr->[0]);
502 push @annotated_errs,
503 [ $self->{'child_names'}[$i], $kr->[0] ];
505 $self->make_combined_error(
506 $params{'finished_cb'}, [ @annotated_errs ]);
509 $params{'finished_cb'}->() if $params{'finished_cb'};
512 $self->_for_each_child(
514 my ($kid_chg, $kid_cb, $args) = @_;
515 if (ref($args) eq "HASH") {
516 $kid_chg->update(%params, finished_cb => $kid_cb, %$args);
517 } elsif ($args eq "ALL") {
518 $kid_chg->update(%params, finished_cb => $kid_cb);
524 parent_cb => $all_kids_done_cb,
533 return if $self->check_error($params{'inventory_cb'});
535 my $steps = define_steps
536 cb_ref => \$params{'inventory_cb'};
538 step get_current => sub {
539 return $self->_get_current_slot($steps->{'got_current_slot'});
542 step got_current_slot => sub {
543 $self->_for_each_child(
545 my ($kid_chg, $kid_cb) = @_;
546 $kid_chg->inventory(inventory_cb => $kid_cb);
549 parent_cb => $steps->{'all_kids_done_cb'},
553 step all_kids_done_cb => sub {
554 my ($kid_results) = @_;
555 if (grep { defined($_->[0]) } @$kid_results) {
556 # we have errors, so collect them and make a "combined" error.
558 for my $i (0 .. $self->{'num_children'}-1) {
559 my $kr = $kid_results->[$i];
560 next unless defined($kr->[0]);
561 push @annotated_errs,
562 [ $self->{'child_names'}[$i], $kr->[0] ];
564 return $self->make_combined_error(
565 $params{'inventory_cb'}, [ @annotated_errs ]);
568 my $inv = $self->_merge_inventories($kid_results);
570 return $self->make_error("failed", $params{'inventory_cb'},
572 message => "could not generate consistent inventory from aggregate child changers");
575 $params{'inventory_cb'}->(undef, $inv);
585 return if $self->check_error($params{'finished_cb'});
587 my $finished_cb = $params{'finished_cb'};
588 my $orig_slot = $params{'slot'};
590 if (!defined $params{'slot'}) {
591 return $self->make_error("failed", $finished_cb,
593 message => "no 'slot' params set.");
596 if (!defined $params{'meta'}) {
597 return $self->make_error("failed", $finished_cb,
599 message => "no 'meta' params set.");
602 my ($kid, $slot) = split(':', $orig_slot, 2);
603 my $child = $self->{'children'}[$kid];
604 if (!defined $child) {
605 return $self->make_error("failed", $finished_cb,
607 message => "no changer $kid");
610 $params{'slot'} = $slot;
611 return $child->set_meta_label(%params);
619 return if $self->check_error($params{'finished_cb'});
621 my $finished_cb = $params{'finished_cb'};
622 my $orig_slot = $params{'slot'};
624 if (!defined $params{'slot'}) {
625 return $self->make_error("failed", $finished_cb,
627 message => "no 'slot' params set.");
630 my ($kid, $slot) = split(':', $orig_slot, 2);
631 my $child = $self->{'children'}[$kid];
632 if (!defined $child) {
633 return $self->make_error("failed", $finished_cb,
635 message => "no changer $kid");
638 $params{'slot'} = $slot;
639 return $child->get_meta_label(%params);
642 # Takes keyword parameters 'oksub', 'errsub', 'parent_cb', and 'args'. For
643 # each child, runs $oksub (or, if the child is "ERROR", $errsub), passing it
644 # the changer, an aggregating callback, and the corresponding element from
645 # @$args (if specified). The callback combines its results with the results
646 # from other changers, and when all results are available, calls $parent_cb.
648 # This forms a kind of "AND" combinator for a parallel operation on multiple
649 # changers, providing the caller with a simple collection of the results of
650 # the operation. The parent_cb is called as
651 # $parent_cb->([ [ <chg_1_results> ], [ <chg_2_results> ], .. ]).
652 sub _for_each_child {
655 my ($oksub, $errsub, $parent_cb, $args) =
656 ($params{'oksub'}, $params{'errsub'}, $params{'parent_cb'}, $params{'args'});
658 if (defined($args)) {
659 confess "number of args did not match number of children"
660 unless (@$args == $self->{'num_children'});
662 $args = [ ( undef ) x $self->{'num_children'} ];
665 my $remaining = $self->{'num_children'};
666 my @results = ( undef ) x $self->{'num_children'};
667 my $maybe_done = sub {
668 return if (--$remaining);
669 $parent_cb->([ @results ]);
672 for my $i (0 .. $self->{'num_children'}-1) {
673 my $child = $self->{'children'}[$i];
674 my $arg = @$args? $args->[$i] : undef;
677 $results[$i] = [ @_ ];
681 if ($child eq "ERROR") {
682 if (defined $errsub) {
683 $errsub->("ERROR", $child_cb, $arg);
685 # no errsub; just call $child_cb directly
686 $child_cb->(undef) if $child_cb;
689 $oksub->($child, $child_cb, $arg) if $oksub;
694 sub _merge_inventories {
696 my ($kid_results) = @_;
700 for my $kid_result (@$kid_results) {
701 my $kid_inv = $kid_result->[1];
703 for my $x (@$kid_inv) {
704 my $slotname = "$nb:" . $x->{'slot'};
705 my $current = $slotname eq $self->{'current_slot'};
707 state => $x->{'state'},
708 device_status => $x->{'device_status'},
709 f_type => $x->{'f_type'},
710 label => $x->{'label'},
711 barcode => $x->{'barcode'},
712 reserved => $x->{'reserved'},
714 import_export => $x->{'import_export'},
715 loaded_in => $x->{'loaded_in'},
722 return [ @combined ];
725 package Amanda::Changer::aggregate::Reservation;
727 @ISA = qw( Amanda::Changer::Reservation );
731 my ($chg, $kid_res, $slot) = @_;
732 my $self = Amanda::Changer::Reservation::new($class);
734 $self->{'chg'} = $chg;
735 $self->{'kid_res'} = $kid_res;
736 $self->{'device'} = $kid_res->{'device'};
737 $self->{'barcode'} = $kid_res->{'barcode'};
738 $self->{'this_slot'} = $slot;
747 # unref the device, for good measure
748 $self->{'device'} = undef;
750 $self->{'kid_res'}->release(%params);
751 $self->{'kid_res'} = undef;
758 $self->{'kid_res'}->get_meta_label(%params);
765 $self->{'kid_res'}->set_meta_label(%params);