Imported Upstream version 3.3.0
[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() if !defined $chg;
72
73     my $self = {
74         initial_chg => $chg,
75         chg         => $chg,
76         scanning    => 0,
77         scan_conf   => $scan_conf,
78         interactivity => $interactivity,
79         seen        => {},
80         scan_num    => 0
81     };
82     return bless ($self, $class);
83 }
84
85
86 sub scan {
87     my $self = shift;
88     my %params = @_;
89
90     die "Can only run one scan at a time" if $self->{'scanning'};
91     $self->{'scanning'} = 1;
92     $self->{'user_msg_fn'} = $params{'user_msg_fn'} || sub {};
93
94     # refresh the tapelist at every scan
95     $self->read_tapelist();
96
97     # count the number of scans we do, so we can only load 'current' on the
98     # first scan
99     $self->{'scan_num'}++;
100
101     $self->_scan(%params);
102 }
103
104 sub _user_msg {
105     my $self = shift;
106     my %params = @_;
107     $self->{'user_msg_fn'}->(%params);
108 }
109
110 sub _scan {
111     my $self = shift;
112     my %params = @_;
113
114     my $user_msg_fn = $params{'user_msg_fn'} || \&_user_msg_fn;
115     my $action;
116     my $action_slot;
117     my $res;
118     my %seen = ();
119     my $inventory;
120     my $current;
121     my $new_slot;
122     my $poll_src;
123     my $scan_running = 0;
124     my $interactivity_running = 0;
125     my $restart_scan = 0;
126     my $abort_scan = undef;
127     my $last_err = undef; # keep the last meaningful error, the one reported
128                           # to the user, most scan end with the notfound error,
129                           # it's more interesting to report an error from the
130                           # device or ...
131     my $slot_scanned;
132     my $remove_undef_state = 0;
133     my $result_cb = $params{'result_cb'};
134
135     my $steps = define_steps
136         cb_ref => \$result_cb;
137
138     step get_first_inventory => sub {
139         $scan_running = 1;
140         $self->{'chg'}->inventory(inventory_cb => $steps->{'got_first_inventory'});
141     };
142
143     step got_first_inventory => sub {
144         (my $err, $inventory) = @_;
145
146         if ($err && $err->notimpl) {
147             #inventory not implemented
148             die("no inventory");
149         } elsif ($err) {
150             #inventory fail
151             return $steps->{'call_result_cb'}->($err, undef);
152         }
153
154         # continue parsing the inventory
155         $steps->{'parse_inventory'}->($err, $inventory);
156     };
157
158     step restart_scan => sub {
159         $restart_scan = 0;
160         return $steps->{'get_inventory'}->();
161     };
162
163     step get_inventory => sub {
164         $self->{'chg'}->inventory(inventory_cb => $steps->{'parse_inventory'});
165     };
166
167     step parse_inventory => sub {
168         (my $err, $inventory) = @_;
169
170         if ($err && $err->notimpl) {
171             #inventory not implemented
172             die("no inventory");
173         }
174         return $steps->{'handle_error'}->($err, undef) if $err;
175
176         # throw out the inventory result and move on if the situation has
177         # changed while we were waiting
178         return $steps->{'abort_scan'}->() if $abort_scan;
179         return $steps->{'restart_scan'}->() if $restart_scan;
180
181         # Remove from seen all slot that have state == SLOT_UNKNOWN
182         # It is done when a scan is restarted from interactivity object.
183         if ($remove_undef_state) {
184             for my $i (0..(scalar(@$inventory)-1)) {
185                 my $slot = $inventory->[$i]->{slot};
186                 if (exists($seen{$slot}) &&
187                     !defined($inventory->[$i]->{state})) {
188                     delete $seen{$slot}
189                 }
190             }
191             $remove_undef_state = 0;
192         }
193
194         # remove any slots where the state has changed from the list of seen slots
195         for my $i (0..(scalar(@$inventory)-1)) {
196             my $sl = $inventory->[$i];
197             my $slot = $sl->{slot};
198             if ($seen{$slot} &&
199                 !defined ($seen{$slot}->{'failed'}) &&
200                 defined($sl->{'state'}) &&
201                 (($seen{$slot}->{'device_status'} != $sl->{'device_status'}) ||
202                  (defined $seen{$slot}->{'device_status'} &&
203                   $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS &&
204                   $seen{$slot}->{'f_type'} != $sl->{'f_type'}) ||
205                  (defined $seen{$slot}->{'device_status'} &&
206                   $seen{$slot}->{'device_status'} == $DEVICE_STATUS_SUCCESS &&
207                   defined $seen{$slot}->{'f_type'} &&
208                   $seen{$slot}->{'f_type'} == $Amanda::Header::F_TAPESTART &&
209                   $seen{$slot}->{'label'} ne $sl->{'label'}))) {
210                 delete $seen{$slot};
211             }
212         }
213
214         ($action, $action_slot) = $self->analyze($inventory, \%seen, $res);
215
216         if ($action == Amanda::ScanInventory::SCAN_DONE) {
217             return $steps->{'call_result_cb'}->(undef, $res);
218         }
219
220         if (defined $res) {
221             $res->release(finished_cb => $steps->{'released'});
222         } else {
223             $steps->{'released'}->();
224         }
225     };
226
227     step released => sub {
228         if ($action == Amanda::ScanInventory::SCAN_LOAD) {
229             $slot_scanned = $action_slot;
230             $self->_user_msg(scan_slot => 1,
231                              slot => $slot_scanned);
232             return $self->{'changer'}->load(
233                         slot => $slot_scanned,
234                         set_current => $params{'set_current'},
235                         res_cb => $steps->{'slot_loaded'});
236         }
237
238         my $err;
239         if ($last_err) {
240             $err = $last_err;
241         } else {
242             $err = Amanda::Changer::Error->new('failed',
243                                 reason => 'notfound',
244                                 message => "No acceptable volumes found");
245         }
246
247         if ($action == Amanda::ScanInventory::SCAN_FAIL) {
248             return $steps->{'handle_error'}->($err, undef);
249         }
250         $scan_running = 0;
251         $steps->{'scan_next'}->($action, $err);
252     };
253
254     step slot_loaded => sub {
255         (my $err, $res) = @_;
256
257         # we don't responsd to abort_scan or restart_scan here, since we
258         # have an open reservation that we should deal with.
259
260         my $label;
261         if ($res && defined $res->{device} &&
262             $res->{device}->status == $DEVICE_STATUS_SUCCESS) {
263             $label = $res->{device}->volume_label;
264         }
265         $self->_user_msg(slot_result => 1,
266                          slot => $slot_scanned,
267                          label => $label,
268                          err  => $err,
269                          res  => $res);
270         if ($res) {
271             my $f_type;
272             if (defined $res->{device}->volume_header) {
273                 $f_type = $res->{device}->volume_header->{type};
274             }
275
276             # The slot did not contain the volume we wanted, so mark it
277             # as seen and try again.
278             $seen{$slot_scanned} = {
279                         device_status => $res->{device}->status,
280                         f_type => $f_type,
281                         label  => $res->{device}->volume_label
282             };
283
284             # notify the user
285             if ($res->{device}->status == $DEVICE_STATUS_SUCCESS) {
286                 $last_err = undef;
287             } else {
288                 $last_err = Amanda::Changer::Error->new('fatal',
289                                 message => $res->{device}->error_or_status());
290             }
291         } else {
292             $seen{$slot_scanned} = { failed => 1 };
293             if ($err->volinuse) {
294                 # Scan semantics for volinuse is different than changer.
295                 # If a slot with unknown label is loaded then we map
296                 # volinuse to driveinuse.
297                 $err->{reason} = "driveinuse";
298             }
299             $last_err = $err if $err->fatal || !$err->notfound;
300         }
301         return $steps->{'load_released'}->();
302     };
303
304     step load_released => sub {
305         my ($err) = @_;
306
307         # TODO: handle error
308
309         # throw out the inventory result and move on if the situation has
310         # changed while we were loading a volume
311         return $steps->{'abort_scan'}->() if $abort_scan;
312         return $steps->{'restart_scan'}->() if $restart_scan;
313
314         $new_slot = $current;
315         $steps->{'get_inventory'}->();
316     };
317
318     step handle_error => sub {
319         my ($err, $continue_cb) = @_;
320
321         my $scan_method = undef;
322         $scan_running = 0;
323         my $message;
324
325         $poll_src->remove() if defined $poll_src;
326         $poll_src = undef;
327
328         # prefer to use scan method for $last_err, if present
329         if ($last_err && $err->failed && $err->notfound) {
330             $message = "$last_err";
331         
332             if ($last_err->isa("Amanda::Changer::Error")) {
333                 if ($last_err->fatal) {
334                     $scan_method = $self->{'scan_conf'}->{'fatal'};
335                 } else {
336                     $scan_method = $self->{'scan_conf'}->{$last_err->{'reason'}};
337                 }
338             } elsif ($continue_cb) {
339                 $scan_method = SCAN_CONTINUE;
340             }
341         }
342
343         #use scan method for $err
344         if (!defined $scan_method) {
345             if ($err) {
346                 $message = "$err" if !defined $message;
347                 if ($err->fatal) {
348                     $scan_method = $self->{'scan_conf'}->{'fatal'};
349                 } else {
350                     $scan_method = $self->{'scan_conf'}->{$err->{'reason'}};
351                 }
352             } else {
353                 die("error not defined");
354                 $scan_method = SCAN_ASK_POLL;
355             }
356         }
357
358         ## implement the desired scan method
359
360         if ($scan_method == SCAN_CONTINUE && !defined $continue_cb) {
361             $scan_method = $self->{'scan_conf'}->{'notfound'};
362             if ($scan_method == SCAN_CONTINUE) {
363                 $scan_method = SCAN_FAIL;
364             }
365         }
366         $steps->{'scan_next'}->($scan_method, $err, $continue_cb);
367     };
368
369     step scan_next => sub {
370         my ($scan_method, $err, $continue_cb) = @_;
371
372         if ($scan_method == SCAN_ASK && !defined $self->{'interactivity'}) {
373             $scan_method = SCAN_FAIL;
374         }
375
376         if ($scan_method == SCAN_ASK_POLL && !defined $self->{'interactivity'}) {
377             $scan_method = SCAN_FAIL;
378         }
379
380         if ($scan_method == SCAN_ASK) {
381             return $steps->{'scan_interactivity'}->("$err");
382         } elsif ($scan_method == SCAN_POLL) {
383             $poll_src = Amanda::MainLoop::call_after(
384                                 $self->{'scan_conf'}->{'poll_delay'},
385                                 $steps->{'after_poll'});
386             return;
387         } elsif ($scan_method == SCAN_ASK_POLL) {
388             $steps->{'scan_interactivity'}->("$err\n");
389             $poll_src = Amanda::MainLoop::call_after(
390                                 $self->{'scan_conf'}->{'poll_delay'},
391                                 $steps->{'after_poll'});
392             return;
393         } elsif ($scan_method == SCAN_FAIL) {
394             return $steps->{'call_result_cb'}->($err, undef);
395         } elsif ($scan_method == SCAN_CONTINUE) {
396             return $continue_cb->($err, undef);
397         } else {
398             die("Invalid SCAN_* value:$err:$err->{'reason'}:$scan_method");
399         }
400     };
401
402     step after_poll => sub {
403         $poll_src->remove() if defined $poll_src;
404         $poll_src = undef;
405         return $steps->{'restart_scan'}->();
406     };
407
408     step scan_interactivity => sub {
409         my ($err_message) = @_;
410         if (!$interactivity_running) {
411             $interactivity_running = 1;
412             my $message = "$err_message\n";
413             if ($self->{'most_prefered_label'}) {
414                 $message .= "Insert volume labeled '$self->{'most_prefered_label'}'";
415             } else {
416                 $message .= "Insert a new volume";
417             }
418             $message .= " in changer and type <enter>\nor type \"^D\" to abort\n";
419             $self->{'interactivity'}->user_request(
420                                 message     => $message,
421                                 label       => $self->{'most_prefered_label'},
422                                 new_volume  => !$self->{'most_prefered_label'},
423                                 err         => "$err_message",
424                                 chg_name    => $self->{'chg'}->{'chg_name'},
425                                 request_cb  => $steps->{'scan_interactivity_cb'});
426         }
427         return;
428     };
429
430     step scan_interactivity_cb => sub {
431         my ($err, $message) = @_;
432         $interactivity_running = 0;
433         $poll_src->remove() if defined $poll_src;
434         $poll_src = undef;
435         $last_err = undef;
436
437         if ($err) {
438             if ($scan_running) {
439                 $abort_scan = $err;
440                 return;
441             } else {
442                 return $steps->{'call_result_cb'}->($err, undef);
443             }
444         }
445
446         if ($message ne '') {
447             # use a new changer
448             my $new_chg;
449             if (ref($message) eq 'HASH' and $message == $DEFAULT_CHANGER) {
450                 $new_chg = Amanda::Changer->new();
451             } else {
452                 $new_chg = Amanda::Changer->new($message);
453             }
454             if ($new_chg->isa("Amanda::Changer::Error")) {
455                 return $steps->{'scan_interactivity'}->("$new_chg");
456             }
457             $self->{'chg'}->quit() if $self->{'chg'} != $self->{'initial_chg'};
458             $self->{'chg'} = $new_chg;
459             %seen = ();
460         } else {
461             $remove_undef_state = 1;
462         }
463
464         if ($scan_running) {
465             $restart_scan = 1;
466             return;
467         } else {
468             return $steps->{'restart_scan'}->();
469         }
470     };
471
472     step abort_scan => sub {
473         if (defined $res) {
474             $res->released(finished_cb => $steps->{'abort_scan_released'});
475         } else {
476             $steps->{'abort_scan_released'}->();
477         }
478     };
479
480     step abort_scan_released => sub {
481         $steps->{'call_result_cb'}->($abort_scan, undef);
482     };
483
484     step call_result_cb => sub {
485         (my $err, $res) = @_;
486
487         # TODO: what happens if the search was aborted or
488         # restarted in the interim?
489
490         $abort_scan = undef;
491         $poll_src->remove() if defined $poll_src;
492         $poll_src = undef;
493         $interactivity_running = 0;
494         $self->{'interactivity'}->abort() if defined $self->{'interactivity'};
495         $self->{'chg'}->quit() if $self->{'chg'} != $self->{'initial_chg'} and !$res;
496         if ($err) {
497             $self->{'scanning'} = 0;
498             return $result_cb->($err, $res);
499         }
500         my $label = $res->{'device'}->volume_label;
501         if (!defined $label) {
502             ($label, my $make_err) = $res->make_new_tape_label();
503             if (!defined $label) {
504                 # make this fatal, rather than silently skipping new tapes
505                 $self->{'scanning'} = 0;
506                 return $result_cb->($make_err, $res);
507             }
508             $self->{'scanning'} = 0;
509             return $result_cb->(undef, $res, $label, $ACCESS_WRITE, 1);
510         }
511         $self->{'scanning'} = 0;
512         return $result_cb->(undef, $res, $label, $ACCESS_WRITE);
513     };
514 }
515
516 package Amanda::ScanInventory::Config;
517
518 sub new {
519     my $class = shift;
520     my ($cc) = @_;
521
522     my $self = bless {}, $class;
523
524     $self->{'poll_delay'} = 10000; #10 seconds
525
526     $self->{'fatal'} = Amanda::ScanInventory::SCAN_CONTINUE;
527     $self->{'driveinuse'} = Amanda::ScanInventory::SCAN_ASK_POLL;
528     $self->{'volinuse'} = Amanda::ScanInventory::SCAN_ASK_POLL;
529     $self->{'notfound'} = Amanda::ScanInventory::SCAN_ASK_POLL;
530     $self->{'unknown'} = Amanda::ScanInventory::SCAN_FAIL;
531     $self->{'invalid'} = Amanda::ScanInventory::SCAN_CONTINUE;
532
533     $self->{'scan'} = 1;
534     $self->{'ask'} = 1;
535     $self->{'new_labeled'} = 'order';
536     $self->{'new_volume'} = 'order';
537
538     return $self;
539 }
540
541 1;