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