Imported Upstream version 2.6.0p2
[debian/amanda] / perl / Amanda / Changer.pm
1 # Copyright (c) 2006 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, 505 N Mathlida Ave, Suite 120
17 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
18
19 package Amanda::Changer;
20
21 use Carp;
22 use POSIX ();
23 use Exporter;
24 @ISA = qw( Exporter );
25
26 @EXPORT_OK = qw(
27     reset clean eject label
28     query loadslot find scan
29 );
30
31 use Amanda::Paths;
32 use Amanda::Util;
33 use Amanda::Device qw( :constants );
34 use Amanda::Config qw( :getconf );
35
36 =head1 NAME
37
38 Amanda::Changer -- interface to changer scripts
39
40 =head1 SYNOPSIS
41
42   use Amanda::Changer;
43
44   my ($error, $slot) = Amanda::Changer::reset();
45
46   my ($nslots, $curslot, $backwards, $searchable) = Amanda::Changer::query();
47
48   my ($tpslot, $tpdevice) = Amanda::Changer::find("TAPE018");
49
50   sub slot_callback {
51     my ($slot, $device, $error) = @_;
52     if (!$error) print "Slot $slot: $device\n";
53     return 0;
54   }
55   Amanda::Changer::scan(\&slot_callback);
56
57 =head1 API STATUS
58
59 Stable
60
61 =head1 FUNCTIONS
62
63 All of these functions return an array of values, beginning with
64 C<$error>, and containing any other results appropriate to the
65 operation.
66
67 The functions C<croak()> in the event of a serious error (problems
68 running the changer script, or an exit status of 2 or higher).
69 "Benign" errors, corresponding to an exit status of 1 or a slot named
70 "<error>", result in the return of a single-element array containing
71 the error message.  Error-handling for calls can be written
72
73 C<$error> and C<$slot>.  The first is false unless a "benign"
74 error, such as a positioning error, has occurred, in which case it
75 contains the message from the changer script, and the other results
76 are undefined.  C<$slot> is the first word returned from the changer
77 script, and is usually a number, but occasionally a string such as
78 "<none>".
79
80 =over
81
82 =item reset
83
84   my ($error, $slot) = reset();
85
86 Resets the tape changer, if supported, by calling
87
88   $tpchanger -reset
89
90 =item clean
91
92   my ($error, $slot) = clean();
93
94 Triggers a cleaning cycle, if supported, by calling
95
96   $tpchanger -clean
97
98 =item eject
99
100   my ($error, $slot) = eject();
101
102 Ejects the tape in the current slot, if supported, by calling
103
104   $tpchanger -eject
105
106 =item label
107
108   my ($error) = label($label);
109
110 Inform the changer that the tape in the current slot is labeled C<$label>.  Calls
111
112   $tpchanger -label $label
113
114 =item query
115
116   my ($error, $slot, $nslots, $backwards, $searchable) = query();
117
118 Query the changer to determine the current slot (C<$slot>), the
119 number of slots (C<$nslots>), whether it can move backward through tapes
120 (C<$backwards>), and whether it is searchable (that is, has a barcode
121 reader; C<$searchable>).  A changer which cannot move backward through
122 tapes is also known as a gravity feeder.
123
124 This function runs
125
126   $tpchanger -info
127
128 =item loadslot
129
130   my ($error, $slot, $device) = loadslot($desired_slot);
131
132 Load the tape in the given slot, returning its slot and device.
133 C<$desired_slot> can be a numeric slot number or one of the symbolic
134 names defined by the changer API, e.g., "next", "current", or "first".
135
136   $tpchanger -slot $slot
137
138 =item find
139
140   my ($error, $tpslot, $tpdevice) = Amanda::Changer::find($label);
141
142 Search the changer for a tape with the given label, returning with
143 C<$tpslot = "<none>"> if the given label is not found.
144
145 If the changer is searchable, this function calls
146
147   $tpchanger -search $label
148
149 Otherwise it scans all slots in order, beginning with the current slot,
150 until it finds one with a label equal to C<$label> or exhausts all
151 slots.  Note that it is considered a fatal error if the label is not
152 found.
153
154 =item scan
155
156   my ($error) = Amanda::Changer::scan(\&slot_callback);
157
158 Call C<slot_callback> for all slots, beginning with the current slot,
159 until C<slot_callback> returns a nonzero value or all slots are
160 exhausted.  C<slot_callback> gets three arguments: a slot number, a
161 device name for that slot, and a boolean value which is true if the
162 changer successfully loaded the slot.
163
164 =back
165
166 =cut
167
168 sub reset {
169     my ($error, $slot, $rest) = run_tpchanger("-reset");
170     return ($error) if $error;
171
172     return (0, $slot);
173 }
174
175 sub clean {
176     my ($error, $slot, $rest) = run_tpchanger("-clean");
177     return ($error) if $error;
178
179     return (0, $slot);
180 }
181
182 sub eject {
183     my ($error, $slot, $rest) = run_tpchanger("-eject");
184     return ($error) if $error;
185
186     return (0, $slot);
187 }
188
189 sub label {
190     my ($label) = @_;
191
192     my ($error, $slot, $rest) = run_tpchanger("-label", $label);
193     return ($error) if $error;
194
195     return (0);
196 }
197
198 sub query {
199     my ($error, $slot, $rest) = run_tpchanger("-info");
200     return ($error) if $error;
201
202     # old, unsearchable changers don't return the third result, so it's optional in the regex
203     $rest =~ /(\d+) (\d+) ?(\d+)?/ or croak("Malformed response from changer -seek: $rest");
204
205     # return array: error, nslots, curslot, backwards, searchable
206     return (0, $slot, $1, $2, $3?1:0);
207 }
208
209 sub loadslot {
210     my ($desired_slot) = @_;
211
212     my ($error, $slot, $rest) = run_tpchanger("-slot", $desired_slot);
213     return ($error) if $error;
214
215     return (0, $slot, $rest);
216 }
217
218 sub find {
219     my ($label) = @_;
220
221     my ($error, $curslot, $nslots, $backwards, $searchable) = query();
222     return ($error) if $error;
223
224     if ($searchable) {
225         # search using the barcode reader, etc.
226         my ($error, $slot, $rest) = run_tpchanger("-search", $label);
227         return ($error) if $error;
228         return ($error, $slot, $rest);
229     } else {
230         # search manually, starting with "current"
231         my $slotstr = "current";
232         for (my $checked = 0; $checked < $nslots; $checked++) {
233             my ($error, $slot, $rest) = run_tpchanger("-slot", $slotstr);
234             $slotstr = "next";
235
236             # ignore "benign" errors
237             next if $error;
238
239             my $device = Amanda::Device->new($rest);
240             next if (!$device);
241             next if ($device->read_label() != $READ_LABEL_STATUS_SUCESS);
242
243             # we found it!
244             if ($device->{'volume_label'} eq $label) {
245                 return (0, $slot, $rest);
246             }
247         }
248
249         croak("Label $label not found in any slot");
250     }
251 }
252
253 sub scan {
254     my ($slot_callback) = @_;
255
256     my ($error, $curslot, $nslots, $backwards, $searchable) = query();
257     return ($error) if $error;
258
259     my $slotstr = "current";
260     my $done = 0;
261     for (my $checked = 0; $checked < $nslots; $checked++) {
262         my ($error, $slot, $rest) = run_tpchanger("-slot", $slotstr);
263         $slotstr = "next";
264
265         if ($error) {
266             $done = $slot_callback->(undef, undef, $error);
267         } else {
268             $done = $slot_callback->($slot, $rest, 0);
269         }
270
271         last if $done;
272     }
273     
274     return (0);
275 }
276
277 # Internal-use function to actually invoke a changer script and parse 
278 # its output.  If the script's exit status is neither 0 nor 1, or if an error
279 # occurs running the script, then run_tpchanger croaks with the error message.
280 #
281 # @params @args: command-line arguments to follow the name of the changer
282 # @returns: array ($error, $slot, $rest), where $error is an error message if
283 #       a benign error occurred, or 0 if no error occurred
284 sub run_tpchanger {
285     my @args = @_;
286
287     # get the tape changer and extend it to a full path
288     my $tapechanger = getconf($CNF_TPCHANGER);
289     if ($tapechanger !~ qr(^/)) {
290         $tapechanger = "$amlibexecdir/$tapechanger";
291     }
292
293     my $pid = open(my $child, "-|");
294     if (!defined($pid)) {
295         croak("Can't fork to run changer script: $!");
296     }
297
298     if (!$pid) {
299         # child
300
301         # cd into the config dir, if one exists
302         # TODO: construct a "fake" config dir including any "-o" overrides
303         my $config_dir = Amanda::Config::get_config_dir();
304         if ($config_dir) {
305             if (!chdir($config_dir)) {
306                 print "<error> Could not chdir to '$config_dir'\n";
307                 exit(2);
308             }
309         }
310
311         %ENV = Amanda::Util::safe_env();
312
313         exec { $tapechanger } $tapechanger, @args or
314             print "<error> Could not exec $tapechanger: $!\n";
315         exit 2;
316     }
317
318     # parent
319     my @child_output = <$child>;
320
321     # close the child and get its exit status
322     my $child_exit = 0;
323     if (!close($child)) {
324         if ($!) {
325             croak("Error running changer script: $!");
326         } else {
327             $child_exit = $?;
328         }
329     }
330
331     # parse the response
332     croak("Malformed output from changer script -- no output")
333         if (@child_output < 1);
334     croak("Malformed output from changer script -- too many lines")
335         if (@child_output > 1);
336     croak("Malformed output from changer script: '$child_output[0]'")
337         if ($child_output[0] !~ /\s*([^\s]+)\s+(.+)?/);
338     my ($slot, $rest) = ($1, $2);
339
340     if ($child_exit == 0) {
341         return (0, $slot, $rest);
342     } elsif (POSIX::WIFEXITED($child_exit) && POSIX::WEXITSTATUS($child_exit) == 1) {
343         return ($rest); # non-fatal error
344     } else {
345         croak("Fatal error from changer script: $rest");
346     }
347 }
348
349 1;