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::disk;
25 @ISA = qw( Amanda::Changer );
27 use File::Glob qw( :glob );
30 use Amanda::Config qw( :getconf string_to_boolean );
34 use Amanda::Device qw( :constants );
42 This changer operates within a root directory, specified in the changer
43 string, which it arranges as follows:
47 | | data -> '../slot4'
49 | | data -> '../slot1'
56 The user should create the desired number of C<slot$n> subdirectories. The
57 changer will take care of dynamically creating the drives as needed, and track
58 the current slot using a "data" symlink. This allows use of "file:$dir" as a
59 device operating on the current slot, although note that it is unlocked.
61 Drives are dynamically allocated as Amanda applications request access to
62 particular slots. Each drive is represented as a subdirectory containing a
63 'data' symlink pointing to the "loaded" slot.
65 See the amanda-changers(7) manpage for usage information.
71 # The device state is shared between all changers accessing the same changer.
72 # It is a hash with keys:
75 # The 'drives' key is a hash, with drive as keys and hashes
76 # as values. Each drive's hash has keys:
77 # pid - the pid that reserved that drive.
83 my ($config, $tpchanger) = @_;
84 my ($dir) = ($tpchanger =~ /chg-disk:(.*)/);
85 my $properties = $config->{'properties'};
87 # note that we don't track outstanding Reservation objects -- we know
88 # they're gone when they delete their drive directory
92 state_filename => "$dir/state",
94 # list of all reservations
97 # this is set to 0 by various test scripts,
98 # notably Amanda_Taper_Scan_traditional
99 support_fast_search => 1,
102 bless ($self, $class);
104 $self->{'num-slot'} = $config->get_property('num-slot');
105 $self->{'auto-create-slot'} = $config->get_boolean_property(
106 'auto-create-slot', 0);
107 $self->{'removable'} = $config->get_boolean_property('removable', 0);
108 $self->{'mount'} = $config->get_boolean_property('mount', 0);
109 $self->{'umount'} = $config->get_boolean_property('umount', 0);
110 $self->{'umount_lockfile'} = $config->get_property('umount-lockfile');
111 $self->{'umount_idle'} = $config->get_property('umount-idle');
112 if (defined $self->{'umount_lockfile'}) {
113 $self->{'fl'} = Amanda::Util::file_lock->new($self->{'umount_lockfile'})
117 return $self->{'fatal_error'} if defined $self->{'fatal_error'};
125 $self->SUPER::DESTROY();
131 $self->force_unlock();
132 delete $self->{'fl'};
133 $self->SUPER::quit();
139 my $old_res_cb = $params{'res_cb'};
142 $self->validate_params('load', \%params);
144 return if $self->check_error($params{'res_cb'});
146 $self->with_disk_locked_state($params{'res_cb'}, sub {
147 my ($state, $res_cb) = @_;
148 $params{'state'} = $state;
150 # overwrite the callback for _load_by_xxx
151 $params{'res_cb'} = $res_cb;
153 if (exists $params{'slot'} or exists $params{'relative_slot'}) {
154 $self->_load_by_slot(%params);
155 } elsif (exists $params{'label'}) {
156 $self->_load_by_label(%params);
163 my ($key, %params) = @_;
165 my $info_cb = $params{'info_cb'};
167 return if $self->check_error($info_cb);
169 my $steps = define_steps
173 $self->try_lock($steps->{'locked'});
177 return if $self->check_error($info_cb);
179 # no need for synchronization -- all of these values are static
181 if ($key eq 'num_slots') {
182 my @slots = $self->_all_slots();
183 $results{$key} = scalar @slots;
184 } elsif ($key eq 'vendor_string') {
185 $results{$key} = 'chg-disk'; # mostly just for testing
186 } elsif ($key eq 'fast_search') {
187 $results{$key} = $self->{'support_fast_search'};
191 $info_cb->(undef, %results) if $info_cb;
199 my @slots = $self->_all_slots();
201 return if $self->check_error($params{'finished_cb'});
203 $self->with_disk_locked_state($params{'finished_cb'}, sub {
204 my ($state, $finished_cb) = @_;
206 $slot = (scalar @slots)? $slots[0] : 0;
207 $self->_set_current($slot);
217 return if $self->check_error($params{'inventory_cb'});
219 $self->with_disk_locked_state($params{'inventory_cb'}, sub {
220 my ($state, $finished_cb) = @_;
223 my @slots = $self->_all_slots();
224 my $current = $self->_get_current();
225 for my $slot (@slots) {
226 my $s = { slot => $slot, state => Amanda::Changer::SLOT_FULL };
227 $s->{'reserved'} = $self->_is_slot_in_use($state, $slot);
228 my $label = $self->_get_slot_label($slot);
230 $s->{'label'} = $self->_get_slot_label($slot);
231 $s->{'f_type'} = "".$Amanda::Header::F_TAPESTART;
232 $s->{'device_status'} = "".$DEVICE_STATUS_SUCCESS;
234 $s->{'label'} = undef;
235 $s->{'f_type'} = "".$Amanda::Header::F_EMPTY;
236 $s->{'device_status'} = "".$DEVICE_STATUS_VOLUME_UNLABELED;
238 $s->{'current'} = 1 if $slot eq $current;
241 $finished_cb->(undef, \@inventory);
249 return if $self->check_error($params{'finished_cb'});
251 $self->with_disk_locked_state($params{'finished_cb'}, sub {
252 my ($state, $finished_cb) = @_;
254 $state->{'meta'} = $params{'meta'};
255 $finished_cb->(undef);
259 sub with_disk_locked_state {
263 my $steps = define_steps
267 $self->try_lock($steps->{'locked'});
272 return $cb->($err) if $err;
273 $self->with_locked_state($self->{'state_filename'},
286 return if $self->check_error($params{'finished_cb'});
288 $self->with_disk_locked_state($params{'finished_cb'}, sub {
289 my ($state, $finished_cb) = @_;
291 $finished_cb->(undef, $state->{'meta'});
301 if (exists $params{'relative_slot'}) {
302 if ($params{'relative_slot'} eq "current") {
303 $slot = $self->_get_current();
304 } elsif ($params{'relative_slot'} eq "next") {
305 if (exists $params{'slot'}) {
306 $slot = $params{'slot'};
308 $slot = $self->_get_current();
310 $slot = $self->_get_next($slot);
311 $self->_set_current($slot) if ($params{'set_current'});
313 return $self->make_error("failed", $params{'res_cb'},
315 message => "Invalid relative slot '$params{relative_slot}'");
318 $slot = $params{'slot'};
321 if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
322 return $self->make_error("failed", $params{'res_cb'},
323 reason => "notfound",
324 message => "all slots have been loaded");
327 if (!$self->_slot_exists($slot)) {
328 return $self->make_error("failed", $params{'res_cb'},
330 message => "Slot $slot not found");
333 if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
334 return $self->make_error("failed", $params{'res_cb'},
335 reason => "volinuse",
337 message => "Slot $slot is already in use by drive '$drive' and process '$params{state}->{drives}->{$drive}->{pid}'");
340 $drive = $self->_alloc_drive();
341 $self->_load_drive($drive, $slot);
342 $self->_set_current($slot) if ($params{'set_current'});
344 $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
350 my $label = $params{'label'};
354 $slot = $self->_find_label($label);
355 if (!defined $slot) {
356 return $self->make_error("failed", $params{'res_cb'},
357 reason => "notfound",
358 message => "Label '$label' not found");
361 if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
362 return $self->make_error("failed", $params{'res_cb'},
363 reason => "volinuse",
364 message => "Slot $slot, containing '$label', is already " .
365 "in use by drive '$drive'");
368 $drive = $self->_alloc_drive();
369 $self->_load_drive($drive, $slot);
370 $self->_set_current($slot) if ($params{'set_current'});
372 $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
377 my ($state, $res_cb, $drive, $slot) = @_;
380 my $device = Amanda::Device->new("file:$drive");
381 if ($device->status != $DEVICE_STATUS_SUCCESS) {
382 return $self->make_error("failed", $res_cb,
384 message => "opening 'file:$drive': " . $device->error_or_status());
387 if (my $err = $self->{'config'}->configure_device($device)) {
388 return $self->make_error("failed", $res_cb,
393 $res = Amanda::Changer::disk::Reservation->new($self, $device, $drive, $slot);
394 $state->{drives}->{$drive}->{pid} = $$;
395 $device->read_label();
397 $res_cb->(undef, $res);
400 # Internal function to find an unused (nonexistent) driveN subdirectory and
401 # create it. Note that this does not add a 'data' symlink inside the directory.
407 my $drive = $self->{'dir'} . "/drive$n";
410 warn "$drive is not a directory; please remove it" if (-e $drive and ! -d $drive);
412 next if (!mkdir($drive)); # TODO probably not a very effective locking mechanism..
418 # Internal function to enumerate all available slots. Slots are described by
422 my $dir = _quote_glob($self->{'dir'});
425 for my $slotname (bsd_glob("$dir/slot*/")) {
427 next unless (($slot) = ($slotname =~ /.*slot([0-9]+)\/$/));
428 push @slots, $slot + 0;
431 return map { "$_"} sort { $a <=> $b } @slots;
434 # Internal function to determine whether a slot exists.
436 my ($self, $slot) = @_;
437 return (-d $self->{'dir'} . "/slot$slot");
440 # Internal function to determine if a slot (specified by number) is in use by a
441 # drive, and return the path for that drive if so.
442 sub _is_slot_in_use {
443 my ($self, $state, $slot) = @_;
444 my $dir = _quote_glob($self->{'dir'});
446 for my $symlink (bsd_glob("$dir/drive*/data")) {
448 warn "'$symlink' is not a symlink; please remove it";
452 my $target = readlink($symlink);
454 warn "could not read '$symlink': $!";
459 if (!(($tslot) = ($target =~ /..\/slot([0-9]+)/))) {
460 warn "invalid changer symlink '$symlink' -> '$target'";
464 if ($tslot+0 == $slot) {
465 my $drive = $symlink;
466 $drive =~ s{/data$}{}; # strip the trailing '/data'
468 #check if process is alive
469 my $pid = $state->{drives}->{$drive}->{pid};
470 if (!defined $pid or !Amanda::Util::is_pid_alive($pid)) {
471 unlink("$drive/data")
472 or warn("Could not unlink '$drive/data': $!");
474 or warn("Could not rmdir '$drive': $!");
475 delete $state->{drives}->{$drive}->{pid};
485 sub _get_slot_label {
486 my ($self, $slot) = @_;
487 my $dir = _quote_glob($self->{'dir'});
489 for my $symlink (bsd_glob("$dir/slot$slot/00000.*")) {
490 my ($label) = ($symlink =~ qr{\/00000\.([^/]*)$});
494 return ''; # known, but blank
497 # Internal function to point a drive to a slot
499 my ($self, $drive, $slot) = @_;
501 confess "'$drive' does not exist" unless (-d $drive);
502 if (-e "$drive/data") {
503 unlink("$drive/data");
506 symlink("../slot$slot", "$drive/data");
507 # TODO: read it to be sure??
510 # Internal function to return the slot containing a volume with the given
511 # label. This takes advantage of the naming convention used by vtapes.
513 my ($self, $label) = @_;
514 my $dir = _quote_glob($self->{'dir'});
515 $label = _quote_glob($label);
517 my @tapelabels = bsd_glob("$dir/slot*/00000.$label");
522 if (scalar @tapelabels > 1) {
523 warn "Multiple slots with label '$label': " . (join ", ", @tapelabels);
526 my ($slot) = ($tapelabels[0] =~ qr{/slot([0-9]+)/00000.});
530 # Internal function to get the next slot after $slot.
532 my ($self, $slot) = @_;
535 # Try just incrementing the slot number
536 $next_slot = $slot+1;
537 return $next_slot if (-d $self->{'dir'} . "/slot$next_slot");
539 # Otherwise, search through all slots
540 my @all_slots = $self->_all_slots();
541 my $prev = $all_slots[-1];
542 for $next_slot (@all_slots) {
543 return $next_slot if ($prev == $slot);
547 # not found? take a guess.
548 return $all_slots[0];
551 # Get the 'current' slot, represented as a symlink named 'data'
554 my $curlink = $self->{'dir'} . "/data";
556 # for 2.6.1-compatibility, also parse a "current" symlink
557 my $oldlink = $self->{'dir'} . "/current";
558 if (-l $oldlink and ! -e $curlink) {
559 rename($oldlink, $curlink);
563 my $target = readlink($curlink);
564 if ($target =~ "^slot([0-9]+)/?") {
569 # get the first slot as a default
570 my @slots = $self->_all_slots();
571 return 0 unless (@slots);
575 # Set the 'current' slot
577 my ($self, $slot) = @_;
578 my $curlink = $self->{'dir'} . "/data";
580 if (-l $curlink or -e $curlink) {
582 or warn("Could not unlink '$curlink'");
586 symlink("slot$slot", $curlink);
592 $filename =~ s/([]{}\\?*[])/\\$1/g;
598 my $dir = $self->{'dir'};
601 return $self->make_error("fatal", undef,
602 message => "directory '$dir' does not exist");
605 if ($self->{'removable'}) {
606 my ($dev, $ino) = stat $dir;
607 my $parentdir = dirname $dir;
608 my ($pdev, $pino) = stat $parentdir;
610 if ($self->{'mount'}) {
611 system $Amanda::Constants::MOUNT, $dir;
612 ($dev, $ino) = stat $dir;
616 return $self->make_error("failed", undef,
617 reason => "notfound",
618 message => "No removable disk mounted on '$dir'");
622 if ($self->{'num-slot'}) {
623 for my $i (1..$self->{'num-slot'}) {
624 my $slot_dir = "$dir/slot$i";
626 if ($self->{'auto-create-slot'}) {
627 if (!mkdir ($slot_dir)) {
628 return $self->make_error("fatal", undef,
629 message => "Can't create '$slot_dir': $!");
632 return $self->make_error("fatal", undef,
633 message => "slot $i doesn't exists '$slot_dir'");
638 if ($self->{'auto-create-slot'}) {
639 return $self->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->{'lock_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->{'lock_done'}->();
680 step lock_done => sub {
681 my $err = $self->_validate();
689 my $dir = $self->{'dir'};
690 if ($self->{'removable'} && $self->{'umount'}) {
691 my ($dev, $ino) = stat $dir;
692 my $parentdir = dirname $dir;
693 my ($pdev, $pino) = stat $parentdir;
695 system $Amanda::Constants::UMOUNT, $dir;
703 if (keys( %{$self->{'reservation'}}) == 0 ) {
705 if ($self->{'fl'}->locked()) {
706 $self->{'fl'}->unlock();
708 if ($self->{'umount'}) {
709 if (defined $self->{'umount_src'}) {
710 $self->{'umount_src'}->remove();
711 $self->{'umount_src'} = undef;
713 if ($self->{'fl'}->lock_wr() == 0) {
715 $self->{'fl'}->unlock();
725 my $do_umount = sub {
728 $self->{'umount_src'} = undef;
729 if ($self->{'fl'}->lock_wr() == 0) {
731 $self->{'fl'}->unlock();
735 if (defined $self->{'umount_idle'}) {
736 if ($self->{'umount_idle'} == 0) {
737 return $self->force_unlock();
739 if (defined $self->{'fl'}) {
740 if (keys( %{$self->{'reservation'}}) == 0 ) {
741 if ($self->{'fl'}->locked()) {
742 $self->{'fl'}->unlock();
744 if ($self->{'umount'}) {
745 if (defined $self->{'umount_src'}) {
746 $self->{'umount_src'}->remove();
747 $self->{'umount_src'} = undef;
749 $self->{'umount_src'} = Amanda::MainLoop::call_after(
750 0+$self->{'umount_idle'},
758 package Amanda::Changer::disk::Reservation;
760 @ISA = qw( Amanda::Changer::Reservation );
764 my ($chg, $device, $drive, $slot) = @_;
765 my $self = Amanda::Changer::Reservation::new($class);
767 $self->{'chg'} = $chg;
768 $self->{'drive'} = $drive;
770 $self->{'device'} = $device;
771 $self->{'this_slot'} = $slot;
773 $self->{'chg'}->{'reservation'}->{$slot} += 1;
780 my $drive = $self->{'drive'};
782 unlink("$drive/data")
783 or warn("Could not unlink '$drive/data': $!");
785 or warn("Could not rmdir '$drive': $!");
787 # unref the device, for good measure
788 $self->{'device'} = undef;
789 my $slot = $self->{'this_slot'};
792 $self->{'chg'}->{'reservation'}->{$slot} -= 1;
793 delete $self->{'chg'}->{'reservation'}->{$slot} if
794 $self->{'chg'}->{'reservation'}->{$slot} == 0;
795 $self->{'chg'}->try_unlock();
796 delete $self->{'chg'};
798 return $params{'finished_cb'}->();
801 if (exists $params{'unlocked'}) {
802 my $state = $params{state};
803 delete $state->{drives}->{$drive}->{pid};
807 $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
809 my ($state, $finished_cb) = @_;
811 delete $state->{drives}->{$drive}->{pid};
821 $params{'slot'} = $self->{'this_slot'};
822 $self->{'chg'}->get_meta_label(%params);
829 $params{'slot'} = $self->{'this_slot'};
830 $self->{'chg'}->set_meta_label(%params);