1 # Copyright (c) 2005-2008 Zmanda, Inc. All Rights Reserved.
3 # This library is free software; you can redistribute it and/or modify it
4 # under the terms of the GNU Lesser General Public License version 2.1 as
5 # published by the Free Software Foundation.
7 # This library 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 Lesser General Public
10 # License for more details.
12 # You should have received a copy of the GNU Lesser General Public License
13 # along with this library; if not, write to the Free Software Foundation,
14 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
16 # Contact information: Zmanda Inc., 465 S Mathlida Ave, Suite 300
17 # Sunnyvale, CA 94086, 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 );
39 This changer operates within a root directory, specified in the changer
40 string, which it arranges as follows:
44 | | data -> '../slot4'
46 | | data -> '../slot1'
53 The user should create the desired number C<slot$n> subdirectories, and
54 the changer will take care of dynamically creating the drives as needed,
55 and track the "current" slot using the eponymous symlink.
57 Drives are dynamically allocated as Amanda applications request access to
58 particular slots. Each drive is represented as a subdirectory containing a
59 'data' symlink pointing to the "loaded" slot.
63 - better locking (at least to work on a shared filesystem, if not NFS)
70 my ($cc, $tpchanger) = @_;
71 my ($dir) = ($tpchanger =~ /chg-disk:(.*)/);
73 # note that we don't track outstanding Reservation objects -- we know
74 # they're gone when they delete their drive directory
79 bless ($self, $class);
87 die "no res_cb supplied" unless (exists $params{'res_cb'});
89 if (exists $params{'slot'}) {
90 $self->_load_by_slot(%params);
91 } elsif (exists $params{'label'}) {
92 $self->_load_by_label(%params);
94 die "Invalid parameters to 'load'";
103 die "no info_cb supplied" unless (exists $params{'info_cb'});
104 die "no info supplied" unless (exists $params{'info'});
106 for my $inf (@{$params{'info'}}) {
107 if ($inf eq 'num_slots') {
108 my @slots = $self->_all_slots();
109 $results{$inf} = scalar @slots;
111 warn "Ignoring request for info key '$inf'";
115 Amanda::MainLoop::call_later($params{'info_cb'}, undef, %results);
122 my @slots = $self->_all_slots();
124 $slot = (scalar @slots)? $slots[0] : 0;
125 $self->_set_current($slot);
127 if (exists $params{'finished_cb'}) {
128 Amanda::MainLoop::call_later($params{'finished_cb'});
135 my $slot = $params{'slot'};
138 if ($slot eq "current") {
139 $slot = $self->_get_current();
140 } elsif ($slot eq "next") {
141 $slot = $self->_get_current();
142 $slot = $self->_get_next($slot);
145 if (!$self->_slot_exists($slot)) {
146 Amanda::MainLoop::call_later($params{'res_cb'},
147 "Slot $slot not found", undef);
151 if ($drive = $self->_is_slot_in_use($slot)) {
152 Amanda::MainLoop::call_later($params{'res_cb'},
153 "Slot $slot is already in use by drive '$drive'", undef);
157 $drive = $self->_alloc_drive();
158 $self->_load_drive($drive, $slot);
159 $self->_set_current($slot) if ($params{'set_current'});
161 my $next_slot = $self->_get_next($slot);
163 Amanda::MainLoop::call_later($params{'res_cb'},
164 undef, Amanda::Changer::disk::Reservation->new($self, $drive, $slot, $next_slot));
170 my $label = $params{'label'};
174 $slot = $self->_find_label($label);
175 if (!defined $slot) {
176 Amanda::MainLoop::call_later($params{'res_cb'},
177 "Label '$label' not found", undef);
181 if ($drive = $self->_is_slot_in_use($slot)) {
182 Amanda::MainLoop::call_later($params{'res_cb'},
183 "Slot $slot, containing '$label', is already in use by drive '$drive'", undef);
186 $drive = $self->_alloc_drive();
187 $self->_load_drive($drive, $slot);
188 $self->_set_current($slot) if ($params{'set_current'});
190 my $next_slot = $self->_get_next($slot);
192 Amanda::MainLoop::call_later($params{'res_cb'},
193 undef, Amanda::Changer::disk::Reservation->new($self, $drive, $slot, $next_slot));
196 # Internal function to find an unused (nonexistent) driveN subdirectory and
197 # create it. Note that this does not add a 'data' symlink inside the directory.
203 my $drive = $self->{'dir'} . "/drive$n";
206 warn "$drive is not a directory; please remove it" if (-e $drive and ! -d $drive);
208 next if (!mkdir($drive)); # TODO probably not a very effective locking mechanism..
214 # Internal function to enumerate all available slots. Slots are described by
218 my $dir = _quote_glob($self->{'dir'});
221 for my $slotname (bsd_glob("$dir/slot*/")) {
223 next unless (($slot) = ($slotname =~ /.*slot([0-9]+)\/$/));
224 push @slots, $slot + 0;
230 # Internal function to determine whether a slot exists.
232 my ($self, $slot) = @_;
233 return (-d $self->{'dir'} . "/slot$slot");
236 # Internal function to determine if a slot (specified by number) is in use by a
237 # drive, and return the path for that drive if so.
238 sub _is_slot_in_use {
239 my ($self, $slot) = @_;
240 my $dir = _quote_glob($self->{'dir'});
242 for my $symlink (bsd_glob("$dir/drive*/data")) {
244 warn "'$symlink' is not a symlink; please remove it";
248 my $target = readlink($symlink);
250 warn "could not read '$symlink': $!";
255 if (!(($tslot) = ($target =~ /..\/slot([0-9]+)/))) {
256 warn "invalid changer symlink '$symlink' -> '$target'";
260 if ($tslot+0 == $slot) {
261 $symlink =~ s{/data$}{}; # strip the trailing '/data'
269 # Internal function to point a drive to a slot
271 my ($self, $drive, $slot) = @_;
273 die "'$drive' does not exist" unless (-d $drive);
274 if (-e "$drive/data") {
275 unlink("$drive/data");
278 symlink("../slot$slot", "$drive/data");
279 # TODO: read it to be sure??
282 # Internal function to return the slot containing a volume with the given
283 # label. This takes advantage of the naming convention used by vtapes.
285 my ($self, $label) = @_;
286 my $dir = _quote_glob($self->{'dir'});
287 $label = _quote_glob($label);
289 my @tapelabels = bsd_glob("$dir/slot*/00000.$label");
294 if (scalar @tapelabels > 1) {
295 warn "Multiple slots with label '$label': " . (join ", ", @tapelabels);
298 my ($slot) = ($tapelabels[0] =~ qr{/slot([0-9]+)/00000.});
302 # Internal function to get the next slot after $slot.
304 my ($self, $slot) = @_;
307 # Try just incrementing the slot number
308 $next_slot = $slot+1;
309 return $next_slot if (-d $self->{'dir'} . "/slot$next_slot");
311 # Otherwise, search through all slots
312 my @all_slots = $self->_all_slots();
313 my $prev = $all_slots[-1];
314 for $next_slot (@all_slots) {
315 return $next_slot if ($prev == $slot);
319 # not found? take a guess.
320 return $all_slots[0];
323 # Get the 'current' slot, represented as a symlink named 'current'
326 my $curlink = $self->{'dir'} . "/current";
329 my $target = readlink($curlink);
330 if ($target =~ "^slot([0-9]+)/?") {
335 # get the first slot as a default
336 my @slots = $self->_all_slots();
337 return 0 unless (@slots);
341 # Set the 'current' slot
343 my ($self, $slot) = @_;
344 my $curlink = $self->{'dir'} . "/current";
348 or die("Could not unlink '$curlink'");
352 symlink("slot$slot", $curlink);
358 $filename =~ s/([]{}\\?*[])/\\$1/g;
362 package Amanda::Changer::disk::Reservation;
364 @ISA = qw( Amanda::Changer::Reservation );
368 my ($chg, $drive, $slot, $next_slot) = @_;
369 my $self = Amanda::Changer::Reservation::new($class);
371 $self->{'chg'} = $chg;
372 $self->{'drive'} = $drive;
374 $self->{'device_name'} = "file:$drive";
375 $self->{'this_slot'} = $slot;
376 $self->{'next_slot'} = $next_slot;
384 my $drive = $self->{'drive'};
386 unlink("$drive/data")
387 or warn("Could not unlink '$drive/data': $!");
389 or warn("Could not rmdir '$drive': $!");
391 if (exists $params{'finished_cb'}) {
392 Amanda::MainLoop::call_later($params{'finished_cb'}, undef);