3b0636252c59dd0bbddcf891c5a22dda97ce364c
[debian/amanda] / perl / Amanda / Changer / disk.pm
1 # Copyright (c) 2005-2008 Zmanda, Inc.  All Rights Reserved.
2 #
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.
6 #
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.
11 #
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.
15 #
16 # Contact information: Zmanda Inc., 465 S Mathlida Ave, Suite 300
17 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
18
19 package Amanda::Changer::disk;
20
21 use strict;
22 use warnings;
23 use vars qw( @ISA );
24 @ISA = qw( Amanda::Changer );
25
26 use File::Glob qw( :glob );
27 use File::Path;
28 use Amanda::Config qw( :getconf );
29 use Amanda::Debug;
30 use Amanda::Changer;
31 use Amanda::MainLoop;
32
33 =head1 NAME
34
35 Amanda::Changer::disk
36
37 =head1 DESCRIPTION
38
39 This changer operates within a root directory, specified in the changer
40 string, which it arranges as follows:
41
42   $dir -|
43         |- drive0/ -|
44         |           | data -> '../slot4'
45         |- drive1/ -|
46         |           | data -> '../slot1'
47         |- current -> slot5
48         |- slot1/
49         |- slot2/
50         |- ...
51         |- slot$n/
52
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.
56
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.
60
61 =head1 TODO
62
63  - better locking (at least to work on a shared filesystem, if not NFS)
64  - manpage
65
66 =cut
67
68 sub new {
69     my $class = shift;
70     my ($cc, $tpchanger) = @_;
71     my ($dir) = ($tpchanger =~ /chg-disk:(.*)/);
72
73     # note that we don't track outstanding Reservation objects -- we know
74     # they're gone when they delete their drive directory
75     my $self = {
76         dir => $dir,
77     };
78
79     bless ($self, $class);
80     return $self;
81 }
82
83 sub load {
84     my $self = shift;
85     my %params = @_;
86
87     die "no res_cb supplied" unless (exists $params{'res_cb'});
88
89     if (exists $params{'slot'}) {
90         $self->_load_by_slot(%params);
91     } elsif (exists $params{'label'}) {
92         $self->_load_by_label(%params);
93     } else {
94         die "Invalid parameters to 'load'";
95     }
96 }
97
98 sub info {
99     my $self = shift;
100     my %params = @_;
101     my %results;
102
103     die "no info_cb supplied" unless (exists $params{'info_cb'});
104     die "no info supplied" unless (exists $params{'info'});
105
106     for my $inf (@{$params{'info'}}) {
107         if ($inf eq 'num_slots') {
108             my @slots = $self->_all_slots();
109             $results{$inf} = scalar @slots;
110         } else {
111             warn "Ignoring request for info key '$inf'";
112         }
113     }
114
115     Amanda::MainLoop::call_later($params{'info_cb'}, undef, %results);
116 }
117
118 sub reset {
119     my $self = shift;
120     my %params = @_;
121     my $slot;
122     my @slots = $self->_all_slots();
123
124     $slot = (scalar @slots)? $slots[0] : 0;
125     $self->_set_current($slot);
126
127     if (exists $params{'finished_cb'}) {
128         Amanda::MainLoop::call_later($params{'finished_cb'});
129     }
130 }
131
132 sub _load_by_slot {
133     my $self = shift;
134     my %params = @_;
135     my $slot = $params{'slot'};
136     my $drive;
137
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);
143     }
144
145     if (!$self->_slot_exists($slot)) {
146         Amanda::MainLoop::call_later($params{'res_cb'},
147                 "Slot $slot not found", undef);
148         return;
149     }
150
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);
154         return;
155     }
156
157     $drive = $self->_alloc_drive();
158     $self->_load_drive($drive, $slot);
159     $self->_set_current($slot) if ($params{'set_current'});
160
161     my $next_slot = $self->_get_next($slot);
162
163     Amanda::MainLoop::call_later($params{'res_cb'},
164             undef, Amanda::Changer::disk::Reservation->new($self, $drive, $slot, $next_slot));
165 }
166
167 sub _load_by_label {
168     my $self = shift;
169     my %params = @_;
170     my $label = $params{'label'};
171     my $slot;
172     my $drive;
173
174     $slot = $self->_find_label($label);
175     if (!defined $slot) {
176         Amanda::MainLoop::call_later($params{'res_cb'},
177             "Label '$label' not found", undef);
178         return;
179     }
180
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);
184     }
185
186     $drive = $self->_alloc_drive();
187     $self->_load_drive($drive, $slot);
188     $self->_set_current($slot) if ($params{'set_current'});
189
190     my $next_slot = $self->_get_next($slot);
191
192     Amanda::MainLoop::call_later($params{'res_cb'},
193             undef, Amanda::Changer::disk::Reservation->new($self, $drive, $slot, $next_slot));
194 }
195
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.
198 sub _alloc_drive {
199     my ($self) = @_;
200     my $n = 0;
201
202     while (1) {
203         my $drive = $self->{'dir'} . "/drive$n";
204         $n++;
205
206         warn "$drive is not a directory; please remove it" if (-e $drive and ! -d $drive);
207         next if (-e $drive);
208         next if (!mkdir($drive)); # TODO probably not a very effective locking mechanism..
209
210         return $drive;
211     }
212 }
213
214 # Internal function to enumerate all available slots.  Slots are described by
215 # integers.
216 sub _all_slots {
217     my ($self) = @_;
218     my $dir = _quote_glob($self->{'dir'});
219     my @slots;
220
221     for my $slotname (bsd_glob("$dir/slot*/")) {
222         my $slot;
223         next unless (($slot) = ($slotname =~ /.*slot([0-9]+)\/$/));
224         push @slots, $slot + 0;
225     }
226
227     return sort @slots;
228 }
229
230 # Internal function to determine whether a slot exists.
231 sub _slot_exists {
232     my ($self, $slot) = @_;
233     return (-d $self->{'dir'} . "/slot$slot");
234 }
235
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'});
241
242     for my $symlink (bsd_glob("$dir/drive*/data")) {
243         if (! -l $symlink) {
244             warn "'$symlink' is not a symlink; please remove it";
245             next;
246         }
247
248         my $target = readlink($symlink);
249         if (!$target) {
250             warn "could not read '$symlink': $!";
251             next;
252         }
253
254         my $tslot;
255         if (!(($tslot) = ($target =~ /..\/slot([0-9]+)/))) {
256             warn "invalid changer symlink '$symlink' -> '$target'";
257             next;
258         }
259
260         if ($tslot+0 == $slot) {
261             $symlink =~ s{/data$}{}; # strip the trailing '/data'
262             return $symlink;
263         }
264     }
265
266     return 0;
267 }
268
269 # Internal function to point a drive to a slot
270 sub _load_drive {
271     my ($self, $drive, $slot) = @_;
272
273     die "'$drive' does not exist" unless (-d $drive);
274     if (-e "$drive/data") {
275         unlink("$drive/data");
276     }
277
278     symlink("../slot$slot", "$drive/data");
279     # TODO: read it to be sure??
280 }
281
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.
284 sub _find_label {
285     my ($self, $label) = @_;
286     my $dir = _quote_glob($self->{'dir'});
287     $label = _quote_glob($label);
288
289     my @tapelabels = bsd_glob("$dir/slot*/00000.$label");
290     if (!@tapelabels) {
291         return undef;
292     }
293
294     if (scalar @tapelabels > 1) {
295         warn "Multiple slots with label '$label': " . (join ", ", @tapelabels);
296     }
297
298     my ($slot) = ($tapelabels[0] =~ qr{/slot([0-9]+)/00000.});
299     return $slot;
300 }
301
302 # Internal function to get the next slot after $slot.
303 sub _get_next {
304     my ($self, $slot) = @_;
305     my $next_slot;
306
307     # Try just incrementing the slot number
308     $next_slot = $slot+1;
309     return $next_slot if (-d $self->{'dir'} . "/slot$next_slot");
310
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);
316         $prev = $next_slot;
317     }
318
319     # not found? take a guess.
320     return $all_slots[0];
321 }
322
323 # Get the 'current' slot, represented as a symlink named 'current'
324 sub _get_current {
325     my ($self) = @_;
326     my $curlink = $self->{'dir'} . "/current";
327
328     if (-l $curlink) {
329         my $target = readlink($curlink);
330         if ($target =~ "^slot([0-9]+)/?") {
331             return $1;
332         }
333     }
334
335     # get the first slot as a default
336     my @slots = $self->_all_slots();
337     return 0 unless (@slots);
338     return $slots[0];
339 }
340
341 # Set the 'current' slot
342 sub _set_current {
343     my ($self, $slot) = @_;
344     my $curlink = $self->{'dir'} . "/current";
345
346     if (-e $curlink) {
347         unlink($curlink)
348             or die("Could not unlink '$curlink'");
349     }
350
351     # TODO: locking
352     symlink("slot$slot", $curlink);
353 }
354
355 # utility function
356 sub _quote_glob {
357     my ($filename) = @_;
358     $filename =~ s/([]{}\\?*[])/\\$1/g;
359     return $filename;
360 }
361
362 package Amanda::Changer::disk::Reservation;
363 use vars qw( @ISA );
364 @ISA = qw( Amanda::Changer::Reservation );
365
366 sub new {
367     my $class = shift;
368     my ($chg, $drive, $slot, $next_slot) = @_;
369     my $self = Amanda::Changer::Reservation::new($class);
370
371     $self->{'chg'} = $chg;
372     $self->{'drive'} = $drive;
373
374     $self->{'device_name'} = "file:$drive";
375     $self->{'this_slot'} = $slot;
376     $self->{'next_slot'} = $next_slot;
377
378     return $self;
379 }
380
381 sub do_release {
382     my $self = shift;
383     my %params = @_;
384     my $drive = $self->{'drive'};
385
386     unlink("$drive/data")
387         or warn("Could not unlink '$drive/data': $!");
388     rmdir("$drive")
389         or warn("Could not rmdir '$drive': $!");
390
391     if (exists $params{'finished_cb'}) {
392         Amanda::MainLoop::call_later($params{'finished_cb'}, undef);
393     }
394 }