Imported Upstream version 3.3.3
[debian/amanda] / perl / Amanda / Taper / Scan / traditional.pm
1 # Copyright (c) 2009-2012 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
20 package Amanda::Taper::Scan::traditional;
21
22 =head1 NAME
23
24 Amanda::Taper::Scan::traditional
25
26 =head1 SYNOPSIS
27
28 This package implements the "traditional" taperscan algorithm.  See
29 C<amanda-taperscan(7)>.
30
31 =cut
32
33 use strict;
34 use warnings;
35 use base qw( Amanda::Taper::Scan );
36 use Amanda::Tapelist;
37 use Amanda::Config qw( :getconf );
38 use Amanda::Device qw( :constants );
39 use Amanda::Header;
40 use Amanda::Debug qw( :logging );
41 use Amanda::MainLoop;
42
43 sub 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
58 sub 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
76 sub _user_msg {
77     my $self = shift;
78     my %params = @_;
79
80     $self->{'user_msg_fn'}->(%params);
81 }
82
83 sub 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
131 sub 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             } else {
198                 return $self->scan_result(error => $err,
199                         res => $res, result_cb => $result_cb);
200             }
201         }
202
203         $self->{'seen'}->{$res->{'this_slot'}} = 1;
204
205         my $status = $res->{'device'}->status;
206         if ($status != $DEVICE_STATUS_SUCCESS) {
207             warning "Error reading label after searching for '$oldest_reusable'";
208             return $self->release_and_stage_2($res, $result_cb);
209         }
210
211         # go on to stage 2 if we didn't get the expected volume
212         my $label = $res->{'device'}->volume_label;
213         my $labelstr = $self->{'labelstr'};
214         if ($label !~ /$labelstr/) {
215             warning "Searched for label '$oldest_reusable' but found a volume labeled '$label'";
216             return $self->release_and_stage_2($res, $result_cb);
217         }
218
219         # great! -- volume found
220         return $self->scan_result(res => $res, label => $oldest_reusable,
221                     mode => $ACCESS_WRITE, is_new => 0, result_cb => $result_cb);
222     };
223 }
224
225 ##
226 # stage 2: scan for any usable volume
227
228 sub release_and_stage_2 {
229     my $self = shift;
230     my ($res, $result_cb) = @_;
231
232     $res->release(finished_cb => sub {
233         my ($error) = @_;
234         if ($error) {
235             $self->scan_result(error => $error, result_cb => $result_cb);
236         } else {
237             $self->stage_2($result_cb);
238         }
239     });
240 }
241
242 sub stage_2 {
243     my $self = shift;
244     my ($result_cb) = @_;
245
246     my $last_slot;
247     my $load_current = ($self->{'scan_num'} == 1);
248     my $steps = define_steps
249         cb_ref => \$result_cb;
250     my $res;
251
252     step load => sub {
253         my ($err) = @_;
254
255         debug("Amanda::Taper::Scan::traditional stage 2: scan for any reusable volume");
256
257         # bail on an error releasing a reservation
258         if ($err) {
259             return $self->scan_result(error => $err, result_cb => $result_cb);
260         }
261
262         # load the current or next slot
263         my @load_args;
264         if ($load_current) {
265             # load 'current' the first time through
266             @load_args = (
267                 relative_slot => 'current',
268             );
269         } else {
270             @load_args = (
271                 relative_slot => 'next',
272                 (defined $last_slot)? (slot => $last_slot) : (),
273             );
274         }
275
276         $self->{'changer'}->load(
277             @load_args,
278             set_current => 1,
279             res_cb => $steps->{'loaded'},
280             except_slots => $self->{'seen'},
281             mode => "write",
282         );
283     };
284
285     step loaded => sub {
286         (my $err, $res) = @_;
287         my $loaded_current = $load_current;
288         $load_current = 0; # don't load current a second time
289
290         # bail out immediately if the scan is complete
291         if ($err and $err->failed and $err->notfound) {
292             $self->_user_msg(search_result => 1, res => $res, err => $err);
293             # no error, no reservation -> end of the scan
294             return $self->scan_result(result_cb => $result_cb);
295         }
296
297         # tell user_msg which slot we're looking at..
298         if (defined $res) {
299             $self->_user_msg(scan_slot => 1, slot => $res->{'this_slot'});
300         } elsif (defined $err->{'slot'}) {
301             $self->_user_msg(scan_slot => 1, slot => $err->{'slot'});
302         } else {
303             $self->_user_msg(scan_slot => 1, slot => "?");
304         }
305
306         # and then tell it the result if already known (error) or try
307         # loading the volume.
308         if ($err) {
309             my $ignore_error = 0;
310             # there are two "acceptable" errors: if the slot exists but the volume
311             # is already in use
312             $ignore_error = 1 if ($err->volinuse && $err->{slot});
313             # or if we loaded the 'current' slot and it was invalid (this happens if
314             # the user changes 'use-slots', for example
315             $ignore_error = 1 if ($loaded_current && $err->invalid);
316             $ignore_error = 1 if ($err->empty);
317
318             if ($ignore_error) {
319                 $self->_user_msg(slot_result => 1, err => $err);
320                 if ($err->{'slot'}) {
321                     $last_slot = $err->{slot};
322                     $self->{'seen'}->{$last_slot} = 1;
323                 }
324                 return $steps->{'load'}->(undef);
325             } else {
326                 # if we have a fatal error or something other than "notfound"
327                 # or "volinuse", bail out.
328                 $self->_user_msg(slot_result => 1, err => $err);
329                 return $self->scan_result(error => $err, res => $res,
330                                         result_cb => $result_cb);
331             }
332         }
333
334         $self->{'seen'}->{$res->{'this_slot'}} = 1;
335
336         $steps->{'try_volume'}->();
337     };
338
339     step try_volume => sub {
340         my $slot = $res->{'this_slot'};
341         my $dev = $res->{'device'};
342         my $status = $dev->status;
343         my $labelstr = $res->{'chg'}->{'labelstr'};
344         my $label;
345         my $autolabel = $res->{'chg'}->{'autolabel'};
346
347         if ($status == $DEVICE_STATUS_SUCCESS) {
348             $label = $dev->volume_label;
349
350             if ($label !~ /$labelstr/) {
351                 if (!$autolabel->{'other_config'}) {
352                     $self->_user_msg(slot_result             => 1,
353                                      does_not_match_labelstr => 1,
354                                      labelstr                => $labelstr,
355                                      slot                    => $slot,
356                                      label                   => $label,
357                                      res                     => $res);
358                     return $steps->{'try_continue'}->();
359                 }
360             } else {
361                 # verify that the label is in the tapelist
362                 my $tle = $self->{'tapelist'}->lookup_tapelabel($label);
363                 if (!$tle) {
364                     $self->_user_msg(slot_result     => 1,
365                                      not_in_tapelist => 1,
366                                      slot            => $slot,
367                                      label           => $label,
368                                      res             => $res);
369                     return $steps->{'try_continue'}->();
370                 }
371
372                 # see if it's reusable
373                 if (!$self->is_reusable_volume(label => $label, new_label_ok => 1)) {
374                     $self->_user_msg(slot_result => 1,
375                                      active      => 1,
376                                      slot        => $slot,
377                                      label       => $label,
378                                      res         => $res);
379                     return $steps->{'try_continue'}->();
380                 }
381                 $self->_user_msg(slot_result => 1,
382                                  slot        => $slot,
383                                  label       => $label,
384                                  res         => $res);
385                 $self->scan_result(res => $res, label => $label,
386                                    mode => $ACCESS_WRITE, is_new => 0,
387                                    result_cb => $result_cb);
388                 return;
389             }
390         }
391
392         if (!defined $autolabel->{'template'} ||
393             $autolabel->{'template'} eq "") {
394             if ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
395                 $dev->volume_header and
396                 $dev->volume_header->{'type'} == $Amanda::Header::F_EMPTY) {
397                 $self->_user_msg(slot_result   => 1,
398                                  not_autolabel => 1,
399                                  empty         => 1,
400                                  slot          => $slot,
401                                  res           => $res);
402             } elsif ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
403                 $dev->volume_header and
404                 $dev->volume_header->{'type'} == $Amanda::Header::F_WEIRD) {
405                 $self->_user_msg(slot_result   => 1,
406                                  not_autolabel => 1,
407                                  non_amanda    => 1,
408                                  slot          => $slot,
409                                  res           => $res);
410             } elsif ($status & $DEVICE_STATUS_VOLUME_ERROR) {
411                 $self->_user_msg(slot_result   => 1,
412                                  not_autolabel => 1,
413                                  volume_error  => 1,
414                                  err           => $dev->error_or_status(),
415                                  slot          => $slot,
416                                  res           => $res);
417             } elsif ($status != $DEVICE_STATUS_SUCCESS) {
418                 $self->_user_msg(slot_result   => 1,
419                                  not_autolabel => 1,
420                                  not_success   => 1,
421                                  err           => $dev->error_or_status(),
422                                  slot          => $slot,
423                                  res           => $res);
424             } else {
425                 $self->_user_msg(slot_result   => 1,
426                                  not_autolabel => 1,
427                                  slot          => $slot,
428                                  res           => $res);
429             }
430             return $steps->{'try_continue'}->();
431         }
432
433         if ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
434             $dev->volume_header and
435             $dev->volume_header->{'type'} == $Amanda::Header::F_EMPTY) {
436             if (!$autolabel->{'empty'}) {
437                 $self->_user_msg(slot_result  => 1,
438                                  empty        => 1,
439                                  slot         => $slot,
440                                  res          => $res);
441                 return $steps->{'try_continue'}->();
442             }
443         } elsif ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
444             $dev->volume_header and
445             $dev->volume_header->{'type'} == $Amanda::Header::F_WEIRD) {
446             if (!$autolabel->{'non_amanda'}) {
447                 $self->_user_msg(slot_result  => 1,
448                                  non_amanda   => 1,
449                                  slot         => $slot,
450                                  res          => $res);
451                 return $steps->{'try_continue'}->();
452             }
453         } elsif ($status & $DEVICE_STATUS_VOLUME_ERROR) {
454             if (!$autolabel->{'volume_error'}) {
455                 $self->_user_msg(slot_result  => 1,
456                                  volume_error => 1,
457                                  err          => $dev->error_or_status(),
458                                  slot         => $slot,
459                                  res          => $res);
460                 return $steps->{'try_continue'}->();
461             }
462         } elsif ($status != $DEVICE_STATUS_SUCCESS) {
463             $self->_user_msg(slot_result  => 1,
464                              not_success  => 1,
465                              err          => $dev->error_or_status(),
466                              slot         => $slot,
467                              res          => $res);
468             return $steps->{'try_continue'}->();
469         }
470
471         $self->_user_msg(slot_result => 1, slot => $slot, res => $res);
472         $res->get_meta_label(finished_cb => $steps->{'got_meta_label'});
473         return;
474     };
475
476     step got_meta_label => sub {
477         my ($err, $meta) = @_;
478
479         if (defined $err) {
480             $self->scan_result(error => $err, res => $res,
481                                result_cb => $result_cb);
482             return;
483         }
484
485         ($meta, $err) = $res->make_new_meta_label() if !defined $meta;
486         if (defined $err) {
487             $self->scan_result(error => $err, res => $res,
488                                result_cb => $result_cb);
489             return;
490         }
491
492         (my $label, $err) = $res->make_new_tape_label(meta => $meta);
493         
494
495         if (!defined $label) {
496             # make this fatal, rather than silently skipping new tapes
497             $self->scan_result(error => $err, res => $res, result_cb => $result_cb);
498             return;
499         }
500
501         $self->scan_result(res => $res, label => $label, mode => $ACCESS_WRITE,
502                            is_new => 1, result_cb => $result_cb);
503         return;
504     };
505
506     step try_continue => sub {
507         # no luck -- release this reservation and get the next
508         $last_slot = $res->{'this_slot'};
509
510         $res->release(finished_cb => $steps->{'load'});
511     };
512 }
513
514 1;