1 # Copyright (c) 2010-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::ScanInventory;
27 This package implements a base class for all scan that use the inventory.
28 see C<amanda-taperscan(7)>.
39 use base qw(Exporter);
40 our @EXPORT_OK = qw($DEFAULT_CHANGER);
44 use Amanda::Device qw( :constants );
45 use Amanda::Debug qw( debug );
48 use Amanda::Interactivity;
50 use constant SCAN_ASK => 1; # call Amanda::Interactivity module
51 use constant SCAN_POLL => 2; # wait 'poll_delay' and retry the scan.
52 use constant SCAN_FAIL => 3; # abort
53 use constant SCAN_CONTINUE => 4; # continue to the next step
54 use constant SCAN_ASK_POLL => 5; # call Amanda::Interactivity module and
55 # poll at the same time.
56 use constant SCAN_LOAD => 6; # load a slot
57 use constant SCAN_DONE => 7; # successful scan
59 our $DEFAULT_CHANGER = {};
64 my $scan_conf = $params{'scan_conf'};
65 my $tapelist = $params{'tapelist'};
66 my $chg = $params{'changer'};
67 my $interactivity = $params{'interactivity'};
69 #until we have a config for it.
70 $scan_conf = Amanda::ScanInventory::Config->new();
71 $chg = Amanda::Changer->new(undef, tapelist => $tapelist) if !defined $chg;
77 scan_conf => $scan_conf,
78 tapelist => $tapelist,
79 interactivity => $interactivity,
83 return bless ($self, $class);
91 die "Can only run one scan at a time" if $self->{'scanning'};
92 $self->{'scanning'} = 1;
93 $self->{'user_msg_fn'} = $params{'user_msg_fn'} || sub {};
95 # refresh the tapelist at every scan
96 $self->read_tapelist();
98 # count the number of scans we do, so we can only load 'current' on the
100 $self->{'scan_num'}++;
102 $self->_scan(%params);
108 $self->{'user_msg_fn'}->(%params);
115 my $user_msg_fn = $params{'user_msg_fn'} || \&_user_msg_fn;
125 my $scan_running = 0;
126 my $interactivity_running = 0;
127 my $restart_scan = 0;
128 my $abort_scan = undef;
129 my $last_err = undef; # keep the last meaningful error, the one reported
130 # to the user, most scan end with the notfound error,
131 # it's more interesting to report an error from the
134 my $remove_undef_state = 0;
135 my $result_cb = $params{'result_cb'};
137 my $steps = define_steps
138 cb_ref => \$result_cb;
140 step get_first_inventory => sub {
142 $self->{'chg'}->inventory(inventory_cb => $steps->{'got_first_inventory'});
145 step got_first_inventory => sub {
146 (my $err, $inventory) = @_;
148 if ($err && $err->notimpl) {
149 #inventory not implemented
151 } elsif ($err and $err->fatal) {
153 return $steps->{'call_result_cb'}->($err, undef);
156 # continue parsing the inventory
157 $steps->{'parse_inventory'}->($err, $inventory);
160 step restart_scan => sub {
162 return $steps->{'get_inventory'}->();
165 step get_inventory => sub {
166 $self->{'chg'}->inventory(inventory_cb => $steps->{'parse_inventory'});
169 step parse_inventory => sub {
170 (my $err, $inventory) = @_;
172 if ($err && $err->notimpl) {
173 #inventory not implemented
176 return $steps->{'handle_error'}->($err, undef) if $err;
178 # throw out the inventory result and move on if the situation has
179 # changed while we were waiting
180 return $steps->{'abort_scan'}->() if $abort_scan;
181 return $steps->{'restart_scan'}->() if $restart_scan;
183 # Remove from seen all slot that have state == SLOT_UNKNOWN
184 # It is done when a scan is restarted from interactivity object.
185 if ($remove_undef_state) {
186 for my $i (0..(scalar(@$inventory)-1)) {
187 my $slot = $inventory->[$i]->{slot};
188 if (exists($seen{$slot}) &&
189 !defined($inventory->[$i]->{state})) {
193 $remove_undef_state = 0;
196 # remove any slots where the state has changed from the list of seen slots
197 for my $i (0..(scalar(@$inventory)-1)) {
198 my $sl = $inventory->[$i];
199 my $slot = $sl->{slot};
201 !defined ($seen{$slot}->{'failed'}) &&
202 defined($sl->{'state'}) &&
203 (($seen{$slot}->{'device_status'} != $sl->{'device_status'}) ||
204 (defined $seen{$slot}->{'device_status'} &&
205 $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS &&
206 $seen{$slot}->{'f_type'} != $sl->{'f_type'}) ||
207 (defined $seen{$slot}->{'device_status'} &&
208 $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS &&
209 defined $seen{$slot}->{'f_type'} &&
210 $seen{$slot}->{'f_type'} == $Amanda::Header::F_TAPESTART &&
211 $seen{$slot}->{'label'} ne $sl->{'label'}))) {
216 ($action, $action_slot) = $self->analyze($inventory, \%seen, $res);
218 if ($action == Amanda::ScanInventory::SCAN_DONE) {
219 return $steps->{'call_result_cb'}->(undef, $res);
223 $res->release(finished_cb => $steps->{'released'});
225 $steps->{'released'}->();
229 step released => sub {
230 if ($action == Amanda::ScanInventory::SCAN_LOAD) {
231 $slot_scanned = $action_slot;
232 $self->_user_msg(scan_slot => 1,
233 slot => $slot_scanned);
234 return $self->{'changer'}->load(
235 slot => $slot_scanned,
236 set_current => $params{'set_current'},
237 res_cb => $steps->{'slot_loaded'});
244 $err = Amanda::Changer::Error->new('failed',
245 reason => 'notfound',
246 message => "No acceptable volumes found");
249 if ($action == Amanda::ScanInventory::SCAN_FAIL) {
250 return $steps->{'handle_error'}->($err, undef);
253 $steps->{'scan_next'}->($action, $err);
256 step slot_loaded => sub {
257 (my $err, $res) = @_;
259 # we don't responsd to abort_scan or restart_scan here, since we
260 # have an open reservation that we should deal with.
263 if ($res && defined $res->{device} &&
264 $res->{device}->status == $DEVICE_STATUS_SUCCESS) {
265 $label = $res->{device}->volume_label;
267 my $relabeled = !defined($label) || $label !~ /$self->{'labelstr'}/;
268 $self->_user_msg(slot_result => 1,
269 slot => $slot_scanned,
272 relabeled => $relabeled,
276 if (defined $res->{device}->volume_header) {
277 $f_type = $res->{device}->volume_header->{type};
280 # The slot did not contain the volume we wanted, so mark it
281 # as seen and try again.
282 $seen{$slot_scanned} = {
283 device_status => $res->{device}->status,
285 label => $res->{device}->volume_label
289 if ($res->{device}->status == $DEVICE_STATUS_SUCCESS) {
292 $last_err = Amanda::Changer::Error->new('fatal',
293 message => $res->{device}->error_or_status());
296 $seen{$slot_scanned} = { failed => 1 };
297 if ($err->volinuse) {
298 # Scan semantics for volinuse is different than changer.
299 # If a slot with unknown label is loaded then we map
300 # volinuse to driveinuse.
301 $err->{reason} = "driveinuse";
303 $last_err = $err if $err->fatal || !$err->notfound;
305 return $steps->{'load_released'}->();
308 step load_released => sub {
313 # throw out the inventory result and move on if the situation has
314 # changed while we were loading a volume
315 return $steps->{'abort_scan'}->() if $abort_scan;
316 return $steps->{'restart_scan'}->() if $restart_scan;
318 $new_slot = $current;
319 $steps->{'get_inventory'}->();
322 step handle_error => sub {
323 my ($err, $continue_cb) = @_;
325 my $scan_method = undef;
329 $poll_src->remove() if defined $poll_src;
332 # prefer to use scan method for $last_err, if present
333 if ($last_err && $err->failed && $err->notfound) {
334 $message = "$last_err";
336 if ($last_err->isa("Amanda::Changer::Error")) {
337 if ($last_err->fatal) {
338 $scan_method = $self->{'scan_conf'}->{'fatal'};
340 $scan_method = $self->{'scan_conf'}->{$last_err->{'reason'}};
342 } elsif ($continue_cb) {
343 $scan_method = SCAN_CONTINUE;
347 #use scan method for $err
348 if (!defined $scan_method) {
350 $message = "$err" if !defined $message;
352 $scan_method = $self->{'scan_conf'}->{'fatal'};
354 $scan_method = $self->{'scan_conf'}->{$err->{'reason'}};
357 die("error not defined");
358 $scan_method = SCAN_ASK_POLL;
362 ## implement the desired scan method
364 if ($scan_method == SCAN_CONTINUE && !defined $continue_cb) {
365 $scan_method = $self->{'scan_conf'}->{'notfound'};
366 if ($scan_method == SCAN_CONTINUE) {
367 $scan_method = SCAN_FAIL;
370 $steps->{'scan_next'}->($scan_method, $err, $continue_cb);
373 step scan_next => sub {
374 my ($scan_method, $err, $continue_cb) = @_;
376 if ($scan_method == SCAN_ASK && !defined $self->{'interactivity'}) {
377 $scan_method = SCAN_FAIL;
380 if ($scan_method == SCAN_ASK_POLL && !defined $self->{'interactivity'}) {
381 $scan_method = SCAN_FAIL;
384 if ($scan_method == SCAN_ASK) {
385 return $steps->{'scan_interactivity'}->("$err");
386 } elsif ($scan_method == SCAN_POLL) {
387 $poll_src = Amanda::MainLoop::call_after(
388 $self->{'scan_conf'}->{'poll_delay'},
389 $steps->{'after_poll'});
391 } elsif ($scan_method == SCAN_ASK_POLL) {
392 $steps->{'scan_interactivity'}->("$err\n");
393 $poll_src = Amanda::MainLoop::call_after(
394 $self->{'scan_conf'}->{'poll_delay'},
395 $steps->{'after_poll'});
397 } elsif ($scan_method == SCAN_FAIL) {
398 return $steps->{'call_result_cb'}->($err, undef);
399 } elsif ($scan_method == SCAN_CONTINUE) {
400 return $continue_cb->($err, undef);
402 die("Invalid SCAN_* value:$err:$err->{'reason'}:$scan_method");
406 step after_poll => sub {
407 $poll_src->remove() if defined $poll_src;
409 return $steps->{'restart_scan'}->();
412 step scan_interactivity => sub {
413 my ($err_message) = @_;
414 if (!$interactivity_running) {
415 $interactivity_running = 1;
416 my $message = "$err_message\n";
417 if ($self->{'most_prefered_label'}) {
418 $message .= "Insert volume labeled '$self->{'most_prefered_label'}'";
420 $message .= "Insert a new volume";
422 $message .= " in changer and type <enter>\nor type \"^D\" to abort\n";
423 $self->{'interactivity'}->user_request(
425 label => $self->{'most_prefered_label'},
426 new_volume => !$self->{'most_prefered_label'},
427 err => "$err_message",
428 chg_name => $self->{'chg'}->{'chg_name'},
429 request_cb => $steps->{'scan_interactivity_cb'});
434 step scan_interactivity_cb => sub {
435 my ($err, $message) = @_;
436 $interactivity_running = 0;
437 $poll_src->remove() if defined $poll_src;
446 return $steps->{'call_result_cb'}->($err, undef);
450 if ($message ne '') {
453 if (ref($message) eq 'HASH' and $message == $DEFAULT_CHANGER) {
456 $new_chg = Amanda::Changer->new($message,
457 tapelist => $self->{'tapelist'});
458 if ($new_chg->isa("Amanda::Changer::Error")) {
459 return $steps->{'scan_interactivity'}->("$new_chg");
461 $self->{'chg'}->quit() if $self->{'chg'} != $self->{'initial_chg'};
462 $self->{'chg'} = $new_chg;
465 $remove_undef_state = 1;
472 return $steps->{'restart_scan'}->();
476 step abort_scan => sub {
478 $res->released(finished_cb => $steps->{'abort_scan_released'});
480 $steps->{'abort_scan_released'}->();
484 step abort_scan_released => sub {
485 $steps->{'call_result_cb'}->($abort_scan, undef);
488 step call_result_cb => sub {
489 (my $err, $res) = @_;
491 # TODO: what happens if the search was aborted or
492 # restarted in the interim?
495 $poll_src->remove() if defined $poll_src;
497 $interactivity_running = 0;
498 $self->{'interactivity'}->abort() if defined $self->{'interactivity'};
499 $self->{'chg'}->quit() if $self->{'chg'} != $self->{'initial_chg'} and !$res;
501 $self->{'scanning'} = 0;
502 return $result_cb->($err, $res);
504 $label = $res->{'device'}->volume_label;
505 if (!defined($label) || $label !~ /$self->{'labelstr'}/) {
506 $res->get_meta_label(finished_cb => $steps->{'got_meta_label'});
509 $self->{'scanning'} = 0;
510 return $result_cb->(undef, $res, $label, $ACCESS_WRITE);
513 step got_meta_label => sub {
514 my ($err, $meta) = @_;
516 return $result_cb->($err, $res);
518 ($label, my $make_err) = $res->make_new_tape_label(meta => $meta);
519 if (!defined $label) {
520 # make this fatal, rather than silently skipping new tapes
521 $self->{'scanning'} = 0;
522 return $result_cb->($make_err, $res);
524 $self->{'scanning'} = 0;
525 return $result_cb->(undef, $res, $label, $ACCESS_WRITE, 1);
529 sub volume_is_labelable {
532 my $dev_status = $sl->{'device_status'};
533 my $f_type = $sl->{'f_type'};
534 my $label = $sl->{'label'};
535 my $slot = $sl->{'slot'};
536 my $chg = $self->{'chg'};
537 my $autolabel = $chg->{'autolabel'};
539 if (!defined $dev_status) {
541 } elsif ($dev_status & $DEVICE_STATUS_VOLUME_UNLABELED and
543 $f_type == $Amanda::Header::F_EMPTY) {
544 if (!$autolabel->{'empty'}) {
545 $self->_user_msg(slot_result => 1,
550 } elsif ($dev_status & $DEVICE_STATUS_VOLUME_UNLABELED and
552 $f_type == $Amanda::Header::F_WEIRD) {
553 if (!$autolabel->{'non_amanda'}) {
554 $self->_user_msg(slot_result => 1,
559 } elsif ($dev_status & $DEVICE_STATUS_VOLUME_ERROR) {
560 if (!$autolabel->{'volume_error'}) {
561 $self->_user_msg(slot_result => 1,
566 } elsif ($dev_status != $DEVICE_STATUS_SUCCESS) {
567 $self->_user_msg(slot_result => 1,
569 err => $sl->{'device_error'},
572 } elsif ($dev_status == $DEVICE_STATUS_SUCCESS and
573 $f_type == $Amanda::Header::F_TAPESTART and
574 $label !~ /$self->{'labelstr'}/) {
575 if (!$autolabel->{'other_config'}) {
576 $self->_user_msg(slot_result => 1,
578 does_not_match_labelstr => 1,
586 package Amanda::ScanInventory::Config;
592 my $self = bless {}, $class;
594 $self->{'poll_delay'} = 10000; #10 seconds
596 $self->{'fatal'} = Amanda::ScanInventory::SCAN_CONTINUE;
597 $self->{'driveinuse'} = Amanda::ScanInventory::SCAN_ASK_POLL;
598 $self->{'volinuse'} = Amanda::ScanInventory::SCAN_ASK_POLL;
599 $self->{'notfound'} = Amanda::ScanInventory::SCAN_ASK_POLL;
600 $self->{'unknown'} = Amanda::ScanInventory::SCAN_FAIL;
601 $self->{'invalid'} = Amanda::ScanInventory::SCAN_CONTINUE;
605 $self->{'new_labeled'} = 'order';
606 $self->{'new_volume'} = 'order';