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