d47f67510a89251635318189451ce261593da63f
[debian/amanda] / perl / Amanda / ScanInventory.pm
1 # Copyright (c) 2010 Zmanda, Inc.  All Rights Reserved.
2 #
3 # This program is free software; you can redistribute it and/or modify it
4 # under the terms of the GNU General Public License version 2 as published
5 # by the Free Software Foundation.
6 #
7 # This program 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 General Public License
10 # for more details.
11 #
12 # You should have received a copy of the GNU General Public License along
13 # with this program; if not, write to the Free Software Foundation, Inc.,
14 # 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 94085, USA, or: http://www.zmanda.com
18
19 package Amanda::ScanInventory;
20
21 =head1 NAME
22
23 Amanda::ScanInventory
24
25 =head1 SYNOPSIS
26
27 This package implements a base class for all scan that use the inventory.
28 see C<amanda-taperscan(7)>.
29
30 =cut
31
32 use strict;
33 use warnings;
34 use Amanda::Tapelist;
35 use Carp;
36 use POSIX ();
37 use Data::Dumper;
38 use vars qw( @ISA );
39 use base qw(Exporter);
40 our @EXPORT_OK = qw($DEFAULT_CHANGER);
41
42 use Amanda::Paths;
43 use Amanda::Util;
44 use Amanda::Device qw( :constants );
45 use Amanda::Debug qw( debug );
46 use Amanda::Changer;
47 use Amanda::MainLoop;
48 use Amanda::Interactivity;
49
50 use constant SCAN_ASK      => 1; # call Amanda::Interactivity module
51 use constant SCAN_POLL     => 2; # wait 'poll_delay' and retry the scan.
52 use constant SCAN_FAIL     => 3; # abort
53 use constant SCAN_CONTINUE => 4; # continue to the next step
54 use constant SCAN_ASK_POLL => 5; # call Amanda::Interactivity module and
55                                  # poll at the same time.
56 use constant SCAN_LOAD     => 6; # load a slot
57 use constant SCAN_DONE     => 7; # successful scan
58
59 our $DEFAULT_CHANGER = {};
60
61 sub new {
62     my $class = shift;
63     my %params = @_;
64     my $scan_conf = $params{'scan_conf'};
65     my $tapelist = $params{'tapelist'};
66     my $chg = $params{'changer'};
67     my $interactivity = $params{'interactivity'};
68
69     #until we have a config for it.
70     $scan_conf = Amanda::ScanInventory::Config->new();
71     $chg = Amanda::Changer->new(undef, tapelist => $tapelist) if !defined $chg;
72
73     my $self = {
74         initial_chg => $chg,
75         chg         => $chg,
76         scanning    => 0,
77         scan_conf   => $scan_conf,
78         tapelist    => $tapelist,
79         interactivity => $interactivity,
80         seen        => {},
81         scan_num    => 0
82     };
83     return bless ($self, $class);
84 }
85
86
87 sub scan {
88     my $self = shift;
89     my %params = @_;
90
91     die "Can only run one scan at a time" if $self->{'scanning'};
92     $self->{'scanning'} = 1;
93     $self->{'user_msg_fn'} = $params{'user_msg_fn'} || sub {};
94
95     # refresh the tapelist at every scan
96     $self->read_tapelist();
97
98     # count the number of scans we do, so we can only load 'current' on the
99     # first scan
100     $self->{'scan_num'}++;
101
102     $self->_scan(%params);
103 }
104
105 sub _user_msg {
106     my $self = shift;
107     my %params = @_;
108     $self->{'user_msg_fn'}->(%params);
109 }
110
111 sub _scan {
112     my $self = shift;
113     my %params = @_;
114
115     my $user_msg_fn = $params{'user_msg_fn'} || \&_user_msg_fn;
116     my $action;
117     my $action_slot;
118     my $res;
119     my $label;
120     my %seen = ();
121     my $inventory;
122     my $current;
123     my $new_slot;
124     my $poll_src;
125     my $scan_running = 0;
126     my $interactivity_running = 0;
127     my $restart_scan = 0;
128     my $abort_scan = undef;
129     my $last_err = undef; # keep the last meaningful error, the one reported
130                           # to the user, most scan end with the notfound error,
131                           # it's more interesting to report an error from the
132                           # device or ...
133     my $slot_scanned;
134     my $remove_undef_state = 0;
135     my $result_cb = $params{'result_cb'};
136
137     my $steps = define_steps
138         cb_ref => \$result_cb;
139
140     step get_first_inventory => sub {
141         $scan_running = 1;
142         $self->{'chg'}->inventory(inventory_cb => $steps->{'got_first_inventory'});
143     };
144
145     step got_first_inventory => sub {
146         (my $err, $inventory) = @_;
147
148         if ($err && $err->notimpl) {
149             #inventory not implemented
150             die("no inventory");
151         } elsif ($err and $err->fatal) {
152             #inventory fail
153             return $steps->{'call_result_cb'}->($err, undef);
154         }
155
156         # continue parsing the inventory
157         $steps->{'parse_inventory'}->($err, $inventory);
158     };
159
160     step restart_scan => sub {
161         $restart_scan = 0;
162         return $steps->{'get_inventory'}->();
163     };
164
165     step get_inventory => sub {
166         $self->{'chg'}->inventory(inventory_cb => $steps->{'parse_inventory'});
167     };
168
169     step parse_inventory => sub {
170         (my $err, $inventory) = @_;
171
172         if ($err && $err->notimpl) {
173             #inventory not implemented
174             die("no inventory");
175         }
176         return $steps->{'handle_error'}->($err, undef) if $err;
177
178         # throw out the inventory result and move on if the situation has
179         # changed while we were waiting
180         return $steps->{'abort_scan'}->() if $abort_scan;
181         return $steps->{'restart_scan'}->() if $restart_scan;
182
183         # Remove from seen all slot that have state == SLOT_UNKNOWN
184         # It is done when a scan is restarted from interactivity object.
185         if ($remove_undef_state) {
186             for my $i (0..(scalar(@$inventory)-1)) {
187                 my $slot = $inventory->[$i]->{slot};
188                 if (exists($seen{$slot}) &&
189                     !defined($inventory->[$i]->{state})) {
190                     delete $seen{$slot}
191                 }
192             }
193             $remove_undef_state = 0;
194         }
195
196         # remove any slots where the state has changed from the list of seen slots
197         for my $i (0..(scalar(@$inventory)-1)) {
198             my $sl = $inventory->[$i];
199             my $slot = $sl->{slot};
200             if ($seen{$slot} &&
201                 !defined ($seen{$slot}->{'failed'}) &&
202                 defined($sl->{'state'}) &&
203                 (($seen{$slot}->{'device_status'} != $sl->{'device_status'}) ||
204                  (defined $seen{$slot}->{'device_status'} &&
205                   $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS &&
206                   $seen{$slot}->{'f_type'} != $sl->{'f_type'}) ||
207                  (defined $seen{$slot}->{'device_status'} &&
208                   $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS &&
209                   defined $seen{$slot}->{'f_type'} &&
210                   $seen{$slot}->{'f_type'} == $Amanda::Header::F_TAPESTART &&
211                   $seen{$slot}->{'label'} ne $sl->{'label'}))) {
212                 delete $seen{$slot};
213             }
214         }
215
216         ($action, $action_slot) = $self->analyze($inventory, \%seen, $res);
217
218         if ($action == Amanda::ScanInventory::SCAN_DONE) {
219             return $steps->{'call_result_cb'}->(undef, $res);
220         }
221
222         if (defined $res) {
223             $res->release(finished_cb => $steps->{'released'});
224         } else {
225             $steps->{'released'}->();
226         }
227     };
228
229     step released => sub {
230         if ($action == Amanda::ScanInventory::SCAN_LOAD) {
231             $slot_scanned = $action_slot;
232             $self->_user_msg(scan_slot => 1,
233                              slot => $slot_scanned);
234             return $self->{'changer'}->load(
235                         slot => $slot_scanned,
236                         set_current => $params{'set_current'},
237                         res_cb => $steps->{'slot_loaded'});
238         }
239
240         my $err;
241         if ($last_err) {
242             $err = $last_err;
243         } else {
244             $err = Amanda::Changer::Error->new('failed',
245                                 reason => 'notfound',
246                                 message => "No acceptable volumes found");
247         }
248
249         if ($action == Amanda::ScanInventory::SCAN_FAIL) {
250             return $steps->{'handle_error'}->($err, undef);
251         }
252         $scan_running = 0;
253         $steps->{'scan_next'}->($action, $err);
254     };
255
256     step slot_loaded => sub {
257         (my $err, $res) = @_;
258
259         # we don't responsd to abort_scan or restart_scan here, since we
260         # have an open reservation that we should deal with.
261
262         my $label;
263         if ($res && defined $res->{device} &&
264             $res->{device}->status == $DEVICE_STATUS_SUCCESS) {
265             $label = $res->{device}->volume_label;
266         }
267         $self->_user_msg(slot_result => 1,
268                          slot => $slot_scanned,
269                          label => $label,
270                          err  => $err,
271                          res  => $res);
272         if ($res) {
273             my $f_type;
274             if (defined $res->{device}->volume_header) {
275                 $f_type = $res->{device}->volume_header->{type};
276             }
277
278             # The slot did not contain the volume we wanted, so mark it
279             # as seen and try again.
280             $seen{$slot_scanned} = {
281                         device_status => $res->{device}->status,
282                         f_type => $f_type,
283                         label  => $res->{device}->volume_label
284             };
285
286             # notify the user
287             if ($res->{device}->status == $DEVICE_STATUS_SUCCESS) {
288                 $last_err = undef;
289             } else {
290                 $last_err = Amanda::Changer::Error->new('fatal',
291                                 message => $res->{device}->error_or_status());
292             }
293         } else {
294             $seen{$slot_scanned} = { failed => 1 };
295             if ($err->volinuse) {
296                 # Scan semantics for volinuse is different than changer.
297                 # If a slot with unknown label is loaded then we map
298                 # volinuse to driveinuse.
299                 $err->{reason} = "driveinuse";
300             }
301             $last_err = $err if $err->fatal || !$err->notfound;
302         }
303         return $steps->{'load_released'}->();
304     };
305
306     step load_released => sub {
307         my ($err) = @_;
308
309         # TODO: handle error
310
311         # throw out the inventory result and move on if the situation has
312         # changed while we were loading a volume
313         return $steps->{'abort_scan'}->() if $abort_scan;
314         return $steps->{'restart_scan'}->() if $restart_scan;
315
316         $new_slot = $current;
317         $steps->{'get_inventory'}->();
318     };
319
320     step handle_error => sub {
321         my ($err, $continue_cb) = @_;
322
323         my $scan_method = undef;
324         $scan_running = 0;
325         my $message;
326
327         $poll_src->remove() if defined $poll_src;
328         $poll_src = undef;
329
330         # prefer to use scan method for $last_err, if present
331         if ($last_err && $err->failed && $err->notfound) {
332             $message = "$last_err";
333         
334             if ($last_err->isa("Amanda::Changer::Error")) {
335                 if ($last_err->fatal) {
336                     $scan_method = $self->{'scan_conf'}->{'fatal'};
337                 } else {
338                     $scan_method = $self->{'scan_conf'}->{$last_err->{'reason'}};
339                 }
340             } elsif ($continue_cb) {
341                 $scan_method = SCAN_CONTINUE;
342             }
343         }
344
345         #use scan method for $err
346         if (!defined $scan_method) {
347             if ($err) {
348                 $message = "$err" if !defined $message;
349                 if ($err->fatal) {
350                     $scan_method = $self->{'scan_conf'}->{'fatal'};
351                 } else {
352                     $scan_method = $self->{'scan_conf'}->{$err->{'reason'}};
353                 }
354             } else {
355                 die("error not defined");
356                 $scan_method = SCAN_ASK_POLL;
357             }
358         }
359
360         ## implement the desired scan method
361
362         if ($scan_method == SCAN_CONTINUE && !defined $continue_cb) {
363             $scan_method = $self->{'scan_conf'}->{'notfound'};
364             if ($scan_method == SCAN_CONTINUE) {
365                 $scan_method = SCAN_FAIL;
366             }
367         }
368         $steps->{'scan_next'}->($scan_method, $err, $continue_cb);
369     };
370
371     step scan_next => sub {
372         my ($scan_method, $err, $continue_cb) = @_;
373
374         if ($scan_method == SCAN_ASK && !defined $self->{'interactivity'}) {
375             $scan_method = SCAN_FAIL;
376         }
377
378         if ($scan_method == SCAN_ASK_POLL && !defined $self->{'interactivity'}) {
379             $scan_method = SCAN_FAIL;
380         }
381
382         if ($scan_method == SCAN_ASK) {
383             return $steps->{'scan_interactivity'}->("$err");
384         } elsif ($scan_method == SCAN_POLL) {
385             $poll_src = Amanda::MainLoop::call_after(
386                                 $self->{'scan_conf'}->{'poll_delay'},
387                                 $steps->{'after_poll'});
388             return;
389         } elsif ($scan_method == SCAN_ASK_POLL) {
390             $steps->{'scan_interactivity'}->("$err\n");
391             $poll_src = Amanda::MainLoop::call_after(
392                                 $self->{'scan_conf'}->{'poll_delay'},
393                                 $steps->{'after_poll'});
394             return;
395         } elsif ($scan_method == SCAN_FAIL) {
396             return $steps->{'call_result_cb'}->($err, undef);
397         } elsif ($scan_method == SCAN_CONTINUE) {
398             return $continue_cb->($err, undef);
399         } else {
400             die("Invalid SCAN_* value:$err:$err->{'reason'}:$scan_method");
401         }
402     };
403
404     step after_poll => sub {
405         $poll_src->remove() if defined $poll_src;
406         $poll_src = undef;
407         return $steps->{'restart_scan'}->();
408     };
409
410     step scan_interactivity => sub {
411         my ($err_message) = @_;
412         if (!$interactivity_running) {
413             $interactivity_running = 1;
414             my $message = "$err_message\n";
415             if ($self->{'most_prefered_label'}) {
416                 $message .= "Insert volume labeled '$self->{'most_prefered_label'}'";
417             } else {
418                 $message .= "Insert a new volume";
419             }
420             $message .= " in changer and type <enter>\nor type \"^D\" to abort\n";
421             $self->{'interactivity'}->user_request(
422                                 message     => $message,
423                                 label       => $self->{'most_prefered_label'},
424                                 new_volume  => !$self->{'most_prefered_label'},
425                                 err         => "$err_message",
426                                 chg_name    => $self->{'chg'}->{'chg_name'},
427                                 request_cb  => $steps->{'scan_interactivity_cb'});
428         }
429         return;
430     };
431
432     step scan_interactivity_cb => sub {
433         my ($err, $message) = @_;
434         $interactivity_running = 0;
435         $poll_src->remove() if defined $poll_src;
436         $poll_src = undef;
437         $last_err = undef;
438
439         if ($err) {
440             if ($scan_running) {
441                 $abort_scan = $err;
442                 return;
443             } else {
444                 return $steps->{'call_result_cb'}->($err, undef);
445             }
446         }
447
448         if ($message ne '') {
449             # use a new changer
450             my $new_chg;
451             if (ref($message) eq 'HASH' and $message == $DEFAULT_CHANGER) {
452                 $message = undef;
453             }
454             $new_chg = Amanda::Changer->new($message,
455                                             tapelist => $self->{'tapelist'});
456             if ($new_chg->isa("Amanda::Changer::Error")) {
457                 return $steps->{'scan_interactivity'}->("$new_chg");
458             }
459             $self->{'chg'}->quit() if $self->{'chg'} != $self->{'initial_chg'};
460             $self->{'chg'} = $new_chg;
461             %seen = ();
462         } else {
463             $remove_undef_state = 1;
464         }
465
466         if ($scan_running) {
467             $restart_scan = 1;
468             return;
469         } else {
470             return $steps->{'restart_scan'}->();
471         }
472     };
473
474     step abort_scan => sub {
475         if (defined $res) {
476             $res->released(finished_cb => $steps->{'abort_scan_released'});
477         } else {
478             $steps->{'abort_scan_released'}->();
479         }
480     };
481
482     step abort_scan_released => sub {
483         $steps->{'call_result_cb'}->($abort_scan, undef);
484     };
485
486     step call_result_cb => sub {
487         (my $err, $res) = @_;
488
489         # TODO: what happens if the search was aborted or
490         # restarted in the interim?
491
492         $abort_scan = undef;
493         $poll_src->remove() if defined $poll_src;
494         $poll_src = undef;
495         $interactivity_running = 0;
496         $self->{'interactivity'}->abort() if defined $self->{'interactivity'};
497         $self->{'chg'}->quit() if $self->{'chg'} != $self->{'initial_chg'} and !$res;
498         if ($err) {
499             $self->{'scanning'} = 0;
500             return $result_cb->($err, $res);
501         }
502         $label = $res->{'device'}->volume_label;
503         if (!defined $label) {
504             $res->get_meta_label(finished_cb => $steps->{'got_meta_label'});
505             return;
506         }
507         $self->{'scanning'} = 0;
508         return $result_cb->(undef, $res, $label, $ACCESS_WRITE);
509     };
510
511     step got_meta_label => sub {
512         my ($err, $meta) = @_;
513         if (defined $err) {
514             return $result_cb->($err, $res);
515         }
516         ($label, my $make_err) = $res->make_new_tape_label(meta => $meta);
517         if (!defined $label) {
518             # make this fatal, rather than silently skipping new tapes
519             $self->{'scanning'} = 0;
520             return $result_cb->($make_err, $res);
521         }
522         $self->{'scanning'} = 0;
523         return $result_cb->(undef, $res, $label, $ACCESS_WRITE, 1);
524     };
525 }
526
527 sub volume_is_labelable {
528     my $self = shift;
529     my $sl = shift;
530     my $dev_status  = $sl->{'device_status'};
531     my $f_type = $sl->{'f_type'};
532     my $label = $sl->{'label'};
533     my $slot = $sl->{'slot'};
534     my $chg = $self->{'chg'};
535     my $autolabel = $chg->{'autolabel'};
536
537     if (!defined $dev_status) {
538         return 0;
539     } elsif ($dev_status & $DEVICE_STATUS_VOLUME_UNLABELED and
540              defined $f_type and
541              $f_type == $Amanda::Header::F_EMPTY) {
542         if (!$autolabel->{'empty'}) {
543             $self->_user_msg(slot_result  => 1,
544                              empty        => 1,
545                              slot         => $slot);
546             return 0;
547         }
548     } elsif ($dev_status & $DEVICE_STATUS_VOLUME_UNLABELED and
549              defined $f_type and
550              $f_type == $Amanda::Header::F_WEIRD) {
551         if (!$autolabel->{'non_amanda'}) {
552             $self->_user_msg(slot_result  => 1,
553                              non_amanda   => 1,
554                              slot         => $slot);
555             return 0;
556         }
557     } elsif ($dev_status & $DEVICE_STATUS_VOLUME_ERROR) {
558         if (!$autolabel->{'volume_error'}) {
559             $self->_user_msg(slot_result  => 1,
560                              volume_error => 1,
561                              slot         => $slot);
562             return 0;
563         }
564     } elsif ($dev_status != $DEVICE_STATUS_SUCCESS) {
565             $self->_user_msg(slot_result  => 1,
566                              not_success  => 1,
567                              err          => $sl->{'device_error'},
568                              slot         => $slot);
569         return 0;
570     } elsif ($dev_status & $DEVICE_STATUS_SUCCESS and
571              $f_type == $Amanda::Header::F_TAPESTART and
572              $label !~ /$self->{'labelstr'}/) {
573         if (!$autolabel->{'other_config'}) {
574             $self->_user_msg(slot_result  => 1,
575                              other_config => 1,
576                              slot         => $slot);
577             return 0;
578         }
579     }
580
581     return 1;
582 }
583 package Amanda::ScanInventory::Config;
584
585 sub new {
586     my $class = shift;
587     my ($cc) = @_;
588
589     my $self = bless {}, $class;
590
591     $self->{'poll_delay'} = 10000; #10 seconds
592
593     $self->{'fatal'} = Amanda::ScanInventory::SCAN_CONTINUE;
594     $self->{'driveinuse'} = Amanda::ScanInventory::SCAN_ASK_POLL;
595     $self->{'volinuse'} = Amanda::ScanInventory::SCAN_ASK_POLL;
596     $self->{'notfound'} = Amanda::ScanInventory::SCAN_ASK_POLL;
597     $self->{'unknown'} = Amanda::ScanInventory::SCAN_FAIL;
598     $self->{'invalid'} = Amanda::ScanInventory::SCAN_CONTINUE;
599
600     $self->{'scan'} = 1;
601     $self->{'ask'} = 1;
602     $self->{'new_labeled'} = 'order';
603     $self->{'new_volume'} = 'order';
604
605     return $self;
606 }
607
608 1;