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::ScanInventory;
28 This package implements a base class for all scan that use the inventory.
29 see C<amanda-taperscan(7)>.
40 use base qw(Exporter);
41 our @EXPORT_OK = qw($DEFAULT_CHANGER);
45 use Amanda::Device qw( :constants );
46 use Amanda::Debug qw( debug );
49 use Amanda::Interactivity;
51 use constant SCAN_ASK => 1; # call Amanda::Interactivity module
52 use constant SCAN_POLL => 2; # wait 'poll_delay' and retry the scan.
53 use constant SCAN_FAIL => 3; # abort
54 use constant SCAN_CONTINUE => 4; # continue to the next step
55 use constant SCAN_ASK_POLL => 5; # call Amanda::Interactivity module and
56 # poll at the same time.
57 use constant SCAN_LOAD => 6; # load a slot
58 use constant SCAN_DONE => 7; # successful scan
60 our $DEFAULT_CHANGER = {};
65 my $scan_conf = $params{'scan_conf'};
66 my $tapelist = $params{'tapelist'};
67 my $chg = $params{'changer'};
68 my $interactivity = $params{'interactivity'};
70 #until we have a config for it.
71 $scan_conf = Amanda::ScanInventory::Config->new();
72 $chg = Amanda::Changer->new(undef, tapelist => $tapelist) if !defined $chg;
78 scan_conf => $scan_conf,
79 tapelist => $tapelist,
80 interactivity => $interactivity,
84 return bless ($self, $class);
92 die "Can only run one scan at a time" if $self->{'scanning'};
93 $self->{'scanning'} = 1;
94 $self->{'user_msg_fn'} = $params{'user_msg_fn'} || sub {};
96 # refresh the tapelist at every scan
97 $self->read_tapelist();
99 # count the number of scans we do, so we can only load 'current' on the
101 $self->{'scan_num'}++;
103 $self->_scan(%params);
109 $self->{'user_msg_fn'}->(%params);
116 my $user_msg_fn = $params{'user_msg_fn'} || \&_user_msg_fn;
126 my $scan_running = 0;
127 my $interactivity_running = 0;
128 my $restart_scan = 0;
129 my $abort_scan = undef;
130 my $last_err = undef; # keep the last meaningful error, the one reported
131 # to the user, most scan end with the notfound error,
132 # it's more interesting to report an error from the
135 my $remove_undef_state = 0;
136 my $result_cb = $params{'result_cb'};
138 my $steps = define_steps
139 cb_ref => \$result_cb;
141 step get_first_inventory => sub {
143 $self->{'chg'}->inventory(inventory_cb => $steps->{'got_first_inventory'});
146 step got_first_inventory => sub {
147 (my $err, $inventory) = @_;
149 if ($err && $err->notimpl) {
150 #inventory not implemented
152 } elsif ($err and $err->fatal) {
154 return $steps->{'call_result_cb'}->($err, undef);
157 # continue parsing the inventory
158 $steps->{'parse_inventory'}->($err, $inventory);
161 step restart_scan => sub {
163 return $steps->{'get_inventory'}->();
166 step get_inventory => sub {
167 $self->{'chg'}->inventory(inventory_cb => $steps->{'parse_inventory'});
170 step parse_inventory => sub {
171 (my $err, $inventory) = @_;
173 if ($err && $err->notimpl) {
174 #inventory not implemented
177 return $steps->{'handle_error'}->($err, undef) if $err;
179 # throw out the inventory result and move on if the situation has
180 # changed while we were waiting
181 return $steps->{'abort_scan'}->() if $abort_scan;
182 return $steps->{'restart_scan'}->() if $restart_scan;
184 # Remove from seen all slot that have state == SLOT_UNKNOWN
185 # It is done when a scan is restarted from interactivity object.
186 if ($remove_undef_state) {
187 for my $i (0..(scalar(@$inventory)-1)) {
188 my $slot = $inventory->[$i]->{slot};
189 if (exists($seen{$slot}) &&
190 !defined($inventory->[$i]->{state})) {
194 $remove_undef_state = 0;
197 # remove any slots where the state has changed from the list of seen slots
198 for my $i (0..(scalar(@$inventory)-1)) {
199 my $sl = $inventory->[$i];
200 my $slot = $sl->{slot};
202 !defined ($seen{$slot}->{'failed'}) &&
203 defined($sl->{'state'}) &&
204 (($seen{$slot}->{'device_status'} != $sl->{'device_status'}) ||
205 (defined $seen{$slot}->{'device_status'} &&
206 $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS &&
207 $seen{$slot}->{'f_type'} != $sl->{'f_type'}) ||
208 (defined $seen{$slot}->{'device_status'} &&
209 $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS &&
210 defined $seen{$slot}->{'f_type'} &&
211 $seen{$slot}->{'f_type'} == $Amanda::Header::F_TAPESTART &&
212 $seen{$slot}->{'label'} ne $sl->{'label'}))) {
217 ($action, $action_slot) = $self->analyze($inventory, \%seen, $res);
219 if ($action == Amanda::ScanInventory::SCAN_DONE) {
220 return $steps->{'call_result_cb'}->(undef, $res);
224 $res->release(finished_cb => $steps->{'released'});
226 $steps->{'released'}->();
230 step released => sub {
231 if ($action == Amanda::ScanInventory::SCAN_LOAD) {
232 $slot_scanned = $action_slot;
233 $self->_user_msg(scan_slot => 1,
234 slot => $slot_scanned);
235 return $self->{'changer'}->load(
236 slot => $slot_scanned,
237 set_current => $params{'set_current'},
238 res_cb => $steps->{'slot_loaded'});
245 $err = Amanda::Changer::Error->new('failed',
246 reason => 'notfound',
247 message => "No acceptable volumes found");
250 if ($action == Amanda::ScanInventory::SCAN_FAIL) {
251 return $steps->{'handle_error'}->($err, undef);
254 $steps->{'scan_next'}->($action, $err);
257 step slot_loaded => sub {
258 (my $err, $res) = @_;
260 # we don't responsd to abort_scan or restart_scan here, since we
261 # have an open reservation that we should deal with.
264 if ($res && defined $res->{device} &&
265 $res->{device}->status == $DEVICE_STATUS_SUCCESS) {
266 $label = $res->{device}->volume_label;
268 my $relabeled = !defined($label) || $label !~ /$self->{'labelstr'}/;
269 $self->_user_msg(slot_result => 1,
270 slot => $slot_scanned,
273 relabeled => $relabeled,
277 if (defined $res->{device}->volume_header) {
278 $f_type = $res->{device}->volume_header->{type};
281 # The slot did not contain the volume we wanted, so mark it
282 # as seen and try again.
283 $seen{$slot_scanned} = {
284 device_status => $res->{device}->status,
286 label => $res->{device}->volume_label
290 if ($res->{device}->status == $DEVICE_STATUS_SUCCESS) {
293 $last_err = Amanda::Changer::Error->new('fatal',
294 message => $res->{device}->error_or_status());
297 $seen{$slot_scanned} = { failed => 1 };
298 if ($err->volinuse) {
299 # Scan semantics for volinuse is different than changer.
300 # If a slot with unknown label is loaded then we map
301 # volinuse to driveinuse.
302 $err->{reason} = "driveinuse";
304 $last_err = $err if $err->fatal || !$err->notfound;
306 return $steps->{'load_released'}->();
309 step load_released => sub {
314 # throw out the inventory result and move on if the situation has
315 # changed while we were loading a volume
316 return $steps->{'abort_scan'}->() if $abort_scan;
317 return $steps->{'restart_scan'}->() if $restart_scan;
319 $new_slot = $current;
320 $steps->{'get_inventory'}->();
323 step handle_error => sub {
324 my ($err, $continue_cb) = @_;
326 my $scan_method = undef;
330 $poll_src->remove() if defined $poll_src;
333 # prefer to use scan method for $last_err, if present
334 if ($last_err && $err->failed && $err->notfound) {
335 $message = "$last_err";
337 if ($last_err->isa("Amanda::Changer::Error")) {
338 if ($last_err->fatal) {
339 $scan_method = $self->{'scan_conf'}->{'fatal'};
341 $scan_method = $self->{'scan_conf'}->{$last_err->{'reason'}};
343 } elsif ($continue_cb) {
344 $scan_method = SCAN_CONTINUE;
348 #use scan method for $err
349 if (!defined $scan_method) {
351 $message = "$err" if !defined $message;
353 $scan_method = $self->{'scan_conf'}->{'fatal'};
355 $scan_method = $self->{'scan_conf'}->{$err->{'reason'}};
358 die("error not defined");
359 $scan_method = SCAN_ASK_POLL;
363 ## implement the desired scan method
365 if ($scan_method == SCAN_CONTINUE && !defined $continue_cb) {
366 $scan_method = $self->{'scan_conf'}->{'notfound'};
367 if ($scan_method == SCAN_CONTINUE) {
368 $scan_method = SCAN_FAIL;
371 $steps->{'scan_next'}->($scan_method, $err, $continue_cb);
374 step scan_next => sub {
375 my ($scan_method, $err, $continue_cb) = @_;
377 if ($scan_method == SCAN_ASK && !defined $self->{'interactivity'}) {
378 $scan_method = SCAN_FAIL;
381 if ($scan_method == SCAN_ASK_POLL && !defined $self->{'interactivity'}) {
382 $scan_method = SCAN_FAIL;
385 if ($scan_method == SCAN_ASK) {
386 return $steps->{'scan_interactivity'}->("$err");
387 } elsif ($scan_method == SCAN_POLL) {
388 $poll_src = Amanda::MainLoop::call_after(
389 $self->{'scan_conf'}->{'poll_delay'},
390 $steps->{'after_poll'});
392 } elsif ($scan_method == SCAN_ASK_POLL) {
393 $steps->{'scan_interactivity'}->("$err\n");
394 $poll_src = Amanda::MainLoop::call_after(
395 $self->{'scan_conf'}->{'poll_delay'},
396 $steps->{'after_poll'});
398 } elsif ($scan_method == SCAN_FAIL) {
399 return $steps->{'call_result_cb'}->($err, undef);
400 } elsif ($scan_method == SCAN_CONTINUE) {
401 return $continue_cb->($err, undef);
403 die("Invalid SCAN_* value:$err:$err->{'reason'}:$scan_method");
407 step after_poll => sub {
408 $poll_src->remove() if defined $poll_src;
410 return $steps->{'restart_scan'}->();
413 step scan_interactivity => sub {
414 my ($err_message) = @_;
415 if (!$interactivity_running) {
416 $interactivity_running = 1;
417 my $message = "$err_message\n";
418 if ($self->{'most_prefered_label'}) {
419 $message .= "Insert volume labeled '$self->{'most_prefered_label'}'";
421 $message .= "Insert a new volume";
423 $message .= " in changer and type <enter>\nor type \"^D\" to abort\n";
424 $self->{'interactivity'}->user_request(
426 label => $self->{'most_prefered_label'},
427 new_volume => !$self->{'most_prefered_label'},
428 err => "$err_message",
429 chg_name => $self->{'chg'}->{'chg_name'},
430 request_cb => $steps->{'scan_interactivity_cb'});
435 step scan_interactivity_cb => sub {
436 my ($err, $message) = @_;
437 $interactivity_running = 0;
438 $poll_src->remove() if defined $poll_src;
447 return $steps->{'call_result_cb'}->($err, undef);
451 if ($message ne '') {
454 if (ref($message) eq 'HASH' and $message == $DEFAULT_CHANGER) {
457 $new_chg = Amanda::Changer->new($message,
458 tapelist => $self->{'tapelist'});
459 if ($new_chg->isa("Amanda::Changer::Error")) {
460 return $steps->{'scan_interactivity'}->("$new_chg");
462 $self->{'chg'}->quit() if $self->{'chg'} != $self->{'initial_chg'};
463 $self->{'chg'} = $new_chg;
466 $remove_undef_state = 1;
473 return $steps->{'restart_scan'}->();
477 step abort_scan => sub {
479 $res->released(finished_cb => $steps->{'abort_scan_released'});
481 $steps->{'abort_scan_released'}->();
485 step abort_scan_released => sub {
486 $steps->{'call_result_cb'}->($abort_scan, undef);
489 step call_result_cb => sub {
490 (my $err, $res) = @_;
492 # TODO: what happens if the search was aborted or
493 # restarted in the interim?
496 $poll_src->remove() if defined $poll_src;
498 $interactivity_running = 0;
499 $self->{'interactivity'}->abort() if defined $self->{'interactivity'};
500 $self->{'chg'}->quit() if $self->{'chg'} != $self->{'initial_chg'} and !$res;
502 $self->{'scanning'} = 0;
503 return $result_cb->($err, $res);
505 $label = $res->{'device'}->volume_label;
506 if (!defined($label) || $label !~ /$self->{'labelstr'}/) {
507 $res->get_meta_label(finished_cb => $steps->{'got_meta_label'});
510 $self->{'scanning'} = 0;
511 return $result_cb->(undef, $res, $label, $ACCESS_WRITE);
514 step got_meta_label => sub {
515 my ($err, $meta) = @_;
517 return $result_cb->($err, $res);
519 ($label, my $make_err) = $res->make_new_tape_label(meta => $meta);
520 if (!defined $label) {
521 # make this fatal, rather than silently skipping new tapes
522 $self->{'scanning'} = 0;
523 return $result_cb->($make_err, $res);
525 $self->{'scanning'} = 0;
526 return $result_cb->(undef, $res, $label, $ACCESS_WRITE, 1);
530 sub volume_is_labelable {
533 my $dev_status = $sl->{'device_status'};
534 my $f_type = $sl->{'f_type'};
535 my $label = $sl->{'label'};
536 my $slot = $sl->{'slot'};
537 my $chg = $self->{'chg'};
538 my $autolabel = $chg->{'autolabel'};
540 if (!defined $dev_status) {
542 } elsif ($dev_status & $DEVICE_STATUS_VOLUME_UNLABELED and
544 $f_type == $Amanda::Header::F_EMPTY) {
545 if (!$autolabel->{'empty'}) {
546 $self->_user_msg(slot_result => 1,
551 } elsif ($dev_status & $DEVICE_STATUS_VOLUME_UNLABELED and
553 $f_type == $Amanda::Header::F_WEIRD) {
554 if (!$autolabel->{'non_amanda'}) {
555 $self->_user_msg(slot_result => 1,
560 } elsif ($dev_status & $DEVICE_STATUS_VOLUME_ERROR) {
561 if (!$autolabel->{'volume_error'}) {
562 $self->_user_msg(slot_result => 1,
567 } elsif ($dev_status != $DEVICE_STATUS_SUCCESS) {
568 $self->_user_msg(slot_result => 1,
570 err => $sl->{'device_error'},
573 } elsif ($dev_status == $DEVICE_STATUS_SUCCESS and
574 $f_type == $Amanda::Header::F_TAPESTART and
575 $label !~ /$self->{'labelstr'}/) {
576 if (!$autolabel->{'other_config'}) {
577 $self->_user_msg(slot_result => 1,
579 labelstr => $self->{'labelstr'},
580 does_not_match_labelstr => 1,
588 package Amanda::ScanInventory::Config;
594 my $self = bless {}, $class;
596 $self->{'poll_delay'} = 10000; #10 seconds
598 $self->{'fatal'} = Amanda::ScanInventory::SCAN_CONTINUE;
599 $self->{'driveinuse'} = Amanda::ScanInventory::SCAN_ASK_POLL;
600 $self->{'volinuse'} = Amanda::ScanInventory::SCAN_ASK_POLL;
601 $self->{'notfound'} = Amanda::ScanInventory::SCAN_ASK_POLL;
602 $self->{'unknown'} = Amanda::ScanInventory::SCAN_FAIL;
603 $self->{'invalid'} = Amanda::ScanInventory::SCAN_CONTINUE;
607 $self->{'new_labeled'} = 'order';
608 $self->{'new_volume'} = 'order';