Imported Upstream version 3.3.0
[debian/amanda] / perl / Amanda / Changer / disk.pm
1 # Copyright (c) 2008,2009,2010 Zmanda, Inc.  All Rights Reserved.
2 #
3 # This program is free software; you can redistribute it and/or modify it
4 # under the terms of the GNU General Public License version 2 as published
5 # by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful, but
8 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
9 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
10 # for more details.
11 #
12 # You should have received a copy of the GNU General Public License along
13 # with this program; if not, write to the Free Software Foundation, Inc.,
14 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
15 #
16 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
17 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
18
19 package Amanda::Changer::disk;
20
21 use strict;
22 use warnings;
23 use vars qw( @ISA );
24 @ISA = qw( Amanda::Changer );
25
26 use File::Glob qw( :glob );
27 use File::Path;
28 use 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         $self->with_locked_state($self->{'state_filename'},
271             sub { my @args = @_;
272                   $self->try_unlock();
273                   $cb->(@args);
274                 },
275             $sub);
276     };
277 }
278
279 sub get_meta_label {
280     my $self = shift;
281     my %params = @_;
282
283     return if $self->check_error($params{'finished_cb'});
284
285     $self->with_disk_locked_state($params{'finished_cb'}, sub {
286         my ($state, $finished_cb) = @_;
287
288         $finished_cb->(undef, $state->{'meta'});
289     });
290 }
291
292 sub _load_by_slot {
293     my $self = shift;
294     my %params = @_;
295     my $drive;
296     my $slot;
297
298     if (exists $params{'relative_slot'}) {
299         if ($params{'relative_slot'} eq "current") {
300             $slot = $self->_get_current();
301         } elsif ($params{'relative_slot'} eq "next") {
302             if (exists $params{'slot'}) {
303                 $slot = $params{'slot'};
304             } else {
305                 $slot = $self->_get_current();
306             }
307             $slot = $self->_get_next($slot);
308             $self->_set_current($slot) if ($params{'set_current'});
309         } else {
310             return $self->make_error("failed", $params{'res_cb'},
311                 reason => "invalid",
312                 message => "Invalid relative slot '$params{relative_slot}'");
313         }
314     } else {
315         $slot = $params{'slot'};
316     }
317
318     if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
319         return $self->make_error("failed", $params{'res_cb'},
320             reason => "notfound",
321             message => "all slots have been loaded");
322     }
323
324     if (!$self->_slot_exists($slot)) {
325         return $self->make_error("failed", $params{'res_cb'},
326             reason => "invalid",
327             message => "Slot $slot not found");
328     }
329
330     if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
331         return $self->make_error("failed", $params{'res_cb'},
332             reason => "volinuse",
333             slot => $slot,
334             message => "Slot $slot is already in use by drive '$drive' and process '$params{state}->{drives}->{$drive}->{pid}'");
335     }
336
337     $drive = $self->_alloc_drive();
338     $self->_load_drive($drive, $slot);
339     $self->_set_current($slot) if ($params{'set_current'});
340
341     $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
342 }
343
344 sub _load_by_label {
345     my $self = shift;
346     my %params = @_;
347     my $label = $params{'label'};
348     my $slot;
349     my $drive;
350
351     $slot = $self->_find_label($label);
352     if (!defined $slot) {
353         return $self->make_error("failed", $params{'res_cb'},
354             reason => "notfound",
355             message => "Label '$label' not found");
356     }
357
358     if ($drive = $self->_is_slot_in_use($params{'state'}, $slot)) {
359         return $self->make_error("failed", $params{'res_cb'},
360             reason => "volinuse",
361             message => "Slot $slot, containing '$label', is already " .
362                         "in use by drive '$drive'");
363     }
364
365     $drive = $self->_alloc_drive();
366     $self->_load_drive($drive, $slot);
367     $self->_set_current($slot) if ($params{'set_current'});
368
369     $self->_make_res($params{'state'}, $params{'res_cb'}, $drive, $slot);
370 }
371
372 sub _make_res {
373     my $self = shift;
374     my ($state, $res_cb, $drive, $slot) = @_;
375     my $res;
376
377     my $device = Amanda::Device->new("file:$drive");
378     if ($device->status != $DEVICE_STATUS_SUCCESS) {
379         return $self->make_error("failed", $res_cb,
380                 reason => "device",
381                 message => "opening 'file:$drive': " . $device->error_or_status());
382     }
383
384     if (my $err = $self->{'config'}->configure_device($device)) {
385         return $self->make_error("failed", $res_cb,
386                 reason => "device",
387                 message => $err);
388     }
389
390     $res = Amanda::Changer::disk::Reservation->new($self, $device, $drive, $slot);
391     $state->{drives}->{$drive}->{pid} = $$;
392     $device->read_label();
393
394     $res_cb->(undef, $res);
395 }
396
397 # Internal function to find an unused (nonexistent) driveN subdirectory and
398 # create it.  Note that this does not add a 'data' symlink inside the directory.
399 sub _alloc_drive {
400     my ($self) = @_;
401     my $n = 0;
402
403     while (1) {
404         my $drive = $self->{'dir'} . "/drive$n";
405         $n++;
406
407         warn "$drive is not a directory; please remove it" if (-e $drive and ! -d $drive);
408         next if (-e $drive);
409         next if (!mkdir($drive)); # TODO probably not a very effective locking mechanism..
410
411         return $drive;
412     }
413 }
414
415 # Internal function to enumerate all available slots.  Slots are described by
416 # strings.
417 sub _all_slots {
418     my ($self) = @_;
419     my $dir = _quote_glob($self->{'dir'});
420     my @slots;
421
422     for my $slotname (bsd_glob("$dir/slot*/")) {
423         my $slot;
424         next unless (($slot) = ($slotname =~ /.*slot([0-9]+)\/$/));
425         push @slots, $slot + 0;
426     }
427
428     return map { "$_"} sort { $a <=> $b } @slots;
429 }
430
431 # Internal function to determine whether a slot exists.
432 sub _slot_exists {
433     my ($self, $slot) = @_;
434     return (-d $self->{'dir'} . "/slot$slot");
435 }
436
437 # Internal function to determine if a slot (specified by number) is in use by a
438 # drive, and return the path for that drive if so.
439 sub _is_slot_in_use {
440     my ($self, $state, $slot) = @_;
441     my $dir = _quote_glob($self->{'dir'});
442
443     for my $symlink (bsd_glob("$dir/drive*/data")) {
444         if (! -l $symlink) {
445             warn "'$symlink' is not a symlink; please remove it";
446             next;
447         }
448
449         my $target = readlink($symlink);
450         if (!$target) {
451             warn "could not read '$symlink': $!";
452             next;
453         }
454
455         my $tslot;
456         if (!(($tslot) = ($target =~ /..\/slot([0-9]+)/))) {
457             warn "invalid changer symlink '$symlink' -> '$target'";
458             next;
459         }
460
461         if ($tslot+0 == $slot) {
462             my $drive = $symlink;
463             $drive =~ s{/data$}{}; # strip the trailing '/data'
464
465             #check if process is alive
466             my $pid = $state->{drives}->{$drive}->{pid};
467             if (!defined $pid or !Amanda::Util::is_pid_alive($pid)) {
468                 unlink("$drive/data")
469                     or warn("Could not unlink '$drive/data': $!");
470                 rmdir("$drive")
471                     or warn("Could not rmdir '$drive': $!");
472                 delete $state->{drives}->{$drive}->{pid};
473                 next;
474             }
475             return $drive;
476         }
477     }
478
479     return 0;
480 }
481
482 sub _get_slot_label {
483     my ($self, $slot) = @_;
484     my $dir = _quote_glob($self->{'dir'});
485
486     for my $symlink (bsd_glob("$dir/slot$slot/00000.*")) {
487         my ($label) = ($symlink =~ qr{\/00000\.([^/]*)$});
488         return $label;
489     }
490
491     return ''; # known, but blank
492 }
493
494 # Internal function to point a drive to a slot
495 sub _load_drive {
496     my ($self, $drive, $slot) = @_;
497
498     die "'$drive' does not exist" unless (-d $drive);
499     if (-e "$drive/data") {
500         unlink("$drive/data");
501     }
502
503     symlink("../slot$slot", "$drive/data");
504     # TODO: read it to be sure??
505 }
506
507 # Internal function to return the slot containing a volume with the given
508 # label.  This takes advantage of the naming convention used by vtapes.
509 sub _find_label {
510     my ($self, $label) = @_;
511     my $dir = _quote_glob($self->{'dir'});
512     $label = _quote_glob($label);
513
514     my @tapelabels = bsd_glob("$dir/slot*/00000.$label");
515     if (!@tapelabels) {
516         return undef;
517     }
518
519     if (scalar @tapelabels > 1) {
520         warn "Multiple slots with label '$label': " . (join ", ", @tapelabels);
521     }
522
523     my ($slot) = ($tapelabels[0] =~ qr{/slot([0-9]+)/00000.});
524     return $slot;
525 }
526
527 # Internal function to get the next slot after $slot.
528 sub _get_next {
529     my ($self, $slot) = @_;
530     my $next_slot;
531
532     # Try just incrementing the slot number
533     $next_slot = $slot+1;
534     return $next_slot if (-d $self->{'dir'} . "/slot$next_slot");
535
536     # Otherwise, search through all slots
537     my @all_slots = $self->_all_slots();
538     my $prev = $all_slots[-1];
539     for $next_slot (@all_slots) {
540         return $next_slot if ($prev == $slot);
541         $prev = $next_slot;
542     }
543
544     # not found? take a guess.
545     return $all_slots[0];
546 }
547
548 # Get the 'current' slot, represented as a symlink named 'data'
549 sub _get_current {
550     my ($self) = @_;
551     my $curlink = $self->{'dir'} . "/data";
552
553     # for 2.6.1-compatibility, also parse a "current" symlink
554     my $oldlink = $self->{'dir'} . "/current";
555     if (-l $oldlink and ! -e $curlink) {
556         rename($oldlink, $curlink);
557     }
558
559     if (-l $curlink) {
560         my $target = readlink($curlink);
561         if ($target =~ "^slot([0-9]+)/?") {
562             return $1;
563         }
564     }
565
566     # get the first slot as a default
567     my @slots = $self->_all_slots();
568     return 0 unless (@slots);
569     return $slots[0];
570 }
571
572 # Set the 'current' slot
573 sub _set_current {
574     my ($self, $slot) = @_;
575     my $curlink = $self->{'dir'} . "/data";
576
577     if (-l $curlink or -e $curlink) {
578         unlink($curlink)
579             or warn("Could not unlink '$curlink'");
580     }
581
582     # TODO: locking
583     symlink("slot$slot", $curlink);
584 }
585
586 # utility function
587 sub _quote_glob {
588     my ($filename) = @_;
589     $filename =~ s/([]{}\\?*[])/\\$1/g;
590     return $filename;
591 }
592
593 sub _validate() {
594     my $self = shift;
595     my $dir = $self->{'dir'};
596
597     unless (-d $dir) {
598         $self->{'fatal_error'} = Amanda::Changer->make_error("fatal", undef,
599             message => "directory '$dir' does not exist");
600         return;
601     }
602
603     if ($self->{'removable'}) {
604         my ($dev, $ino) = stat $dir;
605         my $parentdir = dirname $dir;
606         my ($pdev, $pino) = stat $parentdir;
607         if ($dev == $pdev) {
608             if ($self->{'mount'}) {
609                 system $Amanda::Constants::MOUNT, $dir;
610                 ($dev, $ino) = stat $dir;
611             }
612         }
613         if ($dev == $pdev) {
614             $self->{'fatal_error'} = Amanda::Changer->make_error("fatal", undef,
615                 message => "No removable disk mounted on '$dir'");
616             return;
617         }
618     }
619
620     if ($self->{'num-slot'}) {
621         for my $i (1..$self->{'num-slot'}) {
622             my $slot_dir = "$dir/slot$i";
623             if (!-e $slot_dir) {
624                 if ($self->{'auto-create-slot'}) {
625                     if (!mkdir ($slot_dir)) {
626                         $self->{'fatal_error'} = Amanda::Changer->make_error("fatal", undef,
627                             message => "Can't create '$slot_dir': $!");
628                         return;
629                     }
630                 } else {
631                     $self->{'fatal_error'} = Amanda::Changer->make_error("fatal", undef,
632                         message => "slot $i doesn't exists '$slot_dir'");
633                     return;
634                 }
635             }
636         }
637     } else {
638         if ($self->{'auto-create-slot'}) {
639             $self->{'fatal_error'} = Amanda::Changer->make_error("fatal", undef,
640                 message => "property 'auto-create-slot' set but property 'num-slot' is not set");
641             return;
642         }
643     }
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->{'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->{'done'}->();
677         }
678     };
679
680     step done => sub {
681         $self->_validate();
682         $cb->();
683     };
684
685 }
686
687 sub try_umount {
688     my $self = shift;
689
690     my $dir = $self->{'dir'};
691     if ($self->{'removable'} && $self->{'umount'}) {
692         my ($dev, $ino) = stat $dir;
693         my $parentdir = dirname $dir;
694         my ($pdev, $pino) = stat $parentdir;
695         if ($dev != $pdev) {
696             system $Amanda::Constants::UMOUNT, $dir;
697         }
698     }
699 }
700
701 sub force_unlock {
702     my $self = shift;
703
704     if (keys( %{$self->{'reservation'}}) == 0 ) {
705         if ($self->{'fl'}) {
706             if ($self->{'fl'}->locked()) {
707                 $self->{'fl'}->unlock();
708             }
709             if ($self->{'umount'}) {
710                 if (defined $self->{'umount_src'}) {
711                     $self->{'umount_src'}->remove();
712                     $self->{'umount_src'} = undef;
713                 }
714                 if ($self->{'fl'}->lock_wr() == 0) {
715                     $self->try_umount();
716                     $self->{'fl'}->unlock();
717                 }
718             }
719         }
720     }
721 }
722
723 sub try_unlock {
724     my $self = shift;
725
726     my $do_umount = sub {
727         local $?;
728
729         $self->{'umount_src'} = undef;
730         if ($self->{'fl'}->lock_wr() == 0) {
731             $self->try_umount();
732             $self->{'fl'}->unlock();
733         }
734     };
735
736     if (defined $self->{'umount_idle'}) {
737         if ($self->{'umount_idle'} == 0) {
738             return $self->force_unlock();
739         }
740         if (defined $self->{'fl'}) {
741             if (keys( %{$self->{'reservation'}}) == 0 ) {
742                 if ($self->{'fl'}->locked()) {
743                     $self->{'fl'}->unlock();
744                 }
745                 if ($self->{'umount'}) {
746                     if (defined $self->{'umount_src'}) {
747                         $self->{'umount_src'}->remove();
748                         $self->{'umount_src'} = undef;
749                     }
750                     $self->{'umount_src'} = Amanda::MainLoop::call_after(
751                                                 0+$self->{'umount_idle'},
752                                                 $do_umount);
753                 }
754             }
755         }
756     }
757 }
758
759 package Amanda::Changer::disk::Reservation;
760 use vars qw( @ISA );
761 @ISA = qw( Amanda::Changer::Reservation );
762
763 sub new {
764     my $class = shift;
765     my ($chg, $device, $drive, $slot) = @_;
766     my $self = Amanda::Changer::Reservation::new($class);
767
768     $self->{'chg'} = $chg;
769     $self->{'drive'} = $drive;
770
771     $self->{'device'} = $device;
772     $self->{'this_slot'} = $slot;
773
774     $self->{'chg'}->{'reservation'}->{$slot} += 1;
775     return $self;
776 }
777
778 sub do_release {
779     my $self = shift;
780     my %params = @_;
781     my $drive = $self->{'drive'};
782
783     unlink("$drive/data")
784         or warn("Could not unlink '$drive/data': $!");
785     rmdir("$drive")
786         or warn("Could not rmdir '$drive': $!");
787
788     # unref the device, for good measure
789     $self->{'device'} = undef;
790     my $slot = $self->{'this_slot'};
791
792     my $finish = sub {
793         $self->{'chg'}->{'reservation'}->{$slot} -= 1;
794         delete $self->{'chg'}->{'reservation'}->{$slot} if
795                 $self->{'chg'}->{'reservation'}->{$slot} == 0;
796         $self->{'chg'}->try_unlock();
797         delete $self->{'chg'};
798         $self = undef;
799         return $params{'finished_cb'}->();
800     };
801
802     if (exists $params{'unlocked'}) {
803         my $state = $params{state};
804         delete $state->{drives}->{$drive}->{pid};
805         return $finish->();
806     }
807
808     $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
809                                     $finish, sub {
810         my ($state, $finished_cb) = @_;
811
812         delete $state->{drives}->{$drive}->{pid};
813
814         $finished_cb->();
815     });
816 }
817
818 sub get_meta_label {
819     my $self = shift;
820     my %params = @_;
821
822     $params{'slot'} = $self->{'this_slot'};
823     $self->{'chg'}->get_meta_label(%params);
824 }
825
826 sub set_meta_label {
827     my $self = shift;
828     my %params = @_;
829
830     $params{'slot'} = $self->{'this_slot'};
831     $self->{'chg'}->set_meta_label(%params);
832 }