1 # Copyright (c) 2008-2012 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 # device_error - error message from the device
60 # f_type - the F_TYPE of the fileheader.
61 # label - the label, if known, of the volume in this slot
63 # $self is a hash with keys:
64 # slot : slot number of the current slot
65 # slots : An array with all slot names
66 # unaliased : A hash with slot number as keys and unaliased device name
68 # slot_name : A hash with slot number as keys and device name as value
69 # number : A hash with unaliased device name as keys and slot number
71 # config : The Amanda::Changer::Config for this changer
72 # state_filename : The filename of the state file
73 # first_slot : The number of the first slot
74 # last_slot : The number of the last slot + 1
78 my ($config, $tpchanger) = @_;
79 my $devices = $tpchanger;
80 $devices =~ s/^chg-multi://g;
81 my (@slots) = Amanda::Util::expand_braced_alternates($devices);
83 unless (scalar @slots != 0) {
84 return Amanda::Changer->make_error("fatal", undef,
85 message => "no devices specified");
88 my $properties = $config->{'properties'};
90 if (exists $properties->{'first-slot'}) {
91 $first_slot = @{$properties->{'first-slot'}->{'values'}}[0];
97 my $last_slot = $first_slot;
98 foreach my $slot_name (@slots) {
99 my $unaliased_name = Amanda::Device::unaliased_name($slot_name);
100 $number{$unaliased_name} = $last_slot;
101 $unaliased{$last_slot} = $unaliased_name;
102 $slot_name{$last_slot} = $slot_name;
106 if (!defined $config->{changerfile} ||
107 $config->{changerfile} eq "") {
108 return Amanda::Changer->make_error("fatal", undef,
110 message => "no changerfile specified for changer '$config->{name}'");
113 my $state_filename = Amanda::Config::config_dir_relative($config->{'changerfile'});
114 Amanda::Debug::debug("Using state file: $state_filename");
118 unaliased => \%unaliased,
119 slot_name => \%slot_name,
122 state_filename => $state_filename,
123 first_slot => $first_slot,
124 last_slot => $last_slot,
127 bless ($self, $class);
134 my $old_res_cb = $params{'res_cb'};
137 $self->validate_params('load', \%params);
139 return if $self->check_error($params{'res_cb'});
141 $self->with_locked_state($self->{'state_filename'},
142 $params{'res_cb'}, sub {
143 my ($state, $res_cb) = @_;
145 $params{'state'} = $state;
146 # overwrite the callback for _load_by_xxx
147 $params{'res_cb'} = $res_cb;
149 if (exists $params{'slot'} or exists $params{'relative_slot'}) {
150 $self->_load_by_slot(%params);
151 } elsif (exists $params{'label'}) {
152 $self->_load_by_label(%params);
159 my ($key, %params) = @_;
162 return if $self->check_error($params{'info_cb'});
164 # no need for synchronization -- all of these values are static
166 if ($key eq 'num_slots') {
167 $results{$key} = $self->{last_slot} - $self->{first_slot};
168 } elsif ($key eq 'vendor_string') {
169 $results{$key} = 'chg-multi'; # mostly just for testing
170 } elsif ($key eq 'fast_search') {
174 $params{'info_cb'}->(undef, %results) if $params{'info_cb'};
181 return if $self->check_error($params{'finished_cb'});
183 $self->with_locked_state($self->{'state_filename'},
184 $params{'finished_cb'}, sub {
185 my ($state, $finished_cb) = @_;
188 $params{state} = $state;
189 $slot = $self->{first_slot};
190 $self->{slot} = $slot;
191 $self->_set_current($state, $slot);
202 return if $self->check_error($params{'finished_cb'});
204 $self->with_locked_state($self->{'state_filename'},
205 $params{'finished_cb'}, sub {
206 my ($state, $finished_cb) = @_;
209 $params{state} = $state;
210 if (!exists $params{'drive'}) {
211 $drive = $self->_get_current($params{state});
213 $drive = $params{'drive'};
215 if (!defined $self->{unaliased}->{$drive}) {
216 return $self->make_error("failed", $finished_cb,
218 message => "Invalid slot '$drive'");
221 Amanda::Debug::debug("ejecting drive $drive");
222 my $device = Amanda::Device->new($self->{slot_name}->{$drive});
223 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
224 return $self->make_error("failed", $finished_cb,
226 message => $device->error_or_status);
228 if (my $err = $self->{'config'}->configure_device($device)) {
229 return $self->make_error("failed", $params{'res_cb'},
234 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
235 return $self->make_error("failed", $finished_cb,
237 message => $device->error_or_status);
250 my $set_to_unknown = 0;
252 my $user_msg_fn = $params{'user_msg_fn'};
253 $user_msg_fn ||= sub { Amanda::Debug::info("chg-multi: " . $_[0]); };
255 my $steps = define_steps
256 cb_ref => \$params{'finished_cb'};
259 $self->with_locked_state($self->{'state_filename'},
260 $params{'finished_cb'}, sub {
261 my ($state, $finished_cb) = @_;
263 $params{state} = $state;
264 $params{'finished_cb'} = $finished_cb;
266 $steps->{'handle_assignment'}->();
270 step handle_assignment => sub {
271 $state = $params{state};
272 # check for the SL=LABEL format, and handle it here
273 if (exists $params{'changed'} and
274 $params{'changed'} =~ /^\d+=\S+$/) {
275 my ($slot, $label) = ($params{'changed'} =~ /^(\d+)=(\S+)$/);
277 # let's list the reasons we *can't* do what the user has asked
279 if (!exists $self->{unaliased}->{$slot}) {
280 $whynot = "slot $slot does not exist";
284 return $self->make_error("failed", $params{'finished_cb'},
285 reason => "unknown", message => $whynot);
288 $user_msg_fn->("recording volume '$label' in slot $slot");
289 # ok, now erase all knowledge of that label
290 while (my ($sl, $inf) = each %{$state->{'slots'}}) {
291 if ($inf->{'label'} and $inf->{'label'} eq $label) {
292 $inf->{'label'} = undef;
296 # and add knowledge of the label to the given slot
297 my $unaliased = $self->{unaliased}->{$slot};
298 $state->{'slots'}->{$unaliased}->{'label'} = $label;
300 # that's it -- no changer motion required
301 return $params{'finished_cb'}->(undef);
302 } elsif (exists $params{'changed'} and
303 $params{'changed'} =~ /^(.+)=$/) {
304 $params{'changed'} = $1;
306 $steps->{'calculate_slots'}->();
308 $steps->{'calculate_slots'}->();
312 step calculate_slots => sub {
313 if (exists $params{'changed'}) {
314 # parse the string just like use-slots, using a hash for uniqueness
316 for my $range (split ',', $params{'changed'}) {
317 my ($first, $last) = ($range =~ /(\d+)(?:-(\d+))?/);
318 $last = $first unless defined($last);
319 for ($first .. $last) {
320 $changed{$_} = undef;
324 @slots_to_check = keys %changed;
325 @slots_to_check = grep { exists $self->{'unaliased'}->{$_} } @slots_to_check;
327 @slots_to_check = keys %{ $self->{unaliased} };
330 # sort them so we don't confuse the user with a "random" order
331 @slots_to_check = sort @slots_to_check;
333 $steps->{'update_slot'}->();
336 # TODO: parallelize, we have one drive by slot
338 step update_slot => sub {
339 return $steps->{'done'}->() if (!@slots_to_check);
340 my $slot = shift @slots_to_check;
341 if ($self->_is_slot_in_use($state, $slot)) {
342 $user_msg_fn->("Slot $slot is already in use");
343 return $steps->{'update_slot'}->();
346 if ($set_to_unknown == 1) {
347 $user_msg_fn->("removing entry for slot $slot");
348 my $unaliased = $self->{unaliased}->{$slot};
349 delete $state->{slots}->{$unaliased};
350 return $steps->{'update_slot'}->();
352 $user_msg_fn->("scanning slot $slot");
353 $params{'slot'} = $slot;
354 $params{'res_cb'} = $steps->{'slot_loaded'};
355 $self->_load_by_slot(%params);
359 step slot_loaded => sub {
360 my ($err, $res) = @_;
362 return $params{'finished_cb'}->($err);
365 my $slot = $res->{'this_slot'};
366 my $dev = $res->{device};
367 $self->_update_slot_state(state => $state, dev => $dev, slot =>$slot);
368 if ($dev->status() == $DEVICE_STATUS_SUCCESS) {
369 my $label = $dev->volume_label;
370 $user_msg_fn->("recording volume '$label' in slot $slot");
372 my $status = $dev->error_or_status;
373 $user_msg_fn->("recording device error '" . $status . "' in slot $slot");
376 finished_cb => $steps->{'released'},
381 step released => sub {
384 return $params{'finished_cb'}->($err);
387 $steps->{'update_slot'}->();
391 $params{'finished_cb'}->(undef);
399 return if $self->check_error($params{'inventory_cb'});
401 $self->with_locked_state($self->{'state_filename'},
402 $params{'inventory_cb'}, sub {
403 my ($state, $inventory_cb) = @_;
406 my $current = $self->_get_current($state);
407 foreach ($self->{first_slot} .. ($self->{last_slot} - 1)) {
409 my $unaliased = $self->{unaliased}->{$slot};
410 my $s = { slot => $slot,
411 state => $state->{slots}->{$unaliased}->{state} || Amanda::Changer::SLOT_UNKNOWN,
412 reserved => $self->_is_slot_in_use($state, $slot) };
413 if (defined $state->{slots}->{$unaliased} and
414 exists $state->{slots}->{$unaliased}->{device_status}) {
415 $s->{'device_status'} =
416 $state->{slots}->{$unaliased}->{device_status};
417 if ($s->{'device_status'} != $DEVICE_STATUS_SUCCESS) {
418 $s->{'device_error'} =
419 $state->{slots}->{$unaliased}->{device_error};
421 $s->{'device_error'} = undef;
423 $s->{'f_type'} = $state->{slots}->{$unaliased}->{f_type};
424 $s->{'label'} = $state->{slots}->{$unaliased}->{label};
426 $s->{'device_status'} = undef;
427 $s->{'device_error'} = undef;
428 $s->{'f_type'} = undef;
429 $s->{'label'} = undef;
431 if ($slot eq $current) {
436 $inventory_cb->(undef, \@inventory);
445 if (exists $params{'relative_slot'}) {
446 if ($params{'relative_slot'} eq "current") {
447 $slot = $self->_get_current($params{state});
448 } elsif ($params{'relative_slot'} eq "next") {
449 if (exists $params{'slot'}) {
450 $slot = $params{'slot'};
452 $slot = $self->_get_current($params{state});
454 $slot = $self->_get_next($slot);
455 $self->{slot} = $slot if ($params{'set_current'});
456 $self->_set_current($params{state}, $slot) if ($params{'set_current'});
458 return $self->make_error("failed", $params{'res_cb'},
460 message => "Invalid relative slot '$params{relative_slot}'");
463 $slot = $params{'slot'};
466 if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
467 return $self->make_error("failed", $params{'res_cb'},
468 reason => "notfound",
469 message => "all slots have been loaded");
472 if (!$self->_slot_exists($slot)) {
473 return $self->make_error("failed", $params{'res_cb'},
474 reason => "notfound",
475 message => "Slot $slot not defined");
478 if ($self->_is_slot_in_use($params{state}, $slot)) {
479 my $unaliased = $self->{unaliased}->{$slot};
480 return $self->make_error("failed", $params{'res_cb'},
481 reason => "volinuse",
483 message => "Slot $slot is already in use by process '$params{state}->{slots}->{$unaliased}->{pid}'");
486 $self->{slot} = $slot if ($params{'set_current'});
487 $self->_set_current($params{state}, $slot) if ($params{'set_current'});
489 $self->_make_res($params{state}, $params{'res_cb'}, $slot);
495 my $label = $params{'label'};
498 my $state = $params{state};
500 foreach $slot (keys %{$state->{slots}}) {
501 if (defined $state->{slots}->{$slot} &&
502 $state->{slots}->{$slot}->{label} &&
503 $state->{slots}->{$slot}->{label} eq $label) {
509 if (defined $slot_name &&
510 $state->{slots}->{$slot_name}->{label} eq $label) {
512 $slot = $self->{number}->{$slot_name};
513 delete $params{'label'};
514 $params{'slot'} = $slot;
515 $self->_load_by_slot(%params);
517 return $self->make_error("failed", $params{'res_cb'},
518 reason => "notfound",
519 message => "Label '$label' not found");
526 my ($state, $res_cb, $slot) = @_;
529 my $unaliased = $self->{unaliased}->{$slot};
530 my $slot_name = $self->{slot_name}->{$slot};
531 my $device = Amanda::Device->new($slot_name);
532 if ($device->status != $DEVICE_STATUS_SUCCESS) {
533 return $self->make_error("failed", $res_cb,
535 message => "opening '$slot': " . $device->error_or_status());
538 if (my $err = $self->{'config'}->configure_device($device)) {
539 return $self->make_error("failed", $res_cb,
544 $res = Amanda::Changer::multi::Reservation->new($self, $device, $slot);
545 $state->{slots}->{$unaliased}->{pid} = $$;
546 $device->read_label();
548 $self->_update_slot_state(state => $state, dev => $res->{device}, slot => $slot);
549 $res_cb->(undef, $res);
553 # Internal function to determine whether a slot exists.
555 my ($self, $slot) = @_;
557 return 1 if defined $self->{unaliased}->{$slot};
561 sub _update_slot_state {
564 my $state = $params{state};
565 my $dev = $params{dev};
566 my $slot = $params{slot};
567 my $unaliased = $self->{unaliased}->{$slot};
568 $state->{slots}->{$unaliased}->{device_status} = "".scalar($dev->status);
569 if ($dev->status != $DEVICE_STATUS_SUCCESS) {
570 $state->{slots}->{$unaliased}->{device_error} = $dev->error;
572 $state->{slots}->{$unaliased}->{device_error} = undef;
574 my $label = $dev->volume_label;
575 $state->{slots}->{$unaliased}->{state} = Amanda::Changer::SLOT_FULL;
576 $state->{slots}->{$unaliased}->{label} = $label;
577 my $volume_header = $dev->volume_header;
578 if (defined $volume_header) {
579 $state->{slots}->{$unaliased}->{f_type} = "".scalar($volume_header->{type});
581 delete $state->{slots}->{$unaliased}->{f_type};
584 # Internal function to determine if a slot (specified by number) is in use by a
585 # drive, and return the path for that drive if so.
586 sub _is_slot_in_use {
587 my ($self, $state, $slot) = @_;
589 return 0 if !defined $state;
590 return 0 if !defined $state->{slots};
591 return 0 if !defined $self->{unaliased}->{$slot};
592 my $unaliased = $self->{unaliased}->{$slot};
593 return 0 if !defined $state->{slots}->{$unaliased};
594 return 0 if !defined $state->{slots}->{$unaliased}->{pid};
596 #check if PID is still alive
597 my $pid = $state->{slots}->{$unaliased}->{pid};
598 if (Amanda::Util::is_pid_alive($pid) == 1) {
602 delete $state->{slots}->{$unaliased}->{pid};
606 # Internal function to get the next slot after $slot.
607 # skip over except_slot and slot in use.
609 my ($self, $slot, $except_slot) = @_;
612 $next_slot = $slot + 1;
613 $next_slot = $self->{'first_slot'} if $next_slot >= $self->{'last_slot'};
618 # Get the 'current' slot
620 my ($self, $state) = @_;
622 return $self->{slot} if defined $self->{slot};
623 if (defined $state->{current_slot}) {
624 my $slot = $self->{number}->{$state->{current_slot}};
625 # return the slot if it exist.
626 return $slot if $slot >= $self->{'first_slot'} && $slot < $self->{'last_slot'};
627 Amanda::Debug::debug("statefile current_slot is not configured");
629 # return the first slot
630 return $self->{first_slot};
633 # Set the 'current' slot
635 my ($self, $state, $slot) = @_;
637 $self->{slot} = $slot;
638 $state->{current_slot} = $self->{unaliased}->{$slot};
641 package Amanda::Changer::multi::Reservation;
643 @ISA = qw( Amanda::Changer::Reservation );
644 use Amanda::Device qw( :constants );
648 my ($chg, $device, $slot) = @_;
649 my $self = Amanda::Changer::Reservation::new($class);
651 $self->{'chg'} = $chg;
652 $self->{'device'} = $device;
653 $self->{'this_slot'} = $slot;
662 my $chg = $self->{chg};
663 $chg->with_locked_state($chg->{'state_filename'},
664 $params{'finished_cb'}, sub {
665 my ($state, $finished_cb) = @_;
666 my $label = $params{'label'};
667 my $slot = $self->{'this_slot'};
668 my $unaliased = $chg->{unaliased}->{$slot};
669 my $dev = $self->{'device'};
671 $state->{slots}->{$unaliased}->{label} = $label;
672 $state->{slots}->{$unaliased}->{device_status} =
674 if ($dev->status != $DEVICE_STATUS_SUCCESS) {
675 $state->{slots}->{$unaliased}->{device_error} = $dev->error;
677 $state->{slots}->{$unaliased}->{device_error} = undef;
679 my $volume_header = $dev->volume_header;
680 if (defined $volume_header) {
681 $state->{slots}->{$unaliased}->{f_type} =
682 "".$volume_header->{type};
684 $state->{slots}->{$unaliased}->{f_type} = undef;
694 # if we're in global cleanup and the changer is already dead,
696 return unless $self->{'chg'};
698 $self->{'device'}->eject() if (exists $self->{'device'} and
699 exists $params{'eject'} and
702 # unref the device, for good measure
703 $self->{'device'} = undef;
705 if (exists $params{'unlocked'}) {
706 my $state = $params{state};
707 my $slot = $self->{'this_slot'};
708 my $unaliased = $self->{chg}->{unaliased}->{$slot};
709 delete $state->{slots}->{$unaliased}->{pid};
710 return $params{'finished_cb'}->();
713 $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
714 $params{'finished_cb'}, sub {
715 my ($state, $finished_cb) = @_;
716 $params{state} = $state;
717 my $slot = $self->{'this_slot'};
718 my $unaliased = $self->{chg}->{unaliased}->{$slot};
719 delete $state->{slots}->{$unaliased}->{pid};