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