1 # Copyright (c) 2010 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::Recovery::Scan;
27 use base qw(Exporter);
28 our @EXPORT_OK = qw($DEFAULT_CHANGER);
32 use Amanda::Device qw( :constants );
33 use Amanda::Debug qw( debug );
36 use Amanda::Interactive;
38 use constant SCAN_ASK => 1; # call Amanda::Interactive module
39 use constant SCAN_POLL => 2; # wait 'poll_delay' and retry the scan.
40 use constant SCAN_FAIL => 3; # abort
41 use constant SCAN_CONTINUE => 4; # continue to the next step
42 use constant SCAN_ASK_POLL => 5; # call Amanda::Interactive module and
43 # poll at the same time.
47 Amanda::Recovery::Scan -- interface to scan algorithm
51 use Amanda::Recovey::Scan;
53 # scan the default changer with no interactivity
54 my $scan = Amanda::Recovery::Scan->new();
55 # ..or scan the changer $chg, using $interactive for interactivity
56 $scan = Amanda::Recovery::Scan->new(chg => $chg,
57 interactive => $interactive);
62 my ($err, $reservation) = @_;
66 $dev = $reservation->{device};
71 $reservation->release(finished_cb => $start_next_volume);
76 This package provides a way for programs that need to read data from volumes
77 (loosely called "recovery" programs) to find the volumes they need in a
78 configurable way. It takes care of prompting for volumes when they are not
79 available, juggling multiple changers, and any other unpredictabilities.
83 Like L<Amanda::Changer>, this package operates asynchronously, and thus
84 requires that the caller use L<Amanda::MainLoop> to poll for events.
86 A new Scan object is created with the C<new> function as follows:
88 my $scan = Amanda::Recovery::Scan->new(scan_conf => $scan_conf,
90 interactive => $interactive);
92 C<scan_conf> is the configuration for the scan, which at this point should be
93 omitted, as configuration is not yet supported. The C<chg> parameter specifies
94 the changer to start the scan with. The default changer is used if C<chg> is
95 omitted. The C<interactive> parameter gives an C<Amanda::Interactive> object.
99 Many of the callbacks used by this package are identical to the callbacks of
100 the same name in L<Amanda::Changer>.
102 When a callback is called with an error, it is an object of type
103 C<Amanda::Changer::Error>. The C<volinuse> reason has a different meaning: it
104 means that the volume with that label is present in the changer, but is in use
111 $scan->find_volume(label => $label,
113 user_msg_fn => $user_msg_fn,
116 Find the volume labelled C<$label> and call C<$res_cb>. C<$user_msg_fn> is
117 used to send progress information, The argumnet it takes are describe in
118 the next section. As with the C<load> method
119 of the changer API, C<set_current> should be set to 1 if you want the scan to
120 set the current slot.
124 The user_msg_fn take various arguments
126 Initiate the scan of the slot $slot:
127 $self->user_msg_fn(scan_slot => 1,
130 Initiate the scan of the slot $slot which should have the label $label:
131 $self->user_msg_fn(scan_slot => 1,
135 The result of scanning slot $slot:
136 $self->user_msg_fn(slot_result => 1,
141 Other options can be added at any time. The function can ignore them.
145 our $DEFAULT_CHANGER = {};
150 my $scan_conf = $params{'scan_conf'};
151 my $chg = $params{'chg'};
152 my $interactive = $params{'interactive'};
154 #until we have a config for it.
155 $scan_conf = Amanda::Recovery::Scan::Config->new();
156 $chg = Amanda::Changer->new() if !defined $chg;
161 scan_conf => $scan_conf,
162 interactive => $interactive,
164 return bless ($self, $class);
171 my $label = $params{'label'};
172 my $user_msg_fn = $params{'user_msg_fn'} || \&_user_msg_fn;
179 my $scan_running = 0;
180 my $interactive_running = 0;
181 my $restart_scan = 0;
182 my $abort_scan = undef;
183 my $last_err = undef; # keep the last meaningful error, the one reported
184 # to the user, most scan end with the notfound error,
185 # it's more interesting to report an error from the
188 my $remove_undef_state = 0;
189 my $load_for_label = 0; # 1 = Try to load the slot with the correct label
190 # 0 = Load a slot with an unknown label
192 my $steps = define_steps
193 cb_ref => \$params{'res_cb'};
195 step get_first_inventory => sub {
196 Amanda::Debug::debug("find_volume labeled '$label'");
199 $self->{'chg'}->inventory(inventory_cb => $steps->{'got_first_inventory'});
202 step got_first_inventory => sub {
203 (my $err, $inventory) = @_;
205 if ($err && $err->notimpl) {
206 #inventory not implemented
207 return $self->_find_volume_no_inventory(%params);
210 return $steps->{'call_res_cb'}->($err, undef);
213 # find current slot and keep a private copy of the value
214 for my $i (0..(scalar(@$inventory)-1)) {
215 if ($inventory->[$i]->{current}) {
216 $current = $inventory->[$i]->{slot};
221 if (!defined $current) {
222 if (scalar(@$inventory) == 0) {
225 $current = $inventory->[0]->{slot};
229 # continue parsing the inventory
230 $steps->{'parse_inventory'}->($err, $inventory);
233 step restart_scan => sub {
235 return $steps->{'get_inventory'}->();
238 step get_inventory => sub {
239 $self->{'chg'}->inventory(inventory_cb => $steps->{'parse_inventory'});
242 step parse_inventory => sub {
243 (my $err, $inventory) = @_;
245 if ($err && $err->notimpl) {
246 #inventory not implemented
247 return $self->_find_volume_no_inventory(%params);
249 return $steps->{'handle_error'}->($err, undef) if $err;
251 # throw out the inventory result and move on if the situation has
252 # changed while we were waiting
253 return $steps->{'abort_scan'}->() if $abort_scan;
254 return $steps->{'restart_scan'}->() if $restart_scan;
256 # check if label is in the inventory
257 for my $i (0..(scalar(@$inventory)-1)) {
258 my $sl = $inventory->[$i];
259 if (defined $sl->{'label'} &&
260 $sl->{'label'} eq $label) {
261 $slot_scanned = $sl->{'slot'};
262 if ($sl->{'reserved'}) {
263 return $steps->{'handle_error'}->(
264 Amanda::Changer::Error->new('failed',
265 reason => 'volinuse',
266 message => "Volume '$label' in slot $slot_scanned is reserved"),
269 Amanda::Debug::debug("parse_inventory: load slot $slot_scanned with label '$label'");
270 $user_msg_fn->(scan_slot => 1,
271 slot => $slot_scanned,
273 $seen{$slot_scanned} = { device_status => $sl->{'device_status'},
274 f_type => $sl->{'f_type'},
275 label => $sl->{'label'} };
277 return $self->{'chg'}->load(slot => $slot_scanned,
278 res_cb => $steps->{'slot_loaded'},
279 set_current => $params{'set_current'});
283 # Remove from seen all slot that have state == SLOT_UNKNOWN
284 # It is done when as scan is restarted from interactive object.
285 if ($remove_undef_state) {
286 for my $i (0..(scalar(@$inventory)-1)) {
287 my $slot = $inventory->[$i]->{slot};
288 if (exists($seen{$slot}) &&
289 !defined($inventory->[$i]->{state})) {
293 $remove_undef_state = 0;
296 # remove any slots where the state has changed from the list of seen slots
297 for my $i (0..(scalar(@$inventory)-1)) {
298 my $sl = $inventory->[$i];
299 my $slot = $sl->{slot};
301 defined($sl->{'state'}) &&
302 (($seen{$slot}->{'device_status'} != $sl->{'device_status'}) ||
303 (defined $seen{$slot}->{'device_status'} &&
304 $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS &&
305 $seen{$slot}->{'f_type'} != $sl->{'f_type'}) ||
306 (defined $seen{$slot}->{'device_status'} &&
307 $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS &&
308 defined $seen{$slot}->{'f_type'} &&
309 $seen{$slot}->{'f_type'} == $Amanda::Header::F_TAPESTART &&
310 $seen{$slot}->{'label'} ne $sl->{'label'}))) {
315 # scan any unseen slot already in a drive, if configured to do so
316 if ($self->{'scan_conf'}->{'scan_drive'}) {
317 for my $sl (@$inventory) {
318 my $slot = $sl->{'slot'};
319 if (defined $sl->{'loaded_in'} &&
320 !$sl->{'reserved'} &&
322 $slot_scanned = $slot;
323 $user_msg_fn->(scan_slot => 1, slot => $slot_scanned);
324 $seen{$slot_scanned} = { device_status => $sl->{'device_status'},
325 f_type => $sl->{'f_type'},
326 label => $sl->{'label'} };
328 return $self->{'chg'}->load(slot => $slot_scanned,
329 res_cb => $steps->{'slot_loaded'},
330 set_current => $params{'set_current'});
336 if ($self->{'scan_conf'}->{'scan_unknown_slot'}) {
337 #find index for current slot
338 my $current_index = undef;
339 for my $i (0..(scalar(@$inventory)-1)) {
340 my $slot = $inventory->[$i]->{slot};
341 if ($slot eq $current) {
346 #scan next slot to scan
347 $current_index = 0 if !defined $current_index;
348 for my $i ($current_index..(scalar(@$inventory)-1), 0..($current_index-1)) {
349 my $sl = $inventory->[$i];
350 my $slot = $sl->{slot};
351 # skip slots we've seen
352 next if defined($seen{$slot});
353 # skip slots that are empty
354 next if defined $sl->{'state'} &&
355 $sl->{'state'} == Amanda::Changer::SLOT_EMPTY;
356 # skip slots for which we have a known label, since it's not the
358 next if defined $sl->{'f_type'} &&
359 $sl->{'f_type'} == $Amanda::Header::F_TAPESTART;
360 next if defined $sl->{'label'};
362 # found a slot to check - reset our current slot
364 $slot_scanned = $current;
365 Amanda::Debug::debug("parse_inventory: load slot $current");
366 $user_msg_fn->(scan_slot => 1, slot => $slot_scanned);
367 $seen{$slot_scanned} = { device_status => $sl->{'device_status'},
368 f_type => $sl->{'f_type'},
369 label => $sl->{'label'} };
371 return $self->{'chg'}->load(slot => $slot_scanned,
372 res_cb => $steps->{'slot_loaded'},
373 set_current => $params{'set_current'});
377 #All slots are seen or empty.
379 return $steps->{'handle_error'}->($last_err, undef);
381 return $steps->{'handle_error'}->(
382 Amanda::Changer::Error->new('failed',
383 reason => 'notfound',
384 message => "Volume '$label' not found"),
389 step slot_loaded => sub {
390 (my $err, $res) = @_;
392 # we don't responsd to abort_scan or restart_scan here, since we
393 # have an open reservation that we should deal with.
395 $user_msg_fn->(slot_result => 1,
396 slot => $slot_scanned,
400 if ($res->{device}->status == $DEVICE_STATUS_SUCCESS &&
401 $res->{device}->volume_label &&
402 $res->{device}->volume_label eq $label) {
403 my $volume_label = $res->{device}->volume_label;
404 return $steps->{'call_res_cb'}->(undef, $res);
407 if (defined $res->{device}->volume_header) {
408 $f_type = $res->{device}->volume_header->{type};
413 # The slot did not contain the volume we wanted, so mark it
414 # as seen and try again.
415 $seen{$slot_scanned} = {
416 device_status => $res->{device}->status,
418 label => $res->{device}->volume_label
422 if ($res->{device}->status == $DEVICE_STATUS_SUCCESS) {
425 $last_err = Amanda::Changer::Error->new('fatal',
426 message => $res->{device}->error_or_status());
428 return $res->release(finished_cb => $steps->{'load_released'});
430 if ($load_for_label == 0 && $err->volinuse) {
431 # Scan semantics for volinuse is different than changer.
432 # If a slot with unknown label is loaded then we map
433 # volinuse to driveinuse.
434 $err->{reason} = "driveinuse";
436 $last_err = $err if $err->fatal || !$err->notfound;
437 if ($load_for_label == 1 && $err->failed && $err->volinuse) {
438 # volinuse is an error
439 return $steps->{'handle_error'}->($err, $steps->{'load_released'});
441 return $steps->{'load_released'}->();
445 step load_released => sub {
452 # throw out the inventory result and move on if the situation has
453 # changed while we were loading a volume
454 return $steps->{'abort_scan'}->() if $abort_scan;
455 return $steps->{'restart_scan'}->() if $restart_scan;
457 $new_slot = $current;
458 $steps->{'get_inventory'}->();
461 step handle_error => sub {
462 my ($err, $continue_cb) = @_;
464 my $scan_method = undef;
469 $poll_src->remove() if defined $poll_src;
472 # prefer to use scan method for $last_err, if present
473 if ($last_err && $err->failed && $err->notfound) {
474 $message = "$last_err";
476 if ($last_err->isa("Amanda::Changer::Error")) {
477 if ($last_err->fatal) {
478 $scan_method = $self->{'scan_conf'}->{'fatal'};
480 $scan_method = $self->{'scan_conf'}->{$last_err->{'reason'}};
482 } elsif ($continue_cb) {
483 $scan_method = SCAN_CONTINUE;
487 #use scan method for $err
488 if (!defined $scan_method) {
490 $message = "$err" if !defined $message;
492 $scan_method = $self->{'scan_conf'}->{'fatal'};
494 $scan_method = $self->{'scan_conf'}->{$err->{'reason'}};
497 die("error not defined");
498 $scan_method = SCAN_ASK_POLL;
502 ## implement the desired scan method
504 if ($scan_method == SCAN_CONTINUE && !defined $continue_cb) {
505 $scan_method = $self->{'scan_conf'}->{'notfound'};
506 if ($scan_method == SCAN_CONTINUE) {
507 $scan_method = SCAN_FAIL;
511 if ($scan_method == SCAN_ASK && !defined $self->{'interactive'}) {
512 $scan_method = SCAN_FAIL;
515 if ($scan_method == SCAN_ASK_POLL && !defined $self->{'interactive'}) {
516 $scan_method = SCAN_FAIL;
519 if ($scan_method == SCAN_ASK) {
520 return $steps->{'scan_interactive'}->("$message");
521 } elsif ($scan_method == SCAN_POLL) {
522 $poll_src = Amanda::MainLoop::call_after(
523 $self->{'scan_conf'}->{'poll_delay'},
524 $steps->{'after_poll'});
526 } elsif ($scan_method == SCAN_ASK_POLL) {
527 $steps->{'scan_interactive'}->("$message\n");
528 $poll_src = Amanda::MainLoop::call_after(
529 $self->{'scan_conf'}->{'poll_delay'},
530 $steps->{'after_poll'});
532 } elsif ($scan_method == SCAN_FAIL) {
533 return $steps->{'call_res_cb'}->($err, undef);
534 } elsif ($scan_method == SCAN_CONTINUE) {
535 return $continue_cb->($err, undef);
537 die("Invalid SCAN_* value:$err:$err->{'reason'}:$scan_method");
541 step after_poll => sub {
542 $poll_src->remove() if defined $poll_src;
544 return $steps->{'restart_scan'}->();
547 step scan_interactive => sub {
548 my ($err_message) = @_;
549 if (!$interactive_running) {
550 $interactive_running = 1;
551 my $message = "$err_message\nInsert volume labeled '$label' in changer and type <enter>\nor type \"^D\" to abort\n";
552 $self->{'interactive'}->user_request(
555 err => "$err_message",
556 chg_name => $self->{'chg'}->{'chg_name'},
557 finished_cb => $steps->{'scan_interactive_cb'});
562 step scan_interactive_cb => sub {
563 my ($err, $message) = @_;
564 $interactive_running = 0;
565 $poll_src->remove() if defined $poll_src;
574 return $steps->{'call_res_cb'}->($err, undef);
578 if ($message ne '') {
581 if (ref($message) eq 'HASH' and $message == $DEFAULT_CHANGER) {
582 $new_chg = Amanda::Changer->new();
584 $new_chg = Amanda::Changer->new($message);
586 if ($new_chg->isa("Amanda::Changer::Error")) {
587 return $steps->{'scan_interactive'}->("$new_chg");
589 $self->{'chg'} = $new_chg;
592 $remove_undef_state = 1;
599 return $steps->{'restart_scan'}->();
603 step abort_scan => sub {
604 $steps->{'call_res_cb'}->($abort_scan, undef);
607 step call_res_cb => sub {
608 (my $err, $res) = @_;
610 # TODO: what happens if the search was aborted or
611 # restarted in the interim?
614 $poll_src->remove() if defined $poll_src;
616 $interactive_running = 0;
617 $self->{'interactive'}->abort() if defined $self->{'interactive'};
618 $params{'res_cb'}->($err, $res);
623 sub _find_volume_no_inventory {
627 my $label = $params{'label'};
635 my $steps = define_steps
636 cb_ref => \$params{'res_cb'};
638 step load_label => sub {
639 return $self->{'chg'}->load(relative_slot => "current",
640 res_cb => $steps->{'load_label_cb'});
643 step load_label_cb => sub {
644 (my $err, $res) = @_;
647 if ($err->failed && $err->notfound) {
648 return $params{'res_cb'}->($err, undef);
649 } elsif ($err->failed && $err->volinuse and defined $err->{'slot'}) {
650 $last_slot = $err->{'slot'};
652 #no interactivity yet.
653 return $params{'res_cb'}->($err, undef);
656 $last_slot = $res->{'this_slot'}
659 $seen_slots{$last_slot} = 1 if defined $last_slot;
661 my $dev = $res->{'device'};
662 if (defined $dev->volume_label && $dev->volume_label eq $label) {
663 return $params{'res_cb'}->(undef, $res);
665 return $res->release(finished_cb => $steps->{'released'});
667 return $steps->{'released'}->()
671 step released => sub {
672 $self->{'chg'}->load(relative_slot => "next",
673 except_slots => \%seen_slots,
674 res_cb => $steps->{'load_label_cb'},
683 package Amanda::Recovery::Scan::Config;
689 my $self = bless {}, $class;
691 $self->{'scan_drive'} = 0;
692 $self->{'scan_unknown_slot'} = 1;
693 $self->{'scan_interactivity'} = undef;
696 $self->{'poll_unknown_slot'} = 0;
697 $self->{'poll_drive'} = 0;
698 $self->{'poll_delay'} = 10000; #10 seconds
699 $self->{'user_unknown_slot'} = 1;
700 $self->{'user_drive'} = 0;
701 $self->{'fatal'} = Amanda::Recovery::Scan::SCAN_CONTINUE;
702 $self->{'driveinuse'} = Amanda::Recovery::Scan::SCAN_ASK_POLL;
703 $self->{'volinuse'} = Amanda::Recovery::Scan::SCAN_ASK_POLL;
704 $self->{'notfound'} = Amanda::Recovery::Scan::SCAN_ASK_POLL;
705 $self->{'unknown'} = Amanda::Recovery::Scan::SCAN_FAIL;
706 $self->{'notimpl'} = Amanda::Recovery::Scan::SCAN_FAIL;
707 $self->{'invalid'} = Amanda::Recovery::Scan::SCAN_CONTINUE;