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'});
271 return $cb->($err) if $err;
272 $self->with_locked_state($self->{'state_filename'},
285 return if $self->check_error($params{'finished_cb'});
287 $self->with_disk_locked_state($params{'finished_cb'}, sub {
288 my ($state, $finished_cb) = @_;
290 $finished_cb->(undef, $state->{'meta'});
300 if (exists $params{'relative_slot'}) {
301 if ($params{'relative_slot'} eq "current") {
302 $slot = $self->_get_current();
303 } elsif ($params{'relative_slot'} eq "next") {
304 if (exists $params{'slot'}) {
305 $slot = $params{'slot'};
307 $slot = $self->_get_current();
309 $slot = $self->_get_next($slot);
310 $self->_set_current($slot) if ($params{'set_current'});
312 return $self->make_error("failed", $params{'res_cb'},
314 message => "Invalid relative slot '$params{relative_slot}'");
317 $slot = $params{'slot'};
320 if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
321 return $self->make_error("failed", $params{'res_cb'},
322 reason => "notfound",
323 message => "all slots have been loaded");
326 if (!$self->_slot_exists($slot)) {
327 return $self->make_error("failed", $params{'res_cb'},
329 message => "Slot $slot not found");
332 if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
333 return $self->make_error("failed", $params{'res_cb'},
334 reason => "volinuse",
336 message => "Slot $slot is already in use by drive '$drive' and process '$params{state}->{drives}->{$drive}->{pid}'");
339 $drive = $self->_alloc_drive();
340 $self->_load_drive($drive, $slot);
341 $self->_set_current($slot) if ($params{'set_current'});
343 $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
349 my $label = $params{'label'};
353 $slot = $self->_find_label($label);
354 if (!defined $slot) {
355 return $self->make_error("failed", $params{'res_cb'},
356 reason => "notfound",
357 message => "Label '$label' not found");
360 if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
361 return $self->make_error("failed", $params{'res_cb'},
362 reason => "volinuse",
363 message => "Slot $slot, containing '$label', is already " .
364 "in use by drive '$drive'");
367 $drive = $self->_alloc_drive();
368 $self->_load_drive($drive, $slot);
369 $self->_set_current($slot) if ($params{'set_current'});
371 $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
376 my ($state, $res_cb, $drive, $slot) = @_;
379 my $device = Amanda::Device->new("file:$drive");
380 if ($device->status != $DEVICE_STATUS_SUCCESS) {
381 return $self->make_error("failed", $res_cb,
383 message => "opening 'file:$drive': " . $device->error_or_status());
386 if (my $err = $self->{'config'}->configure_device($device)) {
387 return $self->make_error("failed", $res_cb,
392 $res = Amanda::Changer::disk::Reservation->new($self, $device, $drive, $slot);
393 $state->{drives}->{$drive}->{pid} = $$;
394 $device->read_label();
396 $res_cb->(undef, $res);
399 # Internal function to find an unused (nonexistent) driveN subdirectory and
400 # create it. Note that this does not add a 'data' symlink inside the directory.
406 my $drive = $self->{'dir'} . "/drive$n";
409 warn "$drive is not a directory; please remove it" if (-e $drive and ! -d $drive);
411 next if (!mkdir($drive)); # TODO probably not a very effective locking mechanism..
417 # Internal function to enumerate all available slots. Slots are described by
421 my $dir = _quote_glob($self->{'dir'});
424 for my $slotname (bsd_glob("$dir/slot*/")) {
426 next unless (($slot) = ($slotname =~ /.*slot([0-9]+)\/$/));
427 push @slots, $slot + 0;
430 return map { "$_"} sort { $a <=> $b } @slots;
433 # Internal function to determine whether a slot exists.
435 my ($self, $slot) = @_;
436 return (-d $self->{'dir'} . "/slot$slot");
439 # Internal function to determine if a slot (specified by number) is in use by a
440 # drive, and return the path for that drive if so.
441 sub _is_slot_in_use {
442 my ($self, $state, $slot) = @_;
443 my $dir = _quote_glob($self->{'dir'});
445 for my $symlink (bsd_glob("$dir/drive*/data")) {
447 warn "'$symlink' is not a symlink; please remove it";
451 my $target = readlink($symlink);
453 warn "could not read '$symlink': $!";
458 if (!(($tslot) = ($target =~ /..\/slot([0-9]+)/))) {
459 warn "invalid changer symlink '$symlink' -> '$target'";
463 if ($tslot+0 == $slot) {
464 my $drive = $symlink;
465 $drive =~ s{/data$}{}; # strip the trailing '/data'
467 #check if process is alive
468 my $pid = $state->{drives}->{$drive}->{pid};
469 if (!defined $pid or !Amanda::Util::is_pid_alive($pid)) {
470 unlink("$drive/data")
471 or warn("Could not unlink '$drive/data': $!");
473 or warn("Could not rmdir '$drive': $!");
474 delete $state->{drives}->{$drive}->{pid};
484 sub _get_slot_label {
485 my ($self, $slot) = @_;
486 my $dir = _quote_glob($self->{'dir'});
488 for my $symlink (bsd_glob("$dir/slot$slot/00000.*")) {
489 my ($label) = ($symlink =~ qr{\/00000\.([^/]*)$});
493 return ''; # known, but blank
496 # Internal function to point a drive to a slot
498 my ($self, $drive, $slot) = @_;
500 die "'$drive' does not exist" unless (-d $drive);
501 if (-e "$drive/data") {
502 unlink("$drive/data");
505 symlink("../slot$slot", "$drive/data");
506 # TODO: read it to be sure??
509 # Internal function to return the slot containing a volume with the given
510 # label. This takes advantage of the naming convention used by vtapes.
512 my ($self, $label) = @_;
513 my $dir = _quote_glob($self->{'dir'});
514 $label = _quote_glob($label);
516 my @tapelabels = bsd_glob("$dir/slot*/00000.$label");
521 if (scalar @tapelabels > 1) {
522 warn "Multiple slots with label '$label': " . (join ", ", @tapelabels);
525 my ($slot) = ($tapelabels[0] =~ qr{/slot([0-9]+)/00000.});
529 # Internal function to get the next slot after $slot.
531 my ($self, $slot) = @_;
534 # Try just incrementing the slot number
535 $next_slot = $slot+1;
536 return $next_slot if (-d $self->{'dir'} . "/slot$next_slot");
538 # Otherwise, search through all slots
539 my @all_slots = $self->_all_slots();
540 my $prev = $all_slots[-1];
541 for $next_slot (@all_slots) {
542 return $next_slot if ($prev == $slot);
546 # not found? take a guess.
547 return $all_slots[0];
550 # Get the 'current' slot, represented as a symlink named 'data'
553 my $curlink = $self->{'dir'} . "/data";
555 # for 2.6.1-compatibility, also parse a "current" symlink
556 my $oldlink = $self->{'dir'} . "/current";
557 if (-l $oldlink and ! -e $curlink) {
558 rename($oldlink, $curlink);
562 my $target = readlink($curlink);
563 if ($target =~ "^slot([0-9]+)/?") {
568 # get the first slot as a default
569 my @slots = $self->_all_slots();
570 return 0 unless (@slots);
574 # Set the 'current' slot
576 my ($self, $slot) = @_;
577 my $curlink = $self->{'dir'} . "/data";
579 if (-l $curlink or -e $curlink) {
581 or warn("Could not unlink '$curlink'");
585 symlink("slot$slot", $curlink);
591 $filename =~ s/([]{}\\?*[])/\\$1/g;
597 my $dir = $self->{'dir'};
600 return $self->make_error("fatal", undef,
601 message => "directory '$dir' does not exist");
604 if ($self->{'removable'}) {
605 my ($dev, $ino) = stat $dir;
606 my $parentdir = dirname $dir;
607 my ($pdev, $pino) = stat $parentdir;
609 if ($self->{'mount'}) {
610 system $Amanda::Constants::MOUNT, $dir;
611 ($dev, $ino) = stat $dir;
615 return $self->make_error("failed", undef,
616 reason => "notfound",
617 message => "No removable disk mounted on '$dir'");
621 if ($self->{'num-slot'}) {
622 for my $i (1..$self->{'num-slot'}) {
623 my $slot_dir = "$dir/slot$i";
625 if ($self->{'auto-create-slot'}) {
626 if (!mkdir ($slot_dir)) {
627 return $self->make_error("fatal", undef,
628 message => "Can't create '$slot_dir': $!");
631 return $self->make_error("fatal", undef,
632 message => "slot $i doesn't exists '$slot_dir'");
637 if ($self->{'auto-create-slot'}) {
638 return $self->make_error("fatal", undef,
639 message => "property 'auto-create-slot' set but property 'num-slot' is not set");
648 my $poll = 0; # first delay will be 0.1s; see below
650 my $steps = define_steps
654 if ($self->{'mount'} && defined $self->{'fl'} &&
655 !$self->{'fl'}->locked()) {
656 return $steps->{'lock'}->();
658 $steps->{'lock_done'}->();
662 my $rv = $self->{'fl'}->lock_rd();
664 # loop until we get the lock, increasing $poll to 10s
665 $poll += 100 unless $poll >= 10000;
666 return Amanda::MainLoop::call_after($poll, $steps->{'lock'});
667 } elsif ($rv == -1) {
668 return $self->make_error("fatal", $cb,
669 message => "Error locking '$self->{'umount_lockfile'}'");
671 if (defined $self->{'umount_src'}) {
672 $self->{'umount_src'}->remove();
673 $self->{'umount_src'} = undef;
675 return $steps->{'lock_done'}->();
679 step lock_done => sub {
680 my $err = $self->_validate();
688 my $dir = $self->{'dir'};
689 if ($self->{'removable'} && $self->{'umount'}) {
690 my ($dev, $ino) = stat $dir;
691 my $parentdir = dirname $dir;
692 my ($pdev, $pino) = stat $parentdir;
694 system $Amanda::Constants::UMOUNT, $dir;
702 if (keys( %{$self->{'reservation'}}) == 0 ) {
704 if ($self->{'fl'}->locked()) {
705 $self->{'fl'}->unlock();
707 if ($self->{'umount'}) {
708 if (defined $self->{'umount_src'}) {
709 $self->{'umount_src'}->remove();
710 $self->{'umount_src'} = undef;
712 if ($self->{'fl'}->lock_wr() == 0) {
714 $self->{'fl'}->unlock();
724 my $do_umount = sub {
727 $self->{'umount_src'} = undef;
728 if ($self->{'fl'}->lock_wr() == 0) {
730 $self->{'fl'}->unlock();
734 if (defined $self->{'umount_idle'}) {
735 if ($self->{'umount_idle'} == 0) {
736 return $self->force_unlock();
738 if (defined $self->{'fl'}) {
739 if (keys( %{$self->{'reservation'}}) == 0 ) {
740 if ($self->{'fl'}->locked()) {
741 $self->{'fl'}->unlock();
743 if ($self->{'umount'}) {
744 if (defined $self->{'umount_src'}) {
745 $self->{'umount_src'}->remove();
746 $self->{'umount_src'} = undef;
748 $self->{'umount_src'} = Amanda::MainLoop::call_after(
749 0+$self->{'umount_idle'},
757 package Amanda::Changer::disk::Reservation;
759 @ISA = qw( Amanda::Changer::Reservation );
763 my ($chg, $device, $drive, $slot) = @_;
764 my $self = Amanda::Changer::Reservation::new($class);
766 $self->{'chg'} = $chg;
767 $self->{'drive'} = $drive;
769 $self->{'device'} = $device;
770 $self->{'this_slot'} = $slot;
772 $self->{'chg'}->{'reservation'}->{$slot} += 1;
779 my $drive = $self->{'drive'};
781 unlink("$drive/data")
782 or warn("Could not unlink '$drive/data': $!");
784 or warn("Could not rmdir '$drive': $!");
786 # unref the device, for good measure
787 $self->{'device'} = undef;
788 my $slot = $self->{'this_slot'};
791 $self->{'chg'}->{'reservation'}->{$slot} -= 1;
792 delete $self->{'chg'}->{'reservation'}->{$slot} if
793 $self->{'chg'}->{'reservation'}->{$slot} == 0;
794 $self->{'chg'}->try_unlock();
795 delete $self->{'chg'};
797 return $params{'finished_cb'}->();
800 if (exists $params{'unlocked'}) {
801 my $state = $params{state};
802 delete $state->{drives}->{$drive}->{pid};
806 $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
808 my ($state, $finished_cb) = @_;
810 delete $state->{drives}->{$drive}->{pid};
820 $params{'slot'} = $self->{'this_slot'};
821 $self->{'chg'}->get_meta_label(%params);
828 $params{'slot'} = $self->{'this_slot'};
829 $self->{'chg'}->set_meta_label(%params);