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