26486bc495ce99d5e9c0a92feccb55cf8e2b8e75
[debian/amanda] / perl / Amanda / Changer / rait.pm
1 # Copyright (c) 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::rait;
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 qw( debug warning );
30 use Amanda::Util qw( :alternates );
31 use Amanda::Changer;
32 use Amanda::MainLoop;
33 use Amanda::Device qw( :constants );
34
35 =head1 NAME
36
37 Amanda::Changer::rait
38
39 =head1 DESCRIPTION
40
41 This changer operates several child changers, returning RAIT devices composed of
42 the devices produced by the child changers.  It's modeled on the RAIT device.
43
44 See the amanda-changers(7) manpage for usage information.
45
46 =cut
47
48 sub new {
49     my $class = shift;
50     my ($config, $tpchanger) = @_;
51     my ($kidspecs) = ( $tpchanger =~ /chg-rait:(.*)/ );
52
53     my @kidspecs = Amanda::Util::expand_braced_alternates($kidspecs);
54     if (@kidspecs < 2) {
55         return Amanda::Changer->make_error("fatal", undef,
56             message => "chg-rait needs at least two child changers");
57     }
58
59     my @children = map {
60         ($_ eq "ERROR")? "ERROR" : Amanda::Changer->new($_)
61     } @kidspecs;
62
63     if (grep { $_->isa("Amanda::Changer::Error") } @children) {
64         my @annotated_errs;
65         for my $i (0 .. @children-1) {
66             next unless $children[$i]->isa("Amanda::Changer::Error");
67             if ($children[$i]->isa("Amanda::Changer::Error")) {
68                 push @annotated_errs,
69                     [ $kidspecs[$i], $children[$i] ];
70             } elsif ($children[$i]->isa("Amanda::Changer")) {
71                 $children[$i]->quit();
72             }
73         }
74         return Amanda::Changer->make_combined_error(
75                 "fatal", [ @annotated_errs ]);
76     }
77
78     my $self = {
79         config => $config,
80         child_names => \@kidspecs,
81         children => \@children,
82         num_children => scalar @children,
83     };
84     bless ($self, $class);
85     return $self;
86 }
87
88 sub quit {
89     my $self = shift;
90
91     # quit each child
92     foreach my $child (@{$self->{'children'}}) {
93         $child->quit() if $child ne "ERROR";
94     }
95
96     $self->SUPER::quit();
97 }
98
99 # private method to help handle slot input
100 sub _kid_slots_ok {
101     my ($self, $res_cb, $slot, $kid_slots_ref, $err_ref) = @_;
102     @{$kid_slots_ref} = expand_braced_alternates($slot);
103     return 1 if (@{$kid_slots_ref} == $self->{'num_children'});
104
105     if (@{$kid_slots_ref} == 1) {
106         @{$kid_slots_ref} = ( $slot ) x $self->{'num_children'};
107         return 1;
108     }
109     ${$err_ref} = $self->make_error("failed", $res_cb,
110                                     reason => "invalid",
111                                     message => "slot string '$slot' does not specify " .
112                                     "$self->{num_children} child slots");
113     return 0;
114 }
115
116 sub load {
117     my $self = shift;
118     my %params = @_;
119
120     return if $self->check_error($params{'res_cb'});
121
122     $self->validate_params('load', \%params);
123
124     my $release_on_error = sub {
125         my ($kid_results) = @_;
126
127         # an error has occurred, so we have to release all of the *non*-error
128         # reservations (and handle errors in those releases!), then construct
129         # and return a combined error message.
130
131         my $releases_outstanding = 1; # start at one, in case the releases are immediate
132         my @release_errors = ( undef ) x $self->{'num_children'};
133         my $releases_maybe_done = sub {
134             return if (--$releases_outstanding);
135
136             # gather up the errors and combine them for return to our caller
137             my @annotated_errs;
138             for my $i (0 .. $self->{'num_children'}-1) {
139                 my $child_name = $self->{'child_names'}[$i];
140                 if ($kid_results->[$i][0]) {
141                     push @annotated_errs,
142                         [ "from $child_name", $kid_results->[$i][0] ];
143                 }
144                 if ($release_errors[$i]) {
145                     push @annotated_errs,
146                         [ "while releasing $child_name reservation",
147                           $kid_results->[$i][0] ];
148                 }
149             }
150
151             return $self->make_combined_error(
152                 $params{'res_cb'}, [ @annotated_errs ]);
153         };
154
155         for my $i (0 .. $self->{'num_children'}-1) {
156             next unless (my $res = $kid_results->[$i][1]);
157             $releases_outstanding++;
158             $res->release(finished_cb => sub {
159                 $release_errors[$i] = $_[0];
160                 $releases_maybe_done->();
161             });
162         }
163
164         # we started $releases_outstanding at 1, so decrement it now
165         $releases_maybe_done->();
166     };
167
168     my $all_kids_done_cb = sub {
169         my ($kid_results) = @_;
170         my $result;
171
172         # first, let's see if any changer gave an error
173         if (!grep { defined($_->[0]) } @$kid_results) {
174             # no error .. combine the reservations and return a RAIT reservation
175             return $self->_make_res($params{'res_cb'}, [ map { $_->[1] } @$kid_results ]);
176         } else {
177             return $release_on_error->($kid_results);
178         }
179     };
180
181     # make a template for params for the children
182     my %kid_template = %params;
183     delete $kid_template{'res_cb'};
184     delete $kid_template{'slot'};
185     delete $kid_template{'except_slots'};
186     # $kid_template{'label'} is passed directly to children
187     # $kid_template{'relative_slot'} is passed directly to children
188     # $kid_template{'mode'} is passed directly to children
189
190     # and make a copy for each child
191     my @kid_params;
192     for (0 .. $self->{'num_children'}-1) {
193         push @kid_params, { %kid_template };
194     }
195
196     if (exists $params{'slot'}) {
197         my $slot = $params{'slot'};
198
199         # calculate the slots for each child
200         my (@kid_slots, $err);
201         return $err unless $self->_kid_slots_ok($params{'res_cb'}, $slot, \@kid_slots, \$err);
202         if (@kid_slots != $self->{'num_children'}) {
203             # as a convenience, expand a single slot into the same slot for each child
204             if (@kid_slots == 1) {
205                 @kid_slots = ( $slot ) x $self->{'num_children'};
206             } else {
207                 return $self->make_error("failed", $params{'res_cb'},
208                         reason => "invalid",
209                         message => "slot '$slot' does not specify " .
210                                     "$self->{num_children} child slots");
211             }
212         }
213         for (0 .. $self->{'num_children'}-1) {
214             $kid_params[$_]->{'slot'} = $kid_slots[$_];
215         }
216     }
217
218     # each slot in except_slots needs to get broken down, and the appropriate slot
219     # given to each child
220     if (exists $params{'except_slots'}) {
221         for (0 .. $self->{'num_children'}-1) {
222             $kid_params[$_]->{'except_slots'} = {};
223         }
224
225         # for each slot, split it up, then apportion the result to each child
226         for my $slot ( keys %{$params{'except_slots'}} ) {
227             my (@kid_slots, $err);
228             return $err unless $self->_kid_slots_ok($params{'res_cb'}, $slot, \@kid_slots, \$err);
229             for (0 .. $self->{'num_children'}-1) {
230                 $kid_params[$_]->{'except_slots'}->{$kid_slots[$_]} = 1;
231             }
232         }
233     }
234
235     $self->_for_each_child(
236         oksub => sub {
237             my ($kid_chg, $kid_cb, $kid_params) = @_;
238             $kid_params->{'res_cb'} = $kid_cb;
239             $kid_chg->load(%$kid_params);
240         },
241         errsub => sub {
242             my ($kid_chg, $kid_cb, $kid_slot) = @_;
243             $kid_cb->(undef, "ERROR");
244         },
245         parent_cb => $all_kids_done_cb,
246         args => \@kid_params,
247     );
248 }
249
250 sub _make_res {
251     my $self = shift;
252     my ($res_cb, $kid_reservations) = @_;
253     my @kid_devices = map { ($_ ne "ERROR") ? $_->{'device'} : undef } @$kid_reservations;
254
255     my $rait_device = Amanda::Device->new_rait_from_children(@kid_devices);
256     if ($rait_device->status() != $DEVICE_STATUS_SUCCESS) {
257         return $self->make_error("failed", $res_cb,
258                 reason => "device",
259                 message => $rait_device->error_or_status());
260     }
261
262     if (my $err = $self->{'config'}->configure_device($rait_device)) {
263         return $self->make_error("failed", $res_cb,
264                 reason => "device",
265                 message => $err);
266     }
267
268     my $combined_res = Amanda::Changer::rait::Reservation->new(
269         $kid_reservations, $rait_device);
270     $rait_device->read_label();
271
272     $res_cb->(undef, $combined_res);
273 }
274
275 sub info_key {
276     my $self = shift;
277     my ($key, %params) = @_;
278
279     return if $self->check_error($params{'info_cb'});
280
281     my $check_and_report_errors = sub {
282         my ($kid_results) = @_;
283
284         if (grep { defined($_->[0]) } @$kid_results) {
285             # we have errors, so collect them and make a "combined" error.
286             my @annotated_errs;
287             my @err_slots;
288             for my $i (0 .. $self->{'num_children'}-1) {
289                 my $kr = $kid_results->[$i];
290                 next unless defined($kr->[0]);
291                 push @annotated_errs,
292                     [ $self->{'child_names'}[$i], $kr->[0] ];
293                 push @err_slots, $kr->[0]->{'slot'}
294                     if (defined $kr->[0] and defined $kr->[0]->{'slot'});
295             }
296
297             my @slotarg;
298             if (@err_slots == $self->{'num_children'}) {
299                 @slotarg = (slot => collapse_braced_alternates([@err_slots]));
300             }
301
302             $self->make_combined_error(
303                 $params{'info_cb'}, [ @annotated_errs ],
304                 @slotarg);
305             return 1;
306         }
307     };
308
309     if ($key eq 'num_slots') {
310         my $all_kids_done_cb = sub {
311             my ($kid_results) = @_;
312             return if ($check_and_report_errors->($kid_results));
313
314             # aggregate the results: the consensus if the children agree,
315             # otherwise -1
316             my $num_slots;
317             for (@$kid_results) {
318                 my ($err, %kid_info) = @$_;
319                 next unless exists($kid_info{'num_slots'});
320                 my $kid_num_slots = $kid_info{'num_slots'};
321                 if (defined $num_slots and $num_slots != $kid_num_slots) {
322                     debug("chg-rait: children have different slot counts!");
323                     $num_slots = -1;
324                 } else {
325                     $num_slots = $kid_num_slots;
326                 }
327             }
328             $params{'info_cb'}->(undef, num_slots => $num_slots) if $params{'info_cb'};
329         };
330
331         $self->_for_each_child(
332             oksub => sub {
333                 my ($kid_chg, $kid_cb) = @_;
334                 $kid_chg->info(info => [ 'num_slots' ], info_cb => $kid_cb);
335             },
336             errsub => undef,
337             parent_cb => $all_kids_done_cb,
338         );
339     } elsif ($key eq "vendor_string") {
340         my $all_kids_done_cb = sub {
341             my ($kid_results) = @_;
342             return if ($check_and_report_errors->($kid_results));
343
344             my @kid_vendors =
345                 grep { defined($_) }
346                 map { my ($e, %r) = @$_; $r{'vendor_string'} }
347                 @$kid_results;
348             my $vendor_string;
349             if (@kid_vendors) {
350                 $vendor_string = collapse_braced_alternates([@kid_vendors]);
351                 $params{'info_cb'}->(undef, vendor_string => $vendor_string) if $params{'info_cb'};
352             } else {
353                 $params{'info_cb'}->(undef) if $params{'info_cb'};
354             }
355         };
356
357         $self->_for_each_child(
358             oksub => sub {
359                 my ($kid_chg, $kid_cb) = @_;
360                 $kid_chg->info(info => [ 'vendor_string' ], info_cb => $kid_cb);
361             },
362             errsub => undef,
363             parent_cb => $all_kids_done_cb,
364         );
365     } elsif ($key eq 'fast_search') {
366         my $all_kids_done_cb = sub {
367             my ($kid_results) = @_;
368             return if ($check_and_report_errors->($kid_results));
369
370             my @kid_fastness =
371                 grep { defined($_) }
372                 map { my ($e, %r) = @$_; $r{'fast_search'} }
373                 @$kid_results;
374             if (@kid_fastness) {
375                 my $fast_search = 1;
376                 # conduct a logical AND of all child fastnesses
377                 for my $f (@kid_fastness) {
378                     $fast_search = $fast_search && $f;
379                 }
380                 $params{'info_cb'}->(undef, fast_search => $fast_search) if $params{'info_cb'};
381             } else {
382                 $params{'info_cb'}->(undef, fast_search => 0) if $params{'info_cb'};
383             }
384         };
385
386         $self->_for_each_child(
387             oksub => sub {
388                 my ($kid_chg, $kid_cb) = @_;
389                 $kid_chg->info(info => [ 'fast_search' ], info_cb => $kid_cb);
390             },
391             errsub => undef,
392             parent_cb => $all_kids_done_cb,
393         );
394     }
395 }
396
397 # reset, clean, etc. are all *very* similar to one another, so we create them
398 # generically
399 sub _mk_simple_op {
400     my ($op, $has_drive) = @_;
401     sub {
402         my $self = shift;
403         my %params = @_;
404
405         return if $self->check_error($params{'finished_cb'});
406
407         my $all_kids_done_cb = sub {
408             my ($kid_results) = @_;
409             if (grep { defined($_->[0]) } @$kid_results) {
410                 # we have errors, so collect them and make a "combined" error.
411                 my @annotated_errs;
412                 for my $i (0 .. $self->{'num_children'}-1) {
413                     my $kr = $kid_results->[$i];
414                     next unless defined($kr->[0]);
415                     push @annotated_errs,
416                         [ $self->{'child_names'}[$i], $kr->[0] ];
417                 }
418                 $self->make_combined_error(
419                     $params{'finished_cb'}, [ @annotated_errs ]);
420                 return 1;
421             }
422             $params{'finished_cb'}->() if $params{'finished_cb'};
423         };
424
425         # get the drives for the kids, if necessary
426         my @kid_args;
427         if ($has_drive and $params{'drive'}) {
428             my $drive = $params{'drive'};
429             my @kid_drives = expand_braced_alternates($drive);
430
431             if (@kid_drives == 1) {
432                 @kid_drives = ( $kid_drives[0] ) x $self->{'num_children'};
433             }
434
435             if (@kid_drives != $self->{'num_children'}) {
436                 return $self->make_error("failed", $params{'finished_cb'},
437                         reason => "invalid",
438                         message => "drive string '$drive' does not specify " .
439                         "$self->{num_children} child drives");
440             }
441
442             @kid_args = map { { drive => $_ } } @kid_drives;
443             delete $params{'drive'};
444         } else {
445             @kid_args = ( {} ) x $self->{'num_children'};
446         }
447
448         $self->_for_each_child(
449             oksub => sub {
450                 my ($kid_chg, $kid_cb, $args) = @_;
451                 $kid_chg->$op(%params, finished_cb => $kid_cb, %$args);
452             },
453             errsub => undef,
454             parent_cb => $all_kids_done_cb,
455             args => \@kid_args,
456         );
457     };
458 }
459
460 {
461     # perl doesn't like that these symbols are only mentioned once
462     no warnings;
463
464     *reset = _mk_simple_op("reset", 0);
465     *update = _mk_simple_op("update", 0);
466     *clean = _mk_simple_op("clean", 1);
467     *eject = _mk_simple_op("eject", 1);
468 }
469
470 sub inventory {
471     my $self = shift;
472     my %params = @_;
473
474     return if $self->check_error($params{'inventory_cb'});
475
476     my $all_kids_done_cb = sub {
477         my ($kid_results) = @_;
478         if (grep { defined($_->[0]) } @$kid_results) {
479             # we have errors, so collect them and make a "combined" error.
480             my @annotated_errs;
481             for my $i (0 .. $self->{'num_children'}-1) {
482                 my $kr = $kid_results->[$i];
483                 next unless defined($kr->[0]);
484                 push @annotated_errs,
485                     [ $self->{'child_names'}[$i], $kr->[0] ];
486             }
487             return $self->make_combined_error(
488                 $params{'inventory_cb'}, [ @annotated_errs ]);
489         }
490
491         my $inv = $self->_merge_inventories($kid_results);
492         if (!defined $inv) {
493             return $self->make_error("failed", $params{'inventory_cb'},
494                     reason => "notimpl",
495                     message => "could not generate consistent inventory from rait child changers");
496         }
497
498         $params{'inventory_cb'}->(undef, $inv);
499     };
500
501     $self->_for_each_child(
502         oksub => sub {
503             my ($kid_chg, $kid_cb) = @_;
504             $kid_chg->inventory(inventory_cb => $kid_cb);
505         },
506         errsub => undef,
507         parent_cb => $all_kids_done_cb,
508     );
509 }
510
511 # Takes keyword parameters 'oksub', 'errsub', 'parent_cb', and 'args'.  For
512 # each child, runs $oksub (or, if the child is "ERROR", $errsub), passing it
513 # the changer, an aggregating callback, and the corresponding element from
514 # @$args (if specified).  The callback combines its results with the results
515 # from other changers, and when all results are available, calls $parent_cb.
516 #
517 # This forms a kind of "AND" combinator for a parallel operation on multiple
518 # changers, providing the caller with a simple collection of the results of
519 # the operation. The parent_cb is called as
520 #   $parent_cb->([ [ <chg_1_results> ], [ <chg_2_results> ], .. ]).
521 sub _for_each_child {
522     my $self = shift;
523     my %params = @_;
524     my ($oksub, $errsub, $parent_cb, $args) =
525         ($params{'oksub'}, $params{'errsub'}, $params{'parent_cb'}, $params{'args'});
526
527     if (defined($args)) {
528         die "number of args did not match number of children"
529             unless (@$args == $self->{'num_children'});
530     } else {
531         $args = [ ( undef ) x $self->{'num_children'} ];
532     }
533
534     my $remaining = $self->{'num_children'};
535     my @results = ( undef ) x $self->{'num_children'};
536     my $maybe_done = sub {
537         return if (--$remaining);
538         $parent_cb->([ @results ]);
539     };
540
541     for my $i (0 .. $self->{'num_children'}-1) {
542         my $child = $self->{'children'}[$i];
543         my $arg = @$args? $args->[$i] : undef;
544
545         my $child_cb = sub {
546             $results[$i] = [ @_ ];
547             $maybe_done->();
548         };
549
550         if ($child eq "ERROR") {
551             if (defined $errsub) {
552                 $errsub->("ERROR", $child_cb, $arg);
553             } else {
554                 # no errsub; just call $child_cb directly
555                 $child_cb->(undef) if $child_cb;
556             }
557         } else {
558             $oksub->($child, $child_cb, $arg) if $oksub;
559         }
560     }
561 }
562
563 sub _merge_inventories {
564     my $self = shift;
565     my ($kid_results) = @_;
566
567     my @combined;
568     for my $kid_result (@$kid_results) {
569         my $kid_inv = $kid_result->[1];
570
571         if (!@combined) {
572             for my $x (@$kid_inv) {
573                 push @combined, {
574                     state => Amanda::Changer::SLOT_FULL,
575                     device_status => undef, f_type => undef,
576                     label => undef, barcode => [],
577                     reserved => 0, slot => [],
578                     import_export => 1, loaded_in => [],
579                 };
580             }
581         }
582
583         # if the results have different lengths, then we'll just call it
584         # not implemented; otherwise, we assume that the order of the slots
585         # in each child changer is the same.
586         if (scalar @combined != scalar @$kid_inv) {
587             warning("child changers returned different-length inventories; cannot merge");
588             return undef;
589         }
590
591         my $i;
592         for ($i = 0; $i < @combined; $i++) {
593             my $c = $combined[$i];
594             my $k = $kid_inv->[$i];
595             # mismatches here are just warnings
596             if (defined $c->{'label'}) {
597                 if (defined $k->{'label'} and $c->{'label'} ne $k->{'label'}) {
598                     warning("child changers have different labels in slot at index $i");
599                     $c->{'label_mismatch'} = 1;
600                     $c->{'label'} = undef;
601                 } elsif (!defined $k->{'label'}) {
602                     $c->{'label_mismatch'} = 1;
603                     $c->{'label'} = undef;
604                 }
605             } else {
606                 if (!$c->{'label_mismatch'} && !$c->{'label_set'}) {
607                     $c->{'label'} = $k->{'label'};
608                 }
609             }
610             $c->{'label_set'} = 1;
611
612             $c->{'device_status'} |= $k->{'device_status'}
613                 if defined $k->{'device_status'};
614
615             if (!defined $c->{'f_type'} ||
616                 $k->{'f_type'} != $Amanda::Header::F_TAPESTART) {
617                 $c->{'f_type'} = $k->{'f_type'};
618             }
619             # a slot is empty if any of the child slots are empty
620             $c->{'state'} = Amanda::Changer::SLOT_EMPTY
621                         if $k->{'state'} == Amanda::Changer::SLOT_EMPTY;
622
623             # a slot is reserved if any of the child slots are reserved
624             $c->{'reserved'} = $c->{'reserved'} || $k->{'reserved'};
625
626             # a slot is import-export if all of the child slots are import_export
627             $c->{'import_export'} = $c->{'import_export'} && $k->{'import_export'};
628
629             # barcodes, slots, and loaded_in are lists
630             push @{$c->{'slot'}}, $k->{'slot'};
631             push @{$c->{'barcode'}}, $k->{'barcode'};
632             push @{$c->{'loaded_in'}}, $k->{'loaded_in'};
633         }
634     }
635
636     # now post-process the slots, barcodes, and loaded_in into braced-alternates notation
637     my $i;
638     for ($i = 0; $i < @combined; $i++) {
639         my $c = $combined[$i];
640
641         delete $c->{'label_mismatch'} if $c->{'label_mismatch'};
642         delete $c->{'label_set'} if $c->{'label_set'};
643
644         $c->{'slot'} = collapse_braced_alternates([ @{$c->{'slot'}} ]);
645
646         if (grep { !defined $_ } @{$c->{'barcode'}}) {
647             delete $c->{'barcode'};
648         } else {
649             $c->{'barcode'} = collapse_braced_alternates([ @{$c->{'barcode'}} ]);
650         }
651
652         if (grep { !defined $_ } @{$c->{'loaded_in'}}) {
653             delete $c->{'loaded_in'};
654         } else {
655             $c->{'loaded_in'} = collapse_braced_alternates([ @{$c->{'loaded_in'}} ]);
656         }
657     }
658
659     return [ @combined ];
660 }
661
662 package Amanda::Changer::rait::Reservation;
663
664 use Amanda::Util qw( :alternates );
665 use vars qw( @ISA );
666 @ISA = qw( Amanda::Changer::Reservation );
667
668 # utility function to act like 'map', but pass "ERROR" straight through
669 # (this has to appear before it is used, because it has a prototype)
670 sub errmap (&@) {
671     my $sub = shift;
672     return map { ($_ ne "ERROR")? $sub->($_) : "ERROR" } @_;
673 }
674
675 sub new {
676     my $class = shift;
677     my ($child_reservations, $rait_device) = @_;
678     my $self = Amanda::Changer::Reservation::new($class);
679
680     # note that $child_reservations may contain "ERROR" in place of a reservation
681
682     $self->{'child_reservations'} = $child_reservations;
683
684     $self->{'device'} = $rait_device;
685
686     my @slot_names;
687     @slot_names = errmap { "" . $_->{'this_slot'} } @$child_reservations;
688     $self->{'this_slot'} = collapse_braced_alternates(\@slot_names);
689
690     return $self;
691 }
692
693 sub do_release {
694     my $self = shift;
695     my %params = @_;
696     my $remaining = @{$self->{'child_reservations'}};
697     my @outer_errors;
698
699     my $maybe_finished = sub {
700         my ($err) = @_;
701         push @outer_errors, $err if ($err);
702         return if (--$remaining);
703
704         my $errstr;
705         if (@outer_errors) {
706             $errstr = join("; ", @outer_errors);
707         }
708
709         # unref the device, for good measure
710         $self->{'device'} = undef;
711
712         $params{'finished_cb'}->($errstr) if $params{'finished_cb'};
713     };
714
715     for my $res (@{$self->{'child_reservations'}}) {
716         # short-circuit an "ERROR" reservation
717         if ($res eq "ERROR") {
718             $maybe_finished->(undef);
719             next;
720         }
721         $res->release(%params, finished_cb => $maybe_finished);
722     }
723 }
724