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 );
28 use Amanda::Config qw( :getconf );
32 use Amanda::Device qw( :constants );
40 This changer operates within a root directory, specified in the changer
41 string, which it arranges as follows:
45 | | data -> '../slot4'
47 | | data -> '../slot1'
54 The user should create the desired number of C<slot$n> subdirectories. The
55 changer will take care of dynamically creating the drives as needed, and track
56 the current slot using a "data" symlink. This allows use of "file:$dir" as a
57 device operating on the current slot, although note that it is unlocked.
59 Drives are dynamically allocated as Amanda applications request access to
60 particular slots. Each drive is represented as a subdirectory containing a
61 'data' symlink pointing to the "loaded" slot.
63 See the amanda-changers(7) manpage for usage information.
69 # The device state is shared between all changers accessing the same changer.
70 # It is a hash with keys:
73 # The 'drives' key is a hash, with drive as keys and hashes
74 # as values. Each drive's hash has keys:
75 # pid - the pid that reserved that drive.
81 my ($config, $tpchanger) = @_;
82 my ($dir) = ($tpchanger =~ /chg-disk:(.*)/);
85 return Amanda::Changer->make_error("fatal", undef,
86 message => "directory '$dir' does not exist");
89 # note that we don't track outstanding Reservation objects -- we know
90 # they're gone when they delete their drive directory
94 state_filename => "$dir/state",
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);
108 my $old_res_cb = $params{'res_cb'};
111 $self->validate_params('load', \%params);
113 return if $self->check_error($params{'res_cb'});
115 $self->with_locked_state($self->{'state_filename'},
116 $params{'res_cb'}, sub {
117 my ($state, $res_cb) = @_;
118 $params{'state'} = $state;
120 # overwrite the callback for _load_by_xxx
121 $params{'res_cb'} = $res_cb;
123 if (exists $params{'slot'} or exists $params{'relative_slot'}) {
124 $self->_load_by_slot(%params);
125 } elsif (exists $params{'label'}) {
126 $self->_load_by_label(%params);
133 my ($key, %params) = @_;
136 return if $self->check_error($params{'info_cb'});
138 # no need for synchronization -- all of these values are static
140 if ($key eq 'num_slots') {
141 my @slots = $self->_all_slots();
142 $results{$key} = scalar @slots;
143 } elsif ($key eq 'vendor_string') {
144 $results{$key} = 'chg-disk'; # mostly just for testing
145 } elsif ($key eq 'fast_search') {
146 $results{$key} = $self->{'support_fast_search'};
149 $params{'info_cb'}->(undef, %results) if $params{'info_cb'};
156 my @slots = $self->_all_slots();
158 return if $self->check_error($params{'finished_cb'});
160 $self->with_locked_state($self->{'state_filename'},
161 $params{'finished_cb'}, sub {
162 my ($state, $finished_cb) = @_;
164 $slot = (scalar @slots)? $slots[0] : 0;
165 $self->_set_current($slot);
175 return if $self->check_error($params{'inventory_cb'});
177 my @slots = $self->_all_slots();
179 $self->with_locked_state($self->{'state_filename'},
180 $params{'inventory_cb'}, sub {
181 my ($state, $finished_cb) = @_;
184 my $current = $self->_get_current();
185 for my $slot (@slots) {
186 my $s = { slot => $slot, state => Amanda::Changer::SLOT_FULL };
187 $s->{'reserved'} = $self->_is_slot_in_use($state, $slot);
188 my $label = $self->_get_slot_label($slot);
190 $s->{'label'} = $self->_get_slot_label($slot);
191 $s->{'f_type'} = "".$Amanda::Header::F_TAPESTART;
193 $s->{'label'} = undef;
194 $s->{'f_type'} = "".$Amanda::Header::F_EMPTY;
196 $s->{'device_status'} = "".$DEVICE_STATUS_SUCCESS;
197 $s->{'current'} = 1 if $slot eq $current;
200 $finished_cb->(undef, \@inventory);
210 if (exists $params{'relative_slot'}) {
211 if ($params{'relative_slot'} eq "current") {
212 $slot = $self->_get_current();
213 } elsif ($params{'relative_slot'} eq "next") {
214 if (exists $params{'slot'}) {
215 $slot = $params{'slot'};
217 $slot = $self->_get_current();
219 $slot = $self->_get_next($slot);
220 $self->_set_current($slot) if ($params{'set_current'});
222 return $self->make_error("failed", $params{'res_cb'},
224 message => "Invalid relative slot '$params{relative_slot}'");
227 $slot = $params{'slot'};
230 if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
231 return $self->make_error("failed", $params{'res_cb'},
232 reason => "notfound",
233 message => "all slots have been loaded");
236 if (!$self->_slot_exists($slot)) {
237 return $self->make_error("failed", $params{'res_cb'},
239 message => "Slot $slot not found");
242 if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
243 return $self->make_error("failed", $params{'res_cb'},
244 reason => "volinuse",
246 message => "Slot $slot is already in use by drive '$drive' and process '$params{state}->{drives}->{$drive}->{pid}'");
249 $drive = $self->_alloc_drive();
250 $self->_load_drive($drive, $slot);
251 $self->_set_current($slot) if ($params{'set_current'});
253 $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
259 my $label = $params{'label'};
263 $slot = $self->_find_label($label);
264 if (!defined $slot) {
265 return $self->make_error("failed", $params{'res_cb'},
266 reason => "notfound",
267 message => "Label '$label' not found");
270 if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
271 return $self->make_error("failed", $params{'res_cb'},
272 reason => "volinuse",
273 message => "Slot $slot, containing '$label', is already " .
274 "in use by drive '$drive'");
277 $drive = $self->_alloc_drive();
278 $self->_load_drive($drive, $slot);
279 $self->_set_current($slot) if ($params{'set_current'});
281 $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
286 my ($state, $res_cb, $drive, $slot) = @_;
289 my $device = Amanda::Device->new("file:$drive");
290 if ($device->status != $DEVICE_STATUS_SUCCESS) {
291 return $self->make_error("failed", $res_cb,
293 message => "opening 'file:$drive': " . $device->error_or_status());
296 if (my $err = $self->{'config'}->configure_device($device)) {
297 return $self->make_error("failed", $res_cb,
302 $res = Amanda::Changer::disk::Reservation->new($self, $device, $drive, $slot);
303 $state->{drives}->{$drive}->{pid} = $$;
304 $device->read_label();
306 $res_cb->(undef, $res);
309 # Internal function to find an unused (nonexistent) driveN subdirectory and
310 # create it. Note that this does not add a 'data' symlink inside the directory.
316 my $drive = $self->{'dir'} . "/drive$n";
319 warn "$drive is not a directory; please remove it" if (-e $drive and ! -d $drive);
321 next if (!mkdir($drive)); # TODO probably not a very effective locking mechanism..
327 # Internal function to enumerate all available slots. Slots are described by
331 my $dir = _quote_glob($self->{'dir'});
334 for my $slotname (bsd_glob("$dir/slot*/")) {
336 next unless (($slot) = ($slotname =~ /.*slot([0-9]+)\/$/));
337 push @slots, $slot + 0;
340 return map { "$_"} sort { $a <=> $b } @slots;
343 # Internal function to determine whether a slot exists.
345 my ($self, $slot) = @_;
346 return (-d $self->{'dir'} . "/slot$slot");
349 # Internal function to determine if a slot (specified by number) is in use by a
350 # drive, and return the path for that drive if so.
351 sub _is_slot_in_use {
352 my ($self, $state, $slot) = @_;
353 my $dir = _quote_glob($self->{'dir'});
355 for my $symlink (bsd_glob("$dir/drive*/data")) {
357 warn "'$symlink' is not a symlink; please remove it";
361 my $target = readlink($symlink);
363 warn "could not read '$symlink': $!";
368 if (!(($tslot) = ($target =~ /..\/slot([0-9]+)/))) {
369 warn "invalid changer symlink '$symlink' -> '$target'";
373 if ($tslot+0 == $slot) {
374 my $drive = $symlink;
375 $drive =~ s{/data$}{}; # strip the trailing '/data'
377 #check if process is alive
378 my $pid = $state->{drives}->{$drive}->{pid};
379 if (!defined $pid or !Amanda::Util::is_pid_alive($pid)) {
380 unlink("$drive/data")
381 or warn("Could not unlink '$drive/data': $!");
383 or warn("Could not rmdir '$drive': $!");
384 delete $state->{drives}->{$drive}->{pid};
394 sub _get_slot_label {
395 my ($self, $slot) = @_;
396 my $dir = _quote_glob($self->{'dir'});
398 for my $symlink (bsd_glob("$dir/slot$slot/00000.*")) {
399 my ($label) = ($symlink =~ qr{\/00000\.([^/]*)$});
403 return ''; # known, but blank
406 # Internal function to point a drive to a slot
408 my ($self, $drive, $slot) = @_;
410 die "'$drive' does not exist" unless (-d $drive);
411 if (-e "$drive/data") {
412 unlink("$drive/data");
415 symlink("../slot$slot", "$drive/data");
416 # TODO: read it to be sure??
419 # Internal function to return the slot containing a volume with the given
420 # label. This takes advantage of the naming convention used by vtapes.
422 my ($self, $label) = @_;
423 my $dir = _quote_glob($self->{'dir'});
424 $label = _quote_glob($label);
426 my @tapelabels = bsd_glob("$dir/slot*/00000.$label");
431 if (scalar @tapelabels > 1) {
432 warn "Multiple slots with label '$label': " . (join ", ", @tapelabels);
435 my ($slot) = ($tapelabels[0] =~ qr{/slot([0-9]+)/00000.});
439 # Internal function to get the next slot after $slot.
441 my ($self, $slot) = @_;
444 # Try just incrementing the slot number
445 $next_slot = $slot+1;
446 return $next_slot if (-d $self->{'dir'} . "/slot$next_slot");
448 # Otherwise, search through all slots
449 my @all_slots = $self->_all_slots();
450 my $prev = $all_slots[-1];
451 for $next_slot (@all_slots) {
452 return $next_slot if ($prev == $slot);
456 # not found? take a guess.
457 return $all_slots[0];
460 # Get the 'current' slot, represented as a symlink named 'data'
463 my $curlink = $self->{'dir'} . "/data";
465 # for 2.6.1-compatibility, also parse a "current" symlink
466 my $oldlink = $self->{'dir'} . "/current";
467 if (-l $oldlink and ! -e $curlink) {
468 rename($oldlink, $curlink);
472 my $target = readlink($curlink);
473 if ($target =~ "^slot([0-9]+)/?") {
478 # get the first slot as a default
479 my @slots = $self->_all_slots();
480 return 0 unless (@slots);
484 # Set the 'current' slot
486 my ($self, $slot) = @_;
487 my $curlink = $self->{'dir'} . "/data";
491 or warn("Could not unlink '$curlink'");
495 symlink("slot$slot", $curlink);
501 $filename =~ s/([]{}\\?*[])/\\$1/g;
505 package Amanda::Changer::disk::Reservation;
507 @ISA = qw( Amanda::Changer::Reservation );
511 my ($chg, $device, $drive, $slot) = @_;
512 my $self = Amanda::Changer::Reservation::new($class);
514 $self->{'chg'} = $chg;
515 $self->{'drive'} = $drive;
517 $self->{'device'} = $device;
518 $self->{'this_slot'} = $slot;
526 my $drive = $self->{'drive'};
528 unlink("$drive/data")
529 or warn("Could not unlink '$drive/data': $!");
531 or warn("Could not rmdir '$drive': $!");
533 # unref the device, for good measure
534 $self->{'device'} = undef;
536 if (exists $params{'unlocked'}) {
537 my $state = $params{state};
538 delete $state->{drives}->{$drive}->{pid};
539 return $params{'finished_cb'}->();
542 $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
543 $params{'finished_cb'}, sub {
544 my ($state, $finished_cb) = @_;
546 delete $state->{drives}->{$drive}->{pid};