Imported Upstream version 3.2.0
[debian/amanda] / perl / Amanda / Changer / disk.pm
1 # Copyright (c) 2008,2009,2010 Zmanda, Inc.  All Rights Reserved.
2 #
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.
6 #
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
10 # for more details.
11 #
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
15 #
16 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94085, 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 use Amanda::Device qw( :constants );
33
34 =head1 NAME
35
36 Amanda::Changer::disk
37
38 =head1 DESCRIPTION
39
40 This changer operates within a root directory, specified in the changer
41 string, which it arranges as follows:
42
43   $dir -|
44         |- drive0/ -|
45         |           | data -> '../slot4'
46         |- drive1/ -|
47         |           | data -> '../slot1'
48         |- data -> slot5
49         |- slot1/
50         |- slot2/
51         |- ...
52         |- slot$n/
53
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.
58
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.
62
63 See the amanda-changers(7) manpage for usage information.
64
65 =cut
66
67 # STATE
68 #
69 # The device state is shared between all changers accessing the same changer.
70 # It is a hash with keys:
71 #   drives - see below
72 #
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.
76 #
77
78
79 sub new {
80     my $class = shift;
81     my ($config, $tpchanger) = @_;
82     my ($dir) = ($tpchanger =~ /chg-disk:(.*)/);
83
84     unless (-d $dir) {
85         return Amanda::Changer->make_error("fatal", undef,
86             message => "directory '$dir' does not exist");
87     }
88
89     # note that we don't track outstanding Reservation objects -- we know
90     # they're gone when they delete their drive directory
91     my $self = {
92         dir => $dir,
93         config => $config,
94         state_filename => "$dir/state",
95
96         # this is set to 0 by various test scripts,
97         # notably Amanda_Taper_Scan_traditional
98         support_fast_search => 1,
99     };
100
101     bless ($self, $class);
102     return $self;
103 }
104
105 sub load {
106     my $self = shift;
107     my %params = @_;
108     my $old_res_cb = $params{'res_cb'};
109     my $state;
110
111     $self->validate_params('load', \%params);
112
113     return if $self->check_error($params{'res_cb'});
114
115     $self->with_locked_state($self->{'state_filename'},
116                                      $params{'res_cb'}, sub {
117         my ($state, $res_cb) = @_;
118         $params{'state'} = $state;
119
120         # overwrite the callback for _load_by_xxx
121         $params{'res_cb'} = $res_cb;
122
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);
127         }
128     });
129 }
130
131 sub info_key {
132     my $self = shift;
133     my ($key, %params) = @_;
134     my %results;
135
136     return if $self->check_error($params{'info_cb'});
137
138     # no need for synchronization -- all of these values are static
139
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'};
147     }
148
149     $params{'info_cb'}->(undef, %results) if $params{'info_cb'};
150 }
151
152 sub reset {
153     my $self = shift;
154     my %params = @_;
155     my $slot;
156     my @slots = $self->_all_slots();
157
158     return if $self->check_error($params{'finished_cb'});
159
160     $self->with_locked_state($self->{'state_filename'},
161                                      $params{'finished_cb'}, sub {
162         my ($state, $finished_cb) = @_;
163
164         $slot = (scalar @slots)? $slots[0] : 0;
165         $self->_set_current($slot);
166
167         $finished_cb->();
168     });
169 }
170
171 sub inventory {
172     my $self = shift;
173     my %params = @_;
174
175     return if $self->check_error($params{'inventory_cb'});
176
177     my @slots = $self->_all_slots();
178
179     $self->with_locked_state($self->{'state_filename'},
180                              $params{'inventory_cb'}, sub {
181         my ($state, $finished_cb) = @_;
182         my @inventory;
183
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);
189             if ($label) {
190                 $s->{'label'} = $self->_get_slot_label($slot);
191                 $s->{'f_type'} = "".$Amanda::Header::F_TAPESTART;
192             } else {
193                 $s->{'label'} = undef;
194                 $s->{'f_type'} = "".$Amanda::Header::F_EMPTY;
195             }
196             $s->{'device_status'} = "".$DEVICE_STATUS_SUCCESS;
197             $s->{'current'} = 1 if $slot eq $current;
198             push @inventory, $s;
199         }
200         $finished_cb->(undef, \@inventory);
201     });
202 }
203
204 sub _load_by_slot {
205     my $self = shift;
206     my %params = @_;
207     my $drive;
208     my $slot;
209
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'};
216             } else {
217                 $slot = $self->_get_current();
218             }
219             $slot = $self->_get_next($slot);
220             $self->_set_current($slot) if ($params{'set_current'});
221         } else {
222             return $self->make_error("failed", $params{'res_cb'},
223                 reason => "invalid",
224                 message => "Invalid relative slot '$params{relative_slot}'");
225         }
226     } else {
227         $slot = $params{'slot'};
228     }
229
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");
234     }
235
236     if (!$self->_slot_exists($slot)) {
237         return $self->make_error("failed", $params{'res_cb'},
238             reason => "invalid",
239             message => "Slot $slot not found");
240     }
241
242     if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
243         return $self->make_error("failed", $params{'res_cb'},
244             reason => "volinuse",
245             slot => $slot,
246             message => "Slot $slot is already in use by drive '$drive' and process '$params{state}->{drives}->{$drive}->{pid}'");
247     }
248
249     $drive = $self->_alloc_drive();
250     $self->_load_drive($drive, $slot);
251     $self->_set_current($slot) if ($params{'set_current'});
252
253     $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
254 }
255
256 sub _load_by_label {
257     my $self = shift;
258     my %params = @_;
259     my $label = $params{'label'};
260     my $slot;
261     my $drive;
262
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");
268     }
269
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'");
275     }
276
277     $drive = $self->_alloc_drive();
278     $self->_load_drive($drive, $slot);
279     $self->_set_current($slot) if ($params{'set_current'});
280
281     $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
282 }
283
284 sub _make_res {
285     my $self = shift;
286     my ($state, $res_cb, $drive, $slot) = @_;
287     my $res;
288
289     my $device = Amanda::Device->new("file:$drive");
290     if ($device->status != $DEVICE_STATUS_SUCCESS) {
291         return $self->make_error("failed", $res_cb,
292                 reason => "device",
293                 message => "opening 'file:$drive': " . $device->error_or_status());
294     }
295
296     if (my $err = $self->{'config'}->configure_device($device)) {
297         return $self->make_error("failed", $res_cb,
298                 reason => "device",
299                 message => $err);
300     }
301
302     $res = Amanda::Changer::disk::Reservation->new($self, $device, $drive, $slot);
303     $state->{drives}->{$drive}->{pid} = $$;
304     $device->read_label();
305
306     $res_cb->(undef, $res);
307 }
308
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.
311 sub _alloc_drive {
312     my ($self) = @_;
313     my $n = 0;
314
315     while (1) {
316         my $drive = $self->{'dir'} . "/drive$n";
317         $n++;
318
319         warn "$drive is not a directory; please remove it" if (-e $drive and ! -d $drive);
320         next if (-e $drive);
321         next if (!mkdir($drive)); # TODO probably not a very effective locking mechanism..
322
323         return $drive;
324     }
325 }
326
327 # Internal function to enumerate all available slots.  Slots are described by
328 # strings.
329 sub _all_slots {
330     my ($self) = @_;
331     my $dir = _quote_glob($self->{'dir'});
332     my @slots;
333
334     for my $slotname (bsd_glob("$dir/slot*/")) {
335         my $slot;
336         next unless (($slot) = ($slotname =~ /.*slot([0-9]+)\/$/));
337         push @slots, $slot + 0;
338     }
339
340     return map { "$_"} sort { $a <=> $b } @slots;
341 }
342
343 # Internal function to determine whether a slot exists.
344 sub _slot_exists {
345     my ($self, $slot) = @_;
346     return (-d $self->{'dir'} . "/slot$slot");
347 }
348
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'});
354
355     for my $symlink (bsd_glob("$dir/drive*/data")) {
356         if (! -l $symlink) {
357             warn "'$symlink' is not a symlink; please remove it";
358             next;
359         }
360
361         my $target = readlink($symlink);
362         if (!$target) {
363             warn "could not read '$symlink': $!";
364             next;
365         }
366
367         my $tslot;
368         if (!(($tslot) = ($target =~ /..\/slot([0-9]+)/))) {
369             warn "invalid changer symlink '$symlink' -> '$target'";
370             next;
371         }
372
373         if ($tslot+0 == $slot) {
374             my $drive = $symlink;
375             $drive =~ s{/data$}{}; # strip the trailing '/data'
376
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': $!");
382                 rmdir("$drive")
383                     or warn("Could not rmdir '$drive': $!");
384                 delete $state->{drives}->{$drive}->{pid};
385                 next;
386             }
387             return $drive;
388         }
389     }
390
391     return 0;
392 }
393
394 sub _get_slot_label {
395     my ($self, $slot) = @_;
396     my $dir = _quote_glob($self->{'dir'});
397
398     for my $symlink (bsd_glob("$dir/slot$slot/00000.*")) {
399         my ($label) = ($symlink =~ qr{\/00000\.([^/]*)$});
400         return $label;
401     }
402
403     return ''; # known, but blank
404 }
405
406 # Internal function to point a drive to a slot
407 sub _load_drive {
408     my ($self, $drive, $slot) = @_;
409
410     die "'$drive' does not exist" unless (-d $drive);
411     if (-e "$drive/data") {
412         unlink("$drive/data");
413     }
414
415     symlink("../slot$slot", "$drive/data");
416     # TODO: read it to be sure??
417 }
418
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.
421 sub _find_label {
422     my ($self, $label) = @_;
423     my $dir = _quote_glob($self->{'dir'});
424     $label = _quote_glob($label);
425
426     my @tapelabels = bsd_glob("$dir/slot*/00000.$label");
427     if (!@tapelabels) {
428         return undef;
429     }
430
431     if (scalar @tapelabels > 1) {
432         warn "Multiple slots with label '$label': " . (join ", ", @tapelabels);
433     }
434
435     my ($slot) = ($tapelabels[0] =~ qr{/slot([0-9]+)/00000.});
436     return $slot;
437 }
438
439 # Internal function to get the next slot after $slot.
440 sub _get_next {
441     my ($self, $slot) = @_;
442     my $next_slot;
443
444     # Try just incrementing the slot number
445     $next_slot = $slot+1;
446     return $next_slot if (-d $self->{'dir'} . "/slot$next_slot");
447
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);
453         $prev = $next_slot;
454     }
455
456     # not found? take a guess.
457     return $all_slots[0];
458 }
459
460 # Get the 'current' slot, represented as a symlink named 'data'
461 sub _get_current {
462     my ($self) = @_;
463     my $curlink = $self->{'dir'} . "/data";
464
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);
469     }
470
471     if (-l $curlink) {
472         my $target = readlink($curlink);
473         if ($target =~ "^slot([0-9]+)/?") {
474             return $1;
475         }
476     }
477
478     # get the first slot as a default
479     my @slots = $self->_all_slots();
480     return 0 unless (@slots);
481     return $slots[0];
482 }
483
484 # Set the 'current' slot
485 sub _set_current {
486     my ($self, $slot) = @_;
487     my $curlink = $self->{'dir'} . "/data";
488
489     if (-e $curlink) {
490         unlink($curlink)
491             or warn("Could not unlink '$curlink'");
492     }
493
494     # TODO: locking
495     symlink("slot$slot", $curlink);
496 }
497
498 # utility function
499 sub _quote_glob {
500     my ($filename) = @_;
501     $filename =~ s/([]{}\\?*[])/\\$1/g;
502     return $filename;
503 }
504
505 package Amanda::Changer::disk::Reservation;
506 use vars qw( @ISA );
507 @ISA = qw( Amanda::Changer::Reservation );
508
509 sub new {
510     my $class = shift;
511     my ($chg, $device, $drive, $slot) = @_;
512     my $self = Amanda::Changer::Reservation::new($class);
513
514     $self->{'chg'} = $chg;
515     $self->{'drive'} = $drive;
516
517     $self->{'device'} = $device;
518     $self->{'this_slot'} = $slot;
519
520     return $self;
521 }
522
523 sub do_release {
524     my $self = shift;
525     my %params = @_;
526     my $drive = $self->{'drive'};
527
528     unlink("$drive/data")
529         or warn("Could not unlink '$drive/data': $!");
530     rmdir("$drive")
531         or warn("Could not rmdir '$drive': $!");
532
533     # unref the device, for good measure
534     $self->{'device'} = undef;
535
536     if (exists $params{'unlocked'}) {
537         my $state = $params{state};
538         delete $state->{drives}->{$drive}->{pid};
539         return $params{'finished_cb'}->();
540     }
541
542     $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
543                                     $params{'finished_cb'}, sub {
544         my ($state, $finished_cb) = @_;
545
546         delete $state->{drives}->{$drive}->{pid};
547
548         $finished_cb->();
549     });
550 }