Imported Upstream version 3.2.1
[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                 $s->{'device_status'} = "".$DEVICE_STATUS_SUCCESS;
193             } else {
194                 $s->{'label'} = undef;
195                 $s->{'f_type'} = "".$Amanda::Header::F_EMPTY;
196                 $s->{'device_status'} = "".$DEVICE_STATUS_VOLUME_UNLABELED;
197             }
198             $s->{'current'} = 1 if $slot eq $current;
199             push @inventory, $s;
200         }
201         $finished_cb->(undef, \@inventory);
202     });
203 }
204
205 sub _load_by_slot {
206     my $self = shift;
207     my %params = @_;
208     my $drive;
209     my $slot;
210
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'};
217             } else {
218                 $slot = $self->_get_current();
219             }
220             $slot = $self->_get_next($slot);
221             $self->_set_current($slot) if ($params{'set_current'});
222         } else {
223             return $self->make_error("failed", $params{'res_cb'},
224                 reason => "invalid",
225                 message => "Invalid relative slot '$params{relative_slot}'");
226         }
227     } else {
228         $slot = $params{'slot'};
229     }
230
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");
235     }
236
237     if (!$self->_slot_exists($slot)) {
238         return $self->make_error("failed", $params{'res_cb'},
239             reason => "invalid",
240             message => "Slot $slot not found");
241     }
242
243     if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
244         return $self->make_error("failed", $params{'res_cb'},
245             reason => "volinuse",
246             slot => $slot,
247             message => "Slot $slot is already in use by drive '$drive' and process '$params{state}->{drives}->{$drive}->{pid}'");
248     }
249
250     $drive = $self->_alloc_drive();
251     $self->_load_drive($drive, $slot);
252     $self->_set_current($slot) if ($params{'set_current'});
253
254     $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
255 }
256
257 sub _load_by_label {
258     my $self = shift;
259     my %params = @_;
260     my $label = $params{'label'};
261     my $slot;
262     my $drive;
263
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");
269     }
270
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'");
276     }
277
278     $drive = $self->_alloc_drive();
279     $self->_load_drive($drive, $slot);
280     $self->_set_current($slot) if ($params{'set_current'});
281
282     $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
283 }
284
285 sub _make_res {
286     my $self = shift;
287     my ($state, $res_cb, $drive, $slot) = @_;
288     my $res;
289
290     my $device = Amanda::Device->new("file:$drive");
291     if ($device->status != $DEVICE_STATUS_SUCCESS) {
292         return $self->make_error("failed", $res_cb,
293                 reason => "device",
294                 message => "opening 'file:$drive': " . $device->error_or_status());
295     }
296
297     if (my $err = $self->{'config'}->configure_device($device)) {
298         return $self->make_error("failed", $res_cb,
299                 reason => "device",
300                 message => $err);
301     }
302
303     $res = Amanda::Changer::disk::Reservation->new($self, $device, $drive, $slot);
304     $state->{drives}->{$drive}->{pid} = $$;
305     $device->read_label();
306
307     $res_cb->(undef, $res);
308 }
309
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.
312 sub _alloc_drive {
313     my ($self) = @_;
314     my $n = 0;
315
316     while (1) {
317         my $drive = $self->{'dir'} . "/drive$n";
318         $n++;
319
320         warn "$drive is not a directory; please remove it" if (-e $drive and ! -d $drive);
321         next if (-e $drive);
322         next if (!mkdir($drive)); # TODO probably not a very effective locking mechanism..
323
324         return $drive;
325     }
326 }
327
328 # Internal function to enumerate all available slots.  Slots are described by
329 # strings.
330 sub _all_slots {
331     my ($self) = @_;
332     my $dir = _quote_glob($self->{'dir'});
333     my @slots;
334
335     for my $slotname (bsd_glob("$dir/slot*/")) {
336         my $slot;
337         next unless (($slot) = ($slotname =~ /.*slot([0-9]+)\/$/));
338         push @slots, $slot + 0;
339     }
340
341     return map { "$_"} sort { $a <=> $b } @slots;
342 }
343
344 # Internal function to determine whether a slot exists.
345 sub _slot_exists {
346     my ($self, $slot) = @_;
347     return (-d $self->{'dir'} . "/slot$slot");
348 }
349
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'});
355
356     for my $symlink (bsd_glob("$dir/drive*/data")) {
357         if (! -l $symlink) {
358             warn "'$symlink' is not a symlink; please remove it";
359             next;
360         }
361
362         my $target = readlink($symlink);
363         if (!$target) {
364             warn "could not read '$symlink': $!";
365             next;
366         }
367
368         my $tslot;
369         if (!(($tslot) = ($target =~ /..\/slot([0-9]+)/))) {
370             warn "invalid changer symlink '$symlink' -> '$target'";
371             next;
372         }
373
374         if ($tslot+0 == $slot) {
375             my $drive = $symlink;
376             $drive =~ s{/data$}{}; # strip the trailing '/data'
377
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': $!");
383                 rmdir("$drive")
384                     or warn("Could not rmdir '$drive': $!");
385                 delete $state->{drives}->{$drive}->{pid};
386                 next;
387             }
388             return $drive;
389         }
390     }
391
392     return 0;
393 }
394
395 sub _get_slot_label {
396     my ($self, $slot) = @_;
397     my $dir = _quote_glob($self->{'dir'});
398
399     for my $symlink (bsd_glob("$dir/slot$slot/00000.*")) {
400         my ($label) = ($symlink =~ qr{\/00000\.([^/]*)$});
401         return $label;
402     }
403
404     return ''; # known, but blank
405 }
406
407 # Internal function to point a drive to a slot
408 sub _load_drive {
409     my ($self, $drive, $slot) = @_;
410
411     die "'$drive' does not exist" unless (-d $drive);
412     if (-e "$drive/data") {
413         unlink("$drive/data");
414     }
415
416     symlink("../slot$slot", "$drive/data");
417     # TODO: read it to be sure??
418 }
419
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.
422 sub _find_label {
423     my ($self, $label) = @_;
424     my $dir = _quote_glob($self->{'dir'});
425     $label = _quote_glob($label);
426
427     my @tapelabels = bsd_glob("$dir/slot*/00000.$label");
428     if (!@tapelabels) {
429         return undef;
430     }
431
432     if (scalar @tapelabels > 1) {
433         warn "Multiple slots with label '$label': " . (join ", ", @tapelabels);
434     }
435
436     my ($slot) = ($tapelabels[0] =~ qr{/slot([0-9]+)/00000.});
437     return $slot;
438 }
439
440 # Internal function to get the next slot after $slot.
441 sub _get_next {
442     my ($self, $slot) = @_;
443     my $next_slot;
444
445     # Try just incrementing the slot number
446     $next_slot = $slot+1;
447     return $next_slot if (-d $self->{'dir'} . "/slot$next_slot");
448
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);
454         $prev = $next_slot;
455     }
456
457     # not found? take a guess.
458     return $all_slots[0];
459 }
460
461 # Get the 'current' slot, represented as a symlink named 'data'
462 sub _get_current {
463     my ($self) = @_;
464     my $curlink = $self->{'dir'} . "/data";
465
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);
470     }
471
472     if (-l $curlink) {
473         my $target = readlink($curlink);
474         if ($target =~ "^slot([0-9]+)/?") {
475             return $1;
476         }
477     }
478
479     # get the first slot as a default
480     my @slots = $self->_all_slots();
481     return 0 unless (@slots);
482     return $slots[0];
483 }
484
485 # Set the 'current' slot
486 sub _set_current {
487     my ($self, $slot) = @_;
488     my $curlink = $self->{'dir'} . "/data";
489
490     if (-e $curlink) {
491         unlink($curlink)
492             or warn("Could not unlink '$curlink'");
493     }
494
495     # TODO: locking
496     symlink("slot$slot", $curlink);
497 }
498
499 # utility function
500 sub _quote_glob {
501     my ($filename) = @_;
502     $filename =~ s/([]{}\\?*[])/\\$1/g;
503     return $filename;
504 }
505
506 package Amanda::Changer::disk::Reservation;
507 use vars qw( @ISA );
508 @ISA = qw( Amanda::Changer::Reservation );
509
510 sub new {
511     my $class = shift;
512     my ($chg, $device, $drive, $slot) = @_;
513     my $self = Amanda::Changer::Reservation::new($class);
514
515     $self->{'chg'} = $chg;
516     $self->{'drive'} = $drive;
517
518     $self->{'device'} = $device;
519     $self->{'this_slot'} = $slot;
520
521     return $self;
522 }
523
524 sub do_release {
525     my $self = shift;
526     my %params = @_;
527     my $drive = $self->{'drive'};
528
529     unlink("$drive/data")
530         or warn("Could not unlink '$drive/data': $!");
531     rmdir("$drive")
532         or warn("Could not rmdir '$drive': $!");
533
534     # unref the device, for good measure
535     $self->{'device'} = undef;
536
537     if (exists $params{'unlocked'}) {
538         my $state = $params{state};
539         delete $state->{drives}->{$drive}->{pid};
540         return $params{'finished_cb'}->();
541     }
542
543     $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
544                                     $params{'finished_cb'}, sub {
545         my ($state, $finished_cb) = @_;
546
547         delete $state->{drives}->{$drive}->{pid};
548
549         $finished_cb->();
550     });
551 }