Imported Upstream version 3.2.0
[debian/amanda] / perl / Amanda / Changer.pm
1 # Copyright (c) 2007,2008,2009,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::Changer;
20
21 use strict;
22 use warnings;
23 use Carp qw( confess cluck );
24 use POSIX ();
25 use Fcntl qw( O_RDWR O_CREAT LOCK_EX LOCK_NB );
26 use Data::Dumper;
27 use vars qw( @ISA );
28
29 use Amanda::Paths;
30 use Amanda::Util;
31 use Amanda::Config qw( :getconf string_to_boolean );
32 use Amanda::Device qw( :constants );
33 use Amanda::Debug qw( debug );
34 use Amanda::MainLoop;
35
36 =head1 NAME
37
38 Amanda::Changer -- interface to changer scripts
39
40 =head1 SYNOPSIS
41
42     use Amanda::Changer;
43
44     my $chg = Amanda::Changer->new(); # loads the default changer; OR
45     $chg = Amanda::Changer->new("somechanger"); # references a defined changer in amanda.conf
46
47     $chg->load(
48         label => "TAPE-012",
49         mode => "write",
50         res_cb => sub {
51             my ($err, $reservation) = @_;
52             if ($err) {
53                 die $err->{message};
54             }
55             $dev = $reservation->{'device'};
56             # use device..
57         });
58
59     # later..
60     $reservation->release(finished_cb => $start_next_volume);
61
62 =head1 INTERFACE
63
64 All operations in the module return immediately, and take as an argument a
65 callback function which will indicate completion of the changer operation -- a
66 kind of continuation.  The caller should run a main loop (see
67 L<Amanda::MainLoop>) to allow the interactions with the changer script to
68 continue.
69
70 A new object is created with the C<new> function as follows:
71
72   my $chg = Amanda::Changer->new($changer_name);
73
74 to create a named changer (a name provided by the user, either specifying a
75 changer directly or specifying a changer definition), or
76
77   my $chg = Amanda::Changer->new();
78
79 to run the default changer.  This function handles the many ways a user can
80 configure a changer.
81
82 If there is a problem creating the new object, then the resulting object will
83 be a fatal C<Error> object (described below).  Thus the usual recipe for
84 creating a new changer is
85
86   my $chg = Amanda::Changer->new($changer_name);
87   if ($chg->isa("Amanda::Changer::Error")) {
88     die("Error creating changer $changer_name: $chg");
89   }
90
91 =head2 MEMBER VARIABLES
92
93 Note that these variables are not set until after the subclass constructor is
94 finished.
95
96 =over 4
97
98 =item C<< $chg->{'chg_name'} >>
99
100 Gives the name of the changer.  This name will make sense to the user, but will
101 not necessarily form a valid changer specification.  It should be used to
102 describe the changer in messages to the user.
103
104 =back
105
106 =head2 CALLBACKS
107
108 All changer callbacks take an error object as the first parameter.  If no error
109 occurred, then this parameter is C<undef> and the remaining parameters are
110 defined.
111
112 A res_cb C<$cb> is called back as:
113
114  $cb->($error, undef);
115
116 in the event of an error, or
117
118  $cb->(undef, $reservation);
119
120 with a successful reservation. res_cb must always be specified.  A finished_cb
121 C<$cb> is called back as
122
123  $cb->($error);
124
125 in the event of an error, or
126
127  $cb->(undef);
128
129 on success. A finished_cb may be omitted if no notification of completion is
130 required.
131
132 Other callback types are defined below.
133
134 =head2 ERRORS
135
136 When a callback is made with an error, it is an object of type
137 C<Amanda::Changer::Error>.  When interpolated into a string, this object turns
138 into a simple error message.  However, it has some additional methods that can
139 be used to determine how to respond to the error.  First, the error message is
140 available explicitly as C<< $err->message >>.  The error type is available as
141 C<< $err->{'type'} >>, although checks for particular error types should use
142 the C<TYPE> methods instead, as perl is better able to detect typos with this
143 syntax:
144
145   if ($err->failed) { ... }
146
147 The error types are:
148
149   fatal      Changer is no longer useable
150   failed     Operation failed, but the changer is OK
151
152 The API may add other error types in the future (for example, to indicate
153 that a required resource is already reserved).
154
155 Errors of the type C<fatal> indicate that the changer should not be used any
156 longer, and in most cases the caller should terminate abnormally.  For example,
157 configuration or hardware errors are generally fatal.
158
159 If an operation fails, but the changer remains viable, then the error type is
160 C<failed>.  The reason for the failure is usually clear to the user from the
161 message, but for callers who may need to distinguish, C<< $err->{'reason'} >>
162 has one of the following values:
163
164   notfound          The requested volume was not found
165   invalid           The caller's request was invalid (e.g., bad slot)
166   notimpl           The requested operation is not supported
167   volinuse          The requested volume or slot is already in use
168   driveinuse        All drives are in use
169   unknown           Unknown reason
170
171 Like types, checks for particular reasons should use the methods, to avoid
172 undetected typos:
173
174   if ($err->failed and $err->notimpl) { ... }
175
176 Other reasons may be added in the future, so a caller should check for the
177 reasons it expects, and treat any other failures as of unknown cause.
178
179 When the desired slot cannot be loaded because it is already in use, the
180 C<volinuse> error comes with an extra parameter, C<slot>, giving the slot in
181 question.  This parameter is not defined for other cases.
182
183 =head2 CURRENT SLOT
184
185 Changers maintain a global concept of a "current" slot, for compatibility with
186 Amanda algorithms such as the taperscan.  However, it is not compatible with
187 concurrent use of the same changer, and may be inefficient for some changers,
188 so new algorithms should avoid using it, preferring instead to load the correct
189 tape immediately (with C<load>), and to progress from tape to tape using the
190 C<relative_slot> parameter to C<load>.
191
192 =head2 CHANGER OBJECTS
193
194 =head3 load
195
196 The most common operation with a tape changer is to load a volume.  The C<load>
197 method is heavily overloaded to support a number of different ways to specify a
198 volume.
199
200 In general, the method takes a C<res_cb> giving a callback that will receive
201 the reservation.  If set_current is specified and true, then the changer's
202 current slot should be updated to correspond to C<$slot>. If not, then the changer
203 should not update its current slot (but some changers will anyway -
204 specifically, chg-compat).
205
206 The load method always read the label if it succeed to load a volume.
207
208 The optional C<mode> describes the intended use of the volume by the caller,
209 and should be one of C<"read"> (the default) or C<"write">.  Changers managing
210 WORM media may use this parameter to provide a fresh volume for writing, but to
211 search for already-written volumes when reading.
212
213 The load method has a number of permutations:
214
215   $chg->load(res_cb => $cb,
216              label => $label,
217              mode => $mode,
218              set_current => $sc)
219
220 Load and reserve a volume with the given label. This may leverage any barcodes
221 or other indices that the changer has available.
222
223 Note that the changer I<tries> to load the requested volume, but it's a mean
224 world out there, and you may not get what you want, so check the label on the
225 loaded volume before getting started.
226
227   $chg->load(res_cb => $cb,
228              slot => $slot,
229              mode => $mode,
230              set_current => $sc)
231
232 Load and reserve the volume in the given slot. C<$slot> is a string specifying the slot
233 to load, provided by the user or from some other invocation of this changer.
234 Note that slots are not necessarily numeric, so performing arithmetic on this
235 value is an error.
236
237 If the slot does not exist, C<res_cb> will be called with a C<notfound> error.
238 Empty slots are considered empty.
239
240   $chg->load(res_cb => $cb,
241              relative_slot => "current",
242              mode => $mode)
243
244 Reserve the volume in the "current" slot. This is used by the traditional
245 taperscan algorithm to begin its search.
246
247   $chg->load(res_cb => $cb,
248              relative_slot => "next",
249              slot => $slot,
250              except_slots => { %except_slots },
251              mode => $mode,
252              set_current => $sc)
253
254 Reserve the volume that follows the given slot or, if C<slot> is omitted, the
255 volume that follows the current slot.  This will skip empty slots as if they
256 were not present in the changer.
257
258 The optional C<except_slots> argument specifies a hash of slots that should
259 I<not> be loaded.  Keys are slot names, and the hash values are ignored.  This
260 is useful as a termination condition when scanning all of the slots in a
261 changer: keep a hash of all slots already loaded, and pass that hash in
262 C<except_slots>.  When the load operation returns a C<notfound> error, the scan
263 is complete.
264
265 =head3 info
266
267   $chg->info(info_cb => $cb,
268              info => [ $key1, $key2, .. ])
269
270 Query the changer for miscellaneous information.  Any number of keys may be
271 specified.  The C<info_cb> is called with C<$error> as the first argument,
272 much like a C<res_cb>, but the remaining arguments form a hash giving values
273 for all of the requested keys that are supported by the changer.  The preamble
274 to such a callback is usually
275
276   info_cb => sub {
277     my ($error, %results) = @_;
278     # ..
279   }
280
281 Supported keys are:
282
283 =over 2
284
285 =item num_slots
286
287 The total number of slots in the changer device.  If this key is not present or
288 -1, then the device cannot determine its slot count (for example, an archival
289 device that names slots by timestamp could potentially run until the heat-death
290 of the universe).
291
292 =item vendor_string
293
294 A string describing the name and model of the changer device.
295
296 =item fast_search
297
298 If true, then this changer implements searching (loading by label) with
299 something more efficient than a sequential scan through the volumes.  This
300 information affects some taperscan algorithms and recovery programs, which may
301 choose to do their own manual scan instead of invoking many potentially slow
302 searches.
303
304 =back
305
306 =head3 reset
307
308   $chg->reset(finished_cb => $cb)
309
310 Reset the changer to a "base" state. This will generally reset the "current"
311 slot to something the user would think of as the "first" tape, unload any
312 loaded drives, etc. It is an error to call this while any reservations are
313 outstanding.
314
315 =head3 clean
316
317   $chg->clean(finished_cb => $cb,
318               drive => $drivename)
319
320 Clean a drive, if the changer supports it. Drivename can be omitted for devices
321 with only one drive, or can be an arbitrary string from the user (e.g., an
322 amtape argument). Note that some changers cannot detect the completion of a
323 cleaning cycle; in this case, the user will just need to delay further Amanda
324 activities until the cleaning is complete.
325
326 =head3 eject
327
328   $chg->eject(finished_cb => $cb,
329               drive => $drivename)
330
331 Eject the volume in a drive, if the changer supports it.  Drivename is as
332 specified to C<clean>.  If possible, applications should prefer to eject a
333 reserved volume when finished with it (C<< $res->release(eject => 1) >>), to
334 ensure that the correct volume is ejected from a multi-drive changer.
335
336 =head3 update
337
338   $chg->update(finished_cb => $cb,
339                user_msg_fn => $fn,
340                changed => $changed)
341
342 The user has changed something -- loading or unloading tapes, reconfiguring the
343 changer, etc. -- that may have invalidated the database.  C<$changed> is a
344 changer-specific string indicating what has changed; if it is omitted, the
345 changer will check everything.
346
347 Since updates can take a long time, and users often want to know what's going
348 on, the update method will call C<user_msg_fn>, if specified, with
349 user-oriented messages appropriate to the changer.
350
351 =head3 inventory
352
353   $chg->inventory(inventory_cb => $cb)
354
355 The C<inventory_cb> is called with an error object as the first parameter, or
356 C<undef> if no error occurs.  The second parameter is an arrayref containing an
357 ordered list of information about the slots in the changer. The order never
358 change, but some entries can be added or removed.
359
360 Each slot is represented by a hash with the following keys:
361
362 =over 4
363
364 =item slot
365
366 The slot name
367
368 =item current
369
370 Set to C<1> if it is the current slot.
371
372 =item state
373
374 Set to C<SLOT_FULL> if the slot is full, C<SLOT_EMPTY> if the slot is empty (no
375 volume in slot), C<SLOT_UNKNOWN> if the changer doesn't know if the slot is full
376 or not (but it can know), or undef if the changer can't know if the slot is full or not.
377 A changer that doesn't keep state must set it to undef, like chg-single.
378 These constants are available in the C<:constants> export tag.
379
380 A blank or erased volume is not the same as an empty slot.
381
382 =item device_status
383
384 The device status after the open or read_label, undef if device status is unknown.
385
386 =item f_type
387
388 The file header type as returned by read_label, only if device_status is DEVICE_STATUS_SUCCESS.
389
390 =item label
391
392 The label on the volume in this slot, can be set by barcode or by read_label if f_type is Amanda::Header::F_TAPESTART.
393
394 =item barcode (optional)
395
396 The barcode for the volume in this slot, if barcodes are available.
397
398 =item reserved
399
400 Set to C<1> if this slot is reserved, either by this process or another
401 process.  This is only set for I<exclusive> reservations, meaning that loading
402 the slot would result in an C<volinuse> error.  Devices which can support
403 concurrent access will never set this flag.
404
405 =item loaded_in (optional)
406
407 For changers which have distinct user-visible drives, this gives the drive
408 currently accessing the volume in this slot.
409
410 =item import_export (optional)
411
412 Set to C<1> if this is an import-export slot -- a slot in which the user can
413 easily add or remove volumes.  This information may be useful for operations to
414 bulk-import newly-inserted tapes or bulk-export a set of tapes.
415
416 =back
417
418 =head3 move
419
420   $chg->move(finished_cb => $cb,
421              from_slot => $from,
422              to_slot => $to)
423
424 Move a volume between two slots in the changer. These slots are provided by the
425 user, and have meaning for the changer.
426
427 =head2 RESERVATION OBJECTS
428
429 =head3 $res->{'device'}
430
431 This is the fully configured device for the reserved volume.  The device is not
432 started.
433
434 =head3 $res->{'this_slot'}
435
436 This is the name of this slot.  It is an arbitrary string which will
437 have some meaning to the changer's C<load()> method. It is safe to
438 access this field after the reservation has been released.
439
440 =head3 $res->{'barcode'}
441
442 If this changer supports barcodes, then this is the barcode of the reserved
443 volume.  This can be helpful for labeling tapes using their barcode.
444
445 =head3 $res->release(finished_cb => $cb, eject => $eject)
446
447 This is how an Amanda application indicates that it no longer needs the
448 reserved volume. The callback is called after any related operations are
449 complete -- possibly immediately. Some drives and changers have a notion of
450 "ejecting" a volume, and some don't. In particular, a manual changer can cause
451 the tape drive to eject the tape, while a tape robot can move a tape back to
452 storage, leaving the drive empty. If the eject parameter is given and true, it
453 indicates that Amanda is done with the volume and has reason to believe the
454 user is done with the volume, too -- for example, when a tape has been written
455 completely.
456
457 A reservation will be released automatically when the object is destroyed, but
458 in this case no finished_cb is given, so the release operation may not complete
459 before the process exits. Wherever possible, reservations should be explicitly
460 released.
461
462 =head3 $res->set_label(finished_cb => $cb, label => $label)
463
464 This is how Amanda indicates to the changer that the volume in the device has
465 been (re-)labeled. Changers can keep a database of volume labels by slot or by
466 barcode, or just ignore this function and call $cb immediately. Note that the
467 reservation must still be held when this function is called.
468
469 =head1 SUBCLASS HELPERS
470
471 C<Amanda::Changer> implements some methods and attributes to help subclass
472 implementers.
473
474 =head2 INFO
475
476 Implementing the C<info> method can be tricky, because it can potentially request
477 a number of keys that require asynchronous access.  The C<info> implementation in
478 this class may make the process a bit easier.
479
480 First, if the method C<info_setup> is defined, C<info> calls it, passing it a
481 C<finished_cb> and the list of desired keys, C<info>.  This method is useful to
482 gather information that is useful for several info keys.
483
484 Next, for each requested key, C<info> calls
485
486   $self->info_key($key, %params)
487
488 including a regular C<info_cb> callback.  The C<info> method will wait for
489 all C<info_key> invocations to finish, then collect the results or errors that
490 occur.
491
492 =head2 PROPERTY PARSING
493
494 Many properties are boolean, and Amanda has a habit of accepting a number of
495 different ways of writing boolean values.  The method
496 C<< $self->get_boolean_property($config, $prop, $default) >> will parse such a
497 property, returning 0 or 1 if the property is specified, C<$default> if it is
498 not specified, or C<undef> if the property cannot be parsed.
499
500 =head2 ERROR HANDLING
501
502 To create a new error object, use C<< $self->make_error($type, $cb, %args) >>.
503 This method will create a new C<Amanda::Changer::Error> object and optionally
504 invoke a callback with it.  If C<$type> is C<fatal>, then
505 C<< $chg->{'fatal_error'} >> is made a reference to the new error object.  The
506 callback C<$cb> (which should be made using C<make_cb()> from
507 C<Amanda::MainLoop>) is called with the new error object.  The C<%args> are
508 added to the new error object.  In use, this looks something like:
509
510   if (!$success) {
511     return $self->make_error("failed", $params{'res_cb'},
512             reason => "notfound",
513             message => "Volume '$label' not found");
514   }
515
516 This method can also be called as a class method, e.g., from a constructor.
517 In this case, it returns the resulting error object, which should be fatal.
518
519   if (!$config_ok) {
520     return Amanda::Changer->make_error("fatal", undef,
521             message => "config error");
522   }
523
524 For cases where a number of errors have occurred, it is helpful to make a
525 "combined" error.  The method C<make_combined_error> takes care of this
526 operation, given a callback and an array of tuples C<[ $description, $err ]>
527 for each error.  This method uses some heuristics to figure out the
528 appropriate type and reason for the combined error.
529
530   if ($left_err and $right_err) {
531     return $self->make_combined_error($params{'finished_cb'},
532         [ [ "from the left", $left_err ],
533           [ "from the right", $right_err ] ]);
534   }
535
536 Any additional keyword arguments to C<make_combined_error> are put into the
537 combined error; this is useful to set the C<slot> attribute.
538
539 The method C<< $self->check_error($cb) >> is a useful method for subclasses to
540 avoid doing anything after a fatal error.  This method checks
541 C<< $self->{'fatal_error'} >>.  If the error is defined, the method calls C<$cb>
542 and returns true.  The usual recipe is
543
544   sub load {
545     my $self = shift;
546     my %params = @_;
547
548     return if $self->check_error($params{'res_cb'});
549     # ...
550   }
551
552 =head2 CONFIG
553
554 C<Amanda::Changer->new> calls subclass constructors with two parameters: a
555 configuration object and a changer specification.  The changer specification is
556 the string that led to creation of this changer device.  The configuration
557 object is of type C<Amanda::Changer::Config>, and can be treated as a hashref
558 with the following keys:
559
560   name                  -- name of the changer section (or "default")
561   is_global             -- true if this changer is the default changer
562   tapedev               -- tapedev parameter
563   tpchanger             -- tpchanger parameter
564   changerdev            -- changerdev parameter
565   changerfile           -- changerfile parameter
566   properties            -- all properties for this changer
567   device_properties     -- device properties from this changer
568
569 The four parameters are just as supplied by the user, either in the global
570 config or in a changer section.  Changer authors are cautioned not to try to
571 override any of these parameters as previous changers have done (e.g.,
572 C<changerfile> specifying both configuration and state files).  Use properties
573 instead.
574
575 The C<properties> and C<device_properties> parameters are in the format
576 provided by C<Amanda::Config>.  If C<is_global> is true, then
577 C<device_properties> will include any device properties specified globally, as
578 well as properties culled from the global tapetype.
579
580 The C<configure_device> method generally takes care of the intricacies of
581 handling device properties.  Pass it a newly opened device and it will apply
582 the relevant properties, returning undef on success or an error message on
583 failure.
584
585 The C<get_property> method is a shortcut method to get the value of a changer
586 property, ignoring its the priority and other attributes.  In a list context,
587 it returns all values for the property; in a scalar context, it returns the
588 first value specified.
589
590 =head2 PERSISTENT STATE AND LOCKING
591
592 Many changer subclasses need to track state across invocations and between
593 different processes, and to ensure that the state is read and written
594 atomically.  The C<with_locked_state> provides this functionality by
595 locking a statefile, only unlocking it after any changes have been written back
596 to it.  Subclasses can use this method both for mutual exclusion (ensuring that
597 only one changer operation is in progress at any time) and for atomic state
598 storage.
599
600 The C<with_locked_state> method works like C<synchronized> (in
601 L<Amanda::MainLoop>), but with some extra arguments:
602
603   $self->with_locked_state($filename, $some_cb, sub {
604     # note: $some_cb shadows outer $some_cb; see Amanda::MainLoop::synchronized
605     my ($state, $some_cb) = @_;
606     # ... and eventually:
607     $some_cb->(...);
608   });
609
610 The callback C<$some_cb> is assumed to take a changer error as its first
611 argument, and if there are any errors locking the statefile, they will be
612 reported directly to this callback.  Otherwise, a wrapped version of
613 C<$some_cb> is passed to the inner C<sub>.  When this wrapper is invoked, the
614 state will be written to disk and unlocked before the original callback is
615 invoked.
616
617 The state itself begins as an empty hashref, but subclasses can add arbitrary
618 keys to the hash.  Serialization is currently handled with L<Data::Dumper>.
619
620 =head2 PARAMETER VALIDATION
621
622 The C<validate_params> method is useful to make sure that the proper parameters
623 are present for a particular method, dying if not.  Call it like this:
624
625   $self->validate_params("load", \%params);
626
627 The method currently only supports the "load" method, but can be expanded to
628 cover other methods.
629
630 =head1 SEE ALSO
631
632 The Amanda Wiki (http://wiki.zmanda.com) has a higher-level description of the
633 changer model implemented by this package.
634
635 See amanda-changers(7) for user-level documentation of the changer implementations.
636
637 =cut
638
639 # constants for the states that slots may be in; note that these states still
640 # apply even if the tape is actually loaded in a drive
641
642 # slot is known to contain a volume
643 use constant SLOT_FULL => 1;
644
645 # slot is known to contain no volume
646 use constant SLOT_EMPTY => 2;
647
648 # don't known if slot contains a volume
649 use constant SLOT_UNKNOWN => 3;
650
651 our @EXPORT_OK = qw( SLOT_FULL SLOT_EMPTY SLOT_UNKNOWN );
652 our %EXPORT_TAGS = (
653     constants => [ qw( SLOT_FULL SLOT_EMPTY SLOT_UNKNOWN ) ],
654 );
655
656 # this is a "virtual" constructor which instantiates objects of different
657 # classes based on its argument.  Subclasses should not try to chain up!
658 sub new {
659     shift eq 'Amanda::Changer'
660         or die("Do not call the Amanda::Changer constructor from subclasses");
661     my ($name) = @_;
662     my ($uri, $cc);
663
664     # creating a named changer is a bit easier
665     if (defined($name)) {
666         # first, is it a changer alias?
667         if (($uri,$cc) = _changer_alias_to_uri($name)) {
668             return _new_from_uri($uri, $cc, $name);
669         }
670
671         # maybe a straight-up changer URI?
672         if (_uri_to_pkgname($name)) {
673             return _new_from_uri($name, undef, $name);
674         }
675
676         # assume it's a device name or alias, and invoke the single-changer
677         return _new_from_uri("chg-single:$name", undef, $name);
678     } else { # !defined($name)
679         if ((getconf_linenum($CNF_TPCHANGER) == -2 ||
680              (getconf_seen($CNF_TPCHANGER) &&
681               getconf_linenum($CNF_TAPEDEV) != -2)) &&
682             getconf($CNF_TPCHANGER) ne '') {
683             my $tpchanger = getconf($CNF_TPCHANGER);
684
685             # first, is it an old changer script?
686             if ($uri = _old_script_to_uri($tpchanger)) {
687                 return _new_from_uri($uri, undef, $tpchanger);
688             }
689
690             # if not, then there had better be no tapdev
691             if (getconf_seen($CNF_TAPEDEV) and getconf($CNF_TAPEDEV) ne '' and
692                 ((getconf_linenum($CNF_TAPEDEV) > 0 and
693                   getconf_linenum($CNF_TPCHANGER) > 0) ||
694                  (getconf_linenum($CNF_TAPEDEV) == -2))) {
695                 return Amanda::Changer::Error->new('fatal',
696                     message => "Cannot specify both 'tapedev' and 'tpchanger' " .
697                         "unless using an old-style changer script");
698             }
699
700             # maybe a changer alias?
701             if (($uri,$cc) = _changer_alias_to_uri($tpchanger)) {
702                 return _new_from_uri($uri, $cc, $tpchanger);
703             }
704
705             # maybe a straight-up changer URI?
706             if (_uri_to_pkgname($tpchanger)) {
707                 return _new_from_uri($tpchanger, undef, $tpchanger);
708             }
709
710             # assume it's a device name or alias, and invoke the single-changer
711             return _new_from_uri("chg-single:$tpchanger", undef, $tpchanger);
712         } elsif (getconf_seen($CNF_TAPEDEV) and getconf($CNF_TAPEDEV) ne '') {
713             my $tapedev = getconf($CNF_TAPEDEV);
714
715             # first, is it a changer alias?
716             if (($uri,$cc) = _changer_alias_to_uri($tapedev)) {
717                 return _new_from_uri($uri, $cc, $tapedev);
718             }
719
720             # maybe a straight-up changer URI?
721             if (_uri_to_pkgname($tapedev)) {
722                 return _new_from_uri($tapedev, undef, $tapedev);
723             }
724
725             # assume it's a device name or alias, and invoke chg-single.
726             # chg-single will check the device immediately and error out
727             # if the device name is invalid.
728             return _new_from_uri("chg-single:$tapedev", undef, $tapedev);
729         } else {
730             return Amanda::Changer::Error->new('fatal',
731                 message => "You must specify one of 'tapedev' or 'tpchanger'");
732         }
733     }
734 }
735
736 # helper functions for new
737
738 sub _changer_alias_to_uri {
739     my ($name) = @_;
740
741     my $cc = Amanda::Config::lookup_changer_config($name);
742     if ($cc) {
743         my $tpchanger = changer_config_getconf($cc, $CHANGER_CONFIG_TPCHANGER);
744         if ($tpchanger) {
745             if (my $uri = _old_script_to_uri($tpchanger)) {
746                 return ($uri, $cc);
747             }
748         }
749
750         my $seen_tpchanger = changer_config_seen($cc, $CHANGER_CONFIG_TPCHANGER);
751         my $seen_tapedev = changer_config_seen($cc, $CHANGER_CONFIG_TAPEDEV);
752         if ($seen_tpchanger and $seen_tapedev) {
753             return Amanda::Changer::Error->new('fatal',
754                 message => "Cannot specify both 'tapedev' and 'tpchanger' " .
755                     "**unless using an old-style changer script");
756         }
757         if (!$seen_tpchanger and !$seen_tapedev) {
758             return Amanda::Changer::Error->new('fatal',
759                 message => "You must specify one of 'tapedev' or 'tpchanger'");
760         }
761         $tpchanger ||= changer_config_getconf($cc, $CHANGER_CONFIG_TAPEDEV);
762
763         if (_uri_to_pkgname($tpchanger)) {
764             return ($tpchanger, $cc);
765         } else {
766             die "Changer '$name' specifies invalid tpchanger '$tpchanger'";
767         }
768     }
769
770     # not an alias
771     return;
772 }
773
774 sub _old_script_to_uri {
775     my ($name) = @_;
776
777     die("empty changer script name") unless $name;
778
779     if ((-x "$amlibexecdir/$name") or (($name =~ qr{^/}) and (-x $name))) {
780         return "chg-compat:$name"
781     }
782
783     # not an old script
784     return;
785 }
786
787 # try to load the package for the given URI.  $@ is set properly
788 # if this function returns a false value.
789 sub _uri_to_pkgname {
790     my ($name) = @_;
791
792     my ($type) = ($name =~ /^chg-([A-Za-z_]+):/);
793     if (!defined $type) {
794         $@ = "'$name' is not a changer URI";
795         return 0;
796     }
797
798     $type =~ tr/A-Z-/a-z_/;
799
800     # create a package name to see if it's already imported
801     my $pkgname = "Amanda::Changer::$type";
802     my $filename = $pkgname;
803     $filename =~ s|::|/|g;
804     $filename .= '.pm';
805     return $pkgname if (exists $INC{$filename});
806
807     # try loading it
808     eval "use $pkgname;";
809     if ($@) {
810         my $err = $@;
811
812         # determine whether the module doesn't exist at all, or if there was an
813         # error loading it; die if we found a syntax error
814         if (exists $INC{$filename} or $err =~ /did not return a true value/) {
815             die($err);
816         }
817
818         return 0;
819     }
820
821     return $pkgname;
822 }
823
824 # already-instantiated changer objects (using 'our' so that the installcheck
825 # and reset this list as necessary)
826 our %changers_by_uri_cc = ();
827
828 sub _new_from_uri { # (note: this sub is patched by the installcheck)
829     my ($uri, $cc, $name) = @_;
830
831     # as a special case, if the URI came back as an error, just pass
832     # that along.  This lets the _xxx_to_uri methods return errors more
833     # easily
834     if (ref $uri and $uri->isa("Amanda::Changer::Error")) {
835         return $uri;
836     }
837
838     # make up a key for our hash of already-instantiated objects,
839     # using a newline as a separator, since perl can't use tuples
840     # as keys
841     my $uri_cc = "$uri\n";
842     if (defined $cc) {
843         $uri_cc = $uri_cc . changer_config_name($cc);
844     }
845
846     # return a pre-existing changer, if possible
847
848     if (exists($changers_by_uri_cc{$uri_cc})) {
849         return $changers_by_uri_cc{$uri_cc};
850     }
851
852     # look up the type and load the class
853     my $pkgname = _uri_to_pkgname($uri);
854     if (!$pkgname) {
855         die $@;
856     }
857
858     my $rv = $pkgname->new(Amanda::Changer::Config->new($cc), $uri);
859     die "$pkgname->new did not return an Amanda::Changer object or an Amanda::Changer::Error"
860         unless ($rv->isa("Amanda::Changer") or $rv->isa("Amanda::Changer::Error"));
861
862     if ($rv->isa("Amanda::Changer")) {
863         # add an instance variable or two
864         $rv->{'fatal_error'} = undef;
865
866         # store this in our cache for next time
867         $changers_by_uri_cc{$uri_cc} = $rv;
868     }
869
870     $rv->{'chg_name'} = $name;
871     return $rv;
872 }
873
874 # method stubs that return a "notimpl" error
875
876 sub _stubop {
877     my ($op, $cbname, $self, %params) = @_;
878     return if $self->check_error($params{$cbname});
879
880     my $class = ref($self);
881     my $chg_foo = "chg-" . ($class =~ /Amanda::Changer::(.*)/)[0];
882     return $self->make_error("failed", $params{$cbname},
883         reason => "notimpl",
884         message => "'$chg_foo:' does not support $op");
885 }
886
887 sub load { _stubop("loading volumes", "res_cb", @_); }
888 sub reset { _stubop("reset", "finished_cb", @_); }
889 sub clean { _stubop("clean", "finished_cb", @_); }
890 sub eject { _stubop("eject", "finished_cb", @_); }
891 sub update { _stubop("update", "finished_cb", @_); }
892 sub inventory { _stubop("inventory", "inventory_cb", @_); }
893 sub move { _stubop("move", "finished_cb", @_); }
894
895 # info calls out to info_setup and info_key; see POD above
896 sub info {
897     my $self = shift;
898     my %params = @_;
899
900     if (!$self->can('info_key')) {
901         my $class = ref($self);
902         $params{'info_cb'}->("$class does not support info()");
903         return;
904     }
905
906     my ($do_setup, $start_keys, $all_done);
907
908     $do_setup = sub {
909         if ($self->can('info_setup')) {
910             $self->info_setup(info => $params{'info'},
911                               finished_cb => sub {
912                 my ($err) = @_;
913                 if ($err) {
914                     $params{'info_cb'}->($err);
915                 } else {
916                     $start_keys->();
917                 }
918             });
919         } else {
920             $start_keys->();
921         }
922     };
923
924     $start_keys = sub {
925         my $remaining_keys = 1;
926         my %key_results;
927
928         my $maybe_done = sub {
929             return if (--$remaining_keys);
930             $all_done->(%key_results);
931         };
932
933         for my $key (@{$params{'info'}}) {
934             $remaining_keys++;
935             $self->info_key($key, info_cb => sub {
936                 $key_results{$key} = [ @_ ];
937                 $maybe_done->();
938             });
939         }
940
941         # we started with $remaining_keys = 1, so decrement it now
942         $maybe_done->();
943     };
944
945     $all_done = sub {
946         my %key_results = @_;
947
948         # if there are *any* errors, handle them
949         my @annotated_errs =
950             map { [ sprintf("While getting info key '%s'", $_), $key_results{$_}->[0] ] }
951             grep { defined($key_results{$_}->[0]) }
952             keys %key_results;
953
954         if (@annotated_errs) {
955             return $self->make_combined_error(
956                 $params{'info_cb'}, [ @annotated_errs ]);
957         }
958
959         # no errors, so combine the results and return them
960         my %info;
961         while (my ($key, $result) = each(%key_results)) {
962             my ($err, %key_info) = @$result;
963             if (exists $key_info{$key}) {
964                 $info{$key} = $key_info{$key};
965             } else {
966                 warn("No value available for $key");
967             }
968         }
969
970         $params{'info_cb'}->(undef, %info);
971     };
972
973     $do_setup->();
974 }
975
976 # subclass helpers
977
978 sub get_boolean_property {
979     my ($self) = shift;
980     my ($config, $propname, $default) = @_;
981
982     return $default
983         unless (exists $config->{'properties'}->{$propname});
984
985     my $propinfo = $config->{'properties'}->{$propname};
986     return undef unless @{$propinfo->{'values'}} == 1;
987     return string_to_boolean($propinfo->{'values'}->[0]);
988 }
989
990 sub make_error {
991     my $self = shift;
992     my ($type, $cb, %args) = @_;
993
994     my $classmeth = $self eq "Amanda::Changer";
995
996     if ($classmeth and $type ne 'fatal') {
997         cluck("type must be fatal when calling make_error as a class method");
998         $type = 'fatal';
999     }
1000
1001     my $err = Amanda::Changer::Error->new($type, %args);
1002
1003     if (!$classmeth) {
1004         $self->{'fatal_error'} = $err
1005             if ($err->fatal);
1006
1007         $cb->($err);
1008     }
1009
1010     return $err;
1011 }
1012
1013 sub make_combined_error {
1014     my $self = shift;
1015     my ($cb, $suberrors, %extra_args) = @_;
1016     my $err;
1017
1018     if (@$suberrors == 0) {
1019         die("make_combined_error called with no errors");
1020     }
1021
1022     my $classmeth = $self eq "Amanda::Changer";
1023
1024     # if there's only one suberror, just use it directly
1025     if (@$suberrors == 1) {
1026         $err = $suberrors->[0][1];
1027         die("$err is not an Error object")
1028             unless defined($err) and $err->isa("Amanda::Changer::Error");
1029
1030         $err = Amanda::Changer::Error->new(
1031             $err->{'type'},
1032             reason => $err->{'reason'},
1033             message => $suberrors->[0][0] . ": " . $err->{'message'});
1034     } else {
1035         my $fatal = $classmeth or grep { $_->[1]{'fatal'} } @$suberrors;
1036
1037         my $reason;
1038         if (!$fatal) {
1039             my %reasons =
1040                 map { ($_->[1]{'reason'}, undef) }
1041                 grep { $_->[1]{'reason'} }
1042                 @$suberrors;
1043             if ((keys %reasons) == 1) {
1044                 $reason = (keys %reasons)[0];
1045             } else {
1046                 $reason = 'unknown'; # multiple or 0 "source" reasons
1047             }
1048         }
1049
1050         my $message = join("; ",
1051             map { sprintf("%s: %s", @$_) }
1052             @$suberrors);
1053
1054         my %errargs = ( message => $message, %extra_args );
1055         $errargs{'reason'} = $reason unless ($fatal);
1056         $err = Amanda::Changer::Error->new(
1057             $fatal? "fatal" : "failed",
1058             %errargs);
1059     }
1060
1061     if (!$classmeth) {
1062         $self->{'fatal_error'} = $err
1063             if ($err->fatal);
1064
1065         $cb->($err) if $cb;
1066     }
1067
1068     return $err;
1069 }
1070
1071 sub check_error {
1072     my $self = shift;
1073     my ($cb) = @_;
1074
1075     if (defined $self->{'fatal_error'}) {
1076         $cb->($self->{'fatal_error'}) if $cb;
1077         return 1;
1078     }
1079 }
1080
1081 sub lock_statefile {
1082     my $self = shift;
1083     my %params = @_;
1084
1085     my $statefile = $params{'statefile_filename'};
1086     my $lock_cb = $params{'lock_cb'};
1087     Amanda::Changer::StateFile->new($statefile, $lock_cb);
1088 }
1089
1090 sub with_locked_state {
1091     my $self = shift;
1092     my ($statefile, $cb, $sub) = @_;
1093     my ($filelock, $STATE);
1094     my $poll = 0; # first delay will be 0.1s; see below
1095
1096     my $steps = define_steps
1097         cb_ref => \$cb;
1098
1099     step open => sub {
1100         $filelock = Amanda::Util::file_lock->new($statefile);
1101
1102         $steps->{'lock'}->();
1103     };
1104
1105     step lock => sub {
1106         my $rv = $filelock->lock();
1107         if ($rv == 1) {
1108             # loop until we get the lock, increasing $poll to 10s
1109             $poll += 100 unless $poll >= 10000;
1110             return Amanda::MainLoop::call_after($poll, $steps->{'lock'});
1111         } elsif ($rv == -1) {
1112             return $self->make_error("fatal", $cb,
1113                     message => "Error locking '$statefile'");
1114         }
1115
1116         $steps->{'read'}->();
1117     };
1118
1119     step read => sub {
1120         my $contents = $filelock->data();
1121         if ($contents) {
1122             eval $contents;
1123             if ($@) {
1124                 # $fh goes out of scope here, and is thus automatically
1125                 # unlocked
1126                 return $cb->("error reading '$statefile': $@", undef);
1127             }
1128             if (!defined $STATE or ref($STATE) ne 'HASH') {
1129                 return $cb->("'$statefile' did not define \$STATE properly", undef);
1130             }
1131         } else {
1132             # initial state (blank file)
1133             $STATE = {};
1134         }
1135
1136         $sub->($STATE, $steps->{'cb_wrap'});
1137     };
1138
1139     step cb_wrap =>  sub {
1140         my @args = @_;
1141
1142         my $dumper = Data::Dumper->new([ $STATE ], ["STATE"]);
1143         $dumper->Purity(1);
1144         $filelock->write($dumper->Dump);
1145         $filelock->unlock();
1146
1147         # call through to the original callback with the original
1148         # arguments
1149         $cb->(@args);
1150     };
1151 }
1152
1153 sub validate_params {
1154     my ($self, $op, $params) = @_;
1155
1156     if ($op eq 'load') {
1157         unless(exists $params->{'label'} || exists $params->{'slot'} ||
1158                exists $params->{'relative_slot'}) {
1159                 confess "Invalid parameters to 'load'";
1160         }
1161     } else {
1162         confess "don't know how to validate '$op'";
1163     }
1164 }
1165
1166 package Amanda::Changer::Error;
1167 use Amanda::Debug qw( :logging );
1168 use Carp qw( cluck );
1169 use Amanda::Debug;
1170 use overload
1171     '""' => sub { $_[0]->{'message'}; },
1172     'cmp' => sub { $_[0]->{'message'} cmp $_[1]; };
1173
1174 my %known_err_types = map { ($_, 1) } qw( fatal failed );
1175 my %known_err_reasons = map { ($_, 1) } qw( notfound invalid notimpl driveinuse volinuse unknown device );
1176
1177 sub new {
1178     my $class = shift; # ignore class
1179     my ($type, %info) = @_;
1180
1181     my $reason = "";
1182     $reason = ", reason='$info{reason}'" if $type eq "failed";
1183     debug("new Amanda::Changer::Error: type='$type'$reason, message='$info{message}'");
1184
1185     $info{'type'} = $type;
1186
1187     # do some sanity checks.  Note that these sanity checks issue a warning
1188     # with cluck, but add default values to the error.  This is in the hope
1189     # that an unusual Amanda error is not obscured by a problem in the
1190     # make_error invocation.  The stack trace produced by cluck should help to
1191     # track down the bad make_error invocation.
1192
1193     if (!exists $info{'message'}) {
1194         cluck("no message given to A::C::make_error");
1195         $info{'message'} = "unknown error";
1196     }
1197
1198     if (!exists $known_err_types{$type}) {
1199         cluck("invalid Amanda::Changer::Error type '$type'");
1200         $type = 'fatal';
1201     }
1202
1203     if ($type eq 'failed' and !exists $info{'reason'}) {
1204         cluck("no reason given to A::C::make_error");
1205         $info{'reason'} = "unknown";
1206     }
1207
1208     if ($type eq 'failed' and !exists $known_err_reasons{$info{'reason'}}) {
1209         cluck("invalid Amanda::Changer::Error reason '$info{reason}'");
1210         $info{'reason'} = 'unknown';
1211     }
1212
1213     return bless (\%info, $class);
1214 }
1215
1216 # types
1217 sub fatal { $_[0]->{'type'} eq 'fatal'; }
1218 sub failed { $_[0]->{'type'} eq 'failed'; }
1219
1220 # reasons
1221 sub notfound { $_[0]->failed && $_[0]->{'reason'} eq 'notfound'; }
1222 sub invalid { $_[0]->failed && $_[0]->{'reason'} eq 'invalid'; }
1223 sub notimpl { $_[0]->failed && $_[0]->{'reason'} eq 'notimpl'; }
1224 sub driveinuse { $_[0]->failed && $_[0]->{'reason'} eq 'driveinuse'; }
1225 sub volinuse { $_[0]->failed && $_[0]->{'reason'} eq 'volinuse'; }
1226 sub unknown { $_[0]->failed && $_[0]->{'reason'} eq 'unknown'; }
1227
1228 # slot accessor
1229 sub slot { $_[0]->{'slot'}; }
1230
1231 package Amanda::Changer::Reservation;
1232 # this is a simple base class with stub method or two.
1233
1234 sub new {
1235     my $class = shift;
1236     my $self = {
1237         released => 0,
1238     };
1239     return bless ($self, $class)
1240 }
1241
1242 sub DESTROY {
1243     my ($self) = @_;
1244     if (!$self->{'released'}) {
1245         if (defined $self->{this_slot}) {
1246             Amanda::Debug::warning("Changer reservation for slot '$self->{this_slot}' has " .
1247                                    "gone out of scope without release");
1248         } else {
1249             Amanda::Debug::warning("Changer reservation for unknown slot has " .
1250                                    "gone out of scope without release");
1251         }
1252     }
1253 }
1254
1255 sub set_label {
1256     my $self = shift;
1257     my %params = @_;
1258
1259     # nothing to do by default: just call the finished callback
1260     if (exists $params{'finished_cb'}) {
1261         $params{'finished_cb'}->(undef) if $params{'finished_cb'};
1262     }
1263 }
1264
1265 sub release {
1266     my $self = shift;
1267     my %params = @_;
1268
1269     return if $self->{'released'};
1270
1271     # always finish the device on release; it's illegal for anything
1272     # else to use the device after this point, anyway, so we want to
1273     # release the device's resources immediately
1274     if (defined $self->{'device'}) {
1275         $self->{'device'}->finish();
1276     }
1277
1278     $self->{'released'} = 1;
1279     $self->do_release(%params);
1280 }
1281
1282 sub do_release {
1283     my $self = shift;
1284     my %params = @_;
1285
1286     # this is the one subclasses should override
1287
1288     if (exists $params{'finished_cb'}) {
1289         $params{'finished_cb'}->(undef) if $params{'finished_cb'};
1290     }
1291 }
1292
1293 package Amanda::Changer::Config;
1294 use Amanda::Config qw( :getconf );
1295 use Amanda::Device;
1296
1297 sub new {
1298     my $class = shift;
1299     my ($cc) = @_;
1300
1301     my $self = bless {}, $class;
1302
1303     if (defined $cc) {
1304         $self->{'name'} = changer_config_name($cc);
1305         $self->{'is_global'} = 0;
1306
1307         $self->{'tapedev'} = changer_config_getconf($cc, $CHANGER_CONFIG_TAPEDEV);
1308         $self->{'tpchanger'} = changer_config_getconf($cc, $CHANGER_CONFIG_TPCHANGER);
1309         $self->{'changerdev'} = changer_config_getconf($cc, $CHANGER_CONFIG_CHANGERDEV);
1310         $self->{'changerfile'} = changer_config_getconf($cc, $CHANGER_CONFIG_CHANGERFILE);
1311
1312         $self->{'properties'} = changer_config_getconf($cc, $CHANGER_CONFIG_PROPERTY);
1313         $self->{'device_properties'} = changer_config_getconf($cc, $CHANGER_CONFIG_DEVICE_PROPERTY);
1314     } else {
1315         $self->{'name'} = "default";
1316         $self->{'is_global'} = 1;
1317
1318         $self->{'tapedev'} = getconf($CNF_TAPEDEV);
1319         $self->{'tpchanger'} = getconf($CNF_TPCHANGER);
1320         $self->{'changerdev'} = getconf($CNF_CHANGERDEV);
1321         $self->{'changerfile'} = getconf($CNF_CHANGERFILE);
1322
1323         # no changer or device properties, since there's no changer definition to use
1324         $self->{'properties'} = {};
1325         $self->{'device_properties'} = {};
1326     }
1327     return $self;
1328 }
1329
1330 sub configure_device {
1331     my $self = shift;
1332     my ($device) = @_;
1333
1334     # we'll accumulate properties in this hash *overwriting* previous properties
1335     # instead of appending to them
1336     my %properties;
1337
1338     # always use implicit properties
1339     %properties = ( %properties, %{ $self->_get_implicit_properties() } );
1340
1341     # always use global properties
1342     %properties = ( %properties, %{ getconf($CNF_DEVICE_PROPERTY) } );
1343
1344     # if this is a device alias, add properties from its device definition
1345     if (my $dc = lookup_device_config($device->device_name)) {
1346         %properties = ( %properties,
1347                 %{ device_config_getconf($dc, $DEVICE_CONFIG_DEVICE_PROPERTY); } );
1348     }
1349
1350     # finally, add any props from the changer config
1351     %properties = ( %properties, %{ $self->{'device_properties'} } );
1352
1353     while (my ($propname, $propinfo) = each(%properties)) {
1354         for my $value (@{$propinfo->{'values'}}) {
1355             if (!$device->property_set($propname, $value)) {
1356                 my $msg = "Error setting '$propname' on device '".$device->device_name."'";
1357                 if (exists $propinfo->{'optional'}) {
1358                     if ($propinfo->{'optional'} eq 'warn') {
1359                         warn("$msg (ignored)");
1360                     }
1361                 } else {
1362                     return $msg;
1363                 }
1364             }
1365         }
1366     }
1367
1368     return undef;
1369 }
1370
1371 sub get_property {
1372     my $self = shift;
1373     my ($property) = @_;
1374
1375     my $prophash = $self->{'properties'}->{$property};
1376     return undef unless defined($prophash);
1377
1378     return wantarray? @{$prophash->{'values'}} : $prophash->{'values'}->[0];
1379 }
1380
1381 sub _get_implicit_properties {
1382     my $self = shift;
1383     my $props = {};
1384
1385     my $tapetype_name = getconf($CNF_TAPETYPE);
1386     return unless defined($tapetype_name);
1387
1388     my $tapetype = lookup_tapetype($tapetype_name);
1389     return unless defined($tapetype);
1390
1391     # The property hashes used here add the 'optional' key, which indicates
1392     # that the property is implicit and that a failure to set it is not fatal.
1393     # The flag is used by configure_device.
1394     if (tapetype_seen($tapetype, $TAPETYPE_LENGTH)) {
1395         $props->{'max_volume_usage'} = {
1396             optional => 1,
1397             priority => 0,
1398             append => 0,
1399             values => [
1400                 tapetype_getconf($tapetype, $TAPETYPE_LENGTH) * 1024,
1401             ]};
1402     }
1403
1404     if (tapetype_seen($tapetype, $TAPETYPE_READBLOCKSIZE)) {
1405         $props->{'read_block_size'} = {
1406             optional => "warn", # optional, but give a warning
1407             priority => 0,
1408             append => 0,
1409             values => [
1410                 tapetype_getconf($tapetype, $TAPETYPE_READBLOCKSIZE) * 1024,
1411             ]};
1412     }
1413
1414     if (tapetype_seen($tapetype, $TAPETYPE_BLOCKSIZE)) {
1415         $props->{'block_size'} = {
1416             optional => 0,
1417             priority => 0,
1418             append => 0,
1419             values => [
1420                 # convert the length from kb to bytes here
1421                 tapetype_getconf($tapetype, $TAPETYPE_BLOCKSIZE) * 1024,
1422             ]};
1423     }
1424
1425     return $props;
1426 }
1427
1428 1;