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;
192 $s->{'device_status'} = "".$DEVICE_STATUS_SUCCESS;
194 $s->{'label'} = undef;
195 $s->{'f_type'} = "".$Amanda::Header::F_EMPTY;
196 $s->{'device_status'} = "".$DEVICE_STATUS_VOLUME_UNLABELED;
198 $s->{'current'} = 1 if $slot eq $current;
201 $finished_cb->(undef, \@inventory);
211 if (exists $params{'relative_slot'}) {
212 if ($params{'relative_slot'} eq "current") {
213 $slot = $self->_get_current();
214 } elsif ($params{'relative_slot'} eq "next") {
215 if (exists $params{'slot'}) {
216 $slot = $params{'slot'};
218 $slot = $self->_get_current();
220 $slot = $self->_get_next($slot);
221 $self->_set_current($slot) if ($params{'set_current'});
223 return $self->make_error("failed", $params{'res_cb'},
225 message => "Invalid relative slot '$params{relative_slot}'");
228 $slot = $params{'slot'};
231 if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
232 return $self->make_error("failed", $params{'res_cb'},
233 reason => "notfound",
234 message => "all slots have been loaded");
237 if (!$self->_slot_exists($slot)) {
238 return $self->make_error("failed", $params{'res_cb'},
240 message => "Slot $slot not found");
243 if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
244 return $self->make_error("failed", $params{'res_cb'},
245 reason => "volinuse",
247 message => "Slot $slot is already in use by drive '$drive' and process '$params{state}->{drives}->{$drive}->{pid}'");
250 $drive = $self->_alloc_drive();
251 $self->_load_drive($drive, $slot);
252 $self->_set_current($slot) if ($params{'set_current'});
254 $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
260 my $label = $params{'label'};
264 $slot = $self->_find_label($label);
265 if (!defined $slot) {
266 return $self->make_error("failed", $params{'res_cb'},
267 reason => "notfound",
268 message => "Label '$label' not found");
271 if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
272 return $self->make_error("failed", $params{'res_cb'},
273 reason => "volinuse",
274 message => "Slot $slot, containing '$label', is already " .
275 "in use by drive '$drive'");
278 $drive = $self->_alloc_drive();
279 $self->_load_drive($drive, $slot);
280 $self->_set_current($slot) if ($params{'set_current'});
282 $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
287 my ($state, $res_cb, $drive, $slot) = @_;
290 my $device = Amanda::Device->new("file:$drive");
291 if ($device->status != $DEVICE_STATUS_SUCCESS) {
292 return $self->make_error("failed", $res_cb,
294 message => "opening 'file:$drive': " . $device->error_or_status());
297 if (my $err = $self->{'config'}->configure_device($device)) {
298 return $self->make_error("failed", $res_cb,
303 $res = Amanda::Changer::disk::Reservation->new($self, $device, $drive, $slot);
304 $state->{drives}->{$drive}->{pid} = $$;
305 $device->read_label();
307 $res_cb->(undef, $res);
310 # Internal function to find an unused (nonexistent) driveN subdirectory and
311 # create it. Note that this does not add a 'data' symlink inside the directory.
317 my $drive = $self->{'dir'} . "/drive$n";
320 warn "$drive is not a directory; please remove it" if (-e $drive and ! -d $drive);
322 next if (!mkdir($drive)); # TODO probably not a very effective locking mechanism..
328 # Internal function to enumerate all available slots. Slots are described by
332 my $dir = _quote_glob($self->{'dir'});
335 for my $slotname (bsd_glob("$dir/slot*/")) {
337 next unless (($slot) = ($slotname =~ /.*slot([0-9]+)\/$/));
338 push @slots, $slot + 0;
341 return map { "$_"} sort { $a <=> $b } @slots;
344 # Internal function to determine whether a slot exists.
346 my ($self, $slot) = @_;
347 return (-d $self->{'dir'} . "/slot$slot");
350 # Internal function to determine if a slot (specified by number) is in use by a
351 # drive, and return the path for that drive if so.
352 sub _is_slot_in_use {
353 my ($self, $state, $slot) = @_;
354 my $dir = _quote_glob($self->{'dir'});
356 for my $symlink (bsd_glob("$dir/drive*/data")) {
358 warn "'$symlink' is not a symlink; please remove it";
362 my $target = readlink($symlink);
364 warn "could not read '$symlink': $!";
369 if (!(($tslot) = ($target =~ /..\/slot([0-9]+)/))) {
370 warn "invalid changer symlink '$symlink' -> '$target'";
374 if ($tslot+0 == $slot) {
375 my $drive = $symlink;
376 $drive =~ s{/data$}{}; # strip the trailing '/data'
378 #check if process is alive
379 my $pid = $state->{drives}->{$drive}->{pid};
380 if (!defined $pid or !Amanda::Util::is_pid_alive($pid)) {
381 unlink("$drive/data")
382 or warn("Could not unlink '$drive/data': $!");
384 or warn("Could not rmdir '$drive': $!");
385 delete $state->{drives}->{$drive}->{pid};
395 sub _get_slot_label {
396 my ($self, $slot) = @_;
397 my $dir = _quote_glob($self->{'dir'});
399 for my $symlink (bsd_glob("$dir/slot$slot/00000.*")) {
400 my ($label) = ($symlink =~ qr{\/00000\.([^/]*)$});
404 return ''; # known, but blank
407 # Internal function to point a drive to a slot
409 my ($self, $drive, $slot) = @_;
411 die "'$drive' does not exist" unless (-d $drive);
412 if (-e "$drive/data") {
413 unlink("$drive/data");
416 symlink("../slot$slot", "$drive/data");
417 # TODO: read it to be sure??
420 # Internal function to return the slot containing a volume with the given
421 # label. This takes advantage of the naming convention used by vtapes.
423 my ($self, $label) = @_;
424 my $dir = _quote_glob($self->{'dir'});
425 $label = _quote_glob($label);
427 my @tapelabels = bsd_glob("$dir/slot*/00000.$label");
432 if (scalar @tapelabels > 1) {
433 warn "Multiple slots with label '$label': " . (join ", ", @tapelabels);
436 my ($slot) = ($tapelabels[0] =~ qr{/slot([0-9]+)/00000.});
440 # Internal function to get the next slot after $slot.
442 my ($self, $slot) = @_;
445 # Try just incrementing the slot number
446 $next_slot = $slot+1;
447 return $next_slot if (-d $self->{'dir'} . "/slot$next_slot");
449 # Otherwise, search through all slots
450 my @all_slots = $self->_all_slots();
451 my $prev = $all_slots[-1];
452 for $next_slot (@all_slots) {
453 return $next_slot if ($prev == $slot);
457 # not found? take a guess.
458 return $all_slots[0];
461 # Get the 'current' slot, represented as a symlink named 'data'
464 my $curlink = $self->{'dir'} . "/data";
466 # for 2.6.1-compatibility, also parse a "current" symlink
467 my $oldlink = $self->{'dir'} . "/current";
468 if (-l $oldlink and ! -e $curlink) {
469 rename($oldlink, $curlink);
473 my $target = readlink($curlink);
474 if ($target =~ "^slot([0-9]+)/?") {
479 # get the first slot as a default
480 my @slots = $self->_all_slots();
481 return 0 unless (@slots);
485 # Set the 'current' slot
487 my ($self, $slot) = @_;
488 my $curlink = $self->{'dir'} . "/data";
492 or warn("Could not unlink '$curlink'");
496 symlink("slot$slot", $curlink);
502 $filename =~ s/([]{}\\?*[])/\\$1/g;
506 package Amanda::Changer::disk::Reservation;
508 @ISA = qw( Amanda::Changer::Reservation );
512 my ($chg, $device, $drive, $slot) = @_;
513 my $self = Amanda::Changer::Reservation::new($class);
515 $self->{'chg'} = $chg;
516 $self->{'drive'} = $drive;
518 $self->{'device'} = $device;
519 $self->{'this_slot'} = $slot;
527 my $drive = $self->{'drive'};
529 unlink("$drive/data")
530 or warn("Could not unlink '$drive/data': $!");
532 or warn("Could not rmdir '$drive': $!");
534 # unref the device, for good measure
535 $self->{'device'} = undef;
537 if (exists $params{'unlocked'}) {
538 my $state = $params{state};
539 delete $state->{drives}->{$drive}->{pid};
540 return $params{'finished_cb'}->();
543 $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
544 $params{'finished_cb'}, sub {
545 my ($state, $finished_cb) = @_;
547 delete $state->{drives}->{$drive}->{pid};