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