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