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