# Copyright (c) 2009-2012 Zmanda, Inc. All Rights Reserved.
# Copyright (c) 2013-2016 Carbonite, Inc. All Rights Reserved.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
#* License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
#
# Contact information: Carbonite Inc., 756 N Pastoria Ave
# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
package Amanda::Taper::Scan::traditional;
=head1 NAME
Amanda::Taper::Scan::traditional
=head1 SYNOPSIS
This package implements the "traditional" taperscan algorithm. See
C<amanda-taperscan(7)>.
=cut
use strict;
use warnings;
use base qw( Amanda::Taper::Scan );
use Amanda::Tapelist;
use Amanda::Config qw( :getconf );
use Amanda::Device qw( :constants );
use Amanda::Header;
use Amanda::Debug qw( :logging );
use Amanda::MainLoop;
use Amanda::Util qw( match_labelstr );
sub new {
my $class = shift;
my %params = @_;
# parent will set all of the $params{..} keys for us
my $self = bless {
scanning => 0,
tapelist => undef,
seen => {},
scan_num => 0,
}, $class;
return $self;
}
sub scan {
my $self = shift;
my %params = @_;
die "Can only run one scan at a time" if $self->{'scanning'};
$self->{'scanning'} = 1;
$self->{'user_msg_fn'} = $params{'user_msg_fn'} || sub {};
# refresh the tapelist at every scan
$self->read_tapelist();
# count the number of scans we do, so we can only load 'current' on the
# first scan
$self->{'scan_num'}++;
$self->stage_1($params{'result_cb'});
}
sub _user_msg {
my $self = shift;
my %params = @_;
$self->{'user_msg_fn'}->(%params);
}
sub scan_result {
my $self = shift;
my %params = @_;
my @result = ($params{'error'}, $params{'res'}, $params{'label'},
$params{'mode'}, $params{'is_new'});
if ($params{'error'}) {
debug("Amanda::Taper::Scan::traditional result: error=$params{'error'}");
# if we already had a reservation when the error occurred, then we'll need
# to release that reservation before signalling the error
if ($params{'res'}) {
my $finished_cb = make_cb(finished_cb => sub {
my ($err) = @_;
# if there was an error releasing, log it and ignore it
Amanda::Debug::warn("while releasing reservation: $err") if $err;
$self->{'scanning'} = 0;
$params{'result_cb'}->(@result);
});
return $params{'res'}->release(finished_cb => $finished_cb);
}
} elsif ($params{'res'}) {
my $devname = $params{'res'}->{'device'}->device_name;
my $slot = $params{'res'}->{'this_slot'};
debug("Amanda::Taper::Scan::traditional result: '$params{label}' " .
"on $devname slot $slot, mode $params{mode}");
} else {
debug("Amanda::Taper::Scan::traditional result: scan failed");
# we may not ever have looked for this, the oldest reusable volume, if
# the changer is not fast-searchable. But we'll tell the user about it
# anyway.
my $oldest_reusable = $self->oldest_reusable_volume();
$self->_user_msg(scan_failed => 1,
expected_label => $oldest_reusable,
expected_new => 1);
@result = ("No acceptable volumes found");
}
$self->{'scanning'} = 0;
$params{'result_cb'}->(@result);
}
##
# stage 1: search for the oldest reusable volume
sub stage_1 {
my $self = shift;
my ($result_cb) = @_;
my $oldest_reusable;
my $steps = define_steps
cb_ref => \$result_cb;
step setup => sub {
debug("Amanda::Taper::Scan::traditional stage 1: search for oldest reusable volume");
$oldest_reusable = $self->oldest_reusable_volume(
);
if (!defined $oldest_reusable) {
debug("Amanda::Taper::Scan::traditional no oldest reusable volume");
return $self->stage_2($result_cb);
}
debug("Amanda::Taper::Scan::traditional oldest reusable volume is '$oldest_reusable'");
# try loading that oldest volume, but only if the changer is fast-search capable
$steps->{'get_info'}->();
};
step get_info => sub {
$self->{'changer'}->info(
info => [ "fast_search" ],
info_cb => $steps->{'got_info'},
);
};
step got_info => sub {
my ($error, %results) = @_;
if ($error) {
return $self->scan_result(error => $error, result_cb => $result_cb);
}
if ($results{'fast_search'}) {
debug("Amanda::Taper::Scan::traditional stage 1: searching oldest reusable " .
"volume '$oldest_reusable'");
$self->_user_msg(search_label => 1,
label => $oldest_reusable);
$steps->{'do_load'}->();
} else {
# no fast search, so skip to stage 2
debug("Amanda::Taper::Scan::traditional changer is not fast-searchable; skipping to stage 2");
$self->stage_2($result_cb);
}
};
step do_load => sub {
$self->{'changer'}->load(
label => $oldest_reusable,
set_current => 1,
res_cb => $steps->{'load_done'});
};
step load_done => sub {
my ($err, $res) = @_;
$self->_user_msg(search_result => 1, res => $res, err => $err);
if ($err) {
if ($err->failed and $err->notfound) {
debug("Amanda::Taper::Scan::traditional oldest reusable volume not found");
return $self->stage_2($result_cb);
} elsif ($err->failed and $err->volinuse) {
debug("Amanda::Taper::Scan::traditional oldest reusable volume in use");
return $self->stage_2($result_cb);
} elsif ($err->failed and $err->invalid) {
debug("Amanda::Taper::Scan::traditional oldest reusable volume is not accessible");
return $self->stage_2($result_cb);
} elsif ($err->failed and $err->invalid) {
debug("Amanda::Taper::Scan::traditional oldest reusable volume is in an invalid slot");
return $self->stage_2($result_cb);
} else {
return $self->scan_result(error => $err,
res => $res, result_cb => $result_cb);
}
}
$self->{'seen'}->{$res->{'this_slot'}} = 1;
my $status = $res->{'device'}->status;
if ($status != $DEVICE_STATUS_SUCCESS) {
warning "Error reading label after searching for '$oldest_reusable'";
return $self->release_and_stage_2($res, $result_cb);
}
# go on to stage 2 if we didn't get the expected volume
my $label = $res->{'device'}->volume_label;
if ($label ne $oldest_reusable) {
warning "Searched for label '$oldest_reusable' but found a volume labeled '$label'";
return $self->release_and_stage_2($res, $result_cb);
}
my $labelstr = $self->{'labelstr'};
my $autolabel = $self->{'autolabel'};
my $barcode = $res->{'barcode'} || "";
my $meta = $res->{'meta'} || "";
if (!match_labelstr($labelstr, $autolabel, $label,
$barcode, $meta, $self->{'chg'}->{'storage'})) {
warning "Oldest reusable volume '$oldest_reusable' do not match the labelstr '" .
$labelstr->{'match_autolabel'} ? $autolabel->{'template'} : $labelstr->{'template'} . "'";
return $self->release_and_stage_2($res, $result_cb);
}
# great! -- volume found
return $self->scan_result(res => $res, label => $oldest_reusable,
mode => $ACCESS_WRITE, is_new => 0, result_cb => $result_cb);
};
}
##
# stage 2: scan for any usable volume
sub release_and_stage_2 {
my $self = shift;
my ($res, $result_cb) = @_;
$res->release(finished_cb => sub {
my ($error) = @_;
if ($error) {
$self->scan_result(error => $error, result_cb => $result_cb);
} else {
$self->stage_2($result_cb);
}
});
}
sub stage_2 {
my $self = shift;
my ($result_cb) = @_;
my $last_slot;
my $load_current = ($self->{'scan_num'} == 1);
my $steps = define_steps
cb_ref => \$result_cb;
my $res;
step load => sub {
my ($err) = @_;
debug("Amanda::Taper::Scan::traditional stage 2: scan for any reusable volume");
# bail on an error releasing a reservation
if ($err) {
return $self->scan_result(error => $err, result_cb => $result_cb);
}
# load the current or next slot
my @load_args;
if ($load_current) {
# load 'current' the first time through
@load_args = (
relative_slot => 'current',
);
} else {
@load_args = (
relative_slot => 'next',
(defined $last_slot)? (slot => $last_slot) : (),
);
}
$self->{'changer'}->load(
@load_args,
set_current => 1,
res_cb => $steps->{'loaded'},
except_slots => $self->{'seen'},
mode => "write",
);
};
step loaded => sub {
(my $err, $res) = @_;
my $loaded_current = $load_current;
$load_current = 0; # don't load current a second time
# bail out immediately if the scan is complete
if ($err and $err->failed and $err->notfound) {
$self->_user_msg(search_result => 1, res => $res, err => $err);
# no error, no reservation -> end of the scan
return $self->scan_result(result_cb => $result_cb);
}
# tell user_msg which slot we're looking at..
if (defined $res) {
$self->_user_msg(scan_slot => 1, slot => $res->{'this_slot'});
} elsif (defined $err->{'slot'}) {
$self->_user_msg(scan_slot => 1, slot => $err->{'slot'});
} else {
$self->_user_msg(scan_slot => 1, slot => "?");
}
# and then tell it the result if already known (error) or try
# loading the volume.
if ($err) {
my $ignore_error = 0;
# there are two "acceptable" errors: if the slot exists but the volume
# is already in use
$ignore_error = 1 if ($err->volinuse && $err->{slot});
# or if we loaded the 'current' slot and it was invalid (this happens if
# the user changes 'use-slots', for example
$ignore_error = 1 if ($loaded_current && $err->invalid);
$ignore_error = 1 if (defined($err->{'slot'}) && $err->invalid);
$ignore_error = 1 if ($err->empty);
if ($ignore_error) {
$self->_user_msg(slot_result => 1, err => $err);
if ($err->{'slot'}) {
$last_slot = $err->{slot};
$self->{'seen'}->{$last_slot} = 1;
}
return $steps->{'load'}->(undef);
} else {
# if we have a fatal error or something other than "notfound"
# or "volinuse", bail out.
$self->_user_msg(slot_result => 1, err => $err);
return $self->scan_result(error => $err, res => $res,
result_cb => $result_cb);
}
}
$self->{'seen'}->{$res->{'this_slot'}} = 1;
$steps->{'try_volume'}->();
};
step try_volume => sub {
my $slot = $res->{'this_slot'};
my $dev = $res->{'device'};
my $status = $dev->status;
my $labelstr = $res->{'chg'}->{'labelstr'};
my $label;
my $barcode = $res->{'barcode'} || "";
my $meta = $res->{'meta'} || "";
my $autolabel = $res->{'chg'}->{'autolabel'};
if ($status == $DEVICE_STATUS_SUCCESS) {
$label = $dev->volume_label;
# verify that the label is in the tapelist
my $tle = $self->{'tapelist'}->lookup_tapelabel($label);
if (!$tle) {
if (!match_labelstr($labelstr, $autolabel, $label, $barcode,
$meta, $self->{'chg'}->{'storage'})) {
if (!$autolabel->{'other_config'}) {
$self->_user_msg(slot_result => 1,
does_not_match_labelstr => 1,
labelstr => $labelstr->{'match_autolabel'} ? $autolabel->{'template'} : $labelstr->{'template'},,
slot => $slot,
label => $label,
res => $res);
return $steps->{'try_continue'}->();
}
} else {
$self->_user_msg(slot_result => 1,
not_in_tapelist => 1,
slot => $slot,
label => $label,
res => $res);
return $steps->{'try_continue'}->();
}
} else {
if ($tle->{'config'} &&
$tle->{'config'} ne Amanda::Config::get_config_name()) {
$self->_user_msg(slot_result => 1,
other_config => 1,
config => $tle->{'config'},
slot => $slot,
label => $label,
res => $res);
return $steps->{'try_continue'}->();
}
if ($tle->{'pool'} &&
$tle->{'pool'} ne $self->{'tapepool'}) {
$self->_user_msg(slot_result => 1,
other_pool => 1,
pool => $tle->{'pool'},
slot => $slot,
label => $label,
res => $res);
return $steps->{'try_continue'}->();
}
if (!$tle->{'pool'} &&
!match_labelstr($labelstr, $autolabel, $label, $barcode,
$meta, $self->{'chg'}->{'storage'})) {
$self->_user_msg(slot_result => 1,
does_not_match_labelstr => 1,
labelstr => $labelstr->{'match_autolabel'} ? $autolabel->{'template'} : $labelstr->{'template'},
slot => $slot,
label => $label,
res => $res);
return $steps->{'try_continue'}->();
}
# see if it's reusable
if (!$self->is_reusable_volume(label => $label, new_label_ok => 1)) {
$self->_user_msg(slot_result => 1,
active => 1,
slot => $slot,
label => $label,
res => $res);
return $steps->{'try_continue'}->();
} else {
$self->_user_msg(slot_result => 1,
slot => $slot,
label => $label,
res => $res);
$self->scan_result(res => $res, label => $label,
mode => $ACCESS_WRITE, is_new => 0,
result_cb => $result_cb);
return;
}
}
}
if (!defined $autolabel->{'template'} ||
$autolabel->{'template'} eq "") {
if ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
(!$dev->volume_header or
$dev->volume_header->{'type'} == $Amanda::Header::F_EMPTY)) {
$self->_user_msg(slot_result => 1,
not_autolabel => 1,
empty => 1,
slot => $slot,
res => $res);
} elsif ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
$dev->volume_header and
$dev->volume_header->{'type'} == $Amanda::Header::F_WEIRD) {
$self->_user_msg(slot_result => 1,
not_autolabel => 1,
non_amanda => 1,
slot => $slot,
res => $res);
} elsif ($status & $DEVICE_STATUS_VOLUME_ERROR) {
$self->_user_msg(slot_result => 1,
not_autolabel => 1,
volume_error => 1,
err => $dev->error_or_status(),
slot => $slot,
res => $res);
} elsif ($status != $DEVICE_STATUS_SUCCESS) {
$self->_user_msg(slot_result => 1,
not_autolabel => 1,
not_success => 1,
err => $dev->error_or_status(),
slot => $slot,
res => $res);
} else {
$self->_user_msg(slot_result => 1,
not_autolabel => 1,
slot => $slot,
res => $res);
}
return $steps->{'try_continue'}->();
}
if ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
(!$dev->volume_header or
$dev->volume_header->{'type'} == $Amanda::Header::F_EMPTY)) {
if (!$autolabel->{'empty'}) {
$self->_user_msg(slot_result => 1,
empty => 1,
slot => $slot,
res => $res);
return $steps->{'try_continue'}->();
}
} elsif ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
$dev->volume_header and
$dev->volume_header->{'type'} == $Amanda::Header::F_WEIRD) {
if (!$autolabel->{'non_amanda'}) {
$self->_user_msg(slot_result => 1,
non_amanda => 1,
slot => $slot,
res => $res);
return $steps->{'try_continue'}->();
}
} elsif ($status & $DEVICE_STATUS_VOLUME_ERROR) {
if (!$autolabel->{'volume_error'}) {
$self->_user_msg(slot_result => 1,
volume_error => 1,
err => $dev->error_or_status(),
slot => $slot,
res => $res);
return $steps->{'try_continue'}->();
}
} elsif ($status != $DEVICE_STATUS_SUCCESS) {
$self->_user_msg(slot_result => 1,
not_success => 1,
err => $dev->error_or_status(),
slot => $slot,
res => $res);
return $steps->{'try_continue'}->();
}
$self->_user_msg(slot_result => 1, slot => $slot, res => $res);
$res->get_meta_label(finished_cb => $steps->{'got_meta_label'});
return;
};
step got_meta_label => sub {
my ($err, $meta) = @_;
if (defined $err) {
$self->scan_result(error => $err, res => $res,
result_cb => $result_cb);
return;
}
($meta, $err) = $res->make_new_meta_label() if !defined $meta;
if (defined $err) {
$self->scan_result(error => $err, res => $res,
result_cb => $result_cb);
return;
}
(my $label, $err) = $res->make_new_tape_label(meta => $meta);
if (!defined $label) {
# make this fatal, rather than silently skipping new tapes
$self->scan_result(error => $err, res => $res, result_cb => $result_cb);
return;
}
$self->scan_result(res => $res, label => $label, mode => $ACCESS_WRITE,
is_new => 1, result_cb => $result_cb);
return;
};
step try_continue => sub {
# no luck -- release this reservation and get the next
$last_slot = $res->{'this_slot'};
$res->release(finished_cb => $steps->{'load'});
};
}
1;