1 # Copyright (c) 2008,2009,2010 Zmanda, Inc. All Rights Reserved.
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.
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
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
16 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
19 package Amanda::Changer::multi;
24 @ISA = qw( Amanda::Changer );
26 use File::Glob qw( :glob );
28 use Amanda::Config qw( :getconf );
32 use Amanda::Device qw( :constants );
36 Amanda::Changer::multi
40 This changer operates with a list of device, specified in the tpchanger
43 See the amanda-changers(7) manpage for usage information.
49 # The device state is shared between all changers accessing the same changer.
50 # It is a hash with keys:
51 # current_slot - the unaliased device name of the current slot
54 # The 'slots' key is a hash, with unaliased device name as keys and hashes
55 # as values. Each slot's hash has keys:
56 # pid - the pid that reserved that slot.
57 # state - SLOT_FULL/SLOT_EMPTY/SLOT_UNKNOWN
58 # device_status - the status of the device after the open or read_label
59 # f_type - the F_TYPE of the fileheader.
60 # label - the label, if known, of the volume in this slot
62 # $self is a hash with keys:
63 # slot : slot number of the current slot
64 # slots : An array with all slot names
65 # unaliased : A hash with slot number as keys and unaliased device name
67 # slot_name : A hash with slot number as keys and device name as value
68 # number : A hash with unaliased device name as keys and slot number
70 # config : The Amanda::Changer::Config for this changer
71 # state_filename : The filename of the state file
72 # first_slot : The number of the first slot
73 # last_slot : The number of the last slot + 1
77 my ($config, $tpchanger) = @_;
78 my $devices = $tpchanger;
79 $devices =~ s/^chg-multi://g;
80 my (@slots) = Amanda::Util::expand_braced_alternates($devices);
82 unless (scalar @slots != 0) {
83 return Amanda::Changer->make_error("fatal", undef,
84 message => "no devices specified");
87 my $properties = $config->{'properties'};
89 if (exists $properties->{'first-slot'}) {
90 $first_slot = @{$properties->{'first-slot'}->{'values'}}[0];
96 my $last_slot = $first_slot;
97 foreach my $slot_name (@slots) {
98 my $unaliased_name = Amanda::Device::unaliased_name($slot_name);
99 $number{$unaliased_name} = $last_slot;
100 $unaliased{$last_slot} = $unaliased_name;
101 $slot_name{$last_slot} = $slot_name;
105 if (!defined $config->{changerfile} ||
106 $config->{changerfile} eq "") {
107 return Amanda::Changer->make_error("fatal", undef,
109 message => "no changerfile specified for changer '$config->{name}'");
112 my $state_filename = Amanda::Config::config_dir_relative($config->{'changerfile'});
116 unaliased => \%unaliased,
117 slot_name => \%slot_name,
120 state_filename => $state_filename,
121 first_slot => $first_slot,
122 last_slot => $last_slot,
125 bless ($self, $class);
132 my $old_res_cb = $params{'res_cb'};
135 $self->validate_params('load', \%params);
137 return if $self->check_error($params{'res_cb'});
139 $self->with_locked_state($self->{'state_filename'},
140 $params{'res_cb'}, sub {
141 my ($state, $res_cb) = @_;
143 $params{'state'} = $state;
144 # overwrite the callback for _load_by_xxx
145 $params{'res_cb'} = $res_cb;
147 if (exists $params{'slot'} or exists $params{'relative_slot'}) {
148 $self->_load_by_slot(%params);
149 } elsif (exists $params{'label'}) {
150 $self->_load_by_label(%params);
157 my ($key, %params) = @_;
160 return if $self->check_error($params{'info_cb'});
162 # no need for synchronization -- all of these values are static
164 if ($key eq 'num_slots') {
165 $results{$key} = $self->{last_slot} - $self->{first_slot};
166 } elsif ($key eq 'vendor_string') {
167 $results{$key} = 'chg-multi'; # mostly just for testing
168 } elsif ($key eq 'fast_search') {
172 $params{'info_cb'}->(undef, %results) if $params{'info_cb'};
179 return if $self->check_error($params{'finished_cb'});
181 $self->with_locked_state($self->{'state_filename'},
182 $params{'finished_cb'}, sub {
183 my ($state, $finished_cb) = @_;
186 $params{state} = $state;
187 $slot = $self->{first_slot};
188 $self->{slot} = $slot;
189 $self->_set_current($state, $slot);
200 return if $self->check_error($params{'finished_cb'});
202 $self->with_locked_state($self->{'state_filename'},
203 $params{'finished_cb'}, sub {
204 my ($state, $finished_cb) = @_;
207 $params{state} = $state;
208 if (!exists $params{'drive'}) {
209 $drive = $self->_get_current($params{state});
211 $drive = $params{'drive'};
213 if (!defined $self->{unaliased}->{$drive}) {
214 return $self->make_error("failed", $finished_cb,
216 message => "Invalid slot '$drive'");
219 Amanda::Debug::debug("ejecting drive $drive");
220 my $device = Amanda::Device->new($self->{slot_name}->{$drive});
221 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
222 return $self->make_error("failed", $finished_cb,
224 message => $device->error_or_status);
226 if (my $err = $self->{'config'}->configure_device($device)) {
227 return $self->make_error("failed", $params{'res_cb'},
232 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
233 return $self->make_error("failed", $finished_cb,
235 message => $device->error_or_status);
248 my $set_to_unknown = 0;
250 my $user_msg_fn = $params{'user_msg_fn'};
251 $user_msg_fn ||= sub { Amanda::Debug::info("chg-multi: " . $_[0]); };
253 my $steps = define_steps
254 cb_ref => \$params{'finished_cb'};
257 $self->with_locked_state($self->{'state_filename'},
258 $params{'finished_cb'}, sub {
259 my ($state, $finished_cb) = @_;
261 $params{state} = $state;
262 $params{'finished_cb'} = $finished_cb;
264 $steps->{'handle_assignment'}->();
268 step handle_assignment => sub {
269 $state = $params{state};
270 # check for the SL=LABEL format, and handle it here
271 if (exists $params{'changed'} and
272 $params{'changed'} =~ /^\d+=\S+$/) {
273 my ($slot, $label) = ($params{'changed'} =~ /^(\d+)=(\S+)$/);
275 # let's list the reasons we *can't* do what the user has asked
277 if (!exists $self->{unaliased}->{$slot}) {
278 $whynot = "slot $slot does not exist";
282 return $self->make_error("failed", $params{'finished_cb'},
283 reason => "unknown", message => $whynot);
286 $user_msg_fn->("recording volume '$label' in slot $slot");
287 # ok, now erase all knowledge of that label
288 while (my ($sl, $inf) = each %{$state->{'slots'}}) {
289 if ($inf->{'label'} and $inf->{'label'} eq $label) {
290 $inf->{'label'} = undef;
294 # and add knowledge of the label to the given slot
295 my $unaliased = $self->{unaliased}->{$slot};
296 $state->{'slots'}->{$unaliased}->{'label'} = $label;
298 # that's it -- no changer motion required
299 return $params{'finished_cb'}->(undef);
300 } elsif (exists $params{'changed'} and
301 $params{'changed'} =~ /^(.+)=$/) {
302 $params{'changed'} = $1;
304 $steps->{'calculate_slots'}->();
306 $steps->{'calculate_slots'}->();
310 step calculate_slots => sub {
311 if (exists $params{'changed'}) {
312 # parse the string just like use-slots, using a hash for uniqueness
314 for my $range (split ',', $params{'changed'}) {
315 my ($first, $last) = ($range =~ /(\d+)(?:-(\d+))?/);
316 $last = $first unless defined($last);
317 for ($first .. $last) {
318 $changed{$_} = undef;
322 @slots_to_check = keys %changed;
323 @slots_to_check = grep { exists $self->{'unaliased'}->{$_} } @slots_to_check;
325 @slots_to_check = keys %{ $self->{unaliased} };
328 # sort them so we don't confuse the user with a "random" order
329 @slots_to_check = sort @slots_to_check;
331 $steps->{'update_slot'}->();
334 # TODO: parallelize, we have one drive by slot
336 step update_slot => sub {
337 return $steps->{'done'}->() if (!@slots_to_check);
338 my $slot = shift @slots_to_check;
339 if ($self->_is_slot_in_use($state, $slot)) {
340 $user_msg_fn->("Slot $slot is already in use");
341 return $steps->{'update_slot'}->();
344 if ($set_to_unknown == 1) {
345 $user_msg_fn->("removing entry for slot $slot");
346 my $unaliased = $self->{unaliased}->{$slot};
347 delete $state->{slots}->{$unaliased};
348 return $steps->{'update_slot'}->();
350 $user_msg_fn->("scanning slot $slot");
351 $params{'slot'} = $slot;
352 $params{'res_cb'} = $steps->{'slot_loaded'};
353 $self->_load_by_slot(%params);
357 step slot_loaded => sub {
358 my ($err, $res) = @_;
360 return $params{'finished_cb'}->($err);
363 my $slot = $res->{'this_slot'};
364 my $dev = $res->{device};
366 my $label = $dev->volume_label;
367 $self->_update_slot_state(state => $state, dev => $dev, slot =>$slot);
368 $user_msg_fn->("recording volume '$label' in slot $slot");
370 finished_cb => $steps->{'released'},
375 step released => sub {
378 return $params{'finished_cb'}->($err);
381 $steps->{'update_slot'}->();
385 $params{'finished_cb'}->(undef);
393 return if $self->check_error($params{'inventory_cb'});
395 $self->with_locked_state($self->{'state_filename'},
396 $params{'inventory_cb'}, sub {
397 my ($state, $inventory_cb) = @_;
400 my $current = $self->_get_current($state);
401 foreach ($self->{first_slot} .. ($self->{last_slot} - 1)) {
403 my $unaliased = $self->{unaliased}->{$slot};
404 my $s = { slot => $slot,
405 state => $state->{slots}->{$unaliased}->{state} || Amanda::Changer::SLOT_UNKNOWN,
406 reserved => $self->_is_slot_in_use($state, $slot) };
407 if (defined $state->{slots}->{$unaliased}) {
408 $s->{'device_status'} =
409 $state->{slots}->{$unaliased}->{device_status};
410 $s->{'f_type'} = $state->{slots}->{$unaliased}->{f_type};
411 $s->{'label'} = $state->{slots}->{$unaliased}->{label};
413 $s->{'device_status'} = undef;
414 $s->{'f_type'} = undef;
415 $s->{'label'} = undef;
417 if ($slot eq $current) {
422 $inventory_cb->(undef, \@inventory);
431 if (exists $params{'relative_slot'}) {
432 if ($params{'relative_slot'} eq "current") {
433 $slot = $self->_get_current($params{state});
434 } elsif ($params{'relative_slot'} eq "next") {
435 if (exists $params{'slot'}) {
436 $slot = $params{'slot'};
438 $slot = $self->_get_current($params{state});
440 $slot = $self->_get_next($slot);
441 $self->{slot} = $slot if ($params{'set_current'});
442 $self->_set_current($params{state}, $slot) if ($params{'set_current'});
444 return $self->make_error("failed", $params{'res_cb'},
446 message => "Invalid relative slot '$params{relative_slot}'");
449 $slot = $params{'slot'};
452 if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
453 return $self->make_error("failed", $params{'res_cb'},
454 reason => "notfound",
455 message => "all slots have been loaded");
458 if (!$self->_slot_exists($slot)) {
459 return $self->make_error("failed", $params{'res_cb'},
460 reason => "notfound",
461 message => "Slot $slot not defined");
464 if ($self->_is_slot_in_use($params{state}, $slot)) {
465 my $unaliased = $self->{unaliased}->{$slot};
466 return $self->make_error("failed", $params{'res_cb'},
467 reason => "volinuse",
469 message => "Slot $slot is already in use by process '$params{state}->{slots}->{$unaliased}->{pid}'");
472 $self->{slot} = $slot if ($params{'set_current'});
473 $self->_set_current($params{state}, $slot) if ($params{'set_current'});
475 $self->_make_res($params{state}, $params{'res_cb'}, $slot);
481 my $label = $params{'label'};
484 my $state = $params{state};
486 foreach $slot (keys %{$state->{slots}}) {
487 if (defined $state->{slots}->{$slot} &&
488 $state->{slots}->{$slot}->{label} &&
489 $state->{slots}->{$slot}->{label} eq $label) {
495 if (defined $slot_name &&
496 $state->{slots}->{$slot_name}->{label} eq $label) {
498 $slot = $self->{number}->{$slot_name};
499 delete $params{'label'};
500 $params{'slot'} = $slot;
501 $self->_load_by_slot(%params);
503 return $self->make_error("failed", $params{'res_cb'},
504 reason => "notfound",
505 message => "Label '$label' not found");
512 my ($state, $res_cb, $slot) = @_;
515 my $unaliased = $self->{unaliased}->{$slot};
516 my $slot_name = $self->{slot_name}->{$slot};
517 my $device = Amanda::Device->new($slot_name);
518 if ($device->status != $DEVICE_STATUS_SUCCESS) {
519 return $self->make_error("failed", $res_cb,
521 message => "opening '$slot': " . $device->error_or_status());
524 if (my $err = $self->{'config'}->configure_device($device)) {
525 return $self->make_error("failed", $res_cb,
530 $res = Amanda::Changer::multi::Reservation->new($self, $device, $slot);
531 $state->{slots}->{$unaliased}->{pid} = $$;
532 $device->read_label();
534 $self->_update_slot_state(state => $state, dev => $res->{device}, slot => $slot);
535 $res_cb->(undef, $res);
539 # Internal function to determine whether a slot exists.
541 my ($self, $slot) = @_;
543 return 1 if defined $self->{unaliased}->{$slot};
547 sub _update_slot_state {
550 my $state = $params{state};
551 my $dev = $params{dev};
552 my $slot = $params{slot};
553 my $unaliased = $self->{unaliased}->{$slot};
554 $state->{slots}->{$unaliased}->{device_status} = "".scalar($dev->status);
555 my $label = $dev->volume_label;
556 $state->{slots}->{$unaliased}->{state} = Amanda::Changer::SLOT_FULL;
557 $state->{slots}->{$unaliased}->{label} = $label;
558 my $volume_header = $dev->volume_header;
559 if (defined $volume_header) {
560 $state->{slots}->{$unaliased}->{f_type} = "".scalar($volume_header->{type});
562 delete $state->{slots}->{$unaliased}->{f_type};
565 # Internal function to determine if a slot (specified by number) is in use by a
566 # drive, and return the path for that drive if so.
567 sub _is_slot_in_use {
568 my ($self, $state, $slot) = @_;
570 return 0 if !defined $state;
571 return 0 if !defined $state->{slots};
572 return 0 if !defined $self->{unaliased}->{$slot};
573 my $unaliased = $self->{unaliased}->{$slot};
574 return 0 if !defined $state->{slots}->{$unaliased};
575 return 0 if !defined $state->{slots}->{$unaliased}->{pid};
577 #check if PID is still alive
578 my $pid = $state->{slots}->{$unaliased}->{pid};
579 if (Amanda::Util::is_pid_alive($pid) == 1) {
583 delete $state->{slots}->{$unaliased}->{pid};
587 # Internal function to get the next slot after $slot.
588 # skip over except_slot and slot in use.
590 my ($self, $slot, $except_slot) = @_;
593 $next_slot = $slot + 1;
594 $next_slot = $self->{'first_slot'} if $next_slot >= $self->{'last_slot'};
599 # Get the 'current' slot
601 my ($self, $state) = @_;
603 return $self->{slot} if defined $self->{slot};
604 if (defined $state->{current_slot}) {
605 my $slot = $self->{number}->{$state->{current_slot}};
606 # return the slot if it exist.
607 return $slot if $slot >= $self->{'first_slot'} && $slot < $self->{'last_slot'};
608 Amanda::Debug::debug("statefile current_slot is not configured");
610 # return the first slot
611 return $self->{first_slot};
614 # Set the 'current' slot
616 my ($self, $state, $slot) = @_;
618 $self->{slot} = $slot;
619 $state->{current_slot} = $self->{unaliased}->{$slot};
622 package Amanda::Changer::multi::Reservation;
624 @ISA = qw( Amanda::Changer::Reservation );
625 use Amanda::Device qw( :constants );
629 my ($chg, $device, $slot) = @_;
630 my $self = Amanda::Changer::Reservation::new($class);
632 $self->{'chg'} = $chg;
633 $self->{'device'} = $device;
634 $self->{'this_slot'} = $slot;
643 my $chg = $self->{chg};
644 $chg->with_locked_state($chg->{'state_filename'},
645 $params{'finished_cb'}, sub {
646 my ($state, $finished_cb) = @_;
647 my $label = $params{'label'};
648 my $slot = $self->{'this_slot'};
649 my $unaliased = $chg->{unaliased}->{$slot};
651 $state->{slots}->{$unaliased}->{label} = $label;
652 $state->{slots}->{$unaliased}->{device_status} =
653 "".$DEVICE_STATUS_SUCCESS;
654 $state->{slots}->{$unaliased}->{f_type} =
655 "".scalar($Amanda::Header::F_TAPESTART);
664 # if we're in global cleanup and the changer is already dead,
666 return unless $self->{'chg'};
668 # unref the device, for good measure
669 $self->{'device'} = undef;
671 if (exists $params{'unlocked'}) {
672 my $state = $params{state};
673 my $slot = $self->{'this_slot'};
674 my $unaliased = $self->{chg}->{unaliased}->{$slot};
675 delete $state->{slots}->{$unaliased}->{pid};
676 return $params{'finished_cb'}->();
679 $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
680 $params{'finished_cb'}, sub {
681 my ($state, $finished_cb) = @_;
682 $params{state} = $state;
683 my $slot = $self->{'this_slot'};
684 my $unaliased = $self->{chg}->{unaliased}->{$slot};
685 delete $state->{slots}->{$unaliased}->{pid};