1# Copyright (c) 2010-2013 Zmanda, Inc. All Rights Reserved. 2# 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. 7# 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 11# for more details. 12# 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 16# 17# Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300 18# Sunnyvale, CA 94085, USA, or: http://www.zmanda.com 19 20package Amanda::ScanInventory; 21 22=head1 NAME 23 24Amanda::ScanInventory 25 26=head1 SYNOPSIS 27 28This package implements a base class for all scan that use the inventory. 29see C<amanda-taperscan(7)>. 30 31=cut 32 33use strict; 34use warnings; 35use Amanda::Tapelist; 36use Carp; 37use POSIX (); 38use Data::Dumper; 39use vars qw( @ISA ); 40use base qw(Exporter); 41our @EXPORT_OK = qw($DEFAULT_CHANGER); 42 43use Amanda::Paths; 44use Amanda::Util; 45use Amanda::Device qw( :constants ); 46use Amanda::Debug qw( debug ); 47use Amanda::Changer; 48use Amanda::MainLoop; 49use Amanda::Interactivity; 50 51use constant SCAN_ASK => 1; # call Amanda::Interactivity module 52use constant SCAN_POLL => 2; # wait 'poll_delay' and retry the scan. 53use constant SCAN_FAIL => 3; # abort 54use constant SCAN_CONTINUE => 4; # continue to the next step 55use constant SCAN_ASK_POLL => 5; # call Amanda::Interactivity module and 56 # poll at the same time. 57use constant SCAN_LOAD => 6; # load a slot 58use constant SCAN_DONE => 7; # successful scan 59 60our $DEFAULT_CHANGER = {}; 61 62sub new { 63 my $class = shift; 64 my %params = @_; 65 my $scan_conf = $params{'scan_conf'}; 66 my $tapelist = $params{'tapelist'}; 67 my $chg = $params{'changer'}; 68 my $interactivity = $params{'interactivity'}; 69 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; 73 74 my $self = { 75 initial_chg => $chg, 76 chg => $chg, 77 scanning => 0, 78 scan_conf => $scan_conf, 79 tapelist => $tapelist, 80 interactivity => $interactivity, 81 seen => {}, 82 scan_num => 0 83 }; 84 return bless ($self, $class); 85} 86 87 88sub scan { 89 my $self = shift; 90 my %params = @_; 91 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 {}; 95 96 # refresh the tapelist at every scan 97 $self->read_tapelist(); 98 99 # count the number of scans we do, so we can only load 'current' on the 100 # first scan 101 $self->{'scan_num'}++; 102 103 $self->_scan(%params); 104} 105 106sub _user_msg { 107 my $self = shift; 108 my %params = @_; 109 $self->{'user_msg_fn'}->(%params); 110} 111 112sub _scan { 113 my $self = shift; 114 my %params = @_; 115 116 my $user_msg_fn = $params{'user_msg_fn'} || \&_user_msg_fn; 117 my $action; 118 my $action_slot; 119 my $res; 120 my $label; 121 my %seen = (); 122 my $inventory; 123 my $current; 124 my $new_slot; 125 my $poll_src; 126 my $scan_running = 0; 127 my $interactivity_running = 0; 128 my $restart_scan = 0; 129 my $restart_scan_changer = undef; 130 my $abort_scan = undef; 131 my $last_err = undef; # keep the last meaningful error, the one reported 132 # to the user, most scan end with the notfound error, 133 # it's more interesting to report an error from the 134 # device or ... 135 my $slot_scanned; 136 my $remove_undef_state = 0; 137 my $result_cb = $params{'result_cb'}; 138 139 my $steps = define_steps 140 cb_ref => \$result_cb; 141 142 step get_first_inventory => sub { 143 $scan_running = 1; 144 $self->{'chg'}->inventory(inventory_cb => $steps->{'got_first_inventory'}); 145 }; 146 147 step got_first_inventory => sub { 148 (my $err, $inventory) = @_; 149 150 if ($err && $err->notimpl) { 151 #inventory not implemented 152 die("no inventory"); 153 } elsif ($err and $err->fatal) { 154 #inventory fail 155 return $steps->{'call_result_cb'}->($err, undef); 156 } 157 158 # continue parsing the inventory 159 $steps->{'parse_inventory'}->($err, $inventory); 160 }; 161 162 step restart_scan => sub { 163 $restart_scan = 0; 164 165 # Reload the tapelist at every scan. 166 $self->{'tapelist'}->reload(0); 167 168 if ($restart_scan_changer) { 169 $self->{'chg'}->quit() if $self->{'chg'} != $self->{'initial_chg'}; 170 $self->{'chg'} = $restart_scan_changer; 171 $restart_scan_changer = undef; 172 } 173 return $steps->{'get_inventory'}->(); 174 }; 175 176 step get_inventory => sub { 177 if ($remove_undef_state and $self->{'chg'}->{'scan-require-update'}) { 178 $self->{'chg'}->update(); 179 } 180 $self->{'chg'}->inventory(inventory_cb => $steps->{'parse_inventory'}); 181 }; 182 183 step parse_inventory => sub { 184 (my $err, $inventory) = @_; 185 186 if ($err && $err->notimpl) { 187 #inventory not implemented 188 die("no inventory"); 189 } elsif ($err and $err->fatal) { 190 #inventory fail 191 return $steps->{'call_result_cb'}->($err, undef); 192 } 193 return $steps->{'handle_error'}->($err, undef) if $err; 194 195 # throw out the inventory result and move on if the situation has 196 # changed while we were waiting 197 return $steps->{'abort_scan'}->() if $abort_scan; 198 return $steps->{'restart_scan'}->() if $restart_scan; 199 200 # Remove from seen all slot that have state == SLOT_UNKNOWN 201 # It is done when a scan is restarted from interactivity object. 202 if ($remove_undef_state) { 203 for my $i (0..(scalar(@$inventory)-1)) { 204 my $slot = $inventory->[$i]->{slot}; 205 if (exists($seen{$slot}) && 206 !defined($inventory->[$i]->{state})) { 207 delete $seen{$slot} 208 } 209 } 210 $remove_undef_state = 0; 211 } 212 213 # remove any slots where the state has changed from the list of seen slots 214 for my $i (0..(scalar(@$inventory)-1)) { 215 my $sl = $inventory->[$i]; 216 my $slot = $sl->{slot}; 217 if ($seen{$slot} && 218 !defined ($seen{$slot}->{'failed'}) && 219 defined($sl->{'state'}) && 220 (($seen{$slot}->{'device_status'} != $sl->{'device_status'}) || 221 (defined $seen{$slot}->{'device_status'} && 222 $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS && 223 $seen{$slot}->{'f_type'} != $sl->{'f_type'}) || 224 (defined $seen{$slot}->{'device_status'} && 225 $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS && 226 defined $seen{$slot}->{'f_type'} && 227 $seen{$slot}->{'f_type'} == $Amanda::Header::F_TAPESTART && 228 $seen{$slot}->{'label'} ne $sl->{'label'}))) { 229 delete $seen{$slot}; 230 } 231 } 232 233 $self->{'slot-error-message'} = undef; 234 ($action, $action_slot) = $self->analyze($inventory, \%seen, $res); 235 236 if ($action == Amanda::ScanInventory::SCAN_DONE) { 237 return $steps->{'call_result_cb'}->(undef, $res); 238 } 239 240 if (defined $res) { 241 $res->release(finished_cb => $steps->{'released'}); 242 $res = undef; 243 } else { 244 $steps->{'released'}->(); 245 } 246 }; 247 248 step released => sub { 249 if ($action == Amanda::ScanInventory::SCAN_LOAD) { 250 $slot_scanned = $action_slot; 251 $self->_user_msg(scan_slot => 1, 252 slot => $slot_scanned); 253 return $self->{'chg'}->load( 254 slot => $slot_scanned, 255 set_current => $params{'set_current'}, 256 res_cb => $steps->{'slot_loaded'}); 257 } 258 259 my $err; 260 if ($last_err) { 261 $err = $last_err; 262 } else { 263 $err = Amanda::Changer::Error->new('failed', 264 reason => 'notfound', 265 message => "No acceptable volumes found"); 266 } 267 268 if ($action == Amanda::ScanInventory::SCAN_FAIL) { 269 return $steps->{'handle_error'}->($err, undef); 270 } 271 $scan_running = 0; 272 $steps->{'scan_next'}->($action, $err); 273 }; 274 275 step slot_loaded => sub { 276 (my $err, $res) = @_; 277 278 $self->{'slot_loaded_err'} = $err; 279 280 # we don't responsd to abort_scan or restart_scan here, since we 281 # have an open reservation that we should deal with. 282 283 # change status of slot in error if that one succeeded. 284 if (defined $self->{'slot-error-message'} and 285 $res and defined $res->{'device'} and 286 $self->{'slot-error-message'} ne $res->{'device'}->error) { 287 # mark all unseen slots with that error message as unknown state 288 for my $i (0..(scalar(@$inventory)-1)) { 289 my $sl = $inventory->[$i]; 290 next if $seen{$sl->{slot}}; 291 next if $self->{'slot-error-message'} ne $sl->{'device_error'}; 292 # mark the slot as unknown 293 $inventory->[$i] = { slot => $sl->{'slot'}, 294 state => $sl->{'state'}}; 295 } 296 if ($self->{'chg'}->can("set_error_to_unknown")) { 297 $self->{'chg'}->set_error_to_unknown( 298 error_message => $self->{'slot-error-message'}, 299 set_to_unknown_cb => $steps->{'set_to_unknown_cb'}); 300 } 301 } else { 302 return $steps->{'set_to_unknown_cb'}->(); 303 } 304 }; 305 306 step set_to_unknown_cb => sub { 307 my $err = $self->{'slot_loaded_err'}; 308 $self->{'slot_loaded_err'} = undef; 309 310 my $label; 311 if ($res && defined $res->{device} && 312 $res->{device}->status == $DEVICE_STATUS_SUCCESS) { 313 $label = $res->{device}->volume_label; 314 } 315 my $relabeled = !defined($label) || $label !~ /$self->{'labelstr'}/; 316 $self->_user_msg(slot_result => 1, 317 slot => $slot_scanned, 318 label => $label, 319 err => $err, 320 relabeled => $relabeled, 321 res => $res); 322 if ($res) { 323 my $f_type; 324 if (defined $res->{device}->volume_header) { 325 $f_type = $res->{device}->volume_header->{type}; 326 } 327 328 # The slot did not contain the volume we wanted, so mark it 329 # as seen and try again. 330 $seen{$slot_scanned} = { 331 device_status => $res->{device}->status, 332 f_type => $f_type, 333 label => $res->{device}->volume_label 334 }; 335 336 # notify the user 337 if ($res->{device}->status == $DEVICE_STATUS_SUCCESS) { 338 $last_err = undef; 339 } else { 340 $last_err = Amanda::Changer::Error->new('fatal', 341 message => $res->{device}->error_or_status()); 342 } 343 } else { 344 $seen{$slot_scanned} = { failed => 1 }; 345 if ($err->volinuse) { 346 # Scan semantics for volinuse is different than changer. 347 # If a slot with unknown label is loaded then we map 348 # volinuse to driveinuse. 349 $err->{reason} = "driveinuse"; 350 } 351 $last_err = $err if $err->fatal || !$err->notfound; 352 } 353 return $steps->{'load_released'}->(); 354 }; 355 356 step load_released => sub { 357 my ($err) = @_; 358 359 # TODO: handle error 360 361 # throw out the inventory result and move on if the situation has 362 # changed while we were loading a volume 363 return $steps->{'abort_scan'}->() if $abort_scan; 364 return $steps->{'restart_scan'}->() if $restart_scan; 365 366 $new_slot = $current; 367 $steps->{'get_inventory'}->(); 368 }; 369 370 step handle_error => sub { 371 my ($err, $continue_cb) = @_; 372 373 my $scan_method = undef; 374 $scan_running = 0; 375 my $message; 376 377 $poll_src->remove() if defined $poll_src; 378 $poll_src = undef; 379 380 # prefer to use scan method for $last_err, if present 381 if ($last_err && $err->failed && $err->notfound) { 382 $message = "$last_err"; 383 384 if ($last_err->isa("Amanda::Changer::Error")) { 385 if ($last_err->fatal) { 386 $scan_method = $self->{'scan_conf'}->{'fatal'}; 387 } else { 388 $scan_method = $self->{'scan_conf'}->{$last_err->{'reason'}}; 389 } 390 } elsif ($continue_cb) { 391 $scan_method = SCAN_CONTINUE; 392 } 393 } 394 395 #use scan method for $err 396 if (!defined $scan_method) { 397 if ($err) { 398 $message = "$err" if !defined $message; 399 if ($err->fatal) { 400 $scan_method = $self->{'scan_conf'}->{'fatal'}; 401 } else { 402 $scan_method = $self->{'scan_conf'}->{$err->{'reason'}}; 403 } 404 } else { 405 die("error not defined"); 406 $scan_method = SCAN_ASK_POLL; 407 } 408 } 409 410 ## implement the desired scan method 411 412 if ($scan_method == SCAN_CONTINUE && !defined $continue_cb) { 413 $scan_method = $self->{'scan_conf'}->{'notfound'}; 414 if ($scan_method == SCAN_CONTINUE) { 415 $scan_method = SCAN_FAIL; 416 } 417 } 418 $steps->{'scan_next'}->($scan_method, $err, $continue_cb); 419 }; 420 421 step scan_next => sub { 422 my ($scan_method, $err, $continue_cb) = @_; 423 424 if ($scan_method == SCAN_ASK && !defined $self->{'interactivity'}) { 425 $scan_method = SCAN_FAIL; 426 } 427 428 if ($scan_method == SCAN_ASK_POLL && !defined $self->{'interactivity'}) { 429 $scan_method = SCAN_FAIL; 430 } 431 432 if ($scan_method == SCAN_ASK) { 433 return $steps->{'scan_interactivity'}->("$err"); 434 } elsif ($scan_method == SCAN_POLL) { 435 $poll_src = Amanda::MainLoop::call_after( 436 $self->{'scan_conf'}->{'poll_delay'}, 437 $steps->{'after_poll'}); 438 return; 439 } elsif ($scan_method == SCAN_ASK_POLL) { 440 $steps->{'scan_interactivity'}->("$err\n"); 441 $poll_src = Amanda::MainLoop::call_after( 442 $self->{'scan_conf'}->{'poll_delay'}, 443 $steps->{'after_poll'}); 444 return; 445 } elsif ($scan_method == SCAN_FAIL) { 446 return $steps->{'call_result_cb'}->($err, undef); 447 } elsif ($scan_method == SCAN_CONTINUE) { 448 return $continue_cb->($err, undef); 449 } else { 450 die("Invalid SCAN_* value:$err:$err->{'reason'}:$scan_method"); 451 } 452 }; 453 454 step after_poll => sub { 455 if ($poll_src) { 456 $poll_src->remove(); 457 $poll_src = undef; 458 return $steps->{'restart_scan'}->(); 459 } 460 }; 461 462 step scan_interactivity => sub { 463 my ($err_message) = @_; 464 if (!$interactivity_running) { 465 $interactivity_running = 1; 466 my $message = "$err_message\n"; 467 if ($self->{'most_prefered_label'}) { 468 $message .= "Insert volume labeled '$self->{'most_prefered_label'}'"; 469 } else { 470 $message .= "Insert a new volume"; 471 } 472 $message .= " in changer and type <enter>\nor type \"^D\" to abort\n"; 473 $self->{'interactivity'}->user_request( 474 message => $message, 475 label => $self->{'most_prefered_label'}, 476 new_volume => !$self->{'most_prefered_label'}, 477 err => "$err_message", 478 chg_name => $self->{'chg'}->{'chg_name'}, 479 request_cb => $steps->{'scan_interactivity_cb'}); 480 } 481 return; 482 }; 483 484 step scan_interactivity_cb => sub { 485 my ($err, $message) = @_; 486 $interactivity_running = 0; 487 $poll_src->remove() if defined $poll_src; 488 $poll_src = undef; 489 $last_err = undef; 490 491 if ($err) { 492 if ($scan_running) { 493 $abort_scan = $err; 494 return; 495 } else { 496 return $steps->{'call_result_cb'}->($err, undef); 497 } 498 } 499 500 # remove leading and trailing space 501 $message =~ s/^ +//g; 502 $message =~ s/ +$//g; 503 if ($message ne '') { 504 # use a new changer 505 my $new_chg; 506 if (ref($message) eq 'HASH' and $message == $DEFAULT_CHANGER) { 507 $message = undef; 508 } 509 $new_chg = Amanda::Changer->new($message, 510 tapelist => $self->{'tapelist'}); 511 if ($new_chg->isa("Amanda::Changer::Error")) { 512 return $steps->{'scan_interactivity'}->("$new_chg"); 513 } 514 $restart_scan_changer = $new_chg; 515 %seen = (); 516 } else { 517 $remove_undef_state = 1; 518 } 519 520 if ($scan_running) { 521 $restart_scan = 1; 522 return; 523 } else { 524 return $steps->{'restart_scan'}->(); 525 } 526 }; 527 528 step abort_scan => sub { 529 if (defined $res) { 530 $res->released(finished_cb => $steps->{'abort_scan_released'}); 531 } else { 532 $steps->{'abort_scan_released'}->(); 533 } 534 }; 535 536 step abort_scan_released => sub { 537 $steps->{'call_result_cb'}->($abort_scan, undef); 538 }; 539 540 step call_result_cb => sub { 541 (my $err, $res) = @_; 542 543 # TODO: what happens if the search was aborted or 544 # restarted in the interim? 545 546 $abort_scan = undef; 547 $poll_src->remove() if defined $poll_src; 548 $poll_src = undef; 549 $interactivity_running = 0; 550 $self->{'interactivity'}->abort() if defined $self->{'interactivity'}; 551 $self->{'chg'}->quit() if $self->{'chg'} != $self->{'initial_chg'} and 552 !$res; 553 if ($err) { 554 $self->{'scanning'} = 0; 555 return $result_cb->($err, $res); 556 } 557 $label = $res->{'device'}->volume_label; 558 if (!defined($label) || $label !~ /$self->{'labelstr'}/) { 559 $res->get_meta_label(finished_cb => $steps->{'got_meta_label'}); 560 return; 561 } 562 $self->{'scanning'} = 0; 563 return $result_cb->(undef, $res, $label, $ACCESS_WRITE); 564 }; 565 566 step got_meta_label => sub { 567 my ($err, $meta) = @_; 568 if (defined $err) { 569 return $result_cb->($err, $res); 570 } 571 ($label, my $make_err) = $res->make_new_tape_label(meta => $meta); 572 if (!defined $label) { 573 # make this fatal, rather than silently skipping new tapes 574 $self->{'scanning'} = 0; 575 return $result_cb->($make_err, $res); 576 } 577 $self->{'scanning'} = 0; 578 return $result_cb->(undef, $res, $label, $ACCESS_WRITE, 1); 579 }; 580} 581 582sub volume_is_labelable { 583 my $self = shift; 584 my $sl = shift; 585 my $dev_status = $sl->{'device_status'}; 586 my $f_type = $sl->{'f_type'}; 587 my $label = $sl->{'label'}; 588 my $slot = $sl->{'slot'}; 589 my $chg = $self->{'chg'}; 590 my $autolabel = $chg->{'autolabel'}; 591 592 if (!defined $dev_status) { 593 return 0; 594 } elsif ($dev_status & $DEVICE_STATUS_VOLUME_UNLABELED and 595 defined $f_type and 596 $f_type == $Amanda::Header::F_EMPTY) { 597 if (!$autolabel->{'empty'}) { 598 $self->_user_msg(slot_result => 1, 599 empty => 1, 600 slot => $slot); 601 return 0; 602 } 603 } elsif ($dev_status & $DEVICE_STATUS_VOLUME_UNLABELED and 604 defined $f_type and 605 $f_type == $Amanda::Header::F_WEIRD) { 606 if (!$autolabel->{'non_amanda'}) { 607 $self->_user_msg(slot_result => 1, 608 non_amanda => 1, 609 slot => $slot); 610 return 0; 611 } 612 } elsif ($dev_status & $DEVICE_STATUS_VOLUME_ERROR) { 613 if (!$autolabel->{'volume_error'}) { 614 $self->_user_msg(slot_result => 1, 615 volume_error => 1, 616 err => $sl->{'device_error'}, 617 slot => $slot); 618 return 0; 619 } 620 } elsif ($dev_status != $DEVICE_STATUS_SUCCESS) { 621 $self->_user_msg(slot_result => 1, 622 not_success => 1, 623 err => $sl->{'device_error'}, 624 slot => $slot); 625 return 0; 626 } elsif ($dev_status == $DEVICE_STATUS_SUCCESS and 627 $f_type == $Amanda::Header::F_TAPESTART) { 628 if ($label !~ /$self->{'labelstr'}/) { 629 if (!$autolabel->{'other_config'}) { 630 $self->_user_msg(slot_result => 1, 631 label => $label, 632 labelstr => $self->{'labelstr'}, 633 does_not_match_labelstr => 1, 634 slot => $slot); 635 return 0; 636 } 637 } else { 638 my $vol_tle = $self->{'tapelist'}->lookup_tapelabel($label); 639 if (!$vol_tle) { 640 $self->_user_msg(slot_result => 1, 641 label => $label, 642 not_in_tapelist => 1, 643 slot => $slot); 644 return 0; 645 } 646 } 647 } 648 649 return 1; 650} 651package Amanda::ScanInventory::Config; 652 653sub new { 654 my $class = shift; 655 my ($cc) = @_; 656 657 my $self = bless {}, $class; 658 659 $self->{'poll_delay'} = 10000; #10 seconds 660 661 $self->{'fatal'} = Amanda::ScanInventory::SCAN_CONTINUE; 662 $self->{'driveinuse'} = Amanda::ScanInventory::SCAN_ASK_POLL; 663 $self->{'volinuse'} = Amanda::ScanInventory::SCAN_ASK_POLL; 664 $self->{'notfound'} = Amanda::ScanInventory::SCAN_ASK_POLL; 665 $self->{'unknown'} = Amanda::ScanInventory::SCAN_FAIL; 666 $self->{'invalid'} = Amanda::ScanInventory::SCAN_CONTINUE; 667 668 $self->{'scan'} = 1; 669 $self->{'ask'} = 1; 670 $self->{'new_labeled'} = 'order'; 671 $self->{'new_volume'} = 'order'; 672 673 return $self; 674} 675 6761; 677