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