1 # Copyright (c) 2010-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::Recovery::Scan;
28 use base qw(Exporter);
29 our @EXPORT_OK = qw($DEFAULT_CHANGER);
33 use Amanda::Device qw( :constants );
34 use Amanda::Debug qw( debug );
37 use Amanda::Interactivity;
39 use constant SCAN_ASK => 1; # call Amanda::Interactivity module
40 use constant SCAN_POLL => 2; # wait 'poll_delay' and retry the scan.
41 use constant SCAN_FAIL => 3; # abort
42 use constant SCAN_CONTINUE => 4; # continue to the next step
43 use constant SCAN_ASK_POLL => 5; # call Amanda::Interactivity module and
44 # poll at the same time.
48 Amanda::Recovery::Scan -- interface to scan algorithm
52 use Amanda::Recovey::Scan;
54 # scan the default changer with no interactivity
55 my $scan = Amanda::Recovery::Scan->new();
56 # ..or scan the changer $chg, using $interactivity for interactivity
57 $scan = Amanda::Recovery::Scan->new(chg => $chg,
58 interactivity => $interactivity);
63 my ($err, $reservation) = @_;
67 $dev = $reservation->{device};
72 $reservation->release(finished_cb => $start_next_volume);
75 $scan->quit(); # also quit the changer
80 This package provides a way for programs that need to read data from volumes
81 (loosely called "recovery" programs) to find the volumes they need in a
82 configurable way. It takes care of prompting for volumes when they are not
83 available, juggling multiple changers, and any other unpredictabilities.
87 Like L<Amanda::Changer>, this package operates asynchronously, and thus
88 requires that the caller use L<Amanda::MainLoop> to poll for events.
90 A new Scan object is created with the C<new> function as follows:
92 my $scan = Amanda::Recovery::Scan->new(scan_conf => $scan_conf,
94 interactivity => $interactivity);
96 C<scan_conf> is the configuration for the scan, which at this point should be
97 omitted, as configuration is not yet supported. The C<chg> parameter specifies
98 the changer to start the scan with. The default changer is used if C<chg> is
99 omitted. The C<interactivity> parameter gives an C<Amanda::Interactivity> object.
103 Many of the callbacks used by this package are identical to the callbacks of
104 the same name in L<Amanda::Changer>.
106 When a callback is called with an error, it is an object of type
107 C<Amanda::Changer::Error>. The C<volinuse> reason has a different meaning: it
108 means that the volume with that label is present in the changer, but is in use
115 $scan->find_volume(label => $label,
117 user_msg_fn => $user_msg_fn,
120 Find the volume labelled C<$label> and call C<$res_cb>. C<$user_msg_fn> is
121 used to send progress information, The argumnet it takes are describe in
122 the next section. As with the C<load> method
123 of the changer API, C<set_current> should be set to 1 if you want the scan to
124 set the current slot.
130 The cleanly terminate a scan objet, the changer quit is also called.
134 The user_msg_fn take various arguments
136 Initiate the scan of the slot $slot:
137 $self->user_msg_fn(scan_slot => 1,
140 Initiate the scan of the slot $slot which should have the label $label:
141 $self->user_msg_fn(scan_slot => 1,
145 The result of scanning slot $slot:
146 $self->user_msg_fn(slot_result => 1,
151 Other options can be added at any time. The function can ignore them.
155 our $DEFAULT_CHANGER = {};
160 my $scan_conf = $params{'scan_conf'};
161 my $chg = $params{'chg'};
162 my $interactivity = $params{'interactivity'};
164 #until we have a config for it.
165 $scan_conf = Amanda::Recovery::Scan::Config->new();
166 $chg = Amanda::Changer->new() if !defined $chg;
167 return $chg if $chg->isa("Amanda::Changer::Error");
172 scan_conf => $scan_conf,
173 interactivity => $interactivity,
175 return bless ($self, $class);
181 die("Recovery::Scan detroyed without quit") if defined $self->{'scan_conf'};
187 $self->{'chg'}->quit() if defined $self->{'chg'};
189 foreach (keys %$self) {
199 my $label = $params{'label'};
200 my $user_msg_fn = $params{'user_msg_fn'} || \&_user_msg_fn;
207 my $scan_running = 0;
208 my $interactivity_running = 0;
209 my $restart_scan = 0;
210 my $abort_scan = undef;
211 my $last_err = undef; # keep the last meaningful error, the one reported
212 # to the user, most scan end with the notfound error,
213 # it's more interesting to report an error from the
216 my $remove_undef_state = 0;
217 my $load_for_label = 0; # 1 = Try to load the slot with the correct label
218 # 0 = Load a slot with an unknown label
220 my $steps = define_steps
221 cb_ref => \$params{'res_cb'};
223 step get_first_inventory => sub {
224 Amanda::Debug::debug("find_volume labeled '$label'");
227 $self->{'chg'}->inventory(inventory_cb => $steps->{'got_first_inventory'});
230 step got_first_inventory => sub {
231 (my $err, $inventory) = @_;
233 if ($err && $err->notimpl) {
234 #inventory not implemented
235 return $self->_find_volume_no_inventory(%params);
238 return $steps->{'call_res_cb'}->($err, undef);
241 # find current slot and keep a private copy of the value
242 for my $i (0..(scalar(@$inventory)-1)) {
243 if ($inventory->[$i]->{current}) {
244 $current = $inventory->[$i]->{slot};
249 if (!defined $current) {
250 if (scalar(@$inventory) == 0) {
253 $current = $inventory->[0]->{slot};
257 # continue parsing the inventory
258 $steps->{'parse_inventory'}->($err, $inventory);
261 step restart_scan => sub {
263 return $steps->{'get_inventory'}->();
266 step get_inventory => sub {
267 $self->{'chg'}->inventory(inventory_cb => $steps->{'parse_inventory'});
270 step parse_inventory => sub {
271 (my $err, $inventory) = @_;
273 if ($err && $err->notimpl) {
274 #inventory not implemented
275 return $self->_find_volume_no_inventory(%params);
277 return $steps->{'handle_error'}->($err, undef) if $err;
279 # throw out the inventory result and move on if the situation has
280 # changed while we were waiting
281 return $steps->{'abort_scan'}->() if $abort_scan;
282 return $steps->{'restart_scan'}->() if $restart_scan;
284 # check if label is in the inventory
285 for my $i (0..(scalar(@$inventory)-1)) {
286 my $sl = $inventory->[$i];
287 if (defined $sl->{'label'} and
288 $sl->{'label'} eq $label and
289 !defined $seen{$sl->{'slot'}}) {
290 $slot_scanned = $sl->{'slot'};
291 if ($sl->{'reserved'}) {
292 return $steps->{'handle_error'}->(
293 Amanda::Changer::Error->new('failed',
294 reason => 'volinuse',
295 message => "Volume '$label' in slot $slot_scanned is reserved"),
298 Amanda::Debug::debug("parse_inventory: load slot $slot_scanned with label '$label'");
299 $user_msg_fn->(scan_slot => 1,
300 slot => $slot_scanned,
302 $seen{$slot_scanned} = { device_status => $sl->{'device_status'},
303 f_type => $sl->{'f_type'},
304 label => $sl->{'label'} };
306 return $self->{'chg'}->load(slot => $slot_scanned,
307 res_cb => $steps->{'slot_loaded'},
308 set_current => $params{'set_current'});
312 # Remove from seen all slot that have state == SLOT_UNKNOWN
313 # It is done when as scan is restarted from interactivity object.
314 if ($remove_undef_state) {
315 for my $i (0..(scalar(@$inventory)-1)) {
316 my $slot = $inventory->[$i]->{slot};
317 if (exists($seen{$slot}) &&
318 !defined($inventory->[$i]->{state})) {
322 $remove_undef_state = 0;
325 # remove any slots where the state has changed from the list of seen slots
326 for my $i (0..(scalar(@$inventory)-1)) {
327 my $sl = $inventory->[$i];
328 my $slot = $sl->{slot};
330 defined($sl->{'state'}) &&
331 (($seen{$slot}->{'device_status'} != $sl->{'device_status'}) ||
332 (defined $seen{$slot}->{'device_status'} &&
333 $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS &&
334 $seen{$slot}->{'f_type'} != $sl->{'f_type'}) ||
335 (defined $seen{$slot}->{'device_status'} &&
336 $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS &&
337 defined $seen{$slot}->{'f_type'} &&
338 $seen{$slot}->{'f_type'} == $Amanda::Header::F_TAPESTART &&
339 $seen{$slot}->{'label'} ne $sl->{'label'}))) {
344 # scan any unseen slot already in a drive, if configured to do so
345 if ($self->{'scan_conf'}->{'scan_drive'}) {
346 for my $sl (@$inventory) {
347 my $slot = $sl->{'slot'};
348 if (defined $sl->{'loaded_in'} &&
349 !$sl->{'reserved'} &&
351 $slot_scanned = $slot;
352 $user_msg_fn->(scan_slot => 1, slot => $slot_scanned);
353 $seen{$slot_scanned} = { device_status => $sl->{'device_status'},
354 f_type => $sl->{'f_type'},
355 label => $sl->{'label'} };
357 return $self->{'chg'}->load(slot => $slot_scanned,
358 res_cb => $steps->{'slot_loaded'},
359 set_current => $params{'set_current'});
365 if ($self->{'scan_conf'}->{'scan_unknown_slot'}) {
366 #find index for current slot
367 my $current_index = undef;
368 for my $i (0..(scalar(@$inventory)-1)) {
369 my $slot = $inventory->[$i]->{slot};
370 if ($slot eq $current) {
375 #scan next slot to scan
376 $current_index = 0 if !defined $current_index;
377 for my $i ($current_index..(scalar(@$inventory)-1), 0..($current_index-1)) {
378 my $sl = $inventory->[$i];
379 my $slot = $sl->{slot};
380 # skip slots we've seen
381 next if defined($seen{$slot});
382 # skip slots that are empty
383 next if defined $sl->{'state'} &&
384 $sl->{'state'} == Amanda::Changer::SLOT_EMPTY;
385 # skip slots for which we have a known label, since it's not the
387 next if defined $sl->{'f_type'} &&
388 $sl->{'f_type'} == $Amanda::Header::F_TAPESTART;
389 next if defined $sl->{'label'};
391 # found a slot to check - reset our current slot
393 $slot_scanned = $current;
394 Amanda::Debug::debug("parse_inventory: load slot $current");
395 $user_msg_fn->(scan_slot => 1, slot => $slot_scanned);
396 $seen{$slot_scanned} = { device_status => $sl->{'device_status'},
397 f_type => $sl->{'f_type'},
398 label => $sl->{'label'} };
400 return $self->{'chg'}->load(slot => $slot_scanned,
401 res_cb => $steps->{'slot_loaded'},
402 set_current => $params{'set_current'});
406 #All slots are seen or empty.
408 return $steps->{'handle_error'}->($last_err, undef);
410 return $steps->{'handle_error'}->(
411 Amanda::Changer::Error->new('failed',
412 reason => 'notfound',
413 message => "Volume '$label' not found"),
418 step slot_loaded => sub {
419 (my $err, $res) = @_;
421 # we don't responsd to abort_scan or restart_scan here, since we
422 # have an open reservation that we should deal with.
424 $user_msg_fn->(slot_result => 1,
425 slot => $slot_scanned,
429 if ($res->{device}->status == $DEVICE_STATUS_SUCCESS &&
430 $res->{device}->volume_label &&
431 $res->{device}->volume_label eq $label) {
432 my $volume_label = $res->{device}->volume_label;
433 return $steps->{'call_res_cb'}->(undef, $res);
436 if (defined $res->{device}->volume_header) {
437 $f_type = $res->{device}->volume_header->{type};
442 # The slot did not contain the volume we wanted, so mark it
443 # as seen and try again.
444 $seen{$slot_scanned} = {
445 device_status => $res->{device}->status,
447 label => $res->{device}->volume_label
451 if ($res->{device}->status == $DEVICE_STATUS_SUCCESS) {
454 $last_err = Amanda::Changer::Error->new('fatal',
455 message => $res->{device}->error_or_status());
457 return $res->release(finished_cb => $steps->{'load_released'});
459 if ($load_for_label == 0 && $err->volinuse) {
460 # Scan semantics for volinuse is different than changer.
461 # If a slot with unknown label is loaded then we map
462 # volinuse to driveinuse.
463 $err->{reason} = "driveinuse";
465 $last_err = $err if $err->fatal || !$err->notfound;
466 if ($load_for_label == 1 && $err->failed && $err->volinuse) {
467 # volinuse is an error
468 return $steps->{'handle_error'}->($err, $steps->{'load_released'});
470 return $steps->{'load_released'}->();
474 step load_released => sub {
481 # throw out the inventory result and move on if the situation has
482 # changed while we were loading a volume
483 return $steps->{'abort_scan'}->() if $abort_scan;
484 return $steps->{'restart_scan'}->() if $restart_scan;
486 $new_slot = $current;
487 $steps->{'get_inventory'}->();
490 step handle_error => sub {
491 my ($err, $continue_cb) = @_;
493 my $scan_method = undef;
498 $poll_src->remove() if defined $poll_src;
501 # prefer to use scan method for $last_err, if present
502 if ($last_err && $err->failed && $err->notfound) {
503 $message = "$last_err";
505 if ($last_err->isa("Amanda::Changer::Error")) {
506 if ($last_err->fatal) {
507 $scan_method = $self->{'scan_conf'}->{'fatal'};
509 $scan_method = $self->{'scan_conf'}->{$last_err->{'reason'}};
511 } elsif ($continue_cb) {
512 $scan_method = SCAN_CONTINUE;
516 #use scan method for $err
517 if (!defined $scan_method) {
519 $message = "$err" if !defined $message;
521 $scan_method = $self->{'scan_conf'}->{'fatal'};
523 $scan_method = $self->{'scan_conf'}->{$err->{'reason'}};
526 confess("error not defined");
527 $scan_method = SCAN_ASK_POLL;
531 ## implement the desired scan method
533 if ($scan_method == SCAN_CONTINUE && !defined $continue_cb) {
534 $scan_method = $self->{'scan_conf'}->{'notfound'};
535 if ($scan_method == SCAN_CONTINUE) {
536 $scan_method = SCAN_FAIL;
540 if ($scan_method == SCAN_ASK && !defined $self->{'interactivity'}) {
541 $scan_method = SCAN_FAIL;
544 if ($scan_method == SCAN_ASK_POLL && !defined $self->{'interactivity'}) {
545 $scan_method = SCAN_FAIL;
548 if ($scan_method == SCAN_ASK) {
549 return $steps->{'scan_interactivity'}->("$message");
550 } elsif ($scan_method == SCAN_POLL) {
551 $poll_src = Amanda::MainLoop::call_after(
552 $self->{'scan_conf'}->{'poll_delay'},
553 $steps->{'after_poll'});
555 } elsif ($scan_method == SCAN_ASK_POLL) {
556 $steps->{'scan_interactivity'}->("$message\n");
557 $poll_src = Amanda::MainLoop::call_after(
558 $self->{'scan_conf'}->{'poll_delay'},
559 $steps->{'after_poll'});
561 } elsif ($scan_method == SCAN_FAIL) {
562 return $steps->{'call_res_cb'}->($err, undef);
563 } elsif ($scan_method == SCAN_CONTINUE) {
564 return $continue_cb->($err, undef);
566 confess("Invalid SCAN_* value:$err:$err->{'reason'}:$scan_method");
570 step after_poll => sub {
571 $poll_src->remove() if defined $poll_src;
573 return $steps->{'restart_scan'}->();
576 step scan_interactivity => sub {
577 my ($err_message) = @_;
578 if (!$interactivity_running) {
579 $interactivity_running = 1;
580 my $message = "$err_message\nInsert volume labeled '$label' in changer and type <enter>\nor type \"^D\" to abort\n";
581 $self->{'interactivity'}->user_request(
584 err => "$err_message",
585 chg_name => $self->{'chg'}->{'chg_name'},
586 request_cb => $steps->{'scan_interactivity_cb'});
591 step scan_interactivity_cb => sub {
592 my ($err, $message) = @_;
593 $interactivity_running = 0;
594 $poll_src->remove() if defined $poll_src;
602 return $steps->{'call_res_cb'}->($err, undef);
606 if ($message ne '') {
609 if (ref($message) eq 'HASH' and $message == $DEFAULT_CHANGER) {
610 $new_chg = Amanda::Changer->new();
612 $new_chg = Amanda::Changer->new($message);
614 if ($new_chg->isa("Amanda::Changer::Error")) {
615 return $steps->{'scan_interactivity'}->("$new_chg");
618 $self->{'chg'}->quit();
619 $self->{'chg'} = $new_chg;
622 $remove_undef_state = 1;
629 return $steps->{'restart_scan'}->();
633 step abort_scan => sub {
634 $steps->{'call_res_cb'}->($abort_scan, undef);
637 step call_res_cb => sub {
638 (my $err, $res) = @_;
640 # TODO: what happens if the search was aborted or
641 # restarted in the interim?
644 $poll_src->remove() if defined $poll_src;
646 $interactivity_running = 0;
647 $self->{'interactivity'}->abort() if defined $self->{'interactivity'};
648 $params{'res_cb'}->($err, $res);
653 sub _find_volume_no_inventory {
657 my $label = $params{'label'};
665 my $steps = define_steps
666 cb_ref => \$params{'res_cb'};
668 step load_label => sub {
669 return $self->{'chg'}->load(relative_slot => "current",
670 res_cb => $steps->{'load_label_cb'});
673 step load_label_cb => sub {
674 (my $err, $res) = @_;
677 if ($err->failed && $err->notfound) {
678 if ($err->{'message'} eq "all slots have been loaded") {
679 $err->{'message'} = "label '$label' not found";
681 return $params{'res_cb'}->($err, undef);
682 } elsif ($err->failed && $err->volinuse and defined $err->{'slot'}) {
683 $last_slot = $err->{'slot'};
685 #no interactivity yet.
686 return $params{'res_cb'}->($err, undef);
689 $last_slot = $res->{'this_slot'}
692 $seen_slots{$last_slot} = 1 if defined $last_slot;
694 my $dev = $res->{'device'};
695 if (defined $dev->volume_label && $dev->volume_label eq $label) {
696 return $params{'res_cb'}->(undef, $res);
698 return $res->release(finished_cb => $steps->{'released'});
700 return $steps->{'released'}->()
704 step released => sub {
705 $self->{'chg'}->load(relative_slot => "next",
706 except_slots => \%seen_slots,
707 res_cb => $steps->{'load_label_cb'},
716 package Amanda::Recovery::Scan::Config;
722 my $self = bless {}, $class;
724 $self->{'scan_drive'} = 0;
725 $self->{'scan_unknown_slot'} = 1;
726 $self->{'poll_delay'} = 10000; #10 seconds
728 $self->{'fatal'} = Amanda::Recovery::Scan::SCAN_CONTINUE;
729 $self->{'driveinuse'} = Amanda::Recovery::Scan::SCAN_ASK_POLL;
730 $self->{'volinuse'} = Amanda::Recovery::Scan::SCAN_ASK_POLL;
731 $self->{'notfound'} = Amanda::Recovery::Scan::SCAN_ASK_POLL;
732 $self->{'unknown'} = Amanda::Recovery::Scan::SCAN_FAIL;
733 $self->{'notimpl'} = Amanda::Recovery::Scan::SCAN_FAIL;
734 $self->{'invalid'} = Amanda::Recovery::Scan::SCAN_CONTINUE;