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::disk;
24 @ISA = qw( Amanda::Changer );
26 use File::Glob qw( :glob );
29 use Amanda::Config qw( :getconf string_to_boolean );
33 use Amanda::Device qw( :constants );
41 This changer operates within a root directory, specified in the changer
42 string, which it arranges as follows:
46 | | data -> '../slot4'
48 | | data -> '../slot1'
55 The user should create the desired number of C<slot$n> subdirectories. The
56 changer will take care of dynamically creating the drives as needed, and track
57 the current slot using a "data" symlink. This allows use of "file:$dir" as a
58 device operating on the current slot, although note that it is unlocked.
60 Drives are dynamically allocated as Amanda applications request access to
61 particular slots. Each drive is represented as a subdirectory containing a
62 'data' symlink pointing to the "loaded" slot.
64 See the amanda-changers(7) manpage for usage information.
70 # The device state is shared between all changers accessing the same changer.
71 # It is a hash with keys:
74 # The 'drives' key is a hash, with drive as keys and hashes
75 # as values. Each drive's hash has keys:
76 # pid - the pid that reserved that drive.
82 my ($config, $tpchanger) = @_;
83 my ($dir) = ($tpchanger =~ /chg-disk:(.*)/);
84 my $properties = $config->{'properties'};
86 # note that we don't track outstanding Reservation objects -- we know
87 # they're gone when they delete their drive directory
91 state_filename => "$dir/state",
93 # list of all reservations
96 # this is set to 0 by various test scripts,
97 # notably Amanda_Taper_Scan_traditional
98 support_fast_search => 1,
101 bless ($self, $class);
103 $self->{'num-slot'} = $config->get_property('num-slot');
104 $self->{'auto-create-slot'} = $config->get_boolean_property(
105 'auto-create-slot', 0);
106 $self->{'removable'} = $config->get_boolean_property('removable', 0);
107 $self->{'mount'} = $config->get_boolean_property('mount', 0);
108 $self->{'umount'} = $config->get_boolean_property('umount', 0);
109 $self->{'umount_lockfile'} = $config->get_property('umount-lockfile');
110 $self->{'umount_idle'} = $config->get_property('umount-idle');
111 if (defined $self->{'umount_lockfile'}) {
112 $self->{'fl'} = Amanda::Util::file_lock->new($self->{'umount_lockfile'})
116 return $self->{'fatal_error'} if defined $self->{'fatal_error'};
124 $self->SUPER::DESTROY();
130 $self->force_unlock();
131 delete $self->{'fl'};
132 $self->SUPER::quit();
138 my $old_res_cb = $params{'res_cb'};
141 $self->validate_params('load', \%params);
143 return if $self->check_error($params{'res_cb'});
145 $self->with_disk_locked_state($params{'res_cb'}, sub {
146 my ($state, $res_cb) = @_;
147 $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) = @_;
164 my $info_cb = $params{'info_cb'};
166 return if $self->check_error($info_cb);
168 my $steps = define_steps
172 $self->try_lock($steps->{'locked'});
176 return if $self->check_error($info_cb);
178 # no need for synchronization -- all of these values are static
180 if ($key eq 'num_slots') {
181 my @slots = $self->_all_slots();
182 $results{$key} = scalar @slots;
183 } elsif ($key eq 'vendor_string') {
184 $results{$key} = 'chg-disk'; # mostly just for testing
185 } elsif ($key eq 'fast_search') {
186 $results{$key} = $self->{'support_fast_search'};
190 $info_cb->(undef, %results) if $info_cb;
198 my @slots = $self->_all_slots();
200 return if $self->check_error($params{'finished_cb'});
202 $self->with_disk_locked_state($params{'finished_cb'}, sub {
203 my ($state, $finished_cb) = @_;
205 $slot = (scalar @slots)? $slots[0] : 0;
206 $self->_set_current($slot);
216 return if $self->check_error($params{'inventory_cb'});
218 $self->with_disk_locked_state($params{'inventory_cb'}, sub {
219 my ($state, $finished_cb) = @_;
222 my @slots = $self->_all_slots();
223 my $current = $self->_get_current();
224 for my $slot (@slots) {
225 my $s = { slot => $slot, state => Amanda::Changer::SLOT_FULL };
226 $s->{'reserved'} = $self->_is_slot_in_use($state, $slot);
227 my $label = $self->_get_slot_label($slot);
229 $s->{'label'} = $self->_get_slot_label($slot);
230 $s->{'f_type'} = "".$Amanda::Header::F_TAPESTART;
231 $s->{'device_status'} = "".$DEVICE_STATUS_SUCCESS;
233 $s->{'label'} = undef;
234 $s->{'f_type'} = "".$Amanda::Header::F_EMPTY;
235 $s->{'device_status'} = "".$DEVICE_STATUS_VOLUME_UNLABELED;
237 $s->{'current'} = 1 if $slot eq $current;
240 $finished_cb->(undef, \@inventory);
248 return if $self->check_error($params{'finished_cb'});
250 $self->with_disk_locked_state($params{'finished_cb'}, sub {
251 my ($state, $finished_cb) = @_;
253 $state->{'meta'} = $params{'meta'};
254 $finished_cb->(undef);
258 sub with_disk_locked_state {
262 my $steps = define_steps
266 $self->try_lock($steps->{'locked'});
270 $self->with_locked_state($self->{'state_filename'},
283 return if $self->check_error($params{'finished_cb'});
285 $self->with_disk_locked_state($params{'finished_cb'}, sub {
286 my ($state, $finished_cb) = @_;
288 $finished_cb->(undef, $state->{'meta'});
298 if (exists $params{'relative_slot'}) {
299 if ($params{'relative_slot'} eq "current") {
300 $slot = $self->_get_current();
301 } elsif ($params{'relative_slot'} eq "next") {
302 if (exists $params{'slot'}) {
303 $slot = $params{'slot'};
305 $slot = $self->_get_current();
307 $slot = $self->_get_next($slot);
308 $self->_set_current($slot) if ($params{'set_current'});
310 return $self->make_error("failed", $params{'res_cb'},
312 message => "Invalid relative slot '$params{relative_slot}'");
315 $slot = $params{'slot'};
318 if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
319 return $self->make_error("failed", $params{'res_cb'},
320 reason => "notfound",
321 message => "all slots have been loaded");
324 if (!$self->_slot_exists($slot)) {
325 return $self->make_error("failed", $params{'res_cb'},
327 message => "Slot $slot not found");
330 if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
331 return $self->make_error("failed", $params{'res_cb'},
332 reason => "volinuse",
334 message => "Slot $slot is already in use by drive '$drive' and process '$params{state}->{drives}->{$drive}->{pid}'");
337 $drive = $self->_alloc_drive();
338 $self->_load_drive($drive, $slot);
339 $self->_set_current($slot) if ($params{'set_current'});
341 $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
347 my $label = $params{'label'};
351 $slot = $self->_find_label($label);
352 if (!defined $slot) {
353 return $self->make_error("failed", $params{'res_cb'},
354 reason => "notfound",
355 message => "Label '$label' not found");
358 if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
359 return $self->make_error("failed", $params{'res_cb'},
360 reason => "volinuse",
361 message => "Slot $slot, containing '$label', is already " .
362 "in use by drive '$drive'");
365 $drive = $self->_alloc_drive();
366 $self->_load_drive($drive, $slot);
367 $self->_set_current($slot) if ($params{'set_current'});
369 $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
374 my ($state, $res_cb, $drive, $slot) = @_;
377 my $device = Amanda::Device->new("file:$drive");
378 if ($device->status != $DEVICE_STATUS_SUCCESS) {
379 return $self->make_error("failed", $res_cb,
381 message => "opening 'file:$drive': " . $device->error_or_status());
384 if (my $err = $self->{'config'}->configure_device($device)) {
385 return $self->make_error("failed", $res_cb,
390 $res = Amanda::Changer::disk::Reservation->new($self, $device, $drive, $slot);
391 $state->{drives}->{$drive}->{pid} = $$;
392 $device->read_label();
394 $res_cb->(undef, $res);
397 # Internal function to find an unused (nonexistent) driveN subdirectory and
398 # create it. Note that this does not add a 'data' symlink inside the directory.
404 my $drive = $self->{'dir'} . "/drive$n";
407 warn "$drive is not a directory; please remove it" if (-e $drive and ! -d $drive);
409 next if (!mkdir($drive)); # TODO probably not a very effective locking mechanism..
415 # Internal function to enumerate all available slots. Slots are described by
419 my $dir = _quote_glob($self->{'dir'});
422 for my $slotname (bsd_glob("$dir/slot*/")) {
424 next unless (($slot) = ($slotname =~ /.*slot([0-9]+)\/$/));
425 push @slots, $slot + 0;
428 return map { "$_"} sort { $a <=> $b } @slots;
431 # Internal function to determine whether a slot exists.
433 my ($self, $slot) = @_;
434 return (-d $self->{'dir'} . "/slot$slot");
437 # Internal function to determine if a slot (specified by number) is in use by a
438 # drive, and return the path for that drive if so.
439 sub _is_slot_in_use {
440 my ($self, $state, $slot) = @_;
441 my $dir = _quote_glob($self->{'dir'});
443 for my $symlink (bsd_glob("$dir/drive*/data")) {
445 warn "'$symlink' is not a symlink; please remove it";
449 my $target = readlink($symlink);
451 warn "could not read '$symlink': $!";
456 if (!(($tslot) = ($target =~ /..\/slot([0-9]+)/))) {
457 warn "invalid changer symlink '$symlink' -> '$target'";
461 if ($tslot+0 == $slot) {
462 my $drive = $symlink;
463 $drive =~ s{/data$}{}; # strip the trailing '/data'
465 #check if process is alive
466 my $pid = $state->{drives}->{$drive}->{pid};
467 if (!defined $pid or !Amanda::Util::is_pid_alive($pid)) {
468 unlink("$drive/data")
469 or warn("Could not unlink '$drive/data': $!");
471 or warn("Could not rmdir '$drive': $!");
472 delete $state->{drives}->{$drive}->{pid};
482 sub _get_slot_label {
483 my ($self, $slot) = @_;
484 my $dir = _quote_glob($self->{'dir'});
486 for my $symlink (bsd_glob("$dir/slot$slot/00000.*")) {
487 my ($label) = ($symlink =~ qr{\/00000\.([^/]*)$});
491 return ''; # known, but blank
494 # Internal function to point a drive to a slot
496 my ($self, $drive, $slot) = @_;
498 die "'$drive' does not exist" unless (-d $drive);
499 if (-e "$drive/data") {
500 unlink("$drive/data");
503 symlink("../slot$slot", "$drive/data");
504 # TODO: read it to be sure??
507 # Internal function to return the slot containing a volume with the given
508 # label. This takes advantage of the naming convention used by vtapes.
510 my ($self, $label) = @_;
511 my $dir = _quote_glob($self->{'dir'});
512 $label = _quote_glob($label);
514 my @tapelabels = bsd_glob("$dir/slot*/00000.$label");
519 if (scalar @tapelabels > 1) {
520 warn "Multiple slots with label '$label': " . (join ", ", @tapelabels);
523 my ($slot) = ($tapelabels[0] =~ qr{/slot([0-9]+)/00000.});
527 # Internal function to get the next slot after $slot.
529 my ($self, $slot) = @_;
532 # Try just incrementing the slot number
533 $next_slot = $slot+1;
534 return $next_slot if (-d $self->{'dir'} . "/slot$next_slot");
536 # Otherwise, search through all slots
537 my @all_slots = $self->_all_slots();
538 my $prev = $all_slots[-1];
539 for $next_slot (@all_slots) {
540 return $next_slot if ($prev == $slot);
544 # not found? take a guess.
545 return $all_slots[0];
548 # Get the 'current' slot, represented as a symlink named 'data'
551 my $curlink = $self->{'dir'} . "/data";
553 # for 2.6.1-compatibility, also parse a "current" symlink
554 my $oldlink = $self->{'dir'} . "/current";
555 if (-l $oldlink and ! -e $curlink) {
556 rename($oldlink, $curlink);
560 my $target = readlink($curlink);
561 if ($target =~ "^slot([0-9]+)/?") {
566 # get the first slot as a default
567 my @slots = $self->_all_slots();
568 return 0 unless (@slots);
572 # Set the 'current' slot
574 my ($self, $slot) = @_;
575 my $curlink = $self->{'dir'} . "/data";
577 if (-l $curlink or -e $curlink) {
579 or warn("Could not unlink '$curlink'");
583 symlink("slot$slot", $curlink);
589 $filename =~ s/([]{}\\?*[])/\\$1/g;
595 my $dir = $self->{'dir'};
598 $self->{'fatal_error'} = Amanda::Changer->make_error("fatal", undef,
599 message => "directory '$dir' does not exist");
603 if ($self->{'removable'}) {
604 my ($dev, $ino) = stat $dir;
605 my $parentdir = dirname $dir;
606 my ($pdev, $pino) = stat $parentdir;
608 if ($self->{'mount'}) {
609 system $Amanda::Constants::MOUNT, $dir;
610 ($dev, $ino) = stat $dir;
614 $self->{'fatal_error'} = Amanda::Changer->make_error("fatal", undef,
615 message => "No removable disk mounted on '$dir'");
620 if ($self->{'num-slot'}) {
621 for my $i (1..$self->{'num-slot'}) {
622 my $slot_dir = "$dir/slot$i";
624 if ($self->{'auto-create-slot'}) {
625 if (!mkdir ($slot_dir)) {
626 $self->{'fatal_error'} = Amanda::Changer->make_error("fatal", undef,
627 message => "Can't create '$slot_dir': $!");
631 $self->{'fatal_error'} = Amanda::Changer->make_error("fatal", undef,
632 message => "slot $i doesn't exists '$slot_dir'");
638 if ($self->{'auto-create-slot'}) {
639 $self->{'fatal_error'} = Amanda::Changer->make_error("fatal", undef,
640 message => "property 'auto-create-slot' set but property 'num-slot' is not set");
649 my $poll = 0; # first delay will be 0.1s; see below
651 my $steps = define_steps
655 if ($self->{'mount'} && defined $self->{'fl'} &&
656 !$self->{'fl'}->locked()) {
657 return $steps->{'lock'}->();
659 $steps->{'done'}->();
663 my $rv = $self->{'fl'}->lock_rd();
665 # loop until we get the lock, increasing $poll to 10s
666 $poll += 100 unless $poll >= 10000;
667 return Amanda::MainLoop::call_after($poll, $steps->{'lock'});
668 } elsif ($rv == -1) {
669 return $self->make_error("fatal", $cb,
670 message => "Error locking '$self->{'umount_lockfile'}'");
672 if (defined $self->{'umount_src'}) {
673 $self->{'umount_src'}->remove();
674 $self->{'umount_src'} = undef;
676 return $steps->{'done'}->();
690 my $dir = $self->{'dir'};
691 if ($self->{'removable'} && $self->{'umount'}) {
692 my ($dev, $ino) = stat $dir;
693 my $parentdir = dirname $dir;
694 my ($pdev, $pino) = stat $parentdir;
696 system $Amanda::Constants::UMOUNT, $dir;
704 if (keys( %{$self->{'reservation'}}) == 0 ) {
706 if ($self->{'fl'}->locked()) {
707 $self->{'fl'}->unlock();
709 if ($self->{'umount'}) {
710 if (defined $self->{'umount_src'}) {
711 $self->{'umount_src'}->remove();
712 $self->{'umount_src'} = undef;
714 if ($self->{'fl'}->lock_wr() == 0) {
716 $self->{'fl'}->unlock();
726 my $do_umount = sub {
729 $self->{'umount_src'} = undef;
730 if ($self->{'fl'}->lock_wr() == 0) {
732 $self->{'fl'}->unlock();
736 if (defined $self->{'umount_idle'}) {
737 if ($self->{'umount_idle'} == 0) {
738 return $self->force_unlock();
740 if (defined $self->{'fl'}) {
741 if (keys( %{$self->{'reservation'}}) == 0 ) {
742 if ($self->{'fl'}->locked()) {
743 $self->{'fl'}->unlock();
745 if ($self->{'umount'}) {
746 if (defined $self->{'umount_src'}) {
747 $self->{'umount_src'}->remove();
748 $self->{'umount_src'} = undef;
750 $self->{'umount_src'} = Amanda::MainLoop::call_after(
751 0+$self->{'umount_idle'},
759 package Amanda::Changer::disk::Reservation;
761 @ISA = qw( Amanda::Changer::Reservation );
765 my ($chg, $device, $drive, $slot) = @_;
766 my $self = Amanda::Changer::Reservation::new($class);
768 $self->{'chg'} = $chg;
769 $self->{'drive'} = $drive;
771 $self->{'device'} = $device;
772 $self->{'this_slot'} = $slot;
774 $self->{'chg'}->{'reservation'}->{$slot} += 1;
781 my $drive = $self->{'drive'};
783 unlink("$drive/data")
784 or warn("Could not unlink '$drive/data': $!");
786 or warn("Could not rmdir '$drive': $!");
788 # unref the device, for good measure
789 $self->{'device'} = undef;
790 my $slot = $self->{'this_slot'};
793 $self->{'chg'}->{'reservation'}->{$slot} -= 1;
794 delete $self->{'chg'}->{'reservation'}->{$slot} if
795 $self->{'chg'}->{'reservation'}->{$slot} == 0;
796 $self->{'chg'}->try_unlock();
797 delete $self->{'chg'};
799 return $params{'finished_cb'}->();
802 if (exists $params{'unlocked'}) {
803 my $state = $params{state};
804 delete $state->{drives}->{$drive}->{pid};
808 $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
810 my ($state, $finished_cb) = @_;
812 delete $state->{drives}->{$drive}->{pid};
822 $params{'slot'} = $self->{'this_slot'};
823 $self->{'chg'}->get_meta_label(%params);
830 $params{'slot'} = $self->{'this_slot'};
831 $self->{'chg'}->set_meta_label(%params);