1 # Copyright (c) 2008-2012 Zmanda, Inc. All Rights Reserved.
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
20 package Amanda::Changer::multi;
25 @ISA = qw( Amanda::Changer );
27 use File::Glob qw( :glob );
29 use Amanda::Config qw( :getconf );
33 use Amanda::Device qw( :constants );
37 Amanda::Changer::multi
41 This changer operates with a list of device, specified in the tpchanger
44 See the amanda-changers(7) manpage for usage information.
50 # The device state is shared between all changers accessing the same changer.
51 # It is a hash with keys:
52 # current_slot - the unaliased device name of the current slot
55 # The 'slots' key is a hash, with unaliased device name as keys and hashes
56 # as values. Each slot's hash has keys:
57 # pid - the pid that reserved that slot.
58 # state - SLOT_FULL/SLOT_EMPTY/SLOT_UNKNOWN
59 # device_status - the status of the device after the open or read_label
60 # device_error - error message from the device
61 # f_type - the F_TYPE of the fileheader.
62 # label - the label, if known, of the volume in this slot
64 # $self is a hash with keys:
65 # slot : slot number of the current slot
66 # slots : An array with all slot names
67 # unaliased : A hash with slot number as keys and unaliased device name
69 # slot_name : A hash with slot number as keys and device name as value
70 # number : A hash with unaliased device name as keys and slot number
72 # config : The Amanda::Changer::Config for this changer
73 # state_filename : The filename of the state file
74 # first_slot : The number of the first slot
75 # last_slot : The number of the last slot + 1
79 my ($config, $tpchanger) = @_;
80 my $devices = $tpchanger;
81 $devices =~ s/^chg-multi://g;
82 my (@slots) = Amanda::Util::expand_braced_alternates($devices);
84 unless (scalar @slots != 0) {
85 return Amanda::Changer->make_error("fatal", undef,
86 message => "no devices specified");
89 my $properties = $config->{'properties'};
91 if (exists $properties->{'first-slot'}) {
92 $first_slot = @{$properties->{'first-slot'}->{'values'}}[0];
98 my $last_slot = $first_slot;
99 foreach my $slot_name (@slots) {
100 my $unaliased_name = Amanda::Device::unaliased_name($slot_name);
101 $number{$unaliased_name} = $last_slot;
102 $unaliased{$last_slot} = $unaliased_name;
103 $slot_name{$last_slot} = $slot_name;
107 if (!defined $config->{changerfile} ||
108 $config->{changerfile} eq "") {
109 return Amanda::Changer->make_error("fatal", undef,
111 message => "no changerfile specified for changer '$config->{name}'");
114 my $state_filename = Amanda::Config::config_dir_relative($config->{'changerfile'});
115 my $lock_timeout = $config->{'lock-timeout'};
116 Amanda::Debug::debug("Using state file: $state_filename");
120 unaliased => \%unaliased,
121 slot_name => \%slot_name,
124 state_filename => $state_filename,
125 first_slot => $first_slot,
126 last_slot => $last_slot,
127 'lock-timeout' => $lock_timeout,
130 bless ($self, $class);
137 my $old_res_cb = $params{'res_cb'};
140 $self->validate_params('load', \%params);
142 return if $self->check_error($params{'res_cb'});
144 $self->with_locked_state($self->{'state_filename'},
145 $params{'res_cb'}, sub {
146 my ($state, $res_cb) = @_;
148 $params{'state'} = $state;
149 # overwrite the callback for _load_by_xxx
150 $params{'res_cb'} = $res_cb;
152 if (exists $params{'slot'} or exists $params{'relative_slot'}) {
153 $self->_load_by_slot(%params);
154 } elsif (exists $params{'label'}) {
155 $self->_load_by_label(%params);
162 my ($key, %params) = @_;
165 return if $self->check_error($params{'info_cb'});
167 # no need for synchronization -- all of these values are static
169 if ($key eq 'num_slots') {
170 $results{$key} = $self->{last_slot} - $self->{first_slot};
171 } elsif ($key eq 'vendor_string') {
172 $results{$key} = 'chg-multi'; # mostly just for testing
173 } elsif ($key eq 'fast_search') {
177 $params{'info_cb'}->(undef, %results) if $params{'info_cb'};
184 return if $self->check_error($params{'finished_cb'});
186 $self->with_locked_state($self->{'state_filename'},
187 $params{'finished_cb'}, sub {
188 my ($state, $finished_cb) = @_;
191 $params{state} = $state;
192 $slot = $self->{first_slot};
193 $self->{slot} = $slot;
194 $self->_set_current($state, $slot);
205 return if $self->check_error($params{'finished_cb'});
207 $self->with_locked_state($self->{'state_filename'},
208 $params{'finished_cb'}, sub {
209 my ($state, $finished_cb) = @_;
212 $params{state} = $state;
213 if (!exists $params{'drive'}) {
214 $drive = $self->_get_current($params{state});
216 $drive = $params{'drive'};
218 if (!defined $self->{unaliased}->{$drive}) {
219 return $self->make_error("failed", $finished_cb,
221 message => "Invalid slot '$drive'");
224 Amanda::Debug::debug("ejecting drive $drive");
225 my $device = Amanda::Device->new($self->{slot_name}->{$drive});
226 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
227 return $self->make_error("failed", $finished_cb,
229 message => $device->error_or_status);
231 if (my $err = $self->{'config'}->configure_device($device)) {
232 return $self->make_error("failed", $params{'res_cb'},
237 if ($device->status() != $DEVICE_STATUS_SUCCESS) {
238 return $self->make_error("failed", $finished_cb,
240 message => $device->error_or_status);
253 my $set_to_unknown = 0;
255 my $user_msg_fn = $params{'user_msg_fn'};
256 $user_msg_fn ||= sub { Amanda::Debug::info("chg-multi: " . $_[0]); };
258 my $steps = define_steps
259 cb_ref => \$params{'finished_cb'};
262 $self->with_locked_state($self->{'state_filename'},
263 $params{'finished_cb'}, sub {
264 my ($state, $finished_cb) = @_;
266 $params{state} = $state;
267 $params{'finished_cb'} = $finished_cb;
269 $steps->{'handle_assignment'}->();
273 step handle_assignment => sub {
274 $state = $params{state};
275 # check for the SL=LABEL format, and handle it here
276 if (exists $params{'changed'} and
277 $params{'changed'} =~ /^\d+=\S+$/) {
278 my ($slot, $label) = ($params{'changed'} =~ /^(\d+)=(\S+)$/);
280 # let's list the reasons we *can't* do what the user has asked
282 if (!exists $self->{unaliased}->{$slot}) {
283 $whynot = "slot $slot does not exist";
287 return $self->make_error("failed", $params{'finished_cb'},
288 reason => "unknown", message => $whynot);
291 $user_msg_fn->("recording volume '$label' in slot $slot");
292 # ok, now erase all knowledge of that label
293 while (my ($sl, $inf) = each %{$state->{'slots'}}) {
294 if ($inf->{'label'} and $inf->{'label'} eq $label) {
295 $inf->{'label'} = undef;
299 # and add knowledge of the label to the given slot
300 my $unaliased = $self->{unaliased}->{$slot};
301 $state->{'slots'}->{$unaliased}->{'label'} = $label;
303 # that's it -- no changer motion required
304 return $params{'finished_cb'}->(undef);
305 } elsif (exists $params{'changed'} and
306 $params{'changed'} =~ /^(.+)=$/) {
307 $params{'changed'} = $1;
309 $steps->{'calculate_slots'}->();
311 $steps->{'calculate_slots'}->();
315 step calculate_slots => sub {
316 if (exists $params{'changed'}) {
317 # parse the string just like use-slots, using a hash for uniqueness
319 for my $range (split ',', $params{'changed'}) {
320 my ($first, $last) = ($range =~ /(\d+)(?:-(\d+))?/);
321 $last = $first unless defined($last);
322 for ($first .. $last) {
323 $changed{$_} = undef;
327 @slots_to_check = keys %changed;
328 @slots_to_check = grep { exists $self->{'unaliased'}->{$_} } @slots_to_check;
330 @slots_to_check = keys %{ $self->{unaliased} };
333 # sort them so we don't confuse the user with a "random" order
334 @slots_to_check = sort @slots_to_check;
336 $steps->{'update_slot'}->();
339 # TODO: parallelize, we have one drive by slot
341 step update_slot => sub {
342 return $steps->{'done'}->() if (!@slots_to_check);
343 my $slot = shift @slots_to_check;
344 if ($self->_is_slot_in_use($state, $slot)) {
345 $user_msg_fn->("Slot $slot is already in use");
346 return $steps->{'update_slot'}->();
349 if ($set_to_unknown == 1) {
350 $user_msg_fn->("removing entry for slot $slot");
351 my $unaliased = $self->{unaliased}->{$slot};
352 delete $state->{slots}->{$unaliased};
353 return $steps->{'update_slot'}->();
355 $user_msg_fn->("scanning slot $slot");
356 $params{'slot'} = $slot;
357 $params{'res_cb'} = $steps->{'slot_loaded'};
358 $self->_load_by_slot(%params);
362 step slot_loaded => sub {
363 my ($err, $res) = @_;
365 return $params{'finished_cb'}->($err);
368 my $slot = $res->{'this_slot'};
369 my $dev = $res->{device};
370 $self->_update_slot_state(state => $state, dev => $dev, slot =>$slot);
371 if ($dev->status() == $DEVICE_STATUS_SUCCESS) {
372 my $label = $dev->volume_label;
373 $user_msg_fn->("recording volume '$label' in slot $slot");
375 my $status = $dev->error_or_status;
376 $user_msg_fn->("recording device error '" . $status . "' in slot $slot");
379 finished_cb => $steps->{'released'},
384 step released => sub {
387 return $params{'finished_cb'}->($err);
390 $steps->{'update_slot'}->();
394 $params{'finished_cb'}->(undef);
402 return if $self->check_error($params{'inventory_cb'});
404 $self->with_locked_state($self->{'state_filename'},
405 $params{'inventory_cb'}, sub {
406 my ($state, $inventory_cb) = @_;
409 my $current = $self->_get_current($state);
410 foreach ($self->{first_slot} .. ($self->{last_slot} - 1)) {
412 my $unaliased = $self->{unaliased}->{$slot};
413 my $s = { slot => $slot,
414 state => $state->{slots}->{$unaliased}->{state} || Amanda::Changer::SLOT_UNKNOWN,
415 reserved => $self->_is_slot_in_use($state, $slot) };
416 if (defined $state->{slots}->{$unaliased} and
417 exists $state->{slots}->{$unaliased}->{device_status}) {
418 $s->{'device_status'} =
419 $state->{slots}->{$unaliased}->{device_status};
420 if ($s->{'device_status'} != $DEVICE_STATUS_SUCCESS) {
421 $s->{'device_error'} =
422 $state->{slots}->{$unaliased}->{device_error};
424 $s->{'device_error'} = undef;
426 $s->{'f_type'} = $state->{slots}->{$unaliased}->{f_type};
427 $s->{'label'} = $state->{slots}->{$unaliased}->{label};
429 $s->{'device_status'} = undef;
430 $s->{'device_error'} = undef;
431 $s->{'f_type'} = undef;
432 $s->{'label'} = undef;
434 if ($slot eq $current) {
439 $inventory_cb->(undef, \@inventory);
448 if (exists $params{'relative_slot'}) {
449 if ($params{'relative_slot'} eq "current") {
450 $slot = $self->_get_current($params{state});
451 } elsif ($params{'relative_slot'} eq "next") {
452 if (exists $params{'slot'}) {
453 $slot = $params{'slot'};
455 $slot = $self->_get_current($params{state});
457 $slot = $self->_get_next($slot);
458 $self->{slot} = $slot if ($params{'set_current'});
459 $self->_set_current($params{state}, $slot) if ($params{'set_current'});
461 return $self->make_error("failed", $params{'res_cb'},
463 message => "Invalid relative slot '$params{relative_slot}'");
466 $slot = $params{'slot'};
469 if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
470 return $self->make_error("failed", $params{'res_cb'},
471 reason => "notfound",
472 message => "all slots have been loaded");
475 if (!$self->_slot_exists($slot)) {
476 return $self->make_error("failed", $params{'res_cb'},
477 reason => "notfound",
478 message => "Slot $slot not defined");
481 if ($self->_is_slot_in_use($params{state}, $slot)) {
482 my $unaliased = $self->{unaliased}->{$slot};
483 return $self->make_error("failed", $params{'res_cb'},
484 reason => "volinuse",
486 message => "Slot $slot is already in use by process '$params{state}->{slots}->{$unaliased}->{pid}'");
489 $self->{slot} = $slot if ($params{'set_current'});
490 $self->_set_current($params{state}, $slot) if ($params{'set_current'});
492 $self->_make_res($params{state}, $params{'res_cb'}, $slot);
498 my $label = $params{'label'};
501 my $state = $params{state};
503 foreach $slot (keys %{$state->{slots}}) {
504 if (defined $state->{slots}->{$slot} &&
505 $state->{slots}->{$slot}->{label} &&
506 $state->{slots}->{$slot}->{label} eq $label) {
512 if (defined $slot_name &&
513 $state->{slots}->{$slot_name}->{label} eq $label) {
515 $slot = $self->{number}->{$slot_name};
516 delete $params{'label'};
517 $params{'slot'} = $slot;
518 $self->_load_by_slot(%params);
520 return $self->make_error("failed", $params{'res_cb'},
521 reason => "notfound",
522 message => "Label '$label' not found");
529 my ($state, $res_cb, $slot) = @_;
532 my $unaliased = $self->{unaliased}->{$slot};
533 my $slot_name = $self->{slot_name}->{$slot};
534 my $device = Amanda::Device->new($slot_name);
535 if ($device->status != $DEVICE_STATUS_SUCCESS) {
536 return $self->make_error("failed", $res_cb,
538 message => "opening '$slot': " . $device->error_or_status());
541 if (my $err = $self->{'config'}->configure_device($device)) {
542 return $self->make_error("failed", $res_cb,
547 $res = Amanda::Changer::multi::Reservation->new($self, $device, $slot);
548 $state->{slots}->{$unaliased}->{pid} = $$;
549 $device->read_label();
551 $self->_update_slot_state(state => $state, dev => $res->{device}, slot => $slot);
552 $res_cb->(undef, $res);
556 # Internal function to determine whether a slot exists.
558 my ($self, $slot) = @_;
560 return 1 if defined $self->{unaliased}->{$slot};
564 sub _update_slot_state {
567 my $state = $params{state};
568 my $dev = $params{dev};
569 my $slot = $params{slot};
570 my $unaliased = $self->{unaliased}->{$slot};
571 $state->{slots}->{$unaliased}->{device_status} = "".scalar($dev->status);
572 if ($dev->status != $DEVICE_STATUS_SUCCESS) {
573 $state->{slots}->{$unaliased}->{device_error} = $dev->error;
575 $state->{slots}->{$unaliased}->{device_error} = undef;
577 my $label = $dev->volume_label;
578 $state->{slots}->{$unaliased}->{state} = Amanda::Changer::SLOT_FULL;
579 $state->{slots}->{$unaliased}->{label} = $label;
580 my $volume_header = $dev->volume_header;
581 if (defined $volume_header) {
582 $state->{slots}->{$unaliased}->{f_type} = "".scalar($volume_header->{type});
584 delete $state->{slots}->{$unaliased}->{f_type};
587 # Internal function to determine if a slot (specified by number) is in use by a
588 # drive, and return the path for that drive if so.
589 sub _is_slot_in_use {
590 my ($self, $state, $slot) = @_;
592 return 0 if !defined $state;
593 return 0 if !defined $state->{slots};
594 return 0 if !defined $self->{unaliased}->{$slot};
595 my $unaliased = $self->{unaliased}->{$slot};
596 return 0 if !defined $state->{slots}->{$unaliased};
597 return 0 if !defined $state->{slots}->{$unaliased}->{pid};
599 #check if PID is still alive
600 my $pid = $state->{slots}->{$unaliased}->{pid};
601 if (Amanda::Util::is_pid_alive($pid) == 1) {
605 delete $state->{slots}->{$unaliased}->{pid};
609 # Internal function to get the next slot after $slot.
610 # skip over except_slot and slot in use.
612 my ($self, $slot, $except_slot) = @_;
615 $next_slot = $slot + 1;
616 $next_slot = $self->{'first_slot'} if $next_slot >= $self->{'last_slot'};
621 # Get the 'current' slot
623 my ($self, $state) = @_;
625 return $self->{slot} if defined $self->{slot};
626 if (defined $state->{current_slot}) {
627 my $slot = $self->{number}->{$state->{current_slot}};
628 # return the slot if it exist.
629 return $slot if defined $slot and
630 $slot >= $self->{'first_slot'} and
631 $slot < $self->{'last_slot'};
632 Amanda::Debug::debug("statefile current_slot is not configured");
634 # return the first slot
635 return $self->{first_slot};
638 # Set the 'current' slot
640 my ($self, $state, $slot) = @_;
642 $self->{slot} = $slot;
643 $state->{current_slot} = $self->{unaliased}->{$slot};
646 package Amanda::Changer::multi::Reservation;
648 @ISA = qw( Amanda::Changer::Reservation );
649 use Amanda::Device qw( :constants );
653 my ($chg, $device, $slot) = @_;
654 my $self = Amanda::Changer::Reservation::new($class);
656 $self->{'chg'} = $chg;
657 $self->{'device'} = $device;
658 $self->{'this_slot'} = $slot;
667 my $chg = $self->{chg};
668 $chg->with_locked_state($chg->{'state_filename'},
669 $params{'finished_cb'}, sub {
670 my ($state, $finished_cb) = @_;
671 my $label = $params{'label'};
672 my $slot = $self->{'this_slot'};
673 my $unaliased = $chg->{unaliased}->{$slot};
674 my $dev = $self->{'device'};
676 $state->{slots}->{$unaliased}->{label} = $label;
677 $state->{slots}->{$unaliased}->{device_status} =
679 if ($dev->status != $DEVICE_STATUS_SUCCESS) {
680 $state->{slots}->{$unaliased}->{device_error} = $dev->error;
682 $state->{slots}->{$unaliased}->{device_error} = undef;
684 my $volume_header = $dev->volume_header;
685 if (defined $volume_header) {
686 $state->{slots}->{$unaliased}->{f_type} =
687 "".$volume_header->{type};
689 $state->{slots}->{$unaliased}->{f_type} = undef;
699 # if we're in global cleanup and the changer is already dead,
701 return unless $self->{'chg'};
703 $self->{'device'}->eject() if (exists $self->{'device'} and
704 exists $params{'eject'} and
707 # unref the device, for good measure
708 $self->{'device'} = undef;
710 if (exists $params{'unlocked'}) {
711 my $state = $params{state};
712 my $slot = $self->{'this_slot'};
713 my $unaliased = $self->{chg}->{unaliased}->{$slot};
714 delete $state->{slots}->{$unaliased}->{pid};
715 return $params{'finished_cb'}->();
718 $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
719 $params{'finished_cb'}, sub {
720 my ($state, $finished_cb) = @_;
721 $params{state} = $state;
722 my $slot = $self->{'this_slot'};
723 my $unaliased = $self->{chg}->{unaliased}->{$slot};
724 delete $state->{slots}->{$unaliased}->{pid};