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