d13e342fd76df26710591a383dd4feea89fc0670
[debian/amanda] / perl / Amanda / Changer / robot.pm
1 # Copyright (c) 2009,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::Changer::robot;
20
21 use strict;
22 use warnings;
23 use Carp;
24 use vars qw( @ISA );
25 @ISA = qw( Amanda::Changer );
26
27 use Data::Dumper;
28 use File::Path;
29 use Amanda::Paths;
30 use Amanda::MainLoop qw( :GIOCondition make_cb define_steps step );
31 use Amanda::Config qw( :getconf );
32 use Amanda::Debug qw( debug warning );
33 use Amanda::Device qw( :constants );
34 use Amanda::Changer;
35 use Amanda::Constants;
36
37 =head1 NAME
38
39 Amanda::Changer::robot -- control a physical tape changer
40
41 =head1 DESCRIPTION
42
43 This package controls a physical tape changer via 'mtx'.
44
45 See the amanda-changers(7) manpage for usage information.
46
47 =cut
48
49 # NOTES
50 #
51 # This is one of the more sophisticated changers.  Here are some notes that may
52 # help while reading the source code.
53
54 # STATE
55 #
56 # The device state is shared between all changers accessing the same library.
57 # It is a hash with keys:
58 #   slots - see below
59 #   drives - see below
60 #   drive_lru - recently used drives, least recent first
61 #   bc2lb - hash mapping known barcodes to known labels
62 #   current_slot - the current slot
63 #   last_operation_time - time the last operation finished
64 #   last_operation_delay - required delay for that operation
65 #   last_status - last time a 'status' command finished
66 #
67 # The 'slots' key is a hash, with slot numbers as keys and hashes
68 # as values.  Each slot's hash has keys
69 #   state - SLOT_FULL/SLOT_EMPTY/SLOT_UNKNOWN
70 #   device_status - the status of the device
71 #   device_error - the error message of the device
72 #   f_type - The f_type of the header
73 #   label - volume label, if known
74 #   barcode - volume barcode, if available
75 #   loaded_in - drive this volume is loaded in
76 #   ie - 1 if this is an import/export slot
77 # note that this state pretends that a tape physically located
78 # in a tape drive is still located in its original slot.
79 #
80 # The 'drives' key is also a hash by drive numbere, the values of
81 # which are hashes with keys
82 #   state - SLOT_FULL/SLOT_EMPTY/SLOT_UNKNOWN
83 #   label - volume label
84 #   barcode - volume barcode
85 #   orig_slot - slot from which this tape was loaded
86
87 # LOCKING
88 #
89 # This package uses Amanda::Changer's with_locked_state to lock a statefile and
90 # load its contents.  Every time the state is locked, the package also
91 # considers running 'status' to update the state; the status_interval protects
92 # against running status too often.
93 #
94 # Each changer method has an "_unlocked" version that does the actual work, and
95 # is called with an additional 'state' parameter containing the locked state.
96 # This is particularly useful when the load method calls the eject method to
97 # empty a drive that it wants to use.
98
99 # RESERVATIONS
100 #
101 # Reservations are currently represented by a PID in the state file.  If that
102 # pid is no longer running, then the reservation is considered stale and is
103 # discarded (with a warning).
104 #
105 # Reservation objects defer most of the interesting operations back to the
106 # changer itself, since the operations require locked access to the state.
107
108 # INTERFACE
109 #
110 # All of the operating-system-specific functionality is abstracted into the
111 # Interface class.  This is written in such a way that it could be replaced
112 # by a direct SCSI interface.
113
114 sub new {
115     my $class = shift;
116     my ($config, $tpchanger) = @_;
117
118     # strip the "chg-foo:" prefix from $tpchanger
119     my $device_name = $tpchanger;
120     $device_name =~ s/^[^:]*://;
121
122     # get the 'chg-foo' form of this changer script
123     my $class_name = $class;
124     $class_name =~ s/Amanda::Changer::/chg-/;
125
126     my $self = {
127         interface => undef,
128         device_name => $device_name,
129         config => $config,
130
131         # set below from properties
132         statefile => undef,
133         drive2device => {}, # { drive => device name }
134         driveorder => [], # order of tape-device properties
135         drive_choice => 'lru',
136         eject_before_unload => 0,
137         fast_search => 1,
138         use_slots => undef,
139         status_interval => 2, # in seconds
140         load_poll => [0, 2, 120], # delay, poll, until
141         eject_delay => 0, # in seconds
142         unload_delay => 0, # in seconds
143         class_name => $class_name,
144     };
145     bless ($self, $class);
146
147     # handle some config and properties
148     my $properties = $config->{'properties'};
149
150     if (defined $config->{'changerdev'} and $config->{'changerdev'} ne '') {
151         return Amanda::Changer->make_error("fatal", undef,
152             message => "'changerdev' is not allowed with $self->{class_name}");
153     }
154
155     if ($config->{'changerfile'}) {
156         $self->{'statefile'} = Amanda::Config::config_dir_relative($config->{'changerfile'});
157     } else {
158         my $safe_filename = "$self->{class_name}:$device_name";
159         $safe_filename =~ tr/a-zA-Z0-9/-/cs;
160         $safe_filename =~ s/^-*//;
161         $self->{'statefile'} = "$localstatedir/amanda/$safe_filename";
162     }
163     $self->_debug("using statefile '$self->{statefile}'");
164
165     # figure out the drive number to device name mapping
166     if (exists $config->{'tapedev'}
167             and $config->{'tapedev'} ne ''
168             and !exists $properties->{'tape-device'}) {
169         # if the tapedev points to us (the changer), then give an error
170         if ($config->{'tapedev'} eq $tpchanger) {
171             return Amanda::Changer->make_error("fatal", undef,
172                 message => "must specify a tape-device property");
173         }
174         $self->{'drive2device'} = { '0' => $config->{'tapedev'} };
175         push @{$self->{'driveorder'}}, '0';
176     } else {
177         if (!exists $properties->{'tape-device'}) {
178             return Amanda::Changer->make_error("fatal", undef,
179                 message => "no 'tape-device' property specified");
180         }
181         for my $pval (@{$properties->{'tape-device'}->{'values'}}) {
182             my ($drive, $device);
183             unless (($drive, $device) = ($pval =~ /(\d+)=(.*)/)) {
184                 return Amanda::Changer->make_error("fatal", undef,
185                     message => "invalid 'tape-device' property '$pval'");
186             }
187             if (exists $self->{'drive2device'}->{$drive}) {
188                 return Amanda::Changer->make_error("fatal", undef,
189                     message => "tape-device drive $drive defined more than once");
190             }
191             $self->{'drive2device'}->{$drive} = $device;
192             push @{$self->{'driveorder'}}, $drive;
193         }
194     }
195
196     # eject-before-unload
197     my $ebu = $self->{'config'}->get_boolean_property(
198                                             "eject-before-unload", 0);
199     if (!defined $ebu) {
200         return Amanda::Changer->make_error("fatal", undef,
201             message => "invalid 'eject-before-unload' value");
202     }
203     $self->{'eject_before_unload'} = $ebu;
204
205     # fast-search
206     my $fast_search = $self->{'config'}->get_boolean_property(
207                                                 "fast-search", 1);
208     if (!defined $fast_search) {
209         return Amanda::Changer->make_error("fatal", undef,
210             message => "invalid 'fast-search' value");
211     }
212     $self->{'fast_search'} = $fast_search;
213
214     # use-slots
215     if (exists $properties->{'use-slots'}) {
216         $self->{'use_slots'} = join ",", @{$properties->{'use-slots'}->{'values'}};
217         if ($self->{'use_slots'} !~ /\d+(-\d+)?(,\d+(-\d+)?)*/) {
218             return Amanda::Changer->make_error("fatal", undef,
219                 message => "invalid 'use-slots' value '$self->{use_slots}'");
220         }
221     }
222
223     # drive-choice
224     if (exists $properties->{'drive-choice'}) {
225         my $pval = $properties->{'drive-choice'}->{'values'}->[0];
226         if (!grep { lc($_) eq $pval } ('lru', 'firstavail')) {
227             return Amanda::Changer->make_error("fatal", undef,
228                 message => "invalid 'drive-choice' value '$pval'");
229         }
230         $self->{'drive_choice'} = $pval;
231     }
232
233     # load-poll
234     {
235         next unless exists $config->{'properties'}->{'load-poll'};
236         if (@{$config->{'properties'}->{'load-poll'}->{'values'}} > 1) {
237             return Amanda::Changer->make_error("fatal", undef,
238                 message => "only one value allowed for 'load-poll'");
239         }
240         my $propval = $config->{'properties'}->{'load-poll'}->{'values'}->[0];
241         my ($delay, $delayu, $poll, $pollu, $until, $untilu) = ($propval =~ /^
242                 (\d+)\s*([ms]?)
243                 (?:
244                   \s+poll\s+
245                   (\d+)\s*([ms]?)
246                   (?:
247                     \s+until\s+
248                     (\d+)\s*([ms]?)
249                   )?
250                 )?
251                 $/ix);
252
253         if (!defined $delay) {
254             return Amanda::Changer->make_error("fatal", undef,
255                 message => "invalid delay value '$propval' for 'load-poll'");
256         }
257
258         $delay *= 60 if (defined $delayu and $delayu =~ /m/i);
259
260         $poll = 0 unless defined $poll;
261         $poll *= 60 if (defined $pollu and $pollu =~ /m/i);
262
263         $until = 0 unless defined $until;
264         $until *= 60 if (defined $untilu and $untilu =~ /m/i);
265
266         $self->{'load_poll'} = [ $delay, $poll, $until ];
267     }
268
269     # status-interval, eject-delay, unload-delay
270     for my $propname (qw(status-interval eject-delay unload-delay)) {
271         next unless exists $config->{'properties'}->{$propname};
272         if (@{$config->{'properties'}->{$propname}->{'values'}} > 1) {
273             return Amanda::Changer->make_error("fatal", undef,
274                 message => "only one value allowed for $propname");
275         }
276         my $propval = $config->{'properties'}->{$propname}->{'values'}->[0];
277         my ($time, $timeu) = ($propval =~ /^(\d+)([ms]?)/ix);
278
279         if (!defined $time) {
280             return Amanda::Changer->make_error("fatal", undef,
281                 message => "invalid time value '$propval' for '$propname'");
282         }
283
284         $time *= 60 if (defined $timeu and $timeu =~ /m/i);
285
286         my $key = $propname;
287         $key =~ s/-/_/;
288         $self->{$key} = $time;
289     }
290
291     my $ignore_barcodes = $self->{'config'}->get_boolean_property(
292                                             "ignore-barcodes", 0);
293     if (!defined $ignore_barcodes) {
294         return Amanda::Changer->make_error("fatal", undef,
295             message => "invalid 'ignore-barcodes' value");
296     }
297
298     # get the interface, returning an error if we get one
299     $self->{'interface'} = $self->get_interface($device_name, $ignore_barcodes);
300     return $self->{'interface'}
301         if ($self->{'interface'}->isa("Amanda::Changer::Error"));
302
303     return $self;
304 }
305
306 sub load {
307     my $self = shift;
308     my %params = @_;
309     $self->validate_params('load', \%params);
310
311     return if $self->check_error($params{'res_cb'});
312
313     $self->_with_updated_state(\%params, 'res_cb', sub { $self->load_unlocked(@_) });
314 }
315
316 sub load_unlocked {
317     my $self = shift;
318     my %params = @_;
319     my ($slot, $drive, $need_unload);
320     my $state = $params{'state'};
321
322     my $steps = define_steps
323         cb_ref => \$params{'res_cb'};
324
325     step calculate_slot => sub {
326         # make sure the slot is numeric
327         if (exists $params{'slot'}) {
328             if ($params{'slot'} =~ /^\d+$/) {
329                 $params{'slot'} = $params{'slot'}+0;
330             } else {
331                 return $self->make_error("failed", $params{'res_cb'},
332                         reason => "invalid",
333                         message => "invalid slot '$params{slot}'");
334             }
335         }
336
337         if (exists $params{'relative_slot'}) {
338             if ($params{'relative_slot'} eq "next") {
339                 if (exists $params{'slot'}) {
340                     $slot = $self->_get_next_slot($state, $params{'slot'});
341                     $self->_debug("loading next relative to $params{slot}: $slot");
342                 } else {
343                     $slot = $self->_get_next_slot($state, $state->{'current_slot'});
344                     $self->_debug("loading next relative to current slot: $slot");
345                 }
346                 if ($slot == -1) {
347                     return $self->make_error("failed", $params{'res_cb'},
348                             reason => "invalid",
349                             message => "could not find next slot");
350                 }
351             } elsif ($params{'relative_slot'} eq "current") {
352                 $slot = $state->{'current_slot'};
353                 if ($slot == -1) {
354                     # seek to the first slot
355                     $slot = $self->_get_next_slot($state, $state->{'current_slot'});
356                 }
357                 if ($slot == -1) {
358                     return $self->make_error("failed", $params{'res_cb'},
359                             reason => "invalid",
360                             message => "no current slot");
361                 }
362             } else {
363                 return $self->make_error("failed", $params{'res_cb'},
364                         reason => "invalid",
365                         message => "invalid relative_slot '$params{relative_slot}'");
366             }
367
368         } elsif (exists $params{'slot'}) {
369             $slot = $params{'slot'};
370             $self->_debug("loading slot '$params{slot}'");
371
372             if (!defined $slot or !exists $state->{'slots'}->{$slot}) {
373                 return $self->make_error("failed", $params{'res_cb'},
374                         reason => "invalid",
375                         message => "invalid slot '$slot'");
376             }
377
378         } elsif (exists $params{'label'}) {
379             $self->_debug("loading label '$params{label}'");
380             while (my ($sl, $info) = each(%{$state->{'slots'}})) {
381                 if (defined $info->{'label'} and $info->{'label'} eq $params{'label'}) {
382                     $slot = $sl;
383                     last;
384                 }
385             }
386
387             if (!defined $slot) {
388                 return $self->make_error("failed", $params{'res_cb'},
389                         reason => "notfound",
390                         message => "label '$params{label}' not recognized or not found");
391             }
392
393         } else {
394             return $self->make_error("failed", $params{'res_cb'},
395                     reason => "invalid",
396                     message => "no 'slot' or 'label' specified to load()");
397         }
398
399         if (!$self->_is_slot_allowed($slot)) {
400             if (exists $params{'label'}) {
401                 return $self->make_error("failed", $params{'res_cb'},
402                         reason => "invalid",
403                         message => "label '$params{label}' is in slot $slot, which is " .
404                                    "not in use-slots ($self->{use_slots})");
405             } else {
406                 return $self->make_error("failed", $params{'res_cb'},
407                         reason => "invalid",
408                         message => "slot $slot not in use-slots ($self->{use_slots})");
409             }
410         }
411
412         if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
413             return $self->make_error("failed", $params{'res_cb'},
414                 reason => "notfound",
415                 message => "all slots have been loaded");
416         }
417
418         if ($state->{'slots'}->{$slot}->{'state'} eq Amanda::Changer::SLOT_EMPTY) {
419             return $self->make_error("failed", $params{'res_cb'},
420                     reason => "empty",
421                     message => "slot $slot is empty");
422         }
423
424         return $steps->{'calculate_drive'}->();
425     };
426
427     step calculate_drive => sub {
428         # $slot is set
429         $need_unload = 0;
430
431         # see if the tape is already in a drive
432         $drive = $state->{'slots'}->{$slot}->{'loaded_in'};
433         if (defined $drive) {
434             $self->_debug("requested volume is already in drive $drive");
435             my $info = $state->{'drives'}->{$drive};
436
437             # if it's reserved, it can't be used
438             if ($info->{'res_info'} and $self->_res_info_verify($info->{'res_info'})) {
439                 return $self->make_error("failed", $params{'res_cb'},
440                         reason => "volinuse",
441                         slot => $slot,
442                         message => "the requested volume is in use (drive $drive)");
443             }
444
445             # if it's not reserved, but not in our list of drives, well, it still
446             # can't be used
447             if (!exists $self->{'drive2device'}->{$drive}) {
448                 return $self->make_error("failed", $params{'res_cb'},
449                         # not 'volinuse' because we can't expect the tape to be magically
450                         # unloaded any time soon -- it's not actually in use, just inaccessible
451                         reason => "invalid",
452                         message => "the requested volume is in drive $drive, which this " .
453                                    "changer instance cannot access");
454             }
455
456             # otherwise, we can jump all the way to the end of this process
457             return $steps->{'start_polling'}->();
458         }
459
460         # here is where we implement each of the drive-selection algorithms
461         my @check_order;
462         if ($self->{'drive_choice'} eq 'lru') {
463             my %lru = map { $_, 1 } @{$state->{'drive_lru'}};
464             my @unused = grep { ! exists $lru{$_} } @{$self->{'driveorder'}};
465
466             # search through unused drives, then the LRU list
467             @check_order = (@unused, @{$state->{'drive_lru'}});
468         } elsif ($self->{'drive_choice'} eq 'firstavail') {
469             # just the drive order, so we tend to prefer the first drive in
470             # this order
471             @check_order = (@{$self->{'driveorder'}});
472         } else {
473             # the constructor should detect this circumstance
474             die "invalid drive_choice";
475         }
476
477         my %checked;
478         for my $dr (@check_order) {
479             my $info = $state->{'drives'}->{$dr};
480             next unless defined $info;
481             next if exists $checked{$dr}; # don't check drives repeatedly
482             $checked{$dr} = 1;
483
484             # skip drives we don't have rights to use
485             next unless exists $self->{'drive2device'}->{$dr};
486
487             # skip reserved drives
488             if ($info->{'res_info'}) {
489                 if ($self->_res_info_verify($info->{'res_info'})) {
490                     # this is a valid reservation -> skip this drive
491                     $self->_debug("skipping drive $dr - already reserved");
492                     next;
493                 } else {
494                     warning("invalidating stale reservation on drive $dr");
495                     $info->{'res_info'} = undef;
496                 }
497             }
498
499             # otherwise, the drive is available, so use it (whether it contains
500             # a volume or not)
501             $drive = $dr;
502             if ($info->{'state'} != Amanda::Changer::SLOT_EMPTY) {
503                 $need_unload = 1;
504             }
505             last;
506         }
507
508         if (!defined $drive) {
509             return $self->make_error("failed", $params{'res_cb'},
510                     reason => "driveinuse",
511                     message => "no drives available");
512         }
513
514         # remove this drive from the lru and put it at the end
515         $state->{'drive_lru'} = [ grep { $_ ne $drive } @{$state->{'drive_lru'}} ];
516         push @{$state->{'drive_lru'}}, $drive;
517
518         $self->_debug("using drive $drive");
519
520         $steps->{'wait_to_start'}->();
521     };
522
523     step wait_to_start => sub {
524         $self->_after_delay($state, $steps->{'start_operation'});
525     };
526
527     step start_operation => sub {
528         # $need_unload is set in $steps->{calculate_drive}
529         if ($need_unload) {
530             $steps->{'start_eject'}->();
531         } else {
532             $steps->{'start_load'}->();
533         }
534     };
535
536     step start_eject => sub {
537         # we use the 'eject' method to unload here -- it ejects the volume
538         # if the configuration calls for it, then puts the volume away in its
539         # original slot.
540         $self->eject_unlocked(
541                 finished_cb => $steps->{'eject_finished'},
542                 drive => $drive,
543                 state => $state);
544     };
545
546     step eject_finished => sub {
547         my ($err) = @_;
548
549         if ($err) {
550            return $params{'res_cb'}->($err);
551         }
552
553         $steps->{'wait_to_load'}->();
554     };
555
556     step wait_to_load => sub {
557         $self->_after_delay($state, $steps->{'start_load'});
558     };
559
560     step start_load => sub {
561         # $slot and $drive are set
562         $self->{'interface'}->load($slot, $drive, $steps->{'load_finished'});
563     };
564
565     step load_finished => sub {
566         # $slot and $drive are set
567         my ($err) = @_;
568
569         if ($err) {
570             return $self->make_error("failed", $params{'res_cb'},
571                     reason => "unknown",
572                     message => $err);
573         }
574
575         $steps->{'start_polling'}->();
576     };
577
578     my ($next_poll, $last_poll);
579     step start_polling => sub {
580         my ($delay, $poll, $until) = @{ $self->{'load_poll'} };
581         my $now = time;
582         $next_poll = $now + $delay;
583         $last_poll = $now + $until;
584
585         return Amanda::MainLoop::call_after(1000 * ($next_poll - $now), $steps->{'check_device'});
586     };
587
588     step check_device => sub {
589         my $device_name = $self->{'drive2device'}->{$drive};
590         die "drive $drive not found in drive2device" unless $device_name; # shouldn't happen
591
592         $self->_debug("polling '$device_name' to see if it's ready");
593
594         my $device = $self->get_device($device_name);
595         return $params{'res_cb'}->($device) if $device->isa("Amanda::Changer::Error");
596
597         my $label;
598         $device->read_label();
599
600         # see if the device thinks it's possible it's busy or empty
601         if ($device->status & $DEVICE_STATUS_VOLUME_MISSING
602             or $device->status & $DEVICE_STATUS_DEVICE_BUSY) {
603             # device is not ready -- set up for the next polling step
604             my ($delay, $poll, $until) = @{ $self->{'load_poll'} };
605             my $now = time;
606             $next_poll += $poll;
607             $next_poll = $now + 1 if ($next_poll < $now);
608             if ($poll != 0 and $next_poll < $last_poll) {
609                 return Amanda::MainLoop::call_after(
610                         1000 * ($next_poll - $now), $steps->{'check_device'});
611             }
612
613             # (fall through if we're done polling)
614         }
615
616         if ($device->status == $DEVICE_STATUS_SUCCESS) {
617             $label = $device->volume_label;
618         } elsif ($device->status & $DEVICE_STATUS_VOLUME_UNLABELED) {
619             $label = undef;
620         } else {
621             $label = undef;
622         }
623
624         # success!
625         $steps->{'make_res'}->($device, $label);
626     };
627
628     step make_res => sub {
629         my ($device, $label) = @_;
630
631         # check the label against the desired label, in case this isn't the
632         # desired volume
633         if ($label and $params{'label'} and $label ne $params{'label'}) {
634             $self->_debug("Expected label '$params{label}', but got '$label'");
635
636             # update metadata with this new information
637             $state->{'slots'}->{$slot}->{'state'} = Amanda::Changer::SLOT_FULL;
638             $state->{'slots'}->{$slot}->{'device_status'} = $device->status;
639             $state->{'slots'}->{$slot}->{'device_error'} = $device->error;
640             if (defined $device->{'volume_header'}) {
641                 $state->{'slots'}->{$slot}->{'f_type'} = $device->{'volume_header'}->{type};
642             } else {
643                 $state->{'slots'}->{$slot}->{'f_type'} = undef;
644             }
645             $state->{'slots'}->{$slot}->{'label'} = $label;
646             if ($state->{'slots'}->{$slot}->{'barcode'}) {
647                 $state->{'bc2lb'}->{$state->{'slots'}->{$slot}->{'barcode'}} = $label;
648             }
649
650             return $self->make_error("failed", $params{'res_cb'},
651                     reason => "notfound",
652                     message => "Found unexpected tape '$label' while looking " .
653                                "for '$params{label}'");
654         }
655
656         if (!$label and $params{'label'}) {
657             $self->_debug("Expected label '$params{label}', but got an unlabeled tape");
658
659             # update metadata with this new information
660             $state->{'slots'}->{$slot}->{'state'} = Amanda::Changer::SLOT_FULL;
661             $state->{'slots'}->{$slot}->{'device_status'} = $device->status;
662             $state->{'slots'}->{$slot}->{'device_error'} = $device->error;
663             if (defined $device->{'volume_header'}) {
664                 $state->{'slots'}->{$slot}->{'f_type'} = $device->{'volume_header'}->{type};
665             } else {
666                 $state->{'slots'}->{$slot}->{'f_type'} = undef;
667             }
668             $state->{'slots'}->{$slot}->{'label'} = undef;
669             if ($state->{'slots'}->{$slot}->{'barcode'}) {
670                 delete $state->{'bc2lb'}->{$state->{'slots'}->{$slot}->{'barcode'}};
671             }
672
673             return $self->make_error("failed", $params{'res_cb'},
674                     reason => "notfound",
675                     message => "Found unlabeled tape while looking for '$params{label}'");
676         }
677
678         my $res = Amanda::Changer::robot::Reservation->new($self, $slot, $drive,
679                                 $device, $state->{'slots'}->{$slot}->{'barcode'});
680
681         # mark this as reserved
682         $state->{'drives'}->{$drive}->{'res_info'} = $self->_res_info_new();
683
684         # update our state before returning
685         $state->{'slots'}->{$slot}->{'loaded_in'} = $drive;
686         $state->{'drives'}->{$drive}->{'orig_slot'} = $slot;
687         $state->{'slots'}->{$slot}->{'label'} = $label;
688         $state->{'drives'}->{$drive}->{'label'} = $label;
689         $state->{'drives'}->{$drive}->{'state'} = Amanda::Changer::SLOT_FULL;
690         $state->{'drives'}->{$drive}->{'barcode'} = $state->{'slots'}->{$slot}->{'barcode'};
691         $state->{'slots'}->{$slot}->{'device_status'} = $device->status;
692         if ($label and $state->{'slots'}->{$slot}->{'barcode'}) {
693             $state->{'bc2lb'}->{$state->{'slots'}->{$slot}->{'barcode'}} = $label;
694         }
695         if ($params{'set_current'}) {
696                 $self->_debug("setting current slot to $slot");
697             $state->{'current_slot'} = $slot;
698         }
699
700         return $params{'res_cb'}->(undef, $res);
701     };
702 }
703
704 sub info_key {
705     my $self = shift;
706     my ($key, %params) = @_;
707
708     if ($key eq 'fast_search') {
709         $self->info_key_fast_search(%params);
710     } elsif ($key eq 'vendor_string') {
711         $self->info_key_vendor_string(%params);
712     } elsif ($key eq 'num_slots') {
713         $self->info_key_num_slots(%params);
714     }
715 }
716
717 sub info_key_fast_search {
718     my $self = shift;
719     my %params = @_;
720
721     $params{'info_cb'}->(undef,
722         fast_search => $self->{'fast_search'},
723     );
724 }
725
726 sub info_key_vendor_string {
727     my $self = shift;
728     my %params = @_;
729
730     $self->{'interface'}->inquiry(make_cb(inquiry_cb => sub {
731         my ($err, $info) = @_;
732         return $self->make_error("fatal", $params{'info_cb'},
733                 message => "$err") if $err;
734
735         my $vendor_string = sprintf "%s %s",
736             ($info->{'vendor id'} or "<unknown>"),
737             ($info->{'product id'} or "<unknown>");
738
739         $params{'info_cb'}->(undef,
740             vendor_string => $vendor_string,
741         );
742     }));
743 }
744
745 sub info_key_num_slots {
746     my $self = shift;
747     my %params = @_;
748
749     $self->_with_updated_state(\%params, 'info_cb',
750         sub { $self->info_key_num_slots_unlocked(@_) });
751 }
752
753 sub info_key_num_slots_unlocked {
754     my $self = shift;
755     my %params = @_;
756     my $state = $params{'state'};
757
758     my @allowed_slots = grep { $self->_is_slot_allowed($_) }
759                         keys %{$state->{'slots'}};
760
761     $params{'info_cb'}->(undef, num_slots => scalar @allowed_slots);
762 }
763
764 sub get_interface { # (overridden by subclasses)
765     my $self = shift;
766     my ($device_name, $ignore_barcodes) = @_;
767
768     my $mtx;
769     if (exists $self->{'config'}->{'properties'}->{'mtx'}) {
770         if (@{$self->{'config'}->{'properties'}->{'mtx'}->{'values'}} > 1) {
771             return Amanda::Changer->make_error("fatal", undef,
772                 message => "only one value allowed for 'mtx'");
773         }
774         $mtx = $self->{'config'}->{'properties'}->{'mtx'}->{'values'}->[0];
775     } else {
776         $mtx = $Amanda::Constants::MTX;
777     }
778
779     if (!$mtx) {
780         return Amanda::Changer->make_error("fatal", undef,
781             message => "no default value for property MTX");
782     }
783
784     return Amanda::Changer::robot::Interface::MTX->new($device_name, $mtx, $ignore_barcodes),
785 }
786
787 # get, configure, and return a new device, or return a changer error
788 sub get_device { # (overridden by subclasses)
789     my $self = shift;
790     my ($device_name) = @_;
791
792     my $device = Amanda::Device->new($device_name);
793     if ($device->status != $DEVICE_STATUS_SUCCESS) {
794         return Amanda::Changer->make_error("fatal", undef,
795                 reason => "unknown",
796                 message => "opening '$device_name': " . $device->error_or_status());
797     }
798
799     if (my $err = $self->{'config'}->configure_device($device)) {
800         return Amanda::Changer->make_error("fatal", undef,
801                 reason => "unknown",
802                 message => $err);
803     }
804
805     return $device;
806 }
807
808 sub _set_label {
809     my $self = shift;
810     my %params = @_;
811
812     return if $self->check_error($params{'finished_cb'});
813
814     $self->_with_updated_state(\%params, 'finished_cb',
815         sub { $self->_set_label_unlocked(@_); });
816 }
817
818 sub _set_label_unlocked {
819     my $self = shift;
820     my %params = @_;
821     my $state = $params{'state'};
822
823     # update all of the various pieces of cached information
824     my $drive = $params{'drive'};
825     my $slot = $state->{'drives'}->{$drive}->{'orig_slot'};
826     my $label = $params{'label'};
827     my $barcode = $state->{'drives'}->{$drive}->{'barcode'};
828     my $dev = $params{dev};
829
830     $state->{'drives'}->{$drive}->{'label'} = $label;
831     if (defined $slot) {
832         $state->{'slots'}->{$slot}->{'state'} = Amanda::Changer::SLOT_FULL;
833         $state->{'slots'}->{$slot}->{'device_status'} = "".$dev->status;
834         if ($dev->status != $DEVICE_STATUS_SUCCESS) {
835             $state->{'slots'}->{$slot}->{'device_error'} = $dev->error;
836         } else {
837             $state->{'slots'}->{$slot}->{'device_error'} = undef;
838         }
839         my $volume_header = $dev->volume_header;
840         if (defined $volume_header) {
841             $state->{'slots'}->{$slot}->{'f_type'} = "".$volume_header->{type};
842         } else {
843             $state->{'slots'}->{$slot}->{'f_type'} = undef;
844         }
845         $state->{'slots'}->{$slot}->{'label'} = $label;
846     }
847     if (defined $barcode) {
848         $state->{'bc2lb'}->{$barcode} = $label;
849     }
850
851     $params{'finished_cb'}->(undef);
852 }
853
854 sub _release {
855     my $self = shift;
856     my %params = @_;
857
858     return if $self->check_error($params{'finished_cb'});
859
860     $self->_with_updated_state(\%params, 'finished_cb',
861         sub { $self->_release_unlocked(@_); });
862 }
863
864 sub _release_unlocked {
865     my $self = shift;
866     my %params = @_;
867     my $state = $params{'state'};
868     my $drive = $params{'drive'};
869
870     # delete the reservation and save the statefile
871     if (!$self->_res_info_is_mine($state->{'drives'}->{$drive}->{'res_info'})) {
872         # this should *never* happen
873         return $self->make_error("fatal", $params{'finished_cb'},
874                 message => "reservation belongs to another instance");
875     }
876     $state->{'drives'}->{$drive}->{'res_info'} = undef;
877
878     # bounce off to eject if the user has requested it, using the xx_unlocked
879     # variant since we've already got the statefile open
880     if ($params{'eject'}) {
881         $self->eject_unlocked(
882             drive => $drive,
883             finished_cb => $params{'finished_cb'},
884             state => $state,
885         );
886     } else {
887         $params{'finished_cb'}->();
888     }
889 }
890
891 sub reset {
892     my $self = shift;
893     my %params = @_;
894
895     return if $self->check_error($params{'finished_cb'});
896
897     $self->_with_updated_state(\%params, 'finished_cb',
898         sub { $self->reset_unlocked(@_); });
899 }
900
901 sub reset_unlocked {
902     my $self = shift;
903     my %params = @_;
904     my $state = $params{'state'};
905
906     $state->{'current_slot'} = $self->_get_next_slot($state, -1);
907
908     $params{'finished_cb'}->();
909 }
910
911 sub eject {
912     my $self = shift;
913     my %params = @_;
914
915     return if $self->check_error($params{'finished_cb'});
916
917     $self->_with_updated_state(\%params, 'finished_cb',
918         sub { $self->eject_unlocked(@_); });
919 }
920
921 sub eject_unlocked {
922     my $self = shift;
923     my %params = @_;
924     my $state = $params{'state'};
925     my ($drive, $drive_info);
926
927     return if $self->check_error($params{'finished_cb'});
928
929     my $steps = define_steps
930         cb_ref => \$params{'finished_cb'};
931
932     # note that this changer treats "eject" as "unload", which may also require an eject
933     # operation if the eject_before_unload property is set
934
935     step start => sub {
936         # if drive isn't specified, see if we only have one
937         if (!exists $params{'drive'}) {
938             if ((keys %{$self->{'drive2device'}}) == 1) {
939                 $params{'drive'} = (keys %{$self->{'drive2device'}})[0];
940             } else {
941                 return $self->make_error("failed", $params{'finished_cb'},
942                         reason => "invalid",
943                         message => "no drive specified");
944             }
945         }
946         $drive = $params{'drive'};
947
948         $self->_debug("unloading drive $drive");
949         $drive_info = $state->{'drives'}->{$drive};
950         if (!$drive_info) {
951             return $self->make_error("failed", $params{'finished_cb'},
952                     reason => "invalid",
953                     message => "invalid drive '$drive'");
954         }
955
956         # if the drive exists, but not configured in this changer, then
957         # bail out.
958         if (!defined $self->{'drive2device'}->{$drive}) {
959             return $self->make_error("failed", $params{'finished_cb'},
960                     reason => "invalid",
961                     message => "this changer instance is not configured to access drive $drive");
962         }
963
964
965         # check for a reservation
966         if ($drive_info->{'res_info'}
967                     and $self->_res_info_verify($drive_info->{'res_info'})) {
968             return $self->make_error("failed", $params{'finished_cb'},
969                     reason => "volinuse",
970                     message => "tape in drive '$drive' is in use");
971         }
972
973         if ($self->{'eject_before_unload'}) {
974             $steps->{'wait_to_eject'}->();
975         } else {
976             $steps->{'wait_to_unload'}->();
977         }
978     };
979
980     step wait_to_eject => sub {
981         $self->_after_delay($state, $steps->{'eject'});
982     };
983
984     step eject => sub {
985         my $device_name = $self->{'drive2device'}->{$drive};
986         $self->_debug("ejecting $device_name before unload");
987
988         my $device = $self->get_device($device_name);
989         return $device if $device->isa("Amanda::Changer::Error");
990
991         if (!$device->eject()) {
992             return $self->make_error("failed", $params{'finished_cb'},
993                     reason => "unknown",
994                     message => "while ejecting volume: " . $device->error_or_status);
995         }
996         undef $device;
997
998         $self->_set_delay($state, $self->{'eject_delay'});
999
1000         $steps->{'wait_to_unload'}->();
1001     };
1002
1003     step wait_to_unload => sub {
1004         $self->_after_delay($state, $steps->{'unload'});
1005     };
1006
1007     step unload => sub {
1008         # find target slot and unload it - note that the target slot may not be
1009         # in the USE-SLOTS list, as it may belong to another config
1010         my $orig_slot = $drive_info->{'orig_slot'};
1011         $self->{'interface'}->unload($drive, $orig_slot, $steps->{'unload_finished'});
1012     };
1013
1014     step unload_finished => sub {
1015         my ($err) = @_;
1016
1017         if ($err) {
1018             return $self->make_error("failed", $params{'finished_cb'},
1019                     reason => "unknown",
1020                     message => $err);
1021         }
1022
1023         $self->_debug("unload complete");
1024         my $orig_slot = $state->{'drives'}->{$drive}->{'orig_slot'};
1025         $state->{'slots'}->{$orig_slot}->{'state'} = $state->{'drives'}->{$drive}->{'state'};
1026         $state->{'slots'}->{$orig_slot}->{'label'} = $state->{'drives'}->{$drive}->{'label'};
1027         $state->{'slots'}->{$orig_slot}->{'barcode'} = $state->{'drives'}->{$drive}->{'barcode'};
1028         $state->{'slots'}->{$orig_slot}->{'loaded_in'} = undef;
1029         $state->{'drives'}->{$drive}->{'state'} = Amanda::Changer::SLOT_EMPTY;
1030         $state->{'drives'}->{$drive}->{'label'} = undef;
1031         $state->{'drives'}->{$drive}->{'barcode'} = undef;
1032         $state->{'drives'}->{$drive}->{'orig_slot'} = undef;
1033
1034         $self->_set_delay($state, $self->{'unload_delay'});
1035         $params{'finished_cb'}->();
1036     };
1037 }
1038
1039 sub update {
1040     my $self = shift;
1041     my %params = @_;
1042
1043     return if $self->check_error($params{'finished_cb'});
1044
1045     $self->_with_updated_state(\%params, 'finished_cb',
1046         sub { $self->update_unlocked(@_); });
1047 }
1048
1049 sub update_unlocked {
1050     my $self = shift;
1051     my %params = @_;
1052     my @slots_to_check;
1053     my $state = $params{'state'};
1054     my $set_to_unknown = 0;
1055
1056     return if $self->check_error($params{'finished_cb'});
1057
1058     my $user_msg_fn = $params{'user_msg_fn'};
1059     $user_msg_fn ||= sub { $self->_debug($_[0]); };
1060
1061     my $steps = define_steps
1062         cb_ref => \$params{'finished_cb'};
1063
1064     step handle_assignment => sub {
1065         # check for the SL=LABEL format, and handle it here
1066         if (exists $params{'changed'} and $params{'changed'} =~ /^\d+=\S+$/) {
1067             my ($slot, $label) = ($params{'changed'} =~ /^(\d+)=(\S+)$/);
1068
1069             # let's list the reasons we *can't* do what the user has asked
1070             my $whynot;
1071             if (!exists $state->{'slots'}) {
1072                 $whynot = "slot $slot does not exist";
1073             } elsif (!$self->_is_slot_allowed($slot)) {
1074                 $whynot = "slot $slot is not used by this changer";
1075             } elsif ($state->{'slots'}->{$slot}->{'state'} ==
1076                      Amanda::Changer::SLOT_EMPTY) {
1077                 $whynot = "slot $slot is empty";
1078             } elsif (defined $state->{'slots'}->{$slot}->{'loaded_in'}) {
1079                 $whynot = "slot $slot is currently loaded";
1080             }
1081
1082             if ($whynot) {
1083                 return $self->make_error("failed", $params{'finished_cb'},
1084                         reason => "unknown", message => $whynot);
1085             }
1086
1087             $user_msg_fn->("recoding volume '$label' in slot $slot");
1088             # ok, now erase all knowledge of that label
1089             while (my ($bc, $lb) = each %{$state->{'bc2lb'}}) {
1090                 if ($lb eq $label) {
1091                     delete $state->{'bc2lb'}->{$bc};
1092                     last;
1093                 }
1094             }
1095             while (my ($sl, $inf) = each %{$state->{'slots'}}) {
1096                 if ($inf->{'label'} and $inf->{'label'} eq $label) {
1097                     delete $inf->{'device_status'};
1098                     delete $inf->{'device_error'};
1099                     delete $inf->{'f_type'};
1100                     delete $inf->{'label'};
1101                 }
1102             }
1103
1104             # and add knowledge of the label to the given slot
1105             #$state->{'slots'}->{$slot}->{'device_status'} = $DEVICE_STATUS_SUCCESS;
1106             #$state->{'slots'}->{$slot}->{'f_type'} = $Amanda::Header::F_TAPESTART;
1107             $state->{'slots'}->{$slot}->{'label'} = $label;
1108             if ($state->{'slots'}->{$slot}->{'barcode'}) {
1109                 my $bc = $state->{'slots'}->{$slot}->{'barcode'};
1110                 $state->{'bc2lb'}->{$bc} = $label;
1111             }
1112
1113             # that's it -- no changer motion required
1114             return $params{'finished_cb'}->(undef);
1115         } elsif (exists $params{'changed'} and
1116                  $params{'changed'} =~ /^(.+)=$/) {
1117             $params{'changed'} = $1;
1118             $set_to_unknown = 1;
1119             $steps->{'calculate_slots'}->($steps->{'set_to_unknown'});
1120         } else {
1121             $steps->{'calculate_slots'}->($steps->{'update_slot'});
1122         }
1123     };
1124
1125     step calculate_slots => sub {
1126         my ($update_slot_cb) = shift @_;
1127         if (exists $params{'changed'}) {
1128             # parse the string just like use-slots, using a hash for uniqueness
1129             my %changed;
1130             for my $range (split ',', $params{'changed'}) {
1131                 my ($first, $last) = ($range =~ /(\d+)(?:-(\d+))?/);
1132                 $last = $first unless defined($last);
1133                 for ($first .. $last) {
1134                     $changed{$_} = undef;
1135                 }
1136             }
1137
1138             @slots_to_check = keys %changed;
1139             @slots_to_check = grep { exists $state->{'slots'}->{$_} } @slots_to_check;
1140         } else {
1141             @slots_to_check = keys %{ $state->{'slots'} };
1142         }
1143
1144         # limit the update to allowed slots, and sort them so we don't confuse
1145         # the user with a "random" order
1146         @slots_to_check = grep { $self->_is_slot_allowed($_) } @slots_to_check;
1147         @slots_to_check = grep { $state->{'slots'}->{$_}->{'state'} == Amanda::Changer::SLOT_FULL} @slots_to_check;
1148         @slots_to_check = sort { $a <=> $b } @slots_to_check;
1149
1150         $update_slot_cb->();
1151     };
1152
1153     step set_to_unknown => sub {
1154         return $steps->{'done'}->() if (!@slots_to_check);
1155
1156         my $slot = shift @slots_to_check;
1157         $user_msg_fn->("Removing entry for slot $slot");
1158         if (!defined $state->{'slots'}->{$slot}->{'barcode'}) {
1159             $state->{'slots'}->{$slot}->{'label'} = undef;
1160             $state->{'slots'}->{$slot}->{'device_status'} = undef;
1161             $state->{'slots'}->{$slot}->{'device_error'} = undef;
1162             $state->{'slots'}->{$slot}->{'f_type'} = undef;
1163             if (defined $state->{'slots'}->{$slot}->{'loaded_in'}) {
1164                 my $drive = $state->{'slots'}->{$slot}->{'loaded_in'};
1165                 $state->{'drives'}->{$drive}->{'label'} = undef;
1166                 $state->{'drives'}->{$drive}->{'state'} =
1167                                         Amanda::Changer::SLOT_FULL;
1168             }
1169         }
1170         $steps->{'set_to_unknown'}->();
1171     };
1172
1173     # TODO: parallelize this if multiple drives are available
1174
1175     step update_slot => sub {
1176         return $steps->{'done'}->() if (!@slots_to_check);
1177
1178         my $slot = shift @slots_to_check;
1179         $user_msg_fn->("scanning slot $slot");
1180
1181         $self->load_unlocked(
1182                 slot => $slot,
1183                 res_cb => $steps->{'slot_loaded'},
1184                 state => $state);
1185     };
1186
1187     step slot_loaded => sub {
1188         my ($err, $res) = @_;
1189         if ($err) {
1190             return $params{'finished_cb'}->($err);
1191         }
1192
1193         # load() already fixed up the metadata, so just release; but we have to
1194         # be careful to do an unlocked release.
1195         $res->release(
1196             finished_cb => $steps->{'released'},
1197             unlocked => 1,
1198             state => $state);
1199     };
1200
1201     step released => sub {
1202         my ($err) = @_;
1203         if ($err) {
1204             return $params{'finished_cb'}->($err);
1205         }
1206
1207         $steps->{'update_slot'}->();
1208     };
1209
1210     step done => sub {
1211         $params{'finished_cb'}->(undef);
1212     };
1213 }
1214
1215 sub inventory {
1216     my $self = shift;
1217     my %params = @_;
1218
1219     return if $self->check_error($params{'inventory_cb'});
1220
1221     $self->_with_updated_state(\%params, 'inventory_cb',
1222         sub { $self->inventory_unlocked(@_); });
1223 }
1224
1225 sub inventory_unlocked {
1226     my $self = shift;
1227     my %params = @_;
1228     my $state = $params{'state'};
1229
1230     my @slot_names = sort { $a <=> $b } keys %{ $state->{'slots'} };
1231     my @inv;
1232     for my $slot_name (@slot_names) {
1233         my $i = {};
1234         next unless $self->_is_slot_allowed($slot_name);
1235         my $slot = $state->{'slots'}->{$slot_name};
1236
1237         $i->{'slot'} = $slot_name;
1238         $i->{'state'} = $slot->{'state'};
1239         $i->{'device_status'} = $slot->{'device_status'};
1240         $i->{'device_error'} = $slot->{'device_error'};
1241         $i->{'f_type'} = $slot->{'f_type'};
1242         $i->{'label'} = $slot->{'label'};
1243         $i->{'barcode'} = $slot->{'barcode'}
1244                 if ($slot->{'barcode'});
1245         if (defined $slot->{'loaded_in'}) {
1246             $i->{'loaded_in'} = $slot->{'loaded_in'};
1247             my $drive = $state->{'drives'}->{$slot->{'loaded_in'}};
1248             if ($drive->{'res_info'} and $self->_res_info_verify($drive->{'res_info'})) {
1249                 $i->{'reserved'} = 1;
1250             }
1251         }
1252         $i->{'import_export'} = 1
1253             if $slot->{'ie'};
1254
1255         $i->{'current'} = 1
1256             if $slot_name eq $state->{'current_slot'};
1257
1258         push @inv, $i;
1259     }
1260
1261     $params{'inventory_cb'}->(undef, \@inv);
1262 }
1263
1264 sub move {
1265     my $self = shift;
1266     my %params = @_;
1267
1268     return if $self->check_error($params{'finished_cb'});
1269
1270     $self->_with_updated_state(\%params, 'finished_cb',
1271         sub { $self->move_unlocked(@_); });
1272 }
1273
1274 sub move_unlocked {
1275     my $self = shift;
1276     my %params = @_;
1277     my $state = $params{'state'};
1278
1279     my $from_slot = $params{'from_slot'};
1280     my $to_slot = $params{'to_slot'};
1281
1282     # make sure this is OK
1283     for ($from_slot, $to_slot) {
1284         if (!$self->_is_slot_allowed($_)) {
1285             return $self->make_error("failed", $params{'finished_cb'},
1286                     reason => "invalid",
1287                     message => "invalid slot $_");
1288         }
1289     }
1290
1291     if ($state->{'slots'}->{$from_slot}->{'state'} == Amanda::Changer::SLOT_EMPTY) {
1292         return $self->make_error("failed", $params{'finished_cb'},
1293                 reason => "invalid",
1294                 message => "slot $from_slot is empty");
1295     }
1296
1297     my $in_drive = $state->{'slots'}->{$from_slot}->{'loaded_in'};
1298     if (defined $in_drive) {
1299         my $info = $state->{'drives'}->{$in_drive};
1300         if ($info->{'res_info'} and $self->_res_info_verify($info->{'res_info'})) {
1301             return $self->make_error("failed", $params{'finished_cb'},
1302                     reason => "invalid",
1303                     message => "slot $from_slot is currently loaded and reserved");
1304         }
1305     }
1306
1307     if ($state->{'slots'}->{$to_slot}->{'state'} == Amanda::Changer::SLOT_FULL) {
1308         return $self->make_error("failed", $params{'finished_cb'},
1309                 reason => "invalid",
1310                 message => "slot $to_slot is not empty");
1311     }
1312
1313     # if the destination slot is loaded, then we could do an "exchange", but
1314     # should we?
1315
1316     my $transfer_complete = make_cb(transfer_complete => sub {
1317         my ($err) = @_;
1318         return $params{'finished_cb'}->($err) if $err;
1319
1320         # update metadata
1321         if ($from_slot ne $to_slot) {
1322             my $f = $state->{'slots'}->{$from_slot};
1323             my $t = $state->{'slots'}->{$to_slot};
1324
1325             $t->{'device_status'} = $f->{'device_status'};
1326             $f->{'device_status'} = undef;
1327
1328             $t->{'state'} = $f->{'state'};
1329             $f->{'state'} = Amanda::Changer::SLOT_EMPTY;
1330
1331             $t->{'f_type'} = $f->{'f_type'};
1332             $f->{'f_type'} = undef;
1333
1334             $t->{'label'} = $f->{'label'};
1335             $f->{'label'} = undef;
1336
1337             $t->{'barcode'} = $f->{'barcode'};
1338             $f->{'barcode'} = undef;
1339         }
1340
1341         # properly represent the unload operation, if it was performed
1342         if (defined $in_drive) {
1343             $state->{'slots'}->{$from_slot}->{'loaded_in'} = undef;
1344             $state->{'slots'}->{$to_slot}->{'loaded_in'} = undef;
1345
1346             $state->{'drives'}->{$in_drive}->{'state'} =
1347                                                     Amanda::Changer::SLOT_EMPTY;
1348             $state->{'drives'}->{$in_drive}->{'label'} = undef;
1349             $state->{'drives'}->{$in_drive}->{'barcode'} = undef;
1350             $state->{'drives'}->{$in_drive}->{'orig_slot'} = undef;
1351         }
1352
1353         $params{'finished_cb'}->();
1354     });
1355
1356     # if the source slot is loaded, then this is just a directed unload operation;
1357     # otherwise, it's a transfer.
1358     if (defined $in_drive) {
1359         Amanda::Debug::debug("move(): unloading drive $in_drive to slot $to_slot");
1360         $self->{'interface'}->unload($in_drive, $to_slot, $transfer_complete);
1361     } else {
1362         $self->{'interface'}->transfer($from_slot, $to_slot, $transfer_complete);
1363     }
1364 }
1365
1366 ##
1367 # Utilities
1368
1369 # calculate the next highest non-empty slot after $slot (assuming that
1370 # the changer status has been updated)
1371 sub _get_next_slot {
1372     my $self = shift;
1373     my ($state, $slot) = @_;
1374
1375     my @nonempty = sort { $a <=> $b } grep {
1376         $state->{'slots'}->{$_}->{'state'} == Amanda::Changer::SLOT_FULL
1377         and $self->_is_slot_allowed($_)
1378     } keys(%{$state->{'slots'}});
1379
1380     my @higher = grep { $_ > $slot } @nonempty;
1381
1382     # return the next higher slot, or the first nonempty slot (to loop around)
1383     return $higher[0] if (@higher);
1384     return $nonempty[0];
1385 }
1386
1387 # is $slot in the slots specified by the use-slots property?
1388 sub _is_slot_allowed {
1389     my $self = shift;
1390     my ($slot) = @_;
1391
1392     # if use-slots is not specified, all slots are available
1393     return 1 unless ($self->{'use_slots'});
1394
1395     for my $range (split ',', $self->{'use_slots'}) {
1396         my ($first, $last) = ($range =~ /(\d+)(?:-(\d+))?/);
1397         $last = $first unless defined($last);
1398         return 1 if ($slot >= $first and $slot <= $last);
1399     }
1400
1401     return 0;
1402 }
1403
1404 # add a prefix and call Amanda::Debug::debug
1405 sub _debug {
1406     my $self = shift;
1407     my ($msg) = @_;
1408     # chg_name is not set until *after* the constructor finishes
1409     my $chg_name = $self->{'chg_name'} || $self->{'class_name'};
1410     debug("$chg_name: $msg");
1411 }
1412
1413 ##
1414 # Timing management
1415
1416 # Wait until the delay from the last operation has expired, and call the
1417 # given callback with the given arguments
1418 sub _after_delay {
1419     my $self = shift;
1420     my ($state, $cb, @args) = @_;
1421
1422     confess("undefined \$cb") unless (defined $cb);
1423
1424     # if the current time is before $start, then we'll perform the action anyway; this
1425     # saves us from long delays when clocks fall out of sync or run backward, but delays
1426     # are short.
1427     my ($start, $end, $now);
1428     $start = $state->{'last_operation_time'};
1429     if (!defined $start) {
1430         return $cb->(@args);
1431     }
1432
1433     $end = $start + $state->{'last_operation_delay'};
1434     $now = time;
1435
1436     if ($now >= $start and $now < $end) {
1437         Amanda::MainLoop::call_after(1000 * ($end - $now), $cb, @args);
1438     } else {
1439         return $cb->(@args);
1440     }
1441 }
1442
1443 # set the delay parameters in the statefile
1444 sub _set_delay {
1445     my $self = shift;
1446     my ($state, $delay) = @_;
1447
1448     $state->{'last_operation_time'} = time;
1449     $state->{'last_operation_delay'} = $delay;
1450 }
1451
1452 ##
1453 # Statefile management
1454
1455 # wrapper around Amanda::Changer's with_locked_state to lock the statefile and
1456 # then update the state with the results of the 'status' command.
1457 #
1458 # Like with_locked_state, this method assumes the keyword-based parameter
1459 # style, and adds a 'state' parameter with the new state.  Also like
1460 # with_locked_state, it replaces the $cbname key with a wrapped version of that
1461 # callback.  It then calls $sub.
1462 sub _with_updated_state {
1463     my $self = shift;
1464     my ($paramsref, $cbname, $sub) = @_;
1465     my %params = %$paramsref;
1466     my $state;
1467
1468     my $steps = define_steps
1469         cb_ref => \$paramsref->{$cbname};
1470
1471     step start => sub {
1472         $self->with_locked_state($self->{'statefile'},
1473             $params{$cbname}, $steps->{'got_lock'});
1474     };
1475
1476     step got_lock => sub {
1477         ($state, my $new_cb) = @_;
1478
1479         # set up params for calling through to $sub later
1480         $params{'state'} = $state;
1481         $params{$cbname} = $new_cb;
1482
1483         if (!keys %$state) {
1484             $state->{'slots'} = {};
1485             $state->{'drives'} = {};
1486             $state->{'drive_lru'} = [];
1487             $state->{'bc2lb'} = {};
1488             $state->{'current_slot'} = -1;
1489         }
1490
1491         # this is for testing ONLY!
1492         $self->{'__last_state'} = $state;
1493
1494         # if it's not time for another run of the status command yet, then just skip to
1495         # the end.
1496         if (defined $state->{'last_status'}
1497             and time < $state->{'last_status'} + $self->{'status_interval'}) {
1498             $self->_debug("too early for another 'status' invocation");
1499             $steps->{'done'}->();
1500         } else {
1501             $steps->{'wait'}->();
1502         }
1503     };
1504
1505     step wait => sub {
1506         $self->_after_delay($state, $steps->{'call_status'});
1507     };
1508
1509     step call_status => sub {
1510         $self->{'interface'}->status($steps->{'status_cb'});
1511     };
1512
1513     step status_cb => sub {
1514         my ($err, $status) = @_;
1515         if ($err) {
1516             return $self->make_error("fatal", $params{$cbname},
1517                 message => $err);
1518         }
1519
1520         $state->{'last_status'} = time;
1521         $self->_debug("updating state");
1522
1523         # process the results; $status can update our slot->label
1524         # mapping, but the barcode->label mapping stays the same.
1525
1526         my $new_slots = {};
1527         my ($drv, $slot, $info);
1528
1529         # note that loaded_in is always undef; it will be set correctly
1530         # when the drives are scanned
1531         while (($slot, $info) = each %{$status->{'slots'}}) {
1532             if ($info->{'empty'}) {
1533                 # empty slot
1534                 $new_slots->{$slot} = {
1535                     state => Amanda::Changer::SLOT_EMPTY,
1536                     device_status => undef,
1537                     device_error => undef,
1538                     f_type => undef,
1539                     label => undef,
1540                     barcode => undef,
1541                     loaded_in => undef,
1542                     ie => $info->{'ie'},
1543                 };
1544                 next;
1545             }
1546
1547             if (defined $info->{'barcode'}) {
1548
1549                 my $label = $state->{'bc2lb'}->{$info->{'barcode'}};
1550
1551                 $new_slots->{$slot} = {
1552                     state => Amanda::Changer::SLOT_FULL,
1553                     device_status => $state->{'slots'}->{$slot}->{device_status},
1554                     device_error => $state->{'slots'}->{$slot}->{device_error},
1555                     f_type => $state->{'slots'}->{$slot}->{f_type},
1556                     label => $label,
1557                     barcode => $info->{'barcode'},
1558                     loaded_in => undef,
1559                     ie => $info->{'ie'},
1560                 };
1561             } else {
1562                 # assume the status of this slot has not changed since the last
1563                 # time we looked at it, although mark it as not loaded in a slot
1564                 if (exists $state->{'slots'}->{$slot}) {
1565                     $new_slots->{$slot} = $state->{'slots'}->{$slot};
1566                     $new_slots->{$slot}->{'loaded_in'} = undef;
1567                 } else {
1568                     $new_slots->{$slot} = {
1569                         state => Amanda::Changer::SLOT_FULL,
1570                         device_status => undef,
1571                         device_error => undef,
1572                         f_type => undef,
1573                         label => undef,
1574                         barcode => undef,
1575                         loaded_in => undef,
1576                         ie => $info->{'ie'},
1577                     };
1578                 }
1579             }
1580         }
1581         my $old_slots_state = $state->{'slots'};
1582         $state->{'slots'} = $new_slots;
1583
1584         # now handle the drives
1585         my $new_drives = {};
1586         while (($drv, $info) = each %{$status->{'drives'}}) {
1587             my $old_drive = $state->{'drives'}->{$drv};
1588
1589             # if this drive still has a valid reservation, don't change it
1590             if (defined $old_drive->{'res_info'}
1591                         and $self->_res_info_verify($old_drive->{'res_info'})) {
1592                 $new_drives->{$drv} = $old_drive;
1593                 next;
1594             }
1595
1596             # if the drive is empty, this is pretty easy
1597             if (!defined $info) {
1598                 $new_drives->{$drv} = {
1599                     state => Amanda::Changer::SLOT_EMPTY,
1600                     label => undef,
1601                     barcode => undef,
1602                     orig_slot => undef,
1603                 };
1604                 next;
1605             }
1606
1607             # trust our own orig_slot over that from the changer, if possible,
1608             # as some changers do not report this information accurately
1609             my ($orig_slot, $label);
1610             if (defined $old_drive->{'orig_slot'}) {
1611                 $orig_slot = $old_drive->{'orig_slot'};
1612                 $label = $old_drive->{'label'};
1613             }
1614
1615             # but don't trust it if the barcode has changed
1616             if (defined $info->{'barcode'}
1617                     and defined $old_drive->{'barcode'}
1618                     and $info->{'barcode'} ne $old_drive->{'barcode'}) {
1619                 $orig_slot = undef;
1620                 $label = undef;
1621             }
1622
1623             # get the robot's notion of the original slot if we don't know ourselves
1624             if (!defined $orig_slot) {
1625                 $orig_slot = $info->{'orig_slot'};
1626             }
1627
1628             # but if there's a tape in that slot, then we've got a problem
1629             if (defined $orig_slot
1630                     and $state->{'slots'}->{$orig_slot}->{'state'} != Amanda::Changer::SLOT_EMPTY) {
1631                 warning("mtx indicates tape in drive $drv should go to slot $orig_slot, " .
1632                         "but that slot is not empty.");
1633                 $orig_slot = undef;
1634                 for my $slot (keys %{ $state->{'slots'} }) {
1635                     if ($state->{'slots'}->{$slot}->{'state'} == Amanda::Changer::SLOT_EMPTY) {
1636                         $orig_slot = $slot;
1637                         last;
1638                     }
1639                 }
1640                 if (!defined $orig_slot) {
1641                     warning("cannot find an empty slot for the tape in drive $drv");
1642                 }
1643             }
1644
1645             # and look up the label by barcode if possible
1646             if (!defined $label && defined $info->{'barcode'}) {
1647                 $label = $state->{'bc2lb'}->{$info->{'barcode'}};
1648             }
1649
1650             $new_drives->{$drv} = {
1651                 state => Amanda::Changer::SLOT_FULL,
1652                 label => $label,
1653                 barcode => $info->{'barcode'},
1654                 orig_slot => $orig_slot,
1655             };
1656         }
1657         $state->{'drives'} = $new_drives;
1658
1659         # update the loaded_in info for the relevant slots
1660         while (($drv, $info) = each %$new_drives) {
1661             # also update the slots with the relevant 'loaded_in' info
1662             if (defined $info->{'orig_slot'}) {
1663                 my $old_state = $old_slots_state->{$info->{'orig_slot'}};
1664                 $state->{'slots'}->{$info->{'orig_slot'}} = {
1665                     state => $info->{'state'},
1666                     device_status => $old_state->{'device_status'},
1667                     device_error => $old_state->{'device_error'},
1668                     f_type => $old_state->{'f_type'},
1669                     label => $info->{'label'},
1670                     barcode => $info->{'barcode'},
1671                     loaded_in => $drv,
1672                 };
1673             }
1674         }
1675
1676         # sanity check that we don't have tape-device info for nonexistent drives
1677         for my $dr (@{$self->{'driveorder'}}) {
1678             if (!exists $state->{'drives'}->{$dr}) {
1679                 warning("tape-device property specified for drive $dr, but no such " .
1680                         "drive exists in the library");
1681             }
1682         }
1683
1684         if ($state->{'current_slot'} == -1) {
1685             $state->{'current_slot'} = $self->_get_next_slot($state, -1);
1686         }
1687
1688         $steps->{'done'}->();
1689     };
1690
1691     step done => sub {
1692         # finally, call through to the user's method; $params{$cbname} has been
1693         # properly patched to release the state lock when this method is done.
1694         $sub->(%params);
1695     };
1696 }
1697
1698 ##
1699 # reservation records
1700
1701 # A reservation record is recorded in the statefile, and is distinct from an
1702 # Amanda::Changer::robot:Reservation object in that it is seen by all users of
1703 # the tape device, whether in this process or another.
1704 #
1705 # This is abstracted out to enable support for a more robust mechanism than
1706 # caching a pid.
1707
1708 sub _res_info_new {
1709     my $self = shift;
1710     return { pid => $$, };
1711 }
1712
1713 sub _res_info_verify {
1714     my $self = shift;
1715     my ($res_info) = @_;
1716
1717     # true if this is our reservation
1718     return 1 if ($res_info->{'pid'} == $$);
1719
1720     # or if the process is dead
1721     return kill 0, $res_info->{'pid'};
1722 }
1723
1724 sub _res_info_is_mine {
1725     my $self = shift;
1726     my ($res_info) = @_;
1727
1728     return 1 if ($res_info and $res_info->{'pid'} == $$);
1729 }
1730
1731 package Amanda::Changer::robot::Reservation;
1732 use vars qw( @ISA );
1733 use Amanda::Debug qw( debug warning );
1734 @ISA = qw( Amanda::Changer::Reservation );
1735
1736 sub new {
1737     my $class = shift;
1738     my ($chg, $slot, $drive, $device, $barcode) = @_;
1739     my $self = Amanda::Changer::Reservation::new($class);
1740
1741     $self->{'chg'} = $chg;
1742
1743     $self->{'drive'} = $drive;
1744     $self->{'device'} = $device;
1745     $self->{'this_slot'} = $slot;
1746     $self->{'barcode'} = $barcode;
1747
1748     return $self;
1749 }
1750
1751 sub do_release {
1752     my $self = shift;
1753     my %params = @_;
1754
1755     # if we're in global cleanup and the changer is already dead,
1756     # then never mind
1757     return unless $self->{'chg'};
1758
1759     # unref the device, for good measure
1760     $self->{'device'} = undef;
1761
1762     # punt this method off to the changer itself, optionally calling
1763     # the unlocked version if we have the 'state' parameter
1764     if (exists $params{'unlocked'} and exists $params{'state'}) {
1765         $self->{'chg'}->_release_unlocked(drive => $self->{'drive'}, %params);
1766     } else {
1767         $self->{'chg'}->_release(drive => $self->{'drive'}, %params);
1768     }
1769 }
1770
1771 sub set_label {
1772     my $self = shift;
1773     my %params = @_;
1774
1775     return unless $self->{'chg'};
1776     $self->{'chg'}->_set_label(drive => $self->{'drive'},
1777                                dev => $self->{device}, %params);
1778 }
1779
1780 package Amanda::Changer::robot::Interface;
1781
1782 # The physical interface to the changer is abstracted out to allow several
1783 # implementations (see chg-ndmp for one of them).  This API is "reasonably
1784 # stable", but is really only known to this changer and its subclasses, so it's
1785 # not documented in POD.  The methods are:
1786 #
1787 #   $iface->inquiry($inquiry_cb);
1788 #
1789 # Inquire as to the relevant information about the changer.  The result is a
1790 # hash table of lowercased key names and values, $info.  The inquiry_cb is
1791 # called as $inquiry_cb->($errmsg, $info).  The resulting strings have quotes
1792 # and whitespace stripped.  Keys include 'vendor id' and 'product id'.
1793 #
1794 #   $iface->status($status_cb)
1795 #
1796 # Get the READ ELEMENT STATUS output for the changer.  The status_cb is called
1797 # as $status_cb->($errmsg, $status).  $status is a hash with keys 'drives' and
1798 # 'slots', each of which is a hash indexed by the element address (note that drive
1799 # element addresses can and usually do overlap with slots.  The values of the slots
1800 # hash are hashes with keys
1801 #  - 'empty' (1 if the slot is empty)
1802 #  - 'barcode' (which may be undef if the changer does not support barcodes)
1803 #  - 'ie' (a boolean indicating whether this is an import/export slot).
1804 # The values of the drives are undef for empty drive, or hashes with keys
1805 #  - 'barcode' (which may be undef if the changer does not support barcodes)
1806 #  - 'orig_slot' (slot from which this volume was taken, if known)
1807 #
1808 #   $iface->load($slot, $drive, $finished_cb)
1809 #
1810 # Load $slot into $drive.  The finished_cb gets a single argument, $error,
1811 # which is only defined if an error occurred.  Note that this does not
1812 # necessarily wait until the load operation is complete (most drives give
1813 # no such indication) (this method also implements unload, if $un=1)
1814 #
1815 #   $iface->unload($drive, $slot, $finished_cb);
1816 #
1817 # Unload $drive into $slot.  Finished_cb is just as for load().
1818 #
1819 #   $iface->eject($drive_name, $finished_cb);
1820 #
1821 # Eject $drive_name (named /dev/whatever, not the drive number), and call finished_cb.
1822 #
1823 #   $iface->transfer($src_slot, $dst_slot, $finished_cb);
1824 #
1825 # Move the tape in $src_slot into $dst_slot.  The finished_cb gets a single
1826 # argument, $error, which is only defined if an error occurred.  Note that this
1827 # does not necessarily wait until the load operation is complete.
1828
1829 package Amanda::Changer::robot::Interface::MTX;
1830
1831 use Amanda::Paths;
1832 use Amanda::Config qw( :getconf );
1833 use Amanda::Debug qw( debug warning );
1834 use Amanda::MainLoop qw( :GIOCondition synchronized make_cb define_steps step );
1835 use Amanda::Device qw( :constants );
1836
1837 sub new {
1838     my $class = shift;
1839     my ($device_name, $mtx, $ignore_barcodes) = @_;
1840
1841     unless (-e $device_name) {
1842         return Amanda::Changer->make_error("fatal", undef,
1843             message => "'$device_name' not found");
1844     }
1845
1846     return bless {
1847         # This object uses a big lock to block *all* operations, not just mtx
1848         # invocations.  This allows us to add delays to certain operations, while still
1849         # holding the lock.
1850         lock => [],
1851         device_name => $device_name,
1852         mtx => $mtx,
1853         ignore_barcodes => $ignore_barcodes,
1854     }, $class;
1855 }
1856
1857 sub inquiry {
1858     my $self = shift;
1859     my ($inquiry_cb) = @_;
1860
1861     synchronized($self->{'lock'}, $inquiry_cb, sub {
1862         my ($inquiry_cb) = @_;
1863         my $sys_cb = make_cb(sys_cb => sub {
1864             my ($exitstatus, $output) = @_;
1865             if ($exitstatus != 0) {
1866                 return $inquiry_cb->("error from mtx: " . $output, {});
1867             } else {
1868                 my %info;
1869                 for my $line (split '\n', $output) {
1870                     if (my ($k, $v) = ($line =~ /^(.*):\s*(.*)$/)) {
1871                         $v =~ s/^'(.*)'$/$1/;
1872                         $v =~ s/\s*$//;
1873                         $info{lc $k} = $v;
1874                     }
1875                 }
1876                 return $inquiry_cb->(undef, \%info);
1877             }
1878
1879         });
1880         $self->_run_system_command($sys_cb,
1881             $self->{'mtx'}, "-f", $self->{'device_name'}, 'inquiry');
1882     });
1883 }
1884
1885 sub status {
1886     my $self = shift;
1887     my ($status_cb) = @_;
1888
1889     synchronized($self->{'lock'}, $status_cb, sub {
1890         my ($status_cb) = @_;
1891         my ($counter) = 120;
1892
1893         my $sys_cb;
1894         my $run_mtx = make_cb(run_mtx => sub {
1895             my @nobarcode = ('nobarcode') if $self->{'ignore_barcodes'};
1896             $self->_run_system_command($sys_cb,
1897                     $self->{'mtx'}, "-f", $self->{'device_name'}, @nobarcode,
1898                     'status');
1899         });
1900
1901         $sys_cb = make_cb(sys_cb => sub {
1902             my ($exitstatus, $output) = @_;
1903             if ($exitstatus != 0) {
1904                 my $err = $output;
1905                 # if it's a regular SCSI error, just show the sense key
1906                 my ($sensekey) = ($err =~ /mtx: Request Sense: Sense Key=(.*)\n/);
1907                 $err = "SCSI error; Sense Key=$sensekey" if $sensekey;
1908                 $counter--;
1909                 if ($sensekey eq "Not Ready" and $counter > 0) {
1910                     debug("$output");
1911                     return Amanda::MainLoop::call_after(1000, $run_mtx);
1912                 }
1913                 return $status_cb->("error from mtx: " . $err, {});
1914             } else {
1915                 my %status;
1916                 for my $line (split '\n', $output) {
1917                     my ($slot, $ie, $slinfo);
1918
1919                     # drives (data transfer elements)
1920                     if (($slot, $slinfo) = ($line =~
1921                                 /^Data Transfer Element\s*(\d+)?\s*:\s*(.*)/i)) {
1922                         # assume 0 when not given a drive #
1923                         $slot = 0 unless defined $slot;
1924                         if ($slinfo =~ /^Empty/i) {
1925                             $status{'drives'}->{$slot} = undef;
1926                         } elsif ($slinfo =~ /^Full/i) {
1927                             my ($barcode, $orig_slot);
1928                             ($barcode) = ($slinfo =~ /:VolumeTag\s*=\s*(\S+)/i);
1929                             ($orig_slot) = ($slinfo =~ /\(Storage Element (\d+) Loaded\)/i);
1930                             $status{'drives'}->{$slot} = {
1931                                 barcode => $barcode,
1932                                 orig_slot => $orig_slot,
1933                             };
1934                         }
1935
1936                     # slots (storage elements)
1937                     } elsif (($slot, $ie, $slinfo) = ($line =~
1938                                 /^\s*Storage Element\s*(\d+)\s*(IMPORT\/EXPORT)?\s*:\s*(.*)/i)) {
1939                         $ie = $ie? 1 : 0;
1940                         if ($slinfo =~ /^Empty/i) {
1941                             $status{'slots'}->{$slot} = {
1942                                 empty => 1,
1943                                 ie => $ie,
1944                             };
1945                         } elsif ($slinfo =~ /^Full/i) {
1946                             my $barcode;
1947                             ($barcode) = ($slinfo =~ /:VolumeTag\s*=\s*(\S+)/i)
1948                                 unless ($self->{'ignore_barcodes'});
1949                             $status{'slots'}->{$slot} = {
1950                                 barcode => $barcode,
1951                                 ie => $ie,
1952                             };
1953                         }
1954                     }
1955                 }
1956
1957                 return $status_cb->(undef, \%status);
1958             }
1959
1960         });
1961         $run_mtx->();
1962     });
1963 }
1964
1965 sub load {
1966     my $self = shift;
1967     my ($slot, $drive, $finished_cb, $un) = @_;
1968
1969     synchronized($self->{'lock'}, $finished_cb, sub {
1970         my ($finished_cb) = @_;
1971
1972         my $sys_cb = make_cb(sys_cb => sub {
1973             my ($exitstatus, $output) = @_;
1974             if ($exitstatus != 0) {
1975                 return $finished_cb->("error from mtx: " . $output);
1976             } else {
1977                 return $finished_cb->(undef);
1978             }
1979
1980         });
1981
1982         $self->_run_system_command($sys_cb,
1983             $self->{'mtx'}, "-f", $self->{'device_name'},
1984                             $un? 'unload':'load', $slot, $drive);
1985     });
1986 }
1987
1988 sub unload {
1989     my $self = shift;
1990     my ($drive, $slot, $finished_cb) = @_;
1991     return $self->load($slot, $drive, $finished_cb, 1);
1992 }
1993
1994 sub transfer {
1995     my $self = shift;
1996     my ($src_slot, $dst_slot, $finished_cb) = @_;
1997
1998     synchronized($self->{'lock'}, $finished_cb, sub {
1999         my ($finished_cb) = @_;
2000
2001         my $sys_cb = make_cb(sys_cb => sub {
2002             my ($exitstatus, $output) = @_;
2003             if ($exitstatus != 0) {
2004                 return $finished_cb->("error from mtx: " . $output);
2005             } else {
2006                 return $finished_cb->(undef);
2007             }
2008
2009         });
2010         $self->_run_system_command($sys_cb,
2011             $self->{'mtx'}, "-f", $self->{'device_name'},
2012                             'transfer', $src_slot, $dst_slot);
2013     });
2014 }
2015
2016 # Run 'mtx' and capture the output.  Standard output and error
2017 # are lumped together.
2018 #
2019 # @param $sys_cb: called with ($exitstatus, $output)
2020 # @param @args: args to pass to exec()
2021 sub _run_system_command {
2022     my ($self, $sys_cb, @args) = @_;
2023
2024     debug("invoking " . join(" ", @args));
2025
2026     my ($readfd, $writefd) = POSIX::pipe();
2027     if (!defined($writefd)) {
2028         die("Error creating pipe: $!");
2029     }
2030
2031     my $pid = fork();
2032     if (!defined($pid) or $pid < 0) {
2033         die("Can't fork to run changer script: $!");
2034     }
2035
2036     if (!$pid) {
2037         ## child
2038
2039         # get our file-handle house in order
2040         POSIX::close($readfd);
2041         POSIX::dup2($writefd, 1);
2042         POSIX::dup2($writefd, 2);
2043         POSIX::close($writefd);
2044
2045         %ENV = Amanda::Util::safe_env();
2046
2047         { exec { $args[0] } @args; } # braces protect against warning
2048         exit 127;
2049     }
2050
2051     ## parent
2052
2053     # clean up file descriptors from the fork
2054     POSIX::close($writefd);
2055
2056     # the callbacks that follow share these lexical variables
2057     my $child_eof = 0;
2058     my $child_output = '';
2059     my $child_dead = 0;
2060     my $child_exit_status = 0;
2061     my ($fdsrc, $cwsrc);
2062     my $open_sources = 0;
2063
2064     my $steps = define_steps
2065         cb_ref => \$sys_cb;
2066
2067     step setup => sub {
2068         $open_sources++;
2069         Amanda::MainLoop::fd_source($readfd, $G_IO_IN | $G_IO_ERR | $G_IO_HUP)
2070             ->set_callback($steps->{'fd_source_cb'});
2071
2072         $open_sources++;
2073         Amanda::MainLoop::child_watch_source($pid)
2074             ->set_callback($steps->{'child_watch_source_cb'});
2075     };
2076
2077     step immediate => 1,
2078          fd_source_cb => sub {
2079         my ($fdsrc) = @_;
2080         my ($len, $bytes);
2081         $len = POSIX::read($readfd, $bytes, 1024);
2082
2083         # if we got an EOF, shut things down.
2084         if ($len == 0) {
2085             $child_eof = 1;
2086             POSIX::close($readfd);
2087             $fdsrc->remove();
2088             $fdsrc = undef; # break a reference loop
2089             $steps->{'maybe_finished'}->();
2090         } else {
2091             # otherwise, just keep the bytes
2092             $child_output .= $bytes;
2093         }
2094     };
2095
2096     step immediate => 1,
2097          child_watch_source_cb => sub {
2098         my ($cwsrc, $got_pid, $got_status) = @_;
2099         $cwsrc->remove();
2100         $cwsrc = undef; # break a reference loop
2101         $child_dead = 1;
2102         $child_exit_status = $got_status;
2103
2104         $steps->{'maybe_finished'}->();
2105     };
2106
2107     step maybe_finished => sub {
2108         return if --$open_sources;
2109
2110         # everything is finished -- process the results and invoke the callback
2111         chomp $child_output;
2112
2113         # let the callback take care of any further interpretation
2114         my $exitval = POSIX::WEXITSTATUS($child_exit_status);
2115         $sys_cb->($exitval, $child_output);
2116     };
2117 }
2118
2119 1;