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