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::disk;
26 @ISA = qw( Amanda::Changer );
28 use File::Glob qw( :glob );
31 use Amanda::Config qw( :getconf string_to_boolean );
35 use Amanda::Device qw( :constants );
43 This changer operates within a root directory, specified in the changer
44 string, which it arranges as follows:
48 | | data -> '../slot4'
50 | | data -> '../slot1'
57 The user should create the desired number of C<slot$n> subdirectories. The
58 changer will take care of dynamically creating the drives as needed, and track
59 the current slot using a "data" symlink. This allows use of "file:$dir" as a
60 device operating on the current slot, although note that it is unlocked.
62 Drives are dynamically allocated as Amanda applications request access to
63 particular slots. Each drive is represented as a subdirectory containing a
64 'data' symlink pointing to the "loaded" slot.
66 See the amanda-changers(7) manpage for usage information.
72 # The device state is shared between all changers accessing the same changer.
73 # It is a hash with keys:
76 # The 'drives' key is a hash, with drive as keys and hashes
77 # as values. Each drive's hash has keys:
78 # pid - the pid that reserved that drive.
84 my ($config, $tpchanger) = @_;
85 my ($dir) = ($tpchanger =~ /chg-disk:(.*)/);
86 my $properties = $config->{'properties'};
88 # note that we don't track outstanding Reservation objects -- we know
89 # they're gone when they delete their drive directory
93 state_filename => "$dir/state",
95 # list of all reservations
98 # this is set to 0 by various test scripts,
99 # notably Amanda_Taper_Scan_traditional
100 support_fast_search => 1,
103 bless ($self, $class);
105 if ($config->{'changerfile'}) {
106 $self->{'state_filename'} = Amanda::Config::config_dir_relative($config->{'changerfile'});
108 $self->{'lock-timeout'} = $config->get_property('lock-timeout');
110 $self->{'num-slot'} = $config->get_property('num-slot');
111 $self->{'auto-create-slot'} = $config->get_boolean_property(
112 'auto-create-slot', 0);
113 $self->{'removable'} = $config->get_boolean_property('removable', 0);
114 $self->{'mount'} = $config->get_boolean_property('mount', 0);
115 $self->{'umount'} = $config->get_boolean_property('umount', 0);
116 $self->{'umount_lockfile'} = $config->get_property('umount-lockfile');
117 $self->{'umount_idle'} = $config->get_property('umount-idle');
118 if (defined $self->{'umount_lockfile'}) {
119 $self->{'fl'} = Amanda::Util::file_lock->new($self->{'umount_lockfile'})
123 return $self->{'fatal_error'} if defined $self->{'fatal_error'};
131 $self->SUPER::DESTROY();
137 $self->force_unlock();
138 delete $self->{'fl'};
139 $self->SUPER::quit();
145 my $old_res_cb = $params{'res_cb'};
148 $self->validate_params('load', \%params);
150 return if $self->check_error($params{'res_cb'});
152 $self->with_disk_locked_state($params{'res_cb'}, sub {
153 my ($state, $res_cb) = @_;
154 $params{'state'} = $state;
156 # overwrite the callback for _load_by_xxx
157 $params{'res_cb'} = $res_cb;
159 if (exists $params{'slot'} or exists $params{'relative_slot'}) {
160 $self->_load_by_slot(%params);
161 } elsif (exists $params{'label'}) {
162 $self->_load_by_label(%params);
169 my ($key, %params) = @_;
171 my $info_cb = $params{'info_cb'};
173 return if $self->check_error($info_cb);
175 my $steps = define_steps
179 $self->try_lock($steps->{'locked'});
183 return if $self->check_error($info_cb);
185 # no need for synchronization -- all of these values are static
187 if ($key eq 'num_slots') {
188 my @slots = $self->_all_slots();
189 $results{$key} = scalar @slots;
190 } elsif ($key eq 'vendor_string') {
191 $results{$key} = 'chg-disk'; # mostly just for testing
192 } elsif ($key eq 'fast_search') {
193 $results{$key} = $self->{'support_fast_search'};
197 $info_cb->(undef, %results) if $info_cb;
205 my @slots = $self->_all_slots();
207 return if $self->check_error($params{'finished_cb'});
209 $self->with_disk_locked_state($params{'finished_cb'}, sub {
210 my ($state, $finished_cb) = @_;
212 $slot = (scalar @slots)? $slots[0] : 0;
213 $self->_set_current($slot);
223 return if $self->check_error($params{'inventory_cb'});
225 $self->with_disk_locked_state($params{'inventory_cb'}, sub {
226 my ($state, $finished_cb) = @_;
229 my @slots = $self->_all_slots();
230 my $current = $self->_get_current();
231 for my $slot (@slots) {
232 my $s = { slot => $slot, state => Amanda::Changer::SLOT_FULL };
233 $s->{'reserved'} = $self->_is_slot_in_use($state, $slot);
234 my $label = $self->_get_slot_label($slot);
236 $s->{'label'} = $self->_get_slot_label($slot);
237 $s->{'f_type'} = "".$Amanda::Header::F_TAPESTART;
238 $s->{'device_status'} = "".$DEVICE_STATUS_SUCCESS;
240 $s->{'label'} = undef;
241 $s->{'f_type'} = "".$Amanda::Header::F_EMPTY;
242 $s->{'device_status'} = "".$DEVICE_STATUS_VOLUME_UNLABELED;
244 $s->{'current'} = 1 if $slot eq $current;
247 $finished_cb->(undef, \@inventory);
255 return if $self->check_error($params{'finished_cb'});
257 $self->with_disk_locked_state($params{'finished_cb'}, sub {
258 my ($state, $finished_cb) = @_;
260 $state->{'meta'} = $params{'meta'};
261 $finished_cb->(undef);
265 sub with_disk_locked_state {
269 my $steps = define_steps
273 $self->try_lock($steps->{'locked'});
278 return $cb->($err) if $err;
279 $self->with_locked_state($self->{'state_filename'},
292 return if $self->check_error($params{'finished_cb'});
294 $self->with_disk_locked_state($params{'finished_cb'}, sub {
295 my ($state, $finished_cb) = @_;
297 $finished_cb->(undef, $state->{'meta'});
307 if (exists $params{'relative_slot'}) {
308 if ($params{'relative_slot'} eq "current") {
309 $slot = $self->_get_current();
310 } elsif ($params{'relative_slot'} eq "next") {
311 if (exists $params{'slot'}) {
312 $slot = $params{'slot'};
314 $slot = $self->_get_current();
316 $slot = $self->_get_next($slot);
317 $self->_set_current($slot) if ($params{'set_current'});
319 return $self->make_error("failed", $params{'res_cb'},
321 message => "Invalid relative slot '$params{relative_slot}'");
324 $slot = $params{'slot'};
327 if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
328 return $self->make_error("failed", $params{'res_cb'},
329 reason => "notfound",
330 message => "all slots have been loaded");
333 if (!$self->_slot_exists($slot)) {
334 return $self->make_error("failed", $params{'res_cb'},
336 message => "Slot $slot not found");
339 if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
340 return $self->make_error("failed", $params{'res_cb'},
341 reason => "volinuse",
343 message => "Slot $slot is already in use by drive '$drive' and process '$params{state}->{drives}->{$drive}->{pid}'");
346 $drive = $self->_alloc_drive();
347 $self->_load_drive($drive, $slot);
348 $self->_set_current($slot) if ($params{'set_current'});
350 $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
356 my $label = $params{'label'};
360 $slot = $self->_find_label($label);
361 if (!defined $slot) {
362 return $self->make_error("failed", $params{'res_cb'},
363 reason => "notfound",
364 message => "Label '$label' not found");
367 if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
368 return $self->make_error("failed", $params{'res_cb'},
369 reason => "volinuse",
370 message => "Slot $slot, containing '$label', is already " .
371 "in use by drive '$drive'");
374 $drive = $self->_alloc_drive();
375 $self->_load_drive($drive, $slot);
376 $self->_set_current($slot) if ($params{'set_current'});
378 $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
383 my ($state, $res_cb, $drive, $slot) = @_;
386 my $device = Amanda::Device->new("file:$drive");
387 if ($device->status != $DEVICE_STATUS_SUCCESS) {
388 return $self->make_error("failed", $res_cb,
390 message => "opening 'file:$drive': " . $device->error_or_status());
393 if (my $err = $self->{'config'}->configure_device($device)) {
394 return $self->make_error("failed", $res_cb,
399 $res = Amanda::Changer::disk::Reservation->new($self, $device, $drive, $slot);
400 $state->{drives}->{$drive}->{pid} = $$;
401 $device->read_label();
403 $res_cb->(undef, $res);
406 # Internal function to find an unused (nonexistent) driveN subdirectory and
407 # create it. Note that this does not add a 'data' symlink inside the directory.
413 my $drive = $self->{'dir'} . "/drive$n";
416 warn "$drive is not a directory; please remove it" if (-e $drive and ! -d $drive);
418 next if (!mkdir($drive)); # TODO probably not a very effective locking mechanism..
424 # Internal function to enumerate all available slots. Slots are described by
428 my $dir = _quote_glob($self->{'dir'});
431 for my $slotname (bsd_glob("$dir/slot*/")) {
433 next unless (($slot) = ($slotname =~ /.*slot([0-9]+)\/$/));
434 push @slots, $slot + 0;
437 return map { "$_"} sort { $a <=> $b } @slots;
440 # Internal function to determine whether a slot exists.
442 my ($self, $slot) = @_;
443 return (-d $self->{'dir'} . "/slot$slot");
446 # Internal function to determine if a slot (specified by number) is in use by a
447 # drive, and return the path for that drive if so.
448 sub _is_slot_in_use {
449 my ($self, $state, $slot) = @_;
450 my $dir = _quote_glob($self->{'dir'});
452 for my $symlink (bsd_glob("$dir/drive*/data")) {
454 warn "'$symlink' is not a symlink; please remove it";
458 my $target = readlink($symlink);
460 warn "could not read '$symlink': $!";
465 if (!(($tslot) = ($target =~ /..\/slot([0-9]+)/))) {
466 warn "invalid changer symlink '$symlink' -> '$target'";
470 if ($tslot+0 == $slot) {
471 my $drive = $symlink;
472 $drive =~ s{/data$}{}; # strip the trailing '/data'
474 #check if process is alive
475 my $pid = $state->{drives}->{$drive}->{pid};
476 if (!defined $pid or !Amanda::Util::is_pid_alive($pid)) {
477 unlink("$drive/data")
478 or warn("Could not unlink '$drive/data': $!");
480 or warn("Could not rmdir '$drive': $!");
481 delete $state->{drives}->{$drive}->{pid};
491 sub _get_slot_label {
492 my ($self, $slot) = @_;
493 my $dir = _quote_glob($self->{'dir'});
495 for my $symlink (bsd_glob("$dir/slot$slot/00000.*")) {
496 my ($label) = ($symlink =~ qr{\/00000\.([^/]*)$});
500 return ''; # known, but blank
503 # Internal function to point a drive to a slot
505 my ($self, $drive, $slot) = @_;
507 confess "'$drive' does not exist" unless (-d $drive);
508 if (-e "$drive/data") {
509 unlink("$drive/data");
512 symlink("../slot$slot", "$drive/data");
513 # TODO: read it to be sure??
516 # Internal function to return the slot containing a volume with the given
517 # label. This takes advantage of the naming convention used by vtapes.
519 my ($self, $label) = @_;
520 my $dir = _quote_glob($self->{'dir'});
521 $label = _quote_glob($label);
523 my @tapelabels = bsd_glob("$dir/slot*/00000.$label");
528 if (scalar @tapelabels > 1) {
529 warn "Multiple slots with label '$label': " . (join ", ", @tapelabels);
532 my ($slot) = ($tapelabels[0] =~ qr{/slot([0-9]+)/00000.});
536 # Internal function to get the next slot after $slot.
538 my ($self, $slot) = @_;
541 # Try just incrementing the slot number
542 $next_slot = $slot+1;
543 return $next_slot if (-d $self->{'dir'} . "/slot$next_slot");
545 # Otherwise, search through all slots
546 my @all_slots = $self->_all_slots();
547 my $prev = $all_slots[-1];
548 for $next_slot (@all_slots) {
549 return $next_slot if ($prev == $slot);
553 # not found? take a guess.
554 return $all_slots[0];
557 # Get the 'current' slot, represented as a symlink named 'data'
560 my $curlink = $self->{'dir'} . "/data";
562 # for 2.6.1-compatibility, also parse a "current" symlink
563 my $oldlink = $self->{'dir'} . "/current";
564 if (-l $oldlink and ! -e $curlink) {
565 rename($oldlink, $curlink);
569 my $target = readlink($curlink);
570 if ($target =~ "^slot([0-9]+)/?") {
575 # get the first slot as a default
576 my @slots = $self->_all_slots();
577 return 0 unless (@slots);
581 # Set the 'current' slot
583 my ($self, $slot) = @_;
584 my $curlink = $self->{'dir'} . "/data";
586 if (-l $curlink or -e $curlink) {
588 or warn("Could not unlink '$curlink'");
592 symlink("slot$slot", $curlink);
598 $filename =~ s/([]{}\\?*[])/\\$1/g;
604 my $dir = $self->{'dir'};
607 return $self->make_error("fatal", undef,
608 message => "directory '$dir' does not exist");
611 if ($self->{'removable'}) {
612 my ($dev, $ino) = stat $dir;
613 my $parentdir = dirname $dir;
614 my ($pdev, $pino) = stat $parentdir;
616 if ($self->{'mount'}) {
617 system $Amanda::Constants::MOUNT, $dir;
618 ($dev, $ino) = stat $dir;
622 return $self->make_error("failed", undef,
623 reason => "notfound",
624 message => "No removable disk mounted on '$dir'");
628 if ($self->{'num-slot'}) {
629 for my $i (1..$self->{'num-slot'}) {
630 my $slot_dir = "$dir/slot$i";
632 if ($self->{'auto-create-slot'}) {
633 if (!mkdir ($slot_dir)) {
634 return $self->make_error("fatal", undef,
635 message => "Can't create '$slot_dir': $!");
638 return $self->make_error("fatal", undef,
639 message => "slot $i doesn't exists '$slot_dir'");
644 if ($self->{'auto-create-slot'}) {
645 return $self->make_error("fatal", undef,
646 message => "property 'auto-create-slot' set but property 'num-slot' is not set");
655 my $poll = 0; # first delay will be 0.1s; see below
658 if (defined $self->{'lock-timeout'}) {
659 $time = time() + $self->{'lock-timeout'};
661 $time = time() + 1000;
665 my $steps = define_steps
669 if ($self->{'mount'} && defined $self->{'fl'} &&
670 !$self->{'fl'}->locked()) {
671 return $steps->{'lock'}->();
673 $steps->{'lock_done'}->();
677 my $rv = $self->{'fl'}->lock_rd();
678 if ($rv == 1 && time() < $time) {
679 # loop until we get the lock, increasing $poll to 10s
680 $poll += 100 unless $poll >= 10000;
681 return Amanda::MainLoop::call_after($poll, $steps->{'lock'});
683 return $self->make_error("fatal", $cb,
684 message => "Timeout trying to lock '$self->{'umount_lockfile'}'");
685 } elsif ($rv == -1) {
686 return $self->make_error("fatal", $cb,
687 message => "Error locking '$self->{'umount_lockfile'}'");
689 if (defined $self->{'umount_src'}) {
690 $self->{'umount_src'}->remove();
691 $self->{'umount_src'} = undef;
693 return $steps->{'lock_done'}->();
697 step lock_done => sub {
698 my $err = $self->_validate();
706 my $dir = $self->{'dir'};
707 if ($self->{'removable'} && $self->{'umount'}) {
708 my ($dev, $ino) = stat $dir;
709 my $parentdir = dirname $dir;
710 my ($pdev, $pino) = stat $parentdir;
712 system $Amanda::Constants::UMOUNT, $dir;
720 if (keys( %{$self->{'reservation'}}) == 0 ) {
722 if ($self->{'fl'}->locked()) {
723 $self->{'fl'}->unlock();
725 if ($self->{'umount'}) {
726 if (defined $self->{'umount_src'}) {
727 $self->{'umount_src'}->remove();
728 $self->{'umount_src'} = undef;
730 if ($self->{'fl'}->lock_wr() == 0) {
732 $self->{'fl'}->unlock();
742 my $do_umount = sub {
745 $self->{'umount_src'} = undef;
746 if ($self->{'fl'}->lock_wr() == 0) {
748 $self->{'fl'}->unlock();
752 if (defined $self->{'umount_idle'}) {
753 if ($self->{'umount_idle'} == 0) {
754 return $self->force_unlock();
756 if (defined $self->{'fl'}) {
757 if (keys( %{$self->{'reservation'}}) == 0 ) {
758 if ($self->{'fl'}->locked()) {
759 $self->{'fl'}->unlock();
761 if ($self->{'umount'}) {
762 if (defined $self->{'umount_src'}) {
763 $self->{'umount_src'}->remove();
764 $self->{'umount_src'} = undef;
766 $self->{'umount_src'} = Amanda::MainLoop::call_after(
767 0+$self->{'umount_idle'},
775 package Amanda::Changer::disk::Reservation;
777 @ISA = qw( Amanda::Changer::Reservation );
781 my ($chg, $device, $drive, $slot) = @_;
782 my $self = Amanda::Changer::Reservation::new($class);
784 $self->{'chg'} = $chg;
785 $self->{'drive'} = $drive;
787 $self->{'device'} = $device;
788 $self->{'this_slot'} = $slot;
790 $self->{'chg'}->{'reservation'}->{$slot} += 1;
797 my $drive = $self->{'drive'};
799 unlink("$drive/data")
800 or warn("Could not unlink '$drive/data': $!");
802 or warn("Could not rmdir '$drive': $!");
804 # unref the device, for good measure
805 $self->{'device'} = undef;
806 my $slot = $self->{'this_slot'};
809 $self->{'chg'}->{'reservation'}->{$slot} -= 1;
810 delete $self->{'chg'}->{'reservation'}->{$slot} if
811 $self->{'chg'}->{'reservation'}->{$slot} == 0;
812 $self->{'chg'}->try_unlock();
813 delete $self->{'chg'};
815 return $params{'finished_cb'}->();
818 if (exists $params{'unlocked'}) {
819 my $state = $params{state};
820 delete $state->{drives}->{$drive}->{pid};
824 $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
826 my ($state, $finished_cb) = @_;
828 delete $state->{drives}->{$drive}->{pid};
838 $params{'slot'} = $self->{'this_slot'};
839 $self->{'chg'}->get_meta_label(%params);
846 $params{'slot'} = $self->{'this_slot'};
847 $self->{'chg'}->set_meta_label(%params);