Imported Upstream version 3.2.0
[debian/amanda] / perl / Amanda / Taper / Scan.pm
1 # Copyright (c) 2010 Zmanda, Inc.  All Rights Reserved.
2 #
3 # This library is free software; you can redistribute it and/or modify it
4 # under the terms of the GNU Lesser General Public License version 2.1 as
5 # published by the Free Software Foundation.
6 #
7 # This library 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 Lesser General Public
10 # License for more details.
11 #
12 # You should have received a copy of the GNU Lesser General Public License
13 # along with this library; if not, write to the Free Software Foundation,
14 # Inc., 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 94086, USA, or: http://www.zmanda.com
18
19 package Amanda::Taper::Scan;
20
21 =head1 NAME
22
23 Amanda::Taper::Scan
24
25 =head1 SYNOPSIS
26
27 This is an abstract base class for taperscan algorithms.
28
29   # open the taperscan algorithm specified in the config
30   my $taperscan = Amanda::Taperscan->new(
31         changer => $changer);
32
33   my $result_cb = make_cb(result_cb => sub {
34     my ($err, $reservation, $label, $access_mode, $is_new) = @_;
35     die $err if $err;
36     # write to $reservation->{'device'}, using label $label, and opening
37     # the device with $access_mode (one of $ACCESS_WRITE or $ACCESS_APPEND)
38     # $is_new is set to 1 if the volume is not already labeled.
39     # ..
40   });
41   my $user_msg_fn = sub {
42     print "$_[0]\n";
43   };
44   $taperscan->scan(result_cb => $result_cb, user_msg_fn => $user_msg_fn);
45
46 =head1 OVERVIEW
47
48 C<Amanda::Taper::Scan> subclasses represent algorithms used by
49 C<Amanda::Taper::Scribe> (see L<Amanda::Taper::Scribe>) to scan for and select
50 volumes for writing.
51
52 Call C<< Amanda::Taperscan->new() >> to create a new taperscan
53 algorithm.  The constructor takes the following keyword arguments:
54
55     changer       Amanda::Changer object to use (required)
56     algorithm     Taperscan algorithm to instantiate
57     tapelist_filename
58     tapecycle
59     labelstr
60     autolabel
61
62 The changer object must always be provided, but C<algorithm> may be omitted, in
63 which case the class specified by the user in the Amanda configuration file is
64 instantiated.  The remaining options will be taken from the configuration file
65 if not specified.  Default values for all of these options are applied before a
66 subclass's constructor is called.
67
68 The autolabel option should look like the C<CNF_AUTOLABEL> hash - see
69 L<Amanda::Config>.
70
71 Subclasses must implement a single method: C<scan>.  It takes only one mandatory
72 parameter, C<result_cb>:
73
74   $taperscan->scan(
75     result_cb => $my_result_cb,
76     user_msg_fn => $fn,
77     );
78
79 If C<user_msg_fn> is specified, then it is called with user-oriented messages to
80 indicate the progress of the scan.
81
82 The C<result_cb> takes the following positional parameters:
83
84   $error        an error message, or undef on success
85   $reservation  Amanda::Changer::Reservation object
86   $label        label to apply to the volume
87   $access_mode  access mode with which to start the volume
88
89 The error message can be a simple string or an C<Amanda::Changer::Error> object
90 (see L<Amanda::Changer>).  The C<$label> and C<$access_mode> specify parameters
91 for starting the device contained in C<$reservation>.
92
93 =head1 SUBCLASS UTILITIES
94
95 There are a few common tasks for subclasses that are implemented as methods in
96 the parent class.  Note that this class assumes subclasses will be implemented
97 as blessed hashrefs, and sets keys corresponding to the constructor arguments.
98
99 To read the tapelist, call C<read_tapelist>.  This method caches the result in
100 C<< $self->{'tapelist'} >>, which will be used by the other functions here.  In
101 general, call C<read_tapelist> at most once per C<scan()> invocation.
102
103 To see if a volume is reusable, call the C<is_reusable_volume> method.  This takes
104 several keyword parameters:
105
106     $self->is_reusable_volume(
107         label => $label,         # label to check
108         new_label_ok => $nlo,    # count newly labeled vols as reusable?
109     );
110
111 Similarly, to calculate the oldest reusable volume, call
112 C<oldest_reusable_volume>:
113
114     $self->oldest_reusable_volume(
115         new_label_ok => $nlo,    # count newly labeled vols as reusable?
116     );
117
118 Finally, to devise a new name for a volume, call C<make_new_tape_label>,
119 passing a tapelist, a labelstr, and a template.  This will return C<undef>
120 if no label could be created.
121
122     $label = $self->make_new_tape_label(
123         labelstr => "foo-[0-9]+",
124         template => "foo-%%%%",
125     );
126
127 If no C<template> is provided, the function uses the value of
128 C<autolabel> specified when the object was constructed; similarly,
129 C<labelstr> defaults to the value specified at object construction.
130
131 =head2 user_msg_fn
132
133 This interface is temporary and will change in the next release.
134
135 Initiate a load by label:
136
137   user_msg_fn(search_label => 1,
138                    label        => $label);
139
140 The result of a load by label:
141
142   user_msg_fn(search_result => 1,
143                    res           => $res,
144                    err           => $err);
145
146 Initiate the scan of the slot $slot:
147
148   $self->user_msg_fn(scan_slot => 1,
149                      slot      => $slot);
150
151 Initiate the scan of the slot $slot which should have the label $label:
152
153   $self->user_msg_fn(scan_slot => 1,
154                      slot      => $slot,
155                      label     => $label);
156
157 The result of scanning slot $slot:
158
159   $self->user_msg_fn(slot_result => 1,
160                      slot        => $slot,
161                      err         => $err,
162                      res         => $res);
163
164 The result if the read label doesn't match the labelstr:
165
166   user_msg_fn(slot_result             => 1,
167                    does_not_match_labelstr => 1,
168                    labelstr                => $labelstr,
169                    slot                    => $slot,
170                    res                     => $res);
171
172 The result if the read label is not in the tapelist:
173
174   user_msg_fn(slot_result     => 1,
175                    not_in_tapelist => 1,
176                    slot            => $slot,
177                    res             => $res);
178
179 The result if the read label can't be used because it is active:
180
181   user_msg_fn(slot_result => 1,
182                    active      => 1,
183                    slot        => $slot,
184                    res         => $res);
185
186 The scan has failed, possibly with some additional information as to what the
187 scan was looking for.
188
189   user_msg_fn(scan_failed => 1,
190               expected_label => $label, # optional
191               expected_new => 1); # optional
192
193 =cut
194
195 use strict;
196 use warnings;
197 use Amanda::Config qw( :getconf );
198 use Amanda::Tapelist;
199
200 sub new {
201     my $class = shift;
202     my %params = @_;
203
204     die "No changer given to Amanda::Taper::Scan->new"
205         unless exists $params{'changer'};
206
207     # fill in the optional parameters
208     $params{'algorithm'} = "traditional" # TODO: get from a configuration variable
209         unless exists $params{'algorithm'};
210     $params{'tapecycle'} = getconf($CNF_TAPECYCLE)
211         unless exists $params{'tapecycle'};
212     $params{'tapelist_filename'} =
213         Amanda::Config::config_dir_relative(getconf($CNF_TAPELIST))
214             unless exists $params{'tapelist_filename'};
215     $params{'labelstr'} = getconf($CNF_LABELSTR)
216         unless exists $params{'labelstr'};
217     $params{'autolabel'} = getconf($CNF_AUTOLABEL)
218         unless exists $params{'autolabel'};
219
220     # load the package
221     my $pkgname = "Amanda::Taper::Scan::" . $params{'algorithm'};
222     my $filename = $pkgname;
223     $filename =~ s|::|/|g;
224     $filename .= '.pm';
225     if (!exists $INC{$filename}) {
226         eval "use $pkgname;";
227         if ($@) {
228             # handle compile errors
229             die($@) if (exists $INC{$filename});
230             die("No such taperscan algorithm '$params{algorithm}'");
231         }
232     }
233
234     # instantiate it
235     my $self = $pkgname->new(%params);
236
237     # and set the keys from the parameters
238     $self->{'changer'} = $params{'changer'};
239     $self->{'algorithm'} = $params{'algorithm'};
240     $self->{'tapecycle'} = $params{'tapecycle'};
241     $self->{'tapelist_filename'} = $params{'tapelist_filename'};
242     $self->{'labelstr'} = $params{'labelstr'};
243     $self->{'autolabel'} = $params{'autolabel'};
244     $self->{'tapelist'} = Amanda::Tapelist->new($self->{'tapelist_filename'});
245
246     return $self;
247 }
248
249 sub scan {
250     my $self = shift;
251     my %params = @_;
252
253     $params{'result_cb'}->("not implemented");
254 }
255
256 sub read_tapelist {
257     my $self = shift;
258
259     $self->{'tapelist'}->reload();
260 }
261
262 sub oldest_reusable_volume {
263     my $self = shift;
264     my %params = @_;
265
266     my $best = undef;
267     my $num_acceptable = 0;
268     for my $tle (@{$self->{'tapelist'}->{'tles'}}) {
269         next unless $tle->{'reuse'};
270         next if $tle->{'datestamp'} eq '0' and !$params{'new_label_ok'};
271         $num_acceptable++;
272         $best = $tle;
273     }
274
275     # if we didn't find at least $tapecycle reusable tapes, then
276     # there is no oldest reusable tape
277     return undef unless $num_acceptable >= $self->{'tapecycle'};
278
279     return $best->{'label'};
280 }
281
282 sub is_reusable_volume {
283     my $self = shift;
284     my %params = @_;
285
286     my $vol_tle = $self->{'tapelist'}->lookup_tapelabel($params{'label'});
287     return 0 unless $vol_tle;
288     return 0 unless $vol_tle->{'reuse'};
289     if ($vol_tle->{'datestamp'} eq '0') {
290         return $params{'new_label_ok'};
291     }
292
293     # see if it's in the collection of reusable volumes
294     my @tapelist = @{$self->{'tapelist'}->{'tles'}};
295     my @reusable = @tapelist[$self->{'tapecycle'}-1 .. $#tapelist];
296     for my $tle (@reusable) {
297         return 1 if $tle eq $vol_tle;
298     }
299
300
301     return 0;
302 }
303
304 sub make_new_tape_label {
305     my $self = shift;
306     my %params = @_;
307
308     my $tl = exists $params{'tapelist'}? $params{'tapelist'} : $self->{'tapelist'};
309     my $template = exists $params{'template'}? $params{'template'} : $self->{'autolabel'}->{'template'};
310     my $labelstr = exists $params{'labelstr'}? $params{'labelstr'} : $self->{'labelstr'};
311
312     (my $npercents =
313         $template) =~ s/[^%]*(%+)[^%]*/length($1)/e;
314     my $nlabels = 10 ** $npercents;
315
316     # make up a sprintf pattern
317     (my $sprintf_pat =
318         $template) =~ s/(%+)/"%0" . length($1) . "d"/e;
319
320     my %existing_labels =
321         map { $_->{'label'} => 1 } @{$tl->{'tles'}};
322
323     my ($i, $label);
324     for ($i = 1; $i < $nlabels; $i++) {
325         $label = sprintf($sprintf_pat, $i);
326         last unless (exists $existing_labels{$label});
327     }
328
329     # bail out if we didn't find an unused label
330     return (undef, "Can't label unlabeled volume: All label used") if ($i >= $nlabels);
331
332     # verify $label matches $labelstr
333     if ($label !~ /$labelstr/) {
334         return (undef, "Newly-generated label '$label' does not match labelstr '$labelstr'");
335     }
336
337     return $label;
338 }
339
340 1;