Imported Upstream version 3.3.2
[debian/amanda] / perl / Amanda / ScanInventory.pm
1 # Copyright (c) 2010-2012 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         my $relabeled = !defined($label) || $label !~ /$self->{'labelstr'}/;
268         $self->_user_msg(slot_result => 1,
269                          slot => $slot_scanned,
270                          label => $label,
271                          err  => $err,
272                          relabeled => $relabeled,
273                          res  => $res);
274         if ($res) {
275             my $f_type;
276             if (defined $res->{device}->volume_header) {
277                 $f_type = $res->{device}->volume_header->{type};
278             }
279
280             # The slot did not contain the volume we wanted, so mark it
281             # as seen and try again.
282             $seen{$slot_scanned} = {
283                         device_status => $res->{device}->status,
284                         f_type => $f_type,
285                         label  => $res->{device}->volume_label
286             };
287
288             # notify the user
289             if ($res->{device}->status == $DEVICE_STATUS_SUCCESS) {
290                 $last_err = undef;
291             } else {
292                 $last_err = Amanda::Changer::Error->new('fatal',
293                                 message => $res->{device}->error_or_status());
294             }
295         } else {
296             $seen{$slot_scanned} = { failed => 1 };
297             if ($err->volinuse) {
298                 # Scan semantics for volinuse is different than changer.
299                 # If a slot with unknown label is loaded then we map
300                 # volinuse to driveinuse.
301                 $err->{reason} = "driveinuse";
302             }
303             $last_err = $err if $err->fatal || !$err->notfound;
304         }
305         return $steps->{'load_released'}->();
306     };
307
308     step load_released => sub {
309         my ($err) = @_;
310
311         # TODO: handle error
312
313         # throw out the inventory result and move on if the situation has
314         # changed while we were loading a volume
315         return $steps->{'abort_scan'}->() if $abort_scan;
316         return $steps->{'restart_scan'}->() if $restart_scan;
317
318         $new_slot = $current;
319         $steps->{'get_inventory'}->();
320     };
321
322     step handle_error => sub {
323         my ($err, $continue_cb) = @_;
324
325         my $scan_method = undef;
326         $scan_running = 0;
327         my $message;
328
329         $poll_src->remove() if defined $poll_src;
330         $poll_src = undef;
331
332         # prefer to use scan method for $last_err, if present
333         if ($last_err && $err->failed && $err->notfound) {
334             $message = "$last_err";
335         
336             if ($last_err->isa("Amanda::Changer::Error")) {
337                 if ($last_err->fatal) {
338                     $scan_method = $self->{'scan_conf'}->{'fatal'};
339                 } else {
340                     $scan_method = $self->{'scan_conf'}->{$last_err->{'reason'}};
341                 }
342             } elsif ($continue_cb) {
343                 $scan_method = SCAN_CONTINUE;
344             }
345         }
346
347         #use scan method for $err
348         if (!defined $scan_method) {
349             if ($err) {
350                 $message = "$err" if !defined $message;
351                 if ($err->fatal) {
352                     $scan_method = $self->{'scan_conf'}->{'fatal'};
353                 } else {
354                     $scan_method = $self->{'scan_conf'}->{$err->{'reason'}};
355                 }
356             } else {
357                 die("error not defined");
358                 $scan_method = SCAN_ASK_POLL;
359             }
360         }
361
362         ## implement the desired scan method
363
364         if ($scan_method == SCAN_CONTINUE && !defined $continue_cb) {
365             $scan_method = $self->{'scan_conf'}->{'notfound'};
366             if ($scan_method == SCAN_CONTINUE) {
367                 $scan_method = SCAN_FAIL;
368             }
369         }
370         $steps->{'scan_next'}->($scan_method, $err, $continue_cb);
371     };
372
373     step scan_next => sub {
374         my ($scan_method, $err, $continue_cb) = @_;
375
376         if ($scan_method == SCAN_ASK && !defined $self->{'interactivity'}) {
377             $scan_method = SCAN_FAIL;
378         }
379
380         if ($scan_method == SCAN_ASK_POLL && !defined $self->{'interactivity'}) {
381             $scan_method = SCAN_FAIL;
382         }
383
384         if ($scan_method == SCAN_ASK) {
385             return $steps->{'scan_interactivity'}->("$err");
386         } elsif ($scan_method == SCAN_POLL) {
387             $poll_src = Amanda::MainLoop::call_after(
388                                 $self->{'scan_conf'}->{'poll_delay'},
389                                 $steps->{'after_poll'});
390             return;
391         } elsif ($scan_method == SCAN_ASK_POLL) {
392             $steps->{'scan_interactivity'}->("$err\n");
393             $poll_src = Amanda::MainLoop::call_after(
394                                 $self->{'scan_conf'}->{'poll_delay'},
395                                 $steps->{'after_poll'});
396             return;
397         } elsif ($scan_method == SCAN_FAIL) {
398             return $steps->{'call_result_cb'}->($err, undef);
399         } elsif ($scan_method == SCAN_CONTINUE) {
400             return $continue_cb->($err, undef);
401         } else {
402             die("Invalid SCAN_* value:$err:$err->{'reason'}:$scan_method");
403         }
404     };
405
406     step after_poll => sub {
407         $poll_src->remove() if defined $poll_src;
408         $poll_src = undef;
409         return $steps->{'restart_scan'}->();
410     };
411
412     step scan_interactivity => sub {
413         my ($err_message) = @_;
414         if (!$interactivity_running) {
415             $interactivity_running = 1;
416             my $message = "$err_message\n";
417             if ($self->{'most_prefered_label'}) {
418                 $message .= "Insert volume labeled '$self->{'most_prefered_label'}'";
419             } else {
420                 $message .= "Insert a new volume";
421             }
422             $message .= " in changer and type <enter>\nor type \"^D\" to abort\n";
423             $self->{'interactivity'}->user_request(
424                                 message     => $message,
425                                 label       => $self->{'most_prefered_label'},
426                                 new_volume  => !$self->{'most_prefered_label'},
427                                 err         => "$err_message",
428                                 chg_name    => $self->{'chg'}->{'chg_name'},
429                                 request_cb  => $steps->{'scan_interactivity_cb'});
430         }
431         return;
432     };
433
434     step scan_interactivity_cb => sub {
435         my ($err, $message) = @_;
436         $interactivity_running = 0;
437         $poll_src->remove() if defined $poll_src;
438         $poll_src = undef;
439         $last_err = undef;
440
441         if ($err) {
442             if ($scan_running) {
443                 $abort_scan = $err;
444                 return;
445             } else {
446                 return $steps->{'call_result_cb'}->($err, undef);
447             }
448         }
449
450         if ($message ne '') {
451             # use a new changer
452             my $new_chg;
453             if (ref($message) eq 'HASH' and $message == $DEFAULT_CHANGER) {
454                 $message = undef;
455             }
456             $new_chg = Amanda::Changer->new($message,
457                                             tapelist => $self->{'tapelist'});
458             if ($new_chg->isa("Amanda::Changer::Error")) {
459                 return $steps->{'scan_interactivity'}->("$new_chg");
460             }
461             $self->{'chg'}->quit() if $self->{'chg'} != $self->{'initial_chg'};
462             $self->{'chg'} = $new_chg;
463             %seen = ();
464         } else {
465             $remove_undef_state = 1;
466         }
467
468         if ($scan_running) {
469             $restart_scan = 1;
470             return;
471         } else {
472             return $steps->{'restart_scan'}->();
473         }
474     };
475
476     step abort_scan => sub {
477         if (defined $res) {
478             $res->released(finished_cb => $steps->{'abort_scan_released'});
479         } else {
480             $steps->{'abort_scan_released'}->();
481         }
482     };
483
484     step abort_scan_released => sub {
485         $steps->{'call_result_cb'}->($abort_scan, undef);
486     };
487
488     step call_result_cb => sub {
489         (my $err, $res) = @_;
490
491         # TODO: what happens if the search was aborted or
492         # restarted in the interim?
493
494         $abort_scan = undef;
495         $poll_src->remove() if defined $poll_src;
496         $poll_src = undef;
497         $interactivity_running = 0;
498         $self->{'interactivity'}->abort() if defined $self->{'interactivity'};
499         $self->{'chg'}->quit() if $self->{'chg'} != $self->{'initial_chg'} and !$res;
500         if ($err) {
501             $self->{'scanning'} = 0;
502             return $result_cb->($err, $res);
503         }
504         $label = $res->{'device'}->volume_label;
505         if (!defined($label) || $label !~ /$self->{'labelstr'}/) {
506             $res->get_meta_label(finished_cb => $steps->{'got_meta_label'});
507             return;
508         }
509         $self->{'scanning'} = 0;
510         return $result_cb->(undef, $res, $label, $ACCESS_WRITE);
511     };
512
513     step got_meta_label => sub {
514         my ($err, $meta) = @_;
515         if (defined $err) {
516             return $result_cb->($err, $res);
517         }
518         ($label, my $make_err) = $res->make_new_tape_label(meta => $meta);
519         if (!defined $label) {
520             # make this fatal, rather than silently skipping new tapes
521             $self->{'scanning'} = 0;
522             return $result_cb->($make_err, $res);
523         }
524         $self->{'scanning'} = 0;
525         return $result_cb->(undef, $res, $label, $ACCESS_WRITE, 1);
526     };
527 }
528
529 sub volume_is_labelable {
530     my $self = shift;
531     my $sl = shift;
532     my $dev_status  = $sl->{'device_status'};
533     my $f_type = $sl->{'f_type'};
534     my $label = $sl->{'label'};
535     my $slot = $sl->{'slot'};
536     my $chg = $self->{'chg'};
537     my $autolabel = $chg->{'autolabel'};
538
539     if (!defined $dev_status) {
540         return 0;
541     } elsif ($dev_status & $DEVICE_STATUS_VOLUME_UNLABELED and
542              defined $f_type and
543              $f_type == $Amanda::Header::F_EMPTY) {
544         if (!$autolabel->{'empty'}) {
545             $self->_user_msg(slot_result  => 1,
546                              empty        => 1,
547                              slot         => $slot);
548             return 0;
549         }
550     } elsif ($dev_status & $DEVICE_STATUS_VOLUME_UNLABELED and
551              defined $f_type and
552              $f_type == $Amanda::Header::F_WEIRD) {
553         if (!$autolabel->{'non_amanda'}) {
554             $self->_user_msg(slot_result  => 1,
555                              non_amanda   => 1,
556                              slot         => $slot);
557             return 0;
558         }
559     } elsif ($dev_status & $DEVICE_STATUS_VOLUME_ERROR) {
560         if (!$autolabel->{'volume_error'}) {
561             $self->_user_msg(slot_result  => 1,
562                              volume_error => 1,
563                              slot         => $slot);
564             return 0;
565         }
566     } elsif ($dev_status != $DEVICE_STATUS_SUCCESS) {
567             $self->_user_msg(slot_result  => 1,
568                              not_success  => 1,
569                              err          => $sl->{'device_error'},
570                              slot         => $slot);
571         return 0;
572     } elsif ($dev_status == $DEVICE_STATUS_SUCCESS and
573              $f_type == $Amanda::Header::F_TAPESTART and
574              $label !~ /$self->{'labelstr'}/) {
575         if (!$autolabel->{'other_config'}) {
576             $self->_user_msg(slot_result  => 1,
577                              label        => $label,
578                              does_not_match_labelstr => 1,
579                              slot         => $slot);
580             return 0;
581         }
582     }
583
584     return 1;
585 }
586 package Amanda::ScanInventory::Config;
587
588 sub new {
589     my $class = shift;
590     my ($cc) = @_;
591
592     my $self = bless {}, $class;
593
594     $self->{'poll_delay'} = 10000; #10 seconds
595
596     $self->{'fatal'} = Amanda::ScanInventory::SCAN_CONTINUE;
597     $self->{'driveinuse'} = Amanda::ScanInventory::SCAN_ASK_POLL;
598     $self->{'volinuse'} = Amanda::ScanInventory::SCAN_ASK_POLL;
599     $self->{'notfound'} = Amanda::ScanInventory::SCAN_ASK_POLL;
600     $self->{'unknown'} = Amanda::ScanInventory::SCAN_FAIL;
601     $self->{'invalid'} = Amanda::ScanInventory::SCAN_CONTINUE;
602
603     $self->{'scan'} = 1;
604     $self->{'ask'} = 1;
605     $self->{'new_labeled'} = 'order';
606     $self->{'new_volume'} = 'order';
607
608     return $self;
609 }
610
611 1;