Imported Upstream version 3.1.0
[debian/amanda] / perl / Amanda / Changer / multi.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::multi;
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 Amanda::Config qw( :getconf );
29 use Amanda::Debug;
30 use Amanda::Changer;
31 use Amanda::MainLoop;
32 use Amanda::Device qw( :constants );
33
34 =head1 NAME
35
36 Amanda::Changer::multi
37
38 =head1 DESCRIPTION
39
40 This changer operates with a list of device, specified in the tpchanger
41 string.
42
43 See the amanda-changers(7) manpage for usage information.
44
45 =cut
46
47 # STATE
48 #
49 # The device state is shared between all changers accessing the same changer.
50 # It is a hash with keys:
51 #   current_slot - the unaliased device name of the current slot
52 #   slots - see below
53 #
54 # The 'slots' key is a hash, with unaliased device name as keys and hashes
55 # as values.  Each slot's hash has keys:
56 #   pid           - the pid that reserved that slot.
57 #   state         - SLOT_FULL/SLOT_EMPTY/SLOT_UNKNOWN
58 #   device_status - the status of the device after the open or read_label
59 #   f_type        - the F_TYPE of the fileheader.
60 #   label         - the label, if known, of the volume in this slot
61
62 # $self is a hash with keys:
63 #   slot           : slot number of the current slot
64 #   slots          : An array with all slot names
65 #   unaliased      : A hash with slot number as keys and unaliased device name
66 #                    as value
67 #   slot_name      : A hash with slot number as keys and device name as value
68 #   number         : A hash with unaliased device name as keys and slot number
69 #                    as value
70 #   config         : The Amanda::Changer::Config for this changer
71 #   state_filename : The filename of the state file
72 #   first_slot     : The number of the first slot
73 #   last_slot      : The number of the last slot + 1
74
75 sub new {
76     my $class = shift;
77     my ($config, $tpchanger) = @_;
78     my $devices = $tpchanger;
79     $devices =~ s/^chg-multi://g;
80     my (@slots) = Amanda::Util::expand_braced_alternates($devices);
81
82     unless (scalar @slots != 0) {
83         return Amanda::Changer->make_error("fatal", undef,
84             message => "no devices specified");
85     }
86
87     my $properties = $config->{'properties'};
88     my $first_slot = 1;
89     if (exists $properties->{'first-slot'}) {
90         $first_slot = @{$properties->{'first-slot'}->{'values'}}[0];
91     }
92
93     my %number = ();
94     my %unaliased = ();
95     my %slot_name = ();
96     my $last_slot = $first_slot;
97     foreach my $slot_name (@slots) {
98         my $unaliased_name = Amanda::Device::unaliased_name($slot_name);
99         $number{$unaliased_name} = $last_slot;
100         $unaliased{$last_slot} = $unaliased_name;
101         $slot_name{$last_slot} = $slot_name;
102         $last_slot++;
103     }
104
105     if (!defined $config->{changerfile} ||
106         $config->{changerfile} eq "") {
107         return Amanda::Changer->make_error("fatal", undef,
108             reason => "invalid",
109             message => "no changerfile specified for changer '$config->{name}'");
110     }
111
112     my $state_filename = Amanda::Config::config_dir_relative($config->{'changerfile'});
113
114     my $self = {
115         slots => \@slots,
116         unaliased => \%unaliased,
117         slot_name => \%slot_name,
118         number => \%number,
119         config => $config,
120         state_filename => $state_filename,
121         first_slot => $first_slot,
122         last_slot => $last_slot,
123     };
124
125     bless ($self, $class);
126     return $self;
127 }
128
129 sub load {
130     my $self = shift;
131     my %params = @_;
132     my $old_res_cb = $params{'res_cb'};
133     my $state;
134
135     $self->validate_params('load', \%params);
136
137     return if $self->check_error($params{'res_cb'});
138
139     $self->with_locked_state($self->{'state_filename'},
140                                      $params{'res_cb'}, sub {
141         my ($state, $res_cb) = @_;
142
143         $params{'state'} = $state;
144         # overwrite the callback for _load_by_xxx
145         $params{'res_cb'} = $res_cb;
146
147         if (exists $params{'slot'} or exists $params{'relative_slot'}) {
148             $self->_load_by_slot(%params);
149         } elsif (exists $params{'label'}) {
150             $self->_load_by_label(%params);
151         }
152     });
153 }
154
155 sub info_key {
156     my $self = shift;
157     my ($key, %params) = @_;
158     my %results;
159
160     return if $self->check_error($params{'info_cb'});
161
162     # no need for synchronization -- all of these values are static
163
164     if ($key eq 'num_slots') {
165         $results{$key} = $self->{last_slot} - $self->{first_slot};
166     } elsif ($key eq 'vendor_string') {
167         $results{$key} = 'chg-multi'; # mostly just for testing
168     } elsif ($key eq 'fast_search') {
169         $results{$key} = 0;
170     }
171
172     $params{'info_cb'}->(undef, %results) if $params{'info_cb'};
173 }
174
175 sub reset {
176     my $self = shift;
177     my %params = @_;
178
179     return if $self->check_error($params{'finished_cb'});
180
181     $self->with_locked_state($self->{'state_filename'},
182                                      $params{'finished_cb'}, sub {
183         my ($state, $finished_cb) = @_;
184         my $slot;
185
186         $params{state} = $state;
187         $slot = $self->{first_slot};
188         $self->{slot} = $slot;
189         $self->_set_current($state, $slot);
190
191         $finished_cb->();
192     });
193 }
194
195 sub eject {
196     my $self = shift;
197     my %params = @_;
198     my $slot;
199
200     return if $self->check_error($params{'finished_cb'});
201
202     $self->with_locked_state($self->{'state_filename'},
203                                      $params{'finished_cb'}, sub {
204         my ($state, $finished_cb) = @_;
205         my $drive;
206
207         $params{state} = $state;
208         if (!exists $params{'drive'}) {
209             $drive = $self->_get_current($params{state});
210         } else {
211             $drive = $params{'drive'};
212         }
213         if (!defined $self->{unaliased}->{$drive}) {
214             return $self->make_error("failed", $finished_cb,
215                 reason => "invalid",
216                 message => "Invalid slot '$drive'");
217         }
218
219         Amanda::Debug::debug("ejecting drive $drive");
220         my $device = Amanda::Device->new($self->{slot_name}->{$drive});
221         if ($device->status() != $DEVICE_STATUS_SUCCESS) {
222             return $self->make_error("failed", $finished_cb,
223                 reason => "device",
224                 message => $device->error_or_status);
225         }
226         if (my $err = $self->{'config'}->configure_device($device)) {
227             return $self->make_error("failed", $params{'res_cb'},
228                         reason => "device",
229                         message => $err);
230         }
231         $device->eject();
232         if ($device->status() != $DEVICE_STATUS_SUCCESS) {
233             return $self->make_error("failed", $finished_cb,
234                 reason => "invalid",
235                 message => $device->error_or_status);
236         }
237         undef $device;
238
239         $finished_cb->();
240     });
241 }
242
243 sub update {
244     my $self = shift;
245     my %params = @_;
246     my @slots_to_check;
247     my $state;
248     my $set_to_unknown = 0;
249
250     my $user_msg_fn = $params{'user_msg_fn'};
251     $user_msg_fn ||= sub { Amanda::Debug::info("chg-multi: " . $_[0]); };
252
253     my $steps = define_steps
254         cb_ref => \$params{'finished_cb'};
255
256     step lock => sub {
257         $self->with_locked_state($self->{'state_filename'},
258                                  $params{'finished_cb'}, sub {
259             my ($state, $finished_cb) = @_;
260
261             $params{state} = $state;
262             $params{'finished_cb'} = $finished_cb;
263
264             $steps->{'handle_assignment'}->();
265         });
266     };
267
268     step handle_assignment => sub {
269         $state = $params{state};
270         # check for the SL=LABEL format, and handle it here
271         if (exists $params{'changed'} and
272             $params{'changed'} =~ /^\d+=\S+$/) {
273             my ($slot, $label) = ($params{'changed'} =~ /^(\d+)=(\S+)$/);
274
275             # let's list the reasons we *can't* do what the user has asked
276             my $whynot;
277             if (!exists $self->{unaliased}->{$slot}) {
278                 $whynot = "slot $slot does not exist";
279             }
280
281             if ($whynot) {
282                 return $self->make_error("failed", $params{'finished_cb'},
283                         reason => "unknown", message => $whynot);
284             }
285
286             $user_msg_fn->("recording volume '$label' in slot $slot");
287             # ok, now erase all knowledge of that label
288             while (my ($sl, $inf) = each %{$state->{'slots'}}) {
289                 if ($inf->{'label'} and $inf->{'label'} eq $label) {
290                     $inf->{'label'} = undef;
291                 }
292             }
293
294             # and add knowledge of the label to the given slot
295             my $unaliased = $self->{unaliased}->{$slot};
296             $state->{'slots'}->{$unaliased}->{'label'} = $label;
297
298             # that's it -- no changer motion required
299             return $params{'finished_cb'}->(undef);
300         } elsif (exists $params{'changed'} and
301                $params{'changed'} =~ /^(.+)=$/) {
302             $params{'changed'} = $1;
303             $set_to_unknown = 1;
304             $steps->{'calculate_slots'}->();
305         } else {
306             $steps->{'calculate_slots'}->();
307         }
308     };
309
310     step calculate_slots => sub {
311         if (exists $params{'changed'}) {
312             # parse the string just like use-slots, using a hash for uniqueness
313             my %changed;
314             for my $range (split ',', $params{'changed'}) {
315                 my ($first, $last) = ($range =~ /(\d+)(?:-(\d+))?/);
316                 $last = $first unless defined($last);
317                 for ($first .. $last) {
318                     $changed{$_} = undef;
319                 }
320             }
321
322             @slots_to_check = keys %changed;
323             @slots_to_check = grep { exists $self->{'unaliased'}->{$_} } @slots_to_check;
324         } else {
325             @slots_to_check = keys %{ $self->{unaliased} };
326         }
327
328         # sort them so we don't confuse the user with a "random" order
329         @slots_to_check = sort @slots_to_check;
330
331         $steps->{'update_slot'}->();
332     };
333
334     # TODO: parallelize, we have one drive by slot
335
336     step update_slot => sub {
337         return $steps->{'done'}->() if (!@slots_to_check);
338         my $slot = shift @slots_to_check;
339         if ($self->_is_slot_in_use($state, $slot)) {
340             $user_msg_fn->("Slot $slot is already in use");
341             return $steps->{'update_slot'}->();
342         }
343
344         if ($set_to_unknown == 1) {
345             $user_msg_fn->("removing entry for slot $slot");
346             my $unaliased = $self->{unaliased}->{$slot};
347             delete $state->{slots}->{$unaliased};
348             return $steps->{'update_slot'}->();
349         } else {
350             $user_msg_fn->("scanning slot $slot");
351             $params{'slot'} = $slot;
352             $params{'res_cb'} = $steps->{'slot_loaded'};
353             $self->_load_by_slot(%params);
354         }
355     };
356
357     step slot_loaded => sub {
358         my ($err, $res) = @_;
359         if ($err) {
360             return $params{'finished_cb'}->($err);
361         }
362
363         my $slot = $res->{'this_slot'};
364         my $dev = $res->{device};
365         $dev->read_label();
366         my $label = $dev->volume_label;
367         $self->_update_slot_state(state => $state, dev => $dev, slot =>$slot);
368         $user_msg_fn->("recording volume '$label' in slot $slot");
369         $res->release(
370             finished_cb => $steps->{'released'},
371             unlocked => 1,
372             state => $state);
373     };
374
375     step released => sub {
376         my ($err) = @_;
377         if ($err) {
378             return $params{'finished_cb'}->($err);
379         }
380
381         $steps->{'update_slot'}->();
382     };
383
384     step done => sub {
385         $params{'finished_cb'}->(undef);
386     };
387 }
388
389 sub inventory {
390     my $self = shift;
391     my %params = @_;
392
393     return if $self->check_error($params{'inventory_cb'});
394
395     $self->with_locked_state($self->{'state_filename'},
396                              $params{'inventory_cb'}, sub {
397         my ($state, $inventory_cb) = @_;
398
399         my @inventory;
400         my $current = $self->_get_current($state);
401         foreach ($self->{first_slot} .. ($self->{last_slot} - 1)) {
402             my $slot = "$_";
403             my $unaliased = $self->{unaliased}->{$slot};
404             my $s = { slot => $slot,
405                       state => $state->{slots}->{$unaliased}->{state} || Amanda::Changer::SLOT_UNKNOWN,
406                       reserved => $self->_is_slot_in_use($state, $slot) };
407             if (defined $state->{slots}->{$unaliased}) {
408                 $s->{'device_status'} =
409                               $state->{slots}->{$unaliased}->{device_status};
410                 $s->{'f_type'} = $state->{slots}->{$unaliased}->{f_type};
411                 $s->{'label'} = $state->{slots}->{$unaliased}->{label};
412             } else {
413                 $s->{'device_status'} = undef;
414                 $s->{'f_type'} = undef;
415                 $s->{'label'} = undef;
416             }
417             if ($slot eq $current) {
418                 $s->{'current'} = 1;
419             }
420             push @inventory, $s;
421         }
422         $inventory_cb->(undef, \@inventory);
423     })
424 }
425
426 sub _load_by_slot {
427     my $self = shift;
428     my %params = @_;
429     my $slot;
430
431     if (exists $params{'relative_slot'}) {
432         if ($params{'relative_slot'} eq "current") {
433             $slot = $self->_get_current($params{state});
434         } elsif ($params{'relative_slot'} eq "next") {
435             if (exists $params{'slot'}) {
436                 $slot = $params{'slot'};
437             } else {
438                 $slot = $self->_get_current($params{state});
439             }
440             $slot = $self->_get_next($slot);
441             $self->{slot} = $slot if ($params{'set_current'});
442             $self->_set_current($params{state}, $slot) if ($params{'set_current'});
443         } else {
444             return $self->make_error("failed", $params{'res_cb'},
445                 reason => "invalid",
446                 message => "Invalid relative slot '$params{relative_slot}'");
447         }
448     } else {
449         $slot = $params{'slot'};
450     }
451
452     if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
453         return $self->make_error("failed", $params{'res_cb'},
454             reason => "notfound",
455             message => "all slots have been loaded");
456     }
457
458     if (!$self->_slot_exists($slot)) {
459         return $self->make_error("failed", $params{'res_cb'},
460             reason => "notfound",
461             message => "Slot $slot not defined");
462     }
463
464     if ($self->_is_slot_in_use($params{state}, $slot)) {
465         my $unaliased = $self->{unaliased}->{$slot};
466         return $self->make_error("failed", $params{'res_cb'},
467             reason => "volinuse",
468             slot => $slot,
469             message => "Slot $slot is already in use by process '$params{state}->{slots}->{$unaliased}->{pid}'");
470     }
471
472     $self->{slot} = $slot if ($params{'set_current'});
473     $self->_set_current($params{state}, $slot) if ($params{'set_current'});
474
475     $self->_make_res($params{state}, $params{'res_cb'}, $slot);
476 }
477
478 sub _load_by_label {
479     my $self = shift;
480     my %params = @_;
481     my $label = $params{'label'};
482     my $slot;
483     my $slot_name;
484     my $state = $params{state};
485
486     foreach $slot (keys %{$state->{slots}}) {
487         if (defined $state->{slots}->{$slot} &&
488             $state->{slots}->{$slot}->{label} &&
489             $state->{slots}->{$slot}->{label} eq $label) {
490             $slot_name = $slot;
491             last;
492         }
493     }
494
495     if (defined $slot_name &&
496         $state->{slots}->{$slot_name}->{label} eq $label) {
497
498         $slot = $self->{number}->{$slot_name};
499         delete $params{'label'};
500         $params{'slot'} = $slot;
501         $self->_load_by_slot(%params);
502     } else {
503         return $self->make_error("failed", $params{'res_cb'},
504                                 reason => "notfound",
505                                 message => "Label '$label' not found");
506     }
507 }
508
509
510 sub _make_res {
511     my $self = shift;
512     my ($state, $res_cb, $slot) = @_;
513     my $res;
514
515     my $unaliased = $self->{unaliased}->{$slot};
516     my $slot_name = $self->{slot_name}->{$slot};
517     my $device = Amanda::Device->new($slot_name);
518     if ($device->status != $DEVICE_STATUS_SUCCESS) {
519         return $self->make_error("failed", $res_cb,
520                 reason => "device",
521                 message => "opening '$slot': " . $device->error_or_status());
522     }
523
524     if (my $err = $self->{'config'}->configure_device($device)) {
525         return $self->make_error("failed", $res_cb,
526                 reason => "device",
527                 message => $err);
528     }
529
530     $res = Amanda::Changer::multi::Reservation->new($self, $device, $slot);
531     $state->{slots}->{$unaliased}->{pid} = $$;
532     $device->read_label();
533
534     $self->_update_slot_state(state => $state, dev => $res->{device}, slot => $slot);
535     $res_cb->(undef, $res);
536 }
537
538
539 # Internal function to determine whether a slot exists.
540 sub _slot_exists {
541     my ($self, $slot) = @_;
542
543     return 1 if defined $self->{unaliased}->{$slot};
544     return 0;
545 }
546
547 sub _update_slot_state {
548     my $self = shift;
549     my %params = @_;
550     my $state = $params{state};
551     my $dev = $params{dev};
552     my $slot = $params{slot};
553     my $unaliased = $self->{unaliased}->{$slot};
554     $state->{slots}->{$unaliased}->{device_status} = "".scalar($dev->status);
555     my $label = $dev->volume_label;
556     $state->{slots}->{$unaliased}->{state} = Amanda::Changer::SLOT_FULL;
557     $state->{slots}->{$unaliased}->{label} = $label;
558     my $volume_header = $dev->volume_header;
559     if (defined $volume_header) {
560         $state->{slots}->{$unaliased}->{f_type} = "".scalar($volume_header->{type});
561     } else {
562         delete $state->{slots}->{$unaliased}->{f_type};
563     }
564 }
565 # Internal function to determine if a slot (specified by number) is in use by a
566 # drive, and return the path for that drive if so.
567 sub _is_slot_in_use {
568     my ($self, $state, $slot) = @_;
569
570     return 0 if !defined $state;
571     return 0 if !defined $state->{slots};
572     return 0 if !defined $self->{unaliased}->{$slot};
573     my $unaliased = $self->{unaliased}->{$slot};
574     return 0 if !defined $state->{slots}->{$unaliased};
575     return 0 if !defined $state->{slots}->{$unaliased}->{pid};
576
577     #check if PID is still alive
578     my $pid = $state->{slots}->{$unaliased}->{pid};
579     if (Amanda::Util::is_pid_alive($pid) == 1) {
580         return 1;
581     }
582
583     delete $state->{slots}->{$unaliased}->{pid};
584     return 0;
585 }
586
587 # Internal function to get the next slot after $slot.
588 # skip over except_slot and slot in use.
589 sub _get_next {
590     my ($self, $slot, $except_slot) = @_;
591     my $next_slot;
592
593     $next_slot = $slot + 1;
594     $next_slot = $self->{'first_slot'} if $next_slot >= $self->{'last_slot'};
595
596     return $next_slot;
597 }
598
599 # Get the 'current' slot
600 sub _get_current {
601     my ($self, $state) = @_;
602
603     return $self->{slot} if defined $self->{slot};
604     if (defined $state->{current_slot}) {
605         my $slot = $self->{number}->{$state->{current_slot}};
606         # return the slot if it exist.
607         return $slot if $slot >= $self->{'first_slot'} && $slot < $self->{'last_slot'};
608         Amanda::Debug::debug("statefile current_slot is not configured");
609     }
610     # return the first slot
611     return $self->{first_slot};
612 }
613
614 # Set the 'current' slot
615 sub _set_current {
616     my ($self, $state, $slot) = @_;
617
618     $self->{slot} = $slot;
619     $state->{current_slot} = $self->{unaliased}->{$slot};
620 }
621
622 package Amanda::Changer::multi::Reservation;
623 use vars qw( @ISA );
624 @ISA = qw( Amanda::Changer::Reservation );
625 use Amanda::Device qw( :constants );
626
627 sub new {
628     my $class = shift;
629     my ($chg, $device, $slot) = @_;
630     my $self = Amanda::Changer::Reservation::new($class);
631
632     $self->{'chg'} = $chg;
633     $self->{'device'} = $device;
634     $self->{'this_slot'} = $slot;
635
636     return $self;
637 }
638
639 sub set_label {
640     my $self = shift;
641     my %params = @_;
642
643     my $chg = $self->{chg};
644     $chg->with_locked_state($chg->{'state_filename'},
645                             $params{'finished_cb'}, sub {
646         my ($state, $finished_cb) = @_;
647         my $label = $params{'label'};
648         my $slot = $self->{'this_slot'};
649         my $unaliased = $chg->{unaliased}->{$slot};
650
651         $state->{slots}->{$unaliased}->{label} =  $label;
652         $state->{slots}->{$unaliased}->{device_status} =
653                                 "".$DEVICE_STATUS_SUCCESS;
654         $state->{slots}->{$unaliased}->{f_type} =
655                                 "".scalar($Amanda::Header::F_TAPESTART);
656         $finished_cb->();
657     });
658 }
659
660 sub do_release {
661     my $self = shift;
662     my %params = @_;
663
664     # if we're in global cleanup and the changer is already dead,
665     # then never mind
666     return unless $self->{'chg'};
667
668     # unref the device, for good measure
669     $self->{'device'} = undef;
670
671     if (exists $params{'unlocked'}) {
672         my $state = $params{state};
673         my $slot = $self->{'this_slot'};
674         my $unaliased = $self->{chg}->{unaliased}->{$slot};
675         delete $state->{slots}->{$unaliased}->{pid};
676         return $params{'finished_cb'}->();
677     }
678
679     $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
680                                     $params{'finished_cb'}, sub {
681         my ($state, $finished_cb) = @_;
682         $params{state} = $state;
683         my $slot = $self->{'this_slot'};
684         my $unaliased = $self->{chg}->{unaliased}->{$slot};
685         delete $state->{slots}->{$unaliased}->{pid};
686         $finished_cb->();
687     });
688 }