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