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