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