Imported Upstream version 3.3.3
[debian/amanda] / perl / Amanda / Changer / disk.pm
1 # Copyright (c) 2008-2012 Zmanda, Inc.  All Rights Reserved.
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11 # for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 #
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
19
20 package Amanda::Changer::disk;
21
22 use strict;
23 use warnings;
24 use Carp;
25 use vars qw( @ISA );
26 @ISA = qw( Amanda::Changer );
27
28 use File::Glob qw( :glob );
29 use File::Path;
30 use File::Basename;
31 use Amanda::Config qw( :getconf string_to_boolean );
32 use Amanda::Debug;
33 use Amanda::Changer;
34 use Amanda::MainLoop;
35 use Amanda::Device qw( :constants );
36
37 =head1 NAME
38
39 Amanda::Changer::disk
40
41 =head1 DESCRIPTION
42
43 This changer operates within a root directory, specified in the changer
44 string, which it arranges as follows:
45
46   $dir -|
47         |- drive0/ -|
48         |           | data -> '../slot4'
49         |- drive1/ -|
50         |           | data -> '../slot1'
51         |- data -> slot5
52         |- slot1/
53         |- slot2/
54         |- ...
55         |- slot$n/
56
57 The user should create the desired number of C<slot$n> subdirectories.  The
58 changer will take care of dynamically creating the drives as needed, and track
59 the current slot using a "data" symlink.  This allows use of "file:$dir" as a
60 device operating on the current slot, although note that it is unlocked.
61
62 Drives are dynamically allocated as Amanda applications request access to
63 particular slots.  Each drive is represented as a subdirectory containing a
64 'data' symlink pointing to the "loaded" slot.
65
66 See the amanda-changers(7) manpage for usage information.
67
68 =cut
69
70 # STATE
71 #
72 # The device state is shared between all changers accessing the same changer.
73 # It is a hash with keys:
74 #   drives - see below
75 #
76 # The 'drives' key is a hash, with drive as keys and hashes
77 # as values.  Each drive's hash has keys:
78 #   pid - the pid that reserved that drive.
79 #
80
81
82 sub new {
83     my $class = shift;
84     my ($config, $tpchanger) = @_;
85     my ($dir) = ($tpchanger =~ /chg-disk:(.*)/);
86     my $properties = $config->{'properties'};
87
88     # note that we don't track outstanding Reservation objects -- we know
89     # they're gone when they delete their drive directory
90     my $self = {
91         dir => $dir,
92         config => $config,
93         state_filename => "$dir/state",
94
95         # list of all reservations
96         reservation => {},
97
98         # this is set to 0 by various test scripts,
99         # notably Amanda_Taper_Scan_traditional
100         support_fast_search => 1,
101     };
102
103     bless ($self, $class);
104
105     if ($config->{'changerfile'}) {
106         $self->{'state_filename'} = Amanda::Config::config_dir_relative($config->{'changerfile'});
107     }
108     $self->{'lock-timeout'} = $config->get_property('lock-timeout');
109
110     $self->{'num-slot'} = $config->get_property('num-slot');
111     $self->{'auto-create-slot'} = $config->get_boolean_property(
112                                         'auto-create-slot', 0);
113     $self->{'removable'} = $config->get_boolean_property('removable', 0);
114     $self->{'mount'} = $config->get_boolean_property('mount', 0);
115     $self->{'umount'} = $config->get_boolean_property('umount', 0);
116     $self->{'umount_lockfile'} = $config->get_property('umount-lockfile');
117     $self->{'umount_idle'} = $config->get_property('umount-idle');
118     if (defined $self->{'umount_lockfile'}) {
119         $self->{'fl'} = Amanda::Util::file_lock->new($self->{'umount_lockfile'})
120     }
121
122     $self->_validate();
123     return $self->{'fatal_error'} if defined $self->{'fatal_error'};
124
125     return $self;
126 }
127
128 sub DESTROY {
129     my $self = shift;
130
131     $self->SUPER::DESTROY();
132 }
133
134 sub quit {
135     my $self = shift;
136
137     $self->force_unlock();
138     delete $self->{'fl'};
139     $self->SUPER::quit();
140 }
141
142 sub load {
143     my $self = shift;
144     my %params = @_;
145     my $old_res_cb = $params{'res_cb'};
146     my $state;
147
148     $self->validate_params('load', \%params);
149
150     return if $self->check_error($params{'res_cb'});
151
152     $self->with_disk_locked_state($params{'res_cb'}, sub {
153         my ($state, $res_cb) = @_;
154         $params{'state'} = $state;
155
156         # overwrite the callback for _load_by_xxx
157         $params{'res_cb'} = $res_cb;
158
159         if (exists $params{'slot'} or exists $params{'relative_slot'}) {
160             $self->_load_by_slot(%params);
161         } elsif (exists $params{'label'}) {
162             $self->_load_by_label(%params);
163         }
164     });
165 }
166
167 sub info_key {
168     my $self = shift;
169     my ($key, %params) = @_;
170     my %results;
171     my $info_cb = $params{'info_cb'};
172
173     return if $self->check_error($info_cb);
174
175     my $steps = define_steps
176         cb_ref => \$info_cb;
177
178     step init => sub {
179         $self->try_lock($steps->{'locked'});
180     };
181
182     step locked => sub {
183         return if $self->check_error($info_cb);
184
185         # no need for synchronization -- all of these values are static
186
187         if ($key eq 'num_slots') {
188             my @slots = $self->_all_slots();
189             $results{$key} = scalar @slots;
190         } elsif ($key eq 'vendor_string') {
191             $results{$key} = 'chg-disk'; # mostly just for testing
192         } elsif ($key eq 'fast_search') {
193             $results{$key} = $self->{'support_fast_search'};
194         }
195
196         $self->try_unlock();
197         $info_cb->(undef, %results) if $info_cb;
198     }
199 }
200
201 sub reset {
202     my $self = shift;
203     my %params = @_;
204     my $slot;
205     my @slots = $self->_all_slots();
206
207     return if $self->check_error($params{'finished_cb'});
208
209     $self->with_disk_locked_state($params{'finished_cb'}, sub {
210         my ($state, $finished_cb) = @_;
211
212         $slot = (scalar @slots)? $slots[0] : 0;
213         $self->_set_current($slot);
214
215         $finished_cb->();
216     });
217 }
218
219 sub inventory {
220     my $self = shift;
221     my %params = @_;
222
223     return if $self->check_error($params{'inventory_cb'});
224
225     $self->with_disk_locked_state($params{'inventory_cb'}, sub {
226         my ($state, $finished_cb) = @_;
227         my @inventory;
228
229         my @slots = $self->_all_slots();
230         my $current = $self->_get_current();
231         for my $slot (@slots) {
232             my $s = { slot => $slot, state => Amanda::Changer::SLOT_FULL };
233             $s->{'reserved'} = $self->_is_slot_in_use($state, $slot);
234             my $label = $self->_get_slot_label($slot);
235             if ($label) {
236                 $s->{'label'} = $self->_get_slot_label($slot);
237                 $s->{'f_type'} = "".$Amanda::Header::F_TAPESTART;
238                 $s->{'device_status'} = "".$DEVICE_STATUS_SUCCESS;
239             } else {
240                 $s->{'label'} = undef;
241                 $s->{'f_type'} = "".$Amanda::Header::F_EMPTY;
242                 $s->{'device_status'} = "".$DEVICE_STATUS_VOLUME_UNLABELED;
243             }
244             $s->{'current'} = 1 if $slot eq $current;
245             push @inventory, $s;
246         }
247         $finished_cb->(undef, \@inventory);
248     });
249 }
250
251 sub set_meta_label {
252     my $self = shift;
253     my %params = @_;
254
255     return if $self->check_error($params{'finished_cb'});
256
257     $self->with_disk_locked_state($params{'finished_cb'}, sub {
258         my ($state, $finished_cb) = @_;
259
260         $state->{'meta'} = $params{'meta'};
261         $finished_cb->(undef);
262     });
263 }
264
265 sub with_disk_locked_state {
266     my $self = shift;
267     my ($cb, $sub) = @_;
268
269     my $steps = define_steps
270         cb_ref => \$cb;
271
272     step init => sub {
273         $self->try_lock($steps->{'locked'});
274     };
275
276     step locked => sub {
277         my $err = shift;
278         return $cb->($err) if $err;
279         $self->with_locked_state($self->{'state_filename'},
280             sub { my @args = @_;
281                   $self->try_unlock();
282                   $cb->(@args);
283                 },
284             $sub);
285     };
286 }
287
288 sub get_meta_label {
289     my $self = shift;
290     my %params = @_;
291
292     return if $self->check_error($params{'finished_cb'});
293
294     $self->with_disk_locked_state($params{'finished_cb'}, sub {
295         my ($state, $finished_cb) = @_;
296
297         $finished_cb->(undef, $state->{'meta'});
298     });
299 }
300
301 sub _load_by_slot {
302     my $self = shift;
303     my %params = @_;
304     my $drive;
305     my $slot;
306
307     if (exists $params{'relative_slot'}) {
308         if ($params{'relative_slot'} eq "current") {
309             $slot = $self->_get_current();
310         } elsif ($params{'relative_slot'} eq "next") {
311             if (exists $params{'slot'}) {
312                 $slot = $params{'slot'};
313             } else {
314                 $slot = $self->_get_current();
315             }
316             $slot = $self->_get_next($slot);
317             $self->_set_current($slot) if ($params{'set_current'});
318         } else {
319             return $self->make_error("failed", $params{'res_cb'},
320                 reason => "invalid",
321                 message => "Invalid relative slot '$params{relative_slot}'");
322         }
323     } else {
324         $slot = $params{'slot'};
325     }
326
327     if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
328         return $self->make_error("failed", $params{'res_cb'},
329             reason => "notfound",
330             message => "all slots have been loaded");
331     }
332
333     if (!$self->_slot_exists($slot)) {
334         return $self->make_error("failed", $params{'res_cb'},
335             reason => "invalid",
336             message => "Slot $slot not found");
337     }
338
339     if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
340         return $self->make_error("failed", $params{'res_cb'},
341             reason => "volinuse",
342             slot => $slot,
343             message => "Slot $slot is already in use by drive '$drive' and process '$params{state}->{drives}->{$drive}->{pid}'");
344     }
345
346     $drive = $self->_alloc_drive();
347     $self->_load_drive($drive, $slot);
348     $self->_set_current($slot) if ($params{'set_current'});
349
350     $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
351 }
352
353 sub _load_by_label {
354     my $self = shift;
355     my %params = @_;
356     my $label = $params{'label'};
357     my $slot;
358     my $drive;
359
360     $slot = $self->_find_label($label);
361     if (!defined $slot) {
362         return $self->make_error("failed", $params{'res_cb'},
363             reason => "notfound",
364             message => "Label '$label' not found");
365     }
366
367     if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
368         return $self->make_error("failed", $params{'res_cb'},
369             reason => "volinuse",
370             message => "Slot $slot, containing '$label', is already " .
371                         "in use by drive '$drive'");
372     }
373
374     $drive = $self->_alloc_drive();
375     $self->_load_drive($drive, $slot);
376     $self->_set_current($slot) if ($params{'set_current'});
377
378     $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
379 }
380
381 sub _make_res {
382     my $self = shift;
383     my ($state, $res_cb, $drive, $slot) = @_;
384     my $res;
385
386     my $device = Amanda::Device->new("file:$drive");
387     if ($device->status != $DEVICE_STATUS_SUCCESS) {
388         return $self->make_error("failed", $res_cb,
389                 reason => "device",
390                 message => "opening 'file:$drive': " . $device->error_or_status());
391     }
392
393     if (my $err = $self->{'config'}->configure_device($device)) {
394         return $self->make_error("failed", $res_cb,
395                 reason => "device",
396                 message => $err);
397     }
398
399     $res = Amanda::Changer::disk::Reservation->new($self, $device, $drive, $slot);
400     $state->{drives}->{$drive}->{pid} = $$;
401     $device->read_label();
402
403     $res_cb->(undef, $res);
404 }
405
406 # Internal function to find an unused (nonexistent) driveN subdirectory and
407 # create it.  Note that this does not add a 'data' symlink inside the directory.
408 sub _alloc_drive {
409     my ($self) = @_;
410     my $n = 0;
411
412     while (1) {
413         my $drive = $self->{'dir'} . "/drive$n";
414         $n++;
415
416         warn "$drive is not a directory; please remove it" if (-e $drive and ! -d $drive);
417         next if (-e $drive);
418         next if (!mkdir($drive)); # TODO probably not a very effective locking mechanism..
419
420         return $drive;
421     }
422 }
423
424 # Internal function to enumerate all available slots.  Slots are described by
425 # strings.
426 sub _all_slots {
427     my ($self) = @_;
428     my $dir = _quote_glob($self->{'dir'});
429     my @slots;
430
431     for my $slotname (bsd_glob("$dir/slot*/")) {
432         my $slot;
433         next unless (($slot) = ($slotname =~ /.*slot([0-9]+)\/$/));
434         push @slots, $slot + 0;
435     }
436
437     return map { "$_"} sort { $a <=> $b } @slots;
438 }
439
440 # Internal function to determine whether a slot exists.
441 sub _slot_exists {
442     my ($self, $slot) = @_;
443     return (-d $self->{'dir'} . "/slot$slot");
444 }
445
446 # Internal function to determine if a slot (specified by number) is in use by a
447 # drive, and return the path for that drive if so.
448 sub _is_slot_in_use {
449     my ($self, $state, $slot) = @_;
450     my $dir = _quote_glob($self->{'dir'});
451
452     for my $symlink (bsd_glob("$dir/drive*/data")) {
453         if (! -l $symlink) {
454             warn "'$symlink' is not a symlink; please remove it";
455             next;
456         }
457
458         my $target = readlink($symlink);
459         if (!$target) {
460             warn "could not read '$symlink': $!";
461             next;
462         }
463
464         my $tslot;
465         if (!(($tslot) = ($target =~ /..\/slot([0-9]+)/))) {
466             warn "invalid changer symlink '$symlink' -> '$target'";
467             next;
468         }
469
470         if ($tslot+0 == $slot) {
471             my $drive = $symlink;
472             $drive =~ s{/data$}{}; # strip the trailing '/data'
473
474             #check if process is alive
475             my $pid = $state->{drives}->{$drive}->{pid};
476             if (!defined $pid or !Amanda::Util::is_pid_alive($pid)) {
477                 unlink("$drive/data")
478                     or warn("Could not unlink '$drive/data': $!");
479                 rmdir("$drive")
480                     or warn("Could not rmdir '$drive': $!");
481                 delete $state->{drives}->{$drive}->{pid};
482                 next;
483             }
484             return $drive;
485         }
486     }
487
488     return 0;
489 }
490
491 sub _get_slot_label {
492     my ($self, $slot) = @_;
493     my $dir = _quote_glob($self->{'dir'});
494
495     for my $symlink (bsd_glob("$dir/slot$slot/00000.*")) {
496         my ($label) = ($symlink =~ qr{\/00000\.([^/]*)$});
497         return $label;
498     }
499
500     return ''; # known, but blank
501 }
502
503 # Internal function to point a drive to a slot
504 sub _load_drive {
505     my ($self, $drive, $slot) = @_;
506
507     confess "'$drive' does not exist" unless (-d $drive);
508     if (-e "$drive/data") {
509         unlink("$drive/data");
510     }
511
512     symlink("../slot$slot", "$drive/data");
513     # TODO: read it to be sure??
514 }
515
516 # Internal function to return the slot containing a volume with the given
517 # label.  This takes advantage of the naming convention used by vtapes.
518 sub _find_label {
519     my ($self, $label) = @_;
520     my $dir = _quote_glob($self->{'dir'});
521     $label = _quote_glob($label);
522
523     my @tapelabels = bsd_glob("$dir/slot*/00000.$label");
524     if (!@tapelabels) {
525         return undef;
526     }
527
528     if (scalar @tapelabels > 1) {
529         warn "Multiple slots with label '$label': " . (join ", ", @tapelabels);
530     }
531
532     my ($slot) = ($tapelabels[0] =~ qr{/slot([0-9]+)/00000.});
533     return $slot;
534 }
535
536 # Internal function to get the next slot after $slot.
537 sub _get_next {
538     my ($self, $slot) = @_;
539     my $next_slot;
540
541     # Try just incrementing the slot number
542     $next_slot = $slot+1;
543     return $next_slot if (-d $self->{'dir'} . "/slot$next_slot");
544
545     # Otherwise, search through all slots
546     my @all_slots = $self->_all_slots();
547     my $prev = $all_slots[-1];
548     for $next_slot (@all_slots) {
549         return $next_slot if ($prev == $slot);
550         $prev = $next_slot;
551     }
552
553     # not found? take a guess.
554     return $all_slots[0];
555 }
556
557 # Get the 'current' slot, represented as a symlink named 'data'
558 sub _get_current {
559     my ($self) = @_;
560     my $curlink = $self->{'dir'} . "/data";
561
562     # for 2.6.1-compatibility, also parse a "current" symlink
563     my $oldlink = $self->{'dir'} . "/current";
564     if (-l $oldlink and ! -e $curlink) {
565         rename($oldlink, $curlink);
566     }
567
568     if (-l $curlink) {
569         my $target = readlink($curlink);
570         if ($target =~ "^slot([0-9]+)/?") {
571             return $1;
572         }
573     }
574
575     # get the first slot as a default
576     my @slots = $self->_all_slots();
577     return 0 unless (@slots);
578     return $slots[0];
579 }
580
581 # Set the 'current' slot
582 sub _set_current {
583     my ($self, $slot) = @_;
584     my $curlink = $self->{'dir'} . "/data";
585
586     if (-l $curlink or -e $curlink) {
587         unlink($curlink)
588             or warn("Could not unlink '$curlink'");
589     }
590
591     # TODO: locking
592     symlink("slot$slot", $curlink);
593 }
594
595 # utility function
596 sub _quote_glob {
597     my ($filename) = @_;
598     $filename =~ s/([]{}\\?*[])/\\$1/g;
599     return $filename;
600 }
601
602 sub _validate() {
603     my $self = shift;
604     my $dir = $self->{'dir'};
605
606     unless (-d $dir) {
607         return $self->make_error("fatal", undef,
608             message => "directory '$dir' does not exist");
609     }
610
611     if ($self->{'removable'}) {
612         my ($dev, $ino) = stat $dir;
613         my $parentdir = dirname $dir;
614         my ($pdev, $pino) = stat $parentdir;
615         if ($dev == $pdev) {
616             if ($self->{'mount'}) {
617                 system $Amanda::Constants::MOUNT, $dir;
618                 ($dev, $ino) = stat $dir;
619             }
620         }
621         if ($dev == $pdev) {
622             return $self->make_error("failed", undef,
623                 reason => "notfound",
624                 message => "No removable disk mounted on '$dir'");
625         }
626     }
627
628     if ($self->{'num-slot'}) {
629         for my $i (1..$self->{'num-slot'}) {
630             my $slot_dir = "$dir/slot$i";
631             if (!-e $slot_dir) {
632                 if ($self->{'auto-create-slot'}) {
633                     if (!mkdir ($slot_dir)) {
634                         return $self->make_error("fatal", undef,
635                             message => "Can't create '$slot_dir': $!");
636                     }
637                 } else {
638                     return $self->make_error("fatal", undef,
639                         message => "slot $i doesn't exists '$slot_dir'");
640                 }
641             }
642         }
643     } else {
644         if ($self->{'auto-create-slot'}) {
645             return $self->make_error("fatal", undef,
646                 message => "property 'auto-create-slot' set but property 'num-slot' is not set");
647         }
648     }
649     return undef;
650 }
651
652 sub try_lock {
653     my $self = shift;
654     my $cb = shift;
655     my $poll = 0; # first delay will be 0.1s; see below
656     my $time;
657
658     if (defined $self->{'lock-timeout'}) {
659         $time = time() + $self->{'lock-timeout'};
660     } else {
661         $time = time() + 1000;
662     }
663
664
665     my $steps = define_steps
666         cb_ref => \$cb;
667
668     step init => sub {
669         if ($self->{'mount'} && defined $self->{'fl'} &&
670             !$self->{'fl'}->locked()) {
671             return $steps->{'lock'}->();
672         }
673         $steps->{'lock_done'}->();
674     };
675
676     step lock => sub {
677         my $rv = $self->{'fl'}->lock_rd();
678         if ($rv == 1 && time() < $time) {
679             # loop until we get the lock, increasing $poll to 10s
680             $poll += 100 unless $poll >= 10000;
681             return Amanda::MainLoop::call_after($poll, $steps->{'lock'});
682         } elsif ($rv == 1) {
683             return $self->make_error("fatal", $cb,
684                 message => "Timeout trying to lock '$self->{'umount_lockfile'}'");
685         } elsif ($rv == -1) {
686             return $self->make_error("fatal", $cb,
687                 message => "Error locking '$self->{'umount_lockfile'}'");
688         } elsif ($rv == 0) {
689             if (defined $self->{'umount_src'}) {
690                 $self->{'umount_src'}->remove();
691                 $self->{'umount_src'} = undef;
692             }
693             return $steps->{'lock_done'}->();
694         }
695     };
696
697     step lock_done => sub {
698         my $err = $self->_validate();
699         $cb->($err);
700     };
701 }
702
703 sub try_umount {
704     my $self = shift;
705
706     my $dir = $self->{'dir'};
707     if ($self->{'removable'} && $self->{'umount'}) {
708         my ($dev, $ino) = stat $dir;
709         my $parentdir = dirname $dir;
710         my ($pdev, $pino) = stat $parentdir;
711         if ($dev != $pdev) {
712             system $Amanda::Constants::UMOUNT, $dir;
713         }
714     }
715 }
716
717 sub force_unlock {
718     my $self = shift;
719
720     if (keys( %{$self->{'reservation'}}) == 0 ) {
721         if ($self->{'fl'}) {
722             if ($self->{'fl'}->locked()) {
723                 $self->{'fl'}->unlock();
724             }
725             if ($self->{'umount'}) {
726                 if (defined $self->{'umount_src'}) {
727                     $self->{'umount_src'}->remove();
728                     $self->{'umount_src'} = undef;
729                 }
730                 if ($self->{'fl'}->lock_wr() == 0) {
731                     $self->try_umount();
732                     $self->{'fl'}->unlock();
733                 }
734             }
735         }
736     }
737 }
738
739 sub try_unlock {
740     my $self = shift;
741
742     my $do_umount = sub {
743         local $?;
744
745         $self->{'umount_src'} = undef;
746         if ($self->{'fl'}->lock_wr() == 0) {
747             $self->try_umount();
748             $self->{'fl'}->unlock();
749         }
750     };
751
752     if (defined $self->{'umount_idle'}) {
753         if ($self->{'umount_idle'} == 0) {
754             return $self->force_unlock();
755         }
756         if (defined $self->{'fl'}) {
757             if (keys( %{$self->{'reservation'}}) == 0 ) {
758                 if ($self->{'fl'}->locked()) {
759                     $self->{'fl'}->unlock();
760                 }
761                 if ($self->{'umount'}) {
762                     if (defined $self->{'umount_src'}) {
763                         $self->{'umount_src'}->remove();
764                         $self->{'umount_src'} = undef;
765                     }
766                     $self->{'umount_src'} = Amanda::MainLoop::call_after(
767                                                 0+$self->{'umount_idle'},
768                                                 $do_umount);
769                 }
770             }
771         }
772     }
773 }
774
775 package Amanda::Changer::disk::Reservation;
776 use vars qw( @ISA );
777 @ISA = qw( Amanda::Changer::Reservation );
778
779 sub new {
780     my $class = shift;
781     my ($chg, $device, $drive, $slot) = @_;
782     my $self = Amanda::Changer::Reservation::new($class);
783
784     $self->{'chg'} = $chg;
785     $self->{'drive'} = $drive;
786
787     $self->{'device'} = $device;
788     $self->{'this_slot'} = $slot;
789
790     $self->{'chg'}->{'reservation'}->{$slot} += 1;
791     return $self;
792 }
793
794 sub do_release {
795     my $self = shift;
796     my %params = @_;
797     my $drive = $self->{'drive'};
798
799     unlink("$drive/data")
800         or warn("Could not unlink '$drive/data': $!");
801     rmdir("$drive")
802         or warn("Could not rmdir '$drive': $!");
803
804     # unref the device, for good measure
805     $self->{'device'} = undef;
806     my $slot = $self->{'this_slot'};
807
808     my $finish = sub {
809         $self->{'chg'}->{'reservation'}->{$slot} -= 1;
810         delete $self->{'chg'}->{'reservation'}->{$slot} if
811                 $self->{'chg'}->{'reservation'}->{$slot} == 0;
812         $self->{'chg'}->try_unlock();
813         delete $self->{'chg'};
814         $self = undef;
815         return $params{'finished_cb'}->();
816     };
817
818     if (exists $params{'unlocked'}) {
819         my $state = $params{state};
820         delete $state->{drives}->{$drive}->{pid};
821         return $finish->();
822     }
823
824     $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
825                                     $finish, sub {
826         my ($state, $finished_cb) = @_;
827
828         delete $state->{drives}->{$drive}->{pid};
829
830         $finished_cb->();
831     });
832 }
833
834 sub get_meta_label {
835     my $self = shift;
836     my %params = @_;
837
838     $params{'slot'} = $self->{'this_slot'};
839     $self->{'chg'}->get_meta_label(%params);
840 }
841
842 sub set_meta_label {
843     my $self = shift;
844     my %params = @_;
845
846     $params{'slot'} = $self->{'this_slot'};
847     $self->{'chg'}->set_meta_label(%params);
848 }