7c8235cbd54f2acf1d930856c36426c8a4f7b57e
[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->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
79     $self->{'user_msg_fn'}->(%params);
80 }
81
82 sub scan_result {
83     my $self = shift;
84     my %params = @_;
85
86     my @result = ($params{'error'}, $params{'res'}, $params{'label'},
87                   $params{'mode'}, $params{'is_new'});
88
89     if ($params{'error'}) {
90         debug("Amanda::Taper::Scan::traditional result: error=$params{'error'}");
91
92         # if we already had a reservation when the error occurred, then we'll need
93         # to release that reservation before signalling the error
94         if ($params{'res'}) {
95             my $finished_cb = make_cb(finished_cb => sub {
96                 my ($err) = @_;
97                 # if there was an error releasing, log it and ignore it
98                 Amanda::Debug::warn("while releasing reservation: $err") if $err;
99
100                 $self->{'scanning'} = 0;
101                 $params{'result_cb'}->(@result);
102             });
103             return $params{'res'}->release(finished_cb => $finished_cb);
104         }
105     } elsif ($params{'res'}) {
106         my $devname = $params{'res'}->{'device'}->device_name;
107         my $slot = $params{'res'}->{'this_slot'};
108         debug("Amanda::Taper::Scan::traditional result: '$params{label}' " .
109               "on $devname slot $slot, mode $params{mode}");
110     } else {
111         debug("Amanda::Taper::Scan::traditional result: scan failed");
112
113         # we may not ever have looked for this, the oldest reusable volume, if
114         # the changer is not fast-searchable.  But we'll tell the user about it
115         # anyway.
116         my $oldest_reusable = $self->oldest_reusable_volume(new_label_ok => 0);
117         $self->_user_msg(scan_failed => 1,
118                          expected_label => $oldest_reusable,
119                          expected_new => 1);
120         @result = ("No acceptable volumes found");
121     }
122
123     $self->{'scanning'} = 0;
124     $params{'result_cb'}->(@result);
125 }
126
127 ##
128 # stage 1: search for the oldest reusable volume
129
130 sub stage_1 {
131     my $self = shift;
132     my ($result_cb) = @_;
133     my $oldest_reusable;
134
135     my $steps = define_steps
136         cb_ref => \$result_cb;
137
138     step setup => sub {
139         debug("Amanda::Taper::Scan::traditional stage 1: search for oldest reusable volume");
140         $oldest_reusable = $self->oldest_reusable_volume(
141             new_label_ok => 0,      # stage 1 never selects new volumes
142         );
143
144         if (!defined $oldest_reusable) {
145             debug("Amanda::Taper::Scan::traditional no oldest reusable volume");
146             return $self->stage_2($result_cb);
147         }
148         debug("Amanda::Taper::Scan::traditional oldest reusable volume is '$oldest_reusable'");
149
150         # try loading that oldest volume, but only if the changer is fast-search capable
151         $steps->{'get_info'}->();
152     };
153
154     step get_info => sub {
155         $self->{'changer'}->info(
156             info => [ "fast_search" ],
157             info_cb => $steps->{'got_info'},
158         );
159     };
160
161     step got_info => sub {
162         my ($error, %results) = @_;
163         if ($error) {
164             return $self->scan_result(error => $error, result_cb => $result_cb);
165         }
166
167         if ($results{'fast_search'}) {
168             debug("Amanda::Taper::Scan::traditional stage 1: searching oldest reusable " .
169                   "volume '$oldest_reusable'");
170             $self->_user_msg(search_label => 1,
171                              label        => $oldest_reusable);
172
173             $steps->{'do_load'}->();
174         } else {
175             # no fast search, so skip to stage 2
176             debug("Amanda::Taper::Scan::traditional changer is not fast-searchable; skipping to stage 2");
177             $self->stage_2($result_cb);
178         }
179     };
180
181     step do_load => sub {
182         $self->{'changer'}->load(
183             label => $oldest_reusable,
184             set_current => 1,
185             res_cb => $steps->{'load_done'});
186     };
187
188     step load_done => sub {
189         my ($err, $res) = @_;
190
191         $self->_user_msg(search_result => 1, res => $res, err => $err);
192         if ($err) {
193             if ($err->failed and $err->notfound) {
194                 debug("Amanda::Taper::Scan::traditional oldest reusable volume not found");
195                 return $self->stage_2($result_cb);
196             } else {
197                 return $self->scan_result(error => $err,
198                         res => $res, result_cb => $result_cb);
199             }
200         }
201
202         $self->{'seen'}->{$res->{'this_slot'}} = 1;
203
204         my $status = $res->{'device'}->status;
205         if ($status != $DEVICE_STATUS_SUCCESS) {
206             warning "Error reading label after searching for '$oldest_reusable'";
207             return $self->release_and_stage_2($res, $result_cb);
208         }
209
210         # go on to stage 2 if we didn't get the expected volume
211         my $label = $res->{'device'}->volume_label;
212         my $labelstr = $self->{'labelstr'};
213         if ($label !~ /$labelstr/) {
214             warning "Searched for label '$oldest_reusable' but found a volume labeled '$label'";
215             return $self->release_and_stage_2($res, $result_cb);
216         }
217
218         # great! -- volume found
219         return $self->scan_result(res => $res, label => $oldest_reusable,
220                     mode => $ACCESS_WRITE, is_new => 0, result_cb => $result_cb);
221     };
222 }
223
224 ##
225 # stage 2: scan for any usable volume
226
227 sub release_and_stage_2 {
228     my $self = shift;
229     my ($res, $result_cb) = @_;
230
231     $res->release(finished_cb => sub {
232         my ($error) = @_;
233         if ($error) {
234             $self->scan_result(error => $error, result_cb => $result_cb);
235         } else {
236             $self->stage_2($result_cb);
237         }
238     });
239 }
240
241 sub stage_2 {
242     my $self = shift;
243     my ($result_cb) = @_;
244
245     my $last_slot;
246     my $load_current = ($self->{'scan_num'} == 1);
247     my $steps = define_steps
248         cb_ref => \$result_cb;
249     my $res;
250
251     step load => sub {
252         my ($err) = @_;
253
254         debug("Amanda::Taper::Scan::traditional stage 2: scan for any reusable volume");
255
256         # bail on an error releasing a reservation
257         if ($err) {
258             return $self->scan_result(error => $err, result_cb => $result_cb);
259         }
260
261         # load the current or next slot
262         my @load_args;
263         if ($load_current) {
264             # load 'current' the first time through
265             @load_args = (
266                 relative_slot => 'current',
267             );
268         } else {
269             @load_args = (
270                 relative_slot => 'next',
271                 (defined $last_slot)? (slot => $last_slot) : (),
272             );
273         }
274
275         $self->{'changer'}->load(
276             @load_args,
277             set_current => 1,
278             res_cb => $steps->{'loaded'},
279             except_slots => $self->{'seen'},
280             mode => "write",
281         );
282     };
283
284     step loaded => sub {
285         (my $err, $res) = @_;
286         my $loaded_current = $load_current;
287         $load_current = 0; # don't load current a second time
288
289         $self->_user_msg(search_result => 1, res => $res, err => $err);
290         # bail out immediately if the scan is complete
291         if ($err and $err->failed and $err->notfound) {
292             # no error, no reservation -> end of the scan
293             return $self->scan_result(result_cb => $result_cb);
294         }
295
296         # tell user_msg which slot we're looking at..
297         if (defined $res) {
298             $self->_user_msg(scan_slot => 1, slot => $res->{'this_slot'});
299         } elsif (defined $err->{'slot'}) {
300             $self->_user_msg(scan_slot => 1, slot => $err->{'slot'});
301         } else {
302             $self->_user_msg(scan_slot => 1, slot => "?");
303         }
304
305         # and then tell it the result if already known (error) or try
306         # loading the volume.
307         if ($err) {
308             my $ignore_error = 0;
309             # there are two "acceptable" errors: if the slot exists but the volume
310             # is already in use
311             $ignore_error = 1 if ($err->volinuse && $err->{slot});
312             # or if we loaded the 'current' slot and it was invalid (this happens if
313             # the user changes 'use-slots', for example
314             $ignore_error = 1 if ($loaded_current && $err->invalid);
315             $ignore_error = 1 if ($err->empty);
316
317             if ($ignore_error) {
318                 $self->_user_msg(slot_result => 1, err => $err);
319                 if ($err->{'slot'}) {
320                     $last_slot = $err->{slot};
321                     $self->{'seen'}->{$last_slot} = 1;
322                 }
323                 return $steps->{'load'}->(undef);
324             } else {
325                 # if we have a fatal error or something other than "notfound"
326                 # or "volinuse", bail out.
327                 $self->_user_msg(slot_result => 1, err => $err);
328                 return $self->scan_result(error => $err, res => $res,
329                                         result_cb => $result_cb);
330             }
331         }
332
333         $self->{'seen'}->{$res->{'this_slot'}} = 1;
334
335         $steps->{'try_volume'}->();
336     };
337
338     step try_volume => sub {
339         my $slot = $res->{'this_slot'};
340         my $dev = $res->{'device'};
341         my $status = $dev->status;
342         my $labelstr = $res->{'chg'}->{'labelstr'};
343         my $label;
344         my $autolabel = $res->{'chg'}->{'autolabel'};
345
346         if ($status == $DEVICE_STATUS_SUCCESS) {
347             $label = $dev->volume_label;
348
349             if ($label !~ /$labelstr/) {
350                 if (!$autolabel->{'other_config'}) {
351                     $self->_user_msg(slot_result             => 1,
352                                      does_not_match_labelstr => 1,
353                                      labelstr                => $labelstr,
354                                      slot                    => $slot,
355                                      label                   => $label,
356                                      res                     => $res);
357                     return $steps->{'try_continue'}->();
358                 }
359             } else {
360                 # verify that the label is in the tapelist
361                 my $tle = $self->{'tapelist'}->lookup_tapelabel($label);
362                 if (!$tle) {
363                     $self->_user_msg(slot_result     => 1,
364                                      not_in_tapelist => 1,
365                                      slot            => $slot,
366                                      label           => $label,
367                                      res             => $res);
368                     return $steps->{'try_continue'}->();
369                 }
370
371                 # see if it's reusable
372                 if (!$self->is_reusable_volume(label => $label, new_label_ok => 1)) {
373                     $self->_user_msg(slot_result => 1,
374                                      active      => 1,
375                                      slot        => $slot,
376                                      label       => $label,
377                                      res         => $res);
378                     return $steps->{'try_continue'}->();
379                 }
380                 $self->_user_msg(slot_result => 1,
381                                  slot        => $slot,
382                                  label       => $label,
383                                  res         => $res);
384                 $self->scan_result(res => $res, label => $label,
385                                    mode => $ACCESS_WRITE, is_new => 0,
386                                    result_cb => $result_cb);
387                 return;
388             }
389         }
390
391         if (!defined $autolabel->{'template'} ||
392             $autolabel->{'template'} eq "") {
393             $self->_user_msg(slot_result  => 1,
394                              not_autolabel => 1,
395                              slot         => $slot,
396                              res          => $res);
397             return $steps->{'try_continue'}->();
398         }
399
400         if ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
401             $dev->volume_header and
402             $dev->volume_header->{'type'} == $Amanda::Header::F_EMPTY) {
403             if (!$autolabel->{'empty'}) {
404                 $self->_user_msg(slot_result  => 1,
405                                  empty        => 1,
406                                  slot         => $slot,
407                                  res          => $res);
408                 return $steps->{'try_continue'}->();
409             }
410         } elsif ($status & $DEVICE_STATUS_VOLUME_UNLABELED and
411             $dev->volume_header and
412             $dev->volume_header->{'type'} == $Amanda::Header::F_WEIRD) {
413             if (!$autolabel->{'non_amanda'}) {
414                 $self->_user_msg(slot_result  => 1,
415                                  non_amanda   => 1,
416                                  slot         => $slot,
417                                  res          => $res);
418                 return $steps->{'try_continue'}->();
419             }
420         } elsif ($status & $DEVICE_STATUS_VOLUME_ERROR) {
421             if (!$autolabel->{'volume_error'}) {
422                 $self->_user_msg(slot_result  => 1,
423                                  volume_error => 1,
424                                  err          => $dev->error_or_status(),
425                                  slot         => $slot,
426                                  res          => $res);
427                 return $steps->{'try_continue'}->();
428             }
429         } elsif ($status != $DEVICE_STATUS_SUCCESS) {
430             $self->_user_msg(slot_result  => 1,
431                              not_success  => 1,
432                              err          => $dev->error_or_status(),
433                              slot         => $slot,
434                              res          => $res);
435             return $steps->{'try_continue'}->();
436         }
437
438         $self->_user_msg(slot_result => 1, slot => $slot, res => $res);
439         $res->get_meta_label(finished_cb => $steps->{'got_meta_label'});
440         return;
441     };
442
443     step got_meta_label => sub {
444         my ($err, $meta) = @_;
445
446         if (defined $err) {
447             $self->scan_result(error => $err, res => $res,
448                                result_cb => $result_cb);
449             return;
450         }
451
452         ($meta, $err) = $res->make_new_meta_label() if !defined $meta;
453         if (defined $err) {
454             $self->scan_result(error => $err, res => $res,
455                                result_cb => $result_cb);
456             return;
457         }
458
459         (my $label, $err) = $res->make_new_tape_label(meta => $meta);
460         
461
462         if (!defined $label) {
463             # make this fatal, rather than silently skipping new tapes
464             $self->scan_result(error => $err, res => $res, result_cb => $result_cb);
465             return;
466         }
467
468         $self->scan_result(res => $res, label => $label, mode => $ACCESS_WRITE,
469                            is_new => 1, result_cb => $result_cb);
470         return;
471     };
472
473     step try_continue => sub {
474         # no luck -- release this reservation and get the next
475         $last_slot = $res->{'this_slot'};
476
477         $res->release(finished_cb => $steps->{'load'});
478     };
479 }
480
481 1;