1# Copyright (c) 2009-2013 Zmanda, Inc. All Rights Reserved. 2# 3# This library is free software; you can redistribute it and/or 4# modify it under the terms of the GNU Lesser General Public 5#* License as published by the Free Software Foundation; either 6# version 2.1 of the License, or (at your option) any later version. 7# 8# This library 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 Lesser General Public 11# License for more details. 12# 13# You should have received a copy of the GNU Lesser General Public License 14# along with this library; if not, write to the Free Software Foundation, 15# Inc., 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 94086, USA, or: http://www.zmanda.com 19 20package Amanda::Taper::Scan::traditional; 21 22=head1 NAME 23 24Amanda::Taper::Scan::traditional 25 26=head1 SYNOPSIS 27 28This package implements the "traditional" taperscan algorithm. See 29C<amanda-taperscan(7)>. 30 31=cut 32 33use strict; 34use warnings; 35use base qw( Amanda::Taper::Scan ); 36use Amanda::Tapelist; 37use Amanda::Config qw( :getconf ); 38use Amanda::Device qw( :constants ); 39use Amanda::Header; 40use Amanda::Debug qw( :logging ); 41use Amanda::MainLoop; 42 43sub new { 44 my $class = shift; 45 my %params = @_; 46 47 # parent will set all of the $params{..} keys for us 48 my $self = bless { 49 scanning => 0, 50 tapelist => undef, 51 seen => {}, 52 scan_num => 0, 53 }, $class; 54 55 return $self; 56} 57 58sub scan { 59 my $self = shift; 60 my %params = @_; 61 62 die "Can only run one scan at a time" if $self->{'scanning'}; 63 $self->{'scanning'} = 1; 64 $self->{'user_msg_fn'} = $params{'user_msg_fn'} || sub {}; 65 66 # refresh the tapelist at every scan 67 $self->read_tapelist(); 68 69 # count the number of scans we do, so we can only load 'current' on the 70 # first scan 71 $self->{'scan_num'}++; 72 73 $self->stage_1($params{'result_cb'}); 74} 75 76sub _user_msg { 77 my $self = shift; 78 my %params = @_; 79 80 $self->{'user_msg_fn'}->(%params); 81} 82 83sub scan_result { 84 my $self = shift; 85 my %params = @_; 86 87 my @result = ($params{'error'}, $params{'res'}, $params{'label'}, 88 $params{'mode'}, $params{'is_new'}); 89 90 if ($params{'error'}) { 91 debug("Amanda::Taper::Scan::traditional result: error=$params{'error'}"); 92 93 # if we already had a reservation when the error occurred, then we'll need 94 # to release that reservation before signalling the error 95 if ($params{'res'}) { 96 my $finished_cb = make_cb(finished_cb => sub { 97 my ($err) = @_; 98 # if there was an error releasing, log it and ignore it 99 Amanda::Debug::warn("while releasing reservation: $err") if $err; 100 101 $self->{'scanning'} = 0; 102 $params{'result_cb'}->(@result); 103 }); 104 return $params{'res'}->release(finished_cb => $finished_cb); 105 } 106 } elsif ($params{'res'}) { 107 my $devname = $params{'res'}->{'device'}->device_name; 108 my $slot = $params{'res'}->{'this_slot'}; 109 debug("Amanda::Taper::Scan::traditional result: '$params{label}' " . 110 "on $devname slot $slot, mode $params{mode}"); 111 } else { 112 debug("Amanda::Taper::Scan::traditional result: scan failed"); 113 114 # we may not ever have looked for this, the oldest reusable volume, if 115 # the changer is not fast-searchable. But we'll tell the user about it 116 # anyway. 117 my $oldest_reusable = $self->oldest_reusable_volume(new_label_ok => 0); 118 $self->_user_msg(scan_failed => 1, 119 expected_label => $oldest_reusable, 120 expected_new => 1); 121 @result = ("No acceptable volumes found"); 122 } 123 124 $self->{'scanning'} = 0; 125 $params{'result_cb'}->(@result); 126} 127 128## 129# stage 1: search for the oldest reusable volume 130 131sub stage_1 { 132 my $self = shift; 133 my ($result_cb) = @_; 134 my $oldest_reusable; 135 136 my $steps = define_steps 137 cb_ref => \$result_cb; 138 139 step setup => sub { 140 debug("Amanda::Taper::Scan::traditional stage 1: search for oldest reusable volume"); 141 $oldest_reusable = $self->oldest_reusable_volume( 142 new_label_ok => 0, # stage 1 never selects new volumes 143 ); 144 145 if (!defined $oldest_reusable) { 146 debug("Amanda::Taper::Scan::traditional no oldest reusable volume"); 147 return $self->stage_2($result_cb); 148 } 149 debug("Amanda::Taper::Scan::traditional oldest reusable volume is '$oldest_reusable'"); 150 151 # try loading that oldest volume, but only if the changer is fast-search capable 152 $steps->{'get_info'}->(); 153 }; 154 155 step get_info => sub { 156 $self->{'changer'}->info( 157 info => [ "fast_search" ], 158 info_cb => $steps->{'got_info'}, 159 ); 160 }; 161 162 step got_info => sub { 163 my ($error, %results) = @_; 164 if ($error) { 165 return $self->scan_result(error => $error, result_cb => $result_cb); 166 } 167 168 if ($results{'fast_search'}) { 169 debug("Amanda::Taper::Scan::traditional stage 1: searching oldest reusable " . 170 "volume '$oldest_reusable'"); 171 $self->_user_msg(search_label => 1, 172 label => $oldest_reusable); 173 174 $steps->{'do_load'}->(); 175 } else { 176 # no fast search, so skip to stage 2 177 debug("Amanda::Taper::Scan::traditional changer is not fast-searchable; skipping to stage 2"); 178 $self->stage_2($result_cb); 179 } 180 }; 181 182 step do_load => sub { 183 $self->{'changer'}->load( 184 label => $oldest_reusable, 185 set_current => 1, 186 res_cb => $steps->{'load_done'}); 187 }; 188 189 step load_done => sub { 190 my ($err, $res) = @_; 191 192 $self->_user_msg(search_result => 1, res => $res, err => $err); 193 if ($err) { 194 if ($err->failed and $err->notfound) { 195 debug("Amanda::Taper::Scan::traditional oldest reusable volume not found"); 196 return $self->stage_2($result_cb); 197 } elsif ($err->failed and $err->invalid) { 198 debug("Amanda::Taper::Scan::traditional oldest reusable volume is in an invalid slot"); 199 return $self->stage_2($result_cb); 200 } else { 201 return $self->scan_result(error => $err, 202 res => $res, result_cb => $result_cb); 203 } 204 } 205 206 $self->{'seen'}->{$res->{'this_slot'}} = 1; 207 208 my $status = $res->{'device'}->status; 209 if ($status != $DEVICE_STATUS_SUCCESS) { 210 warning "Error reading label after searching for '$oldest_reusable'"; 211 return $self->release_and_stage_2($res, $result_cb); 212 } 213 214 # go on to stage 2 if we didn't get the expected volume 215 my $label = $res->{'device'}->volume_label; 216 my $labelstr = $self->{'labelstr'}; 217 if ($label !~ /$labelstr/) { 218 warning "Searched for label '$oldest_reusable' but found a volume labeled '$label'"; 219 return $self->release_and_stage_2($res, $result_cb); 220 } 221 222 # great! -- volume found 223 return $self->scan_result(res => $res, label => $oldest_reusable, 224 mode => $ACCESS_WRITE, is_new => 0, result_cb => $result_cb); 225 }; 226} 227 228## 229# stage 2: scan for any usable volume 230 231sub release_and_stage_2 { 232 my $self = shift; 233 my ($res, $result_cb) = @_; 234 235 $res->release(finished_cb => sub { 236 my ($error) = @_; 237 if ($error) { 238 $self->scan_result(error => $error, result_cb => $result_cb); 239 } else { 240 $self->stage_2($result_cb); 241 } 242 }); 243} 244 245sub stage_2 { 246 my $self = shift; 247 my ($result_cb) = @_; 248 249 my $last_slot; 250 my $load_current = ($self->{'scan_num'} == 1); 251 my $steps = define_steps 252 cb_ref => \$result_cb; 253 my $res; 254 255 step load => sub { 256 my ($err) = @_; 257 258 debug("Amanda::Taper::Scan::traditional stage 2: scan for any reusable volume"); 259 260 # bail on an error releasing a reservation 261 if ($err) { 262 return $self->scan_result(error => $err, result_cb => $result_cb); 263 } 264 265 # load the current or next slot 266 my @load_args; 267 if ($load_current) { 268 # load 'current' the first time through 269 @load_args = ( 270 relative_slot => 'current', 271 ); 272 } else { 273 @load_args = ( 274 relative_slot => 'next', 275 (defined $last_slot)? (slot => $last_slot) : (), 276 ); 277 } 278 279 $self->{'changer'}->load( 280 @load_args, 281 set_current => 1, 282 res_cb => $steps->{'loaded'}, 283 except_slots => $self->{'seen'}, 284 mode => "write", 285 ); 286 }; 287 288 step loaded => sub { 289 (my $err, $res) = @_; 290 my $loaded_current = $load_current; 291 $load_current = 0; # don't load current a second time 292 293 # bail out immediately if the scan is complete 294 if ($err and $err->failed and $err->notfound) { 295 $self->_user_msg(search_result => 1, res => $res, err => $err); 296 # no error, no reservation -> end of the scan 297 return $self->scan_result(result_cb => $result_cb); 298 } 299 300 # tell user_msg which slot we're looking at.. 301 if (defined $res) { 302 $self->_user_msg(scan_slot => 1, slot => $res->{'this_slot'}); 303 } elsif (defined $err->{'slot'}) { 304 $self->_user_msg(scan_slot => 1, slot => $err->{'slot'}); 305 } else { 306 $self->_user_msg(scan_slot => 1, slot => "?"); 307 } 308 309 # and then tell it the result if already known (error) or try 310 # loading the volume. 311 if ($err) { 312 my $ignore_error = 0; 313 # there are two "acceptable" errors: if the slot exists but the volume 314 # is already in use 315 $ignore_error = 1 if ($err->volinuse && $err->{slot}); 316 # or if we loaded the 'current' slot and it was invalid (this happens if 317 # the user changes 'use-slots', for example 318 $ignore_error = 1 if ($loaded_current && $err->invalid); 319 $ignore_error = 1 if (defined($err->{'slot'}) && $err->invalid); 320 $ignore_error = 1 if ($err->empty); 321 322 if ($ignore_error) { 323 $self->_user_msg(slot_result => 1, err => $err); 324 if ($err->{'slot'}) { 325 $last_slot = $err->{slot}; 326 $self->{'seen'}->{$last_slot} = 1; 327 } 328 return $steps->{'load'}->(undef); 329 } else { 330 # if we have a fatal error or something other than "notfound" 331 # or "volinuse", bail out. 332 $self->_user_msg(slot_result => 1, err => $err); 333 return $self->scan_result(error => $err, res => $res, 334 result_cb => $result_cb); 335 } 336 } 337 338 $self->{'seen'}->{$res->{'this_slot'}} = 1; 339 340 $steps->{'try_volume'}->(); 341 }; 342 343 step try_volume => sub { 344 my $slot = $res->{'this_slot'}; 345 my $dev = $res->{'device'}; 346 my $status = $dev->status; 347 my $labelstr = $res->{'chg'}->{'labelstr'}; 348 my $label; 349 my $autolabel = $res->{'chg'}->{'autolabel'}; 350 351 if ($status == $DEVICE_STATUS_SUCCESS) { 352 $label = $dev->volume_label; 353 354 if ($label !~ /$labelstr/) { 355 if (!$autolabel->{'other_config'}) { 356 $self->_user_msg(slot_result => 1, 357 does_not_match_labelstr => 1, 358 labelstr => $labelstr, 359 slot => $slot, 360 label => $label, 361 res => $res); 362 return $steps->{'try_continue'}->(); 363 } 364 } else { 365 # verify that the label is in the tapelist 366 my $tle = $self->{'tapelist'}->lookup_tapelabel($label); 367 if (!$tle) { 368 $self->_user_msg(slot_result => 1, 369 not_in_tapelist => 1, 370 slot => $slot, 371 label => $label, 372 res => $res); 373 return $steps->{'try_continue'}->(); 374 } 375 376 # see if it's reusable 377 if (!$self->is_reusable_volume(label => $label, new_label_ok => 1)) { 378 $self->_user_msg(slot_result => 1, 379 active => 1, 380 slot => $slot, 381 label => $label, 382 res => $res); 383 return $steps->{'try_continue'}->(); 384 } 385 $self->_user_msg(slot_result => 1, 386 slot => $slot, 387 label => $label, 388 res => $res); 389 $self->scan_result(res => $res, label => $label, 390 mode => $ACCESS_WRITE, is_new => 0, 391 result_cb => $result_cb); 392 return; 393 } 394 } 395 396 if (!defined $autolabel->{'template'} || 397 $autolabel->{'template'} eq "") { 398 if ($status & $DEVICE_STATUS_VOLUME_UNLABELED and 399 $dev->volume_header and 400 $dev->volume_header->{'type'} == $Amanda::Header::F_EMPTY) { 401 $self->_user_msg(slot_result => 1, 402 not_autolabel => 1, 403 empty => 1, 404 slot => $slot, 405 res => $res); 406 } elsif ($status & $DEVICE_STATUS_VOLUME_UNLABELED and 407 $dev->volume_header and 408 $dev->volume_header->{'type'} == $Amanda::Header::F_WEIRD) { 409 $self->_user_msg(slot_result => 1, 410 not_autolabel => 1, 411 non_amanda => 1, 412 slot => $slot, 413 res => $res); 414 } elsif ($status & $DEVICE_STATUS_VOLUME_ERROR) { 415 $self->_user_msg(slot_result => 1, 416 not_autolabel => 1, 417 volume_error => 1, 418 err => $dev->error_or_status(), 419 slot => $slot, 420 res => $res); 421 } elsif ($status != $DEVICE_STATUS_SUCCESS) { 422 $self->_user_msg(slot_result => 1, 423 not_autolabel => 1, 424 not_success => 1, 425 err => $dev->error_or_status(), 426 slot => $slot, 427 res => $res); 428 } else { 429 $self->_user_msg(slot_result => 1, 430 not_autolabel => 1, 431 slot => $slot, 432 res => $res); 433 } 434 return $steps->{'try_continue'}->(); 435 } 436 437 if ($status & $DEVICE_STATUS_VOLUME_UNLABELED and 438 $dev->volume_header and 439 $dev->volume_header->{'type'} == $Amanda::Header::F_EMPTY) { 440 if (!$autolabel->{'empty'}) { 441 $self->_user_msg(slot_result => 1, 442 empty => 1, 443 slot => $slot, 444 res => $res); 445 return $steps->{'try_continue'}->(); 446 } 447 } elsif ($status & $DEVICE_STATUS_VOLUME_UNLABELED and 448 $dev->volume_header and 449 $dev->volume_header->{'type'} == $Amanda::Header::F_WEIRD) { 450 if (!$autolabel->{'non_amanda'}) { 451 $self->_user_msg(slot_result => 1, 452 non_amanda => 1, 453 slot => $slot, 454 res => $res); 455 return $steps->{'try_continue'}->(); 456 } 457 } elsif ($status & $DEVICE_STATUS_VOLUME_ERROR) { 458 if (!$autolabel->{'volume_error'}) { 459 $self->_user_msg(slot_result => 1, 460 volume_error => 1, 461 err => $dev->error_or_status(), 462 slot => $slot, 463 res => $res); 464 return $steps->{'try_continue'}->(); 465 } 466 } elsif ($status != $DEVICE_STATUS_SUCCESS) { 467 $self->_user_msg(slot_result => 1, 468 not_success => 1, 469 err => $dev->error_or_status(), 470 slot => $slot, 471 res => $res); 472 return $steps->{'try_continue'}->(); 473 } 474 475 $self->_user_msg(slot_result => 1, slot => $slot, res => $res); 476 $res->get_meta_label(finished_cb => $steps->{'got_meta_label'}); 477 return; 478 }; 479 480 step got_meta_label => sub { 481 my ($err, $meta) = @_; 482 483 if (defined $err) { 484 $self->scan_result(error => $err, res => $res, 485 result_cb => $result_cb); 486 return; 487 } 488 489 ($meta, $err) = $res->make_new_meta_label() if !defined $meta; 490 if (defined $err) { 491 $self->scan_result(error => $err, res => $res, 492 result_cb => $result_cb); 493 return; 494 } 495 496 (my $label, $err) = $res->make_new_tape_label(meta => $meta); 497 498 499 if (!defined $label) { 500 # make this fatal, rather than silently skipping new tapes 501 $self->scan_result(error => $err, res => $res, result_cb => $result_cb); 502 return; 503 } 504 505 $self->scan_result(res => $res, label => $label, mode => $ACCESS_WRITE, 506 is_new => 1, result_cb => $result_cb); 507 return; 508 }; 509 510 step try_continue => sub { 511 # no luck -- release this reservation and get the next 512 $last_slot = $res->{'this_slot'}; 513 514 $res->release(finished_cb => $steps->{'load'}); 515 }; 516} 517 5181; 519