6561f058c0c9485f211c237ff2923a34d9ab6fe2
[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                     $num_slots = -1;
308                 } else {
309                     $num_slots = $kid_num_slots;
310                 }
311             }
312             $params{'info_cb'}->(undef, num_slots => $num_slots) if $params{'info_cb'};
313         };
314
315         $self->_for_each_child(
316             oksub => sub {
317                 my ($kid_chg, $kid_cb) = @_;
318                 $kid_chg->info(info => [ 'num_slots' ], info_cb => $kid_cb);
319             },
320             errsub => undef,
321             parent_cb => $all_kids_done_cb,
322         );
323     } elsif ($key eq "vendor_string") {
324         my $all_kids_done_cb = sub {
325             my ($kid_results) = @_;
326             return if ($check_and_report_errors->($kid_results));
327
328             my @kid_vendors =
329                 grep { defined($_) }
330                 map { my ($e, %r) = @$_; $r{'vendor_string'} }
331                 @$kid_results;
332             my $vendor_string;
333             if (@kid_vendors) {
334                 $vendor_string = collapse_braced_alternates([@kid_vendors]);
335                 $params{'info_cb'}->(undef, vendor_string => $vendor_string) if $params{'info_cb'};
336             } else {
337                 $params{'info_cb'}->(undef) if $params{'info_cb'};
338             }
339         };
340
341         $self->_for_each_child(
342             oksub => sub {
343                 my ($kid_chg, $kid_cb) = @_;
344                 $kid_chg->info(info => [ 'vendor_string' ], info_cb => $kid_cb);
345             },
346             errsub => undef,
347             parent_cb => $all_kids_done_cb,
348         );
349     } elsif ($key eq 'fast_search') {
350         my $all_kids_done_cb = sub {
351             my ($kid_results) = @_;
352             return if ($check_and_report_errors->($kid_results));
353
354             my @kid_fastness =
355                 grep { defined($_) }
356                 map { my ($e, %r) = @$_; $r{'fast_search'} }
357                 @$kid_results;
358             if (@kid_fastness) {
359                 my $fast_search = 1;
360                 # conduct a logical AND of all child fastnesses
361                 for my $f (@kid_fastness) {
362                     $fast_search = $fast_search && $f;
363                 }
364                 $params{'info_cb'}->(undef, fast_search => $fast_search) if $params{'info_cb'};
365             } else {
366                 $params{'info_cb'}->(undef, fast_search => 0) if $params{'info_cb'};
367             }
368         };
369
370         $self->_for_each_child(
371             oksub => sub {
372                 my ($kid_chg, $kid_cb) = @_;
373                 $kid_chg->info(info => [ 'fast_search' ], info_cb => $kid_cb);
374             },
375             errsub => undef,
376             parent_cb => $all_kids_done_cb,
377         );
378     }
379 }
380
381 # reset, clean, etc. are all *very* similar to one another, so we create them
382 # generically
383 sub _mk_simple_op {
384     my ($op, $has_drive) = @_;
385     sub {
386         my $self = shift;
387         my %params = @_;
388
389         return if $self->check_error($params{'finished_cb'});
390
391         my $all_kids_done_cb = sub {
392             my ($kid_results) = @_;
393             if (grep { defined($_->[0]) } @$kid_results) {
394                 # we have errors, so collect them and make a "combined" error.
395                 my @annotated_errs;
396                 for my $i (0 .. $self->{'num_children'}-1) {
397                     my $kr = $kid_results->[$i];
398                     next unless defined($kr->[0]);
399                     push @annotated_errs,
400                         [ $self->{'child_names'}[$i], $kr->[0] ];
401                 }
402                 $self->make_combined_error(
403                     $params{'finished_cb'}, [ @annotated_errs ]);
404                 return 1;
405             }
406             $params{'finished_cb'}->() if $params{'finished_cb'};
407         };
408
409         # get the drives for the kids, if necessary
410         my @kid_args;
411         if ($has_drive and $params{'drive'}) {
412             my $drive = $params{'drive'};
413             my @kid_drives = expand_braced_alternates($drive);
414
415             if (@kid_drives == 1) {
416                 @kid_drives = ( $kid_drives[0] ) x $self->{'num_children'};
417             }
418
419             if (@kid_drives != $self->{'num_children'}) {
420                 return $self->make_error("failed", $params{'finished_cb'},
421                         reason => "invalid",
422                         message => "drive string '$drive' does not specify " .
423                         "$self->{num_children} child drives");
424             }
425
426             @kid_args = map { { drive => $_ } } @kid_drives;
427             delete $params{'drive'};
428         } else {
429             @kid_args = ( {} ) x $self->{'num_children'};
430         }
431
432         $self->_for_each_child(
433             oksub => sub {
434                 my ($kid_chg, $kid_cb, $args) = @_;
435                 $kid_chg->$op(%params, finished_cb => $kid_cb, %$args);
436             },
437             errsub => undef,
438             parent_cb => $all_kids_done_cb,
439             args => \@kid_args,
440         );
441     };
442 }
443
444 {
445     # perl doesn't like that these symbols are only mentioned once
446     no warnings;
447
448     *reset = _mk_simple_op("reset", 0);
449     *update = _mk_simple_op("update", 0);
450     *clean = _mk_simple_op("clean", 1);
451     *eject = _mk_simple_op("eject", 1);
452 }
453
454 sub inventory {
455     my $self = shift;
456     my %params = @_;
457
458     return if $self->check_error($params{'inventory_cb'});
459
460     my $all_kids_done_cb = sub {
461         my ($kid_results) = @_;
462         if (grep { defined($_->[0]) } @$kid_results) {
463             # we have errors, so collect them and make a "combined" error.
464             my @annotated_errs;
465             for my $i (0 .. $self->{'num_children'}-1) {
466                 my $kr = $kid_results->[$i];
467                 next unless defined($kr->[0]);
468                 push @annotated_errs,
469                     [ $self->{'child_names'}[$i], $kr->[0] ];
470             }
471             return $self->make_combined_error(
472                 $params{'inventory_cb'}, [ @annotated_errs ]);
473         }
474
475         my $inv = $self->_merge_inventories($kid_results);
476         if (!defined $inv) {
477             return $self->make_error("failed", $params{'inventory_cb'},
478                     reason => "notimpl",
479                     message => "could not generate consistent inventory from rait child changers");
480         }
481
482         $params{'inventory_cb'}->(undef, $inv);
483     };
484
485     $self->_for_each_child(
486         oksub => sub {
487             my ($kid_chg, $kid_cb) = @_;
488             $kid_chg->inventory(inventory_cb => $kid_cb);
489         },
490         errsub => undef,
491         parent_cb => $all_kids_done_cb,
492     );
493 }
494
495 # Takes keyword parameters 'oksub', 'errsub', 'parent_cb', and 'args'.  For
496 # each child, runs $oksub (or, if the child is "ERROR", $errsub), passing it
497 # the changer, an aggregating callback, and the corresponding element from
498 # @$args (if specified).  The callback combines its results with the results
499 # from other changers, and when all results are available, calls $parent_cb.
500 #
501 # This forms a kind of "AND" combinator for a parallel operation on multiple
502 # changers, providing the caller with a simple collection of the results of
503 # the operation. The parent_cb is called as
504 #   $parent_cb->([ [ <chg_1_results> ], [ <chg_2_results> ], .. ]).
505 sub _for_each_child {
506     my $self = shift;
507     my %params = @_;
508     my ($oksub, $errsub, $parent_cb, $args) =
509         ($params{'oksub'}, $params{'errsub'}, $params{'parent_cb'}, $params{'args'});
510
511     if (defined($args)) {
512         die "number of args did not match number of children"
513             unless (@$args == $self->{'num_children'});
514     } else {
515         $args = [ ( undef ) x $self->{'num_children'} ];
516     }
517
518     my $remaining = $self->{'num_children'};
519     my @results = ( undef ) x $self->{'num_children'};
520     my $maybe_done = sub {
521         return if (--$remaining);
522         $parent_cb->([ @results ]);
523     };
524
525     for my $i (0 .. $self->{'num_children'}-1) {
526         my $child = $self->{'children'}[$i];
527         my $arg = @$args? $args->[$i] : undef;
528
529         my $child_cb = sub {
530             $results[$i] = [ @_ ];
531             $maybe_done->();
532         };
533
534         if ($child eq "ERROR") {
535             if (defined $errsub) {
536                 $errsub->("ERROR", $child_cb, $arg);
537             } else {
538                 # no errsub; just call $child_cb directly
539                 $child_cb->(undef) if $child_cb;
540             }
541         } else {
542             $oksub->($child, $child_cb, $arg) if $oksub;
543         }
544     }
545 }
546
547 sub _merge_inventories {
548     my $self = shift;
549     my ($kid_results) = @_;
550
551     my @combined;
552     for my $kid_result (@$kid_results) {
553         my $kid_inv = $kid_result->[1];
554
555         if (!@combined) {
556             for my $x (@$kid_inv) {
557                 push @combined, {
558                     state => Amanda::Changer::SLOT_FULL,
559                     device_status => undef, f_type => undef,
560                     label => undef, barcode => [],
561                     reserved => 0, slot => [],
562                     import_export => 1, loaded_in => [],
563                 };
564             }
565         }
566
567         # if the results have different lengths, then we'll just call it
568         # not implemented; otherwise, we assume that the order of the slots
569         # in each child changer is the same.
570         if (scalar @combined != scalar @$kid_inv) {
571             warning("child changers returned different-length inventories; cannot merge");
572             return undef;
573         }
574
575         my $i;
576         for ($i = 0; $i < @combined; $i++) {
577             my $c = $combined[$i];
578             my $k = $kid_inv->[$i];
579             # mismatches here are just warnings
580             if (defined $c->{'label'}) {
581                 if (defined $k->{'label'} and $c->{'label'} ne $k->{'label'}) {
582                     warning("child changers have different labels in slot at index $i");
583                     $c->{'label_mismatch'} = 1;
584                     $c->{'label'} = undef;
585                 } elsif (!defined $k->{'label'}) {
586                     $c->{'label_mismatch'} = 1;
587                     $c->{'label'} = undef;
588                 }
589             } else {
590                 if (!$c->{'label_mismatch'} && !$c->{'label_set'}) {
591                     $c->{'label'} = $k->{'label'};
592                 }
593             }
594             $c->{'label_set'} = 1;
595
596             $c->{'device_status'} |= $k->{'device_status'}
597                 if defined $k->{'device_status'};
598
599             if (!defined $c->{'f_type'} ||
600                 $k->{'f_type'} != $Amanda::Header::F_TAPESTART) {
601                 $c->{'f_type'} = $k->{'f_type'};
602             }
603             # a slot is empty if any of the child slots are empty
604             $c->{'state'} = Amanda::Changer::SLOT_EMPTY
605                         if $k->{'state'} == Amanda::Changer::SLOT_EMPTY;
606
607             # a slot is reserved if any of the child slots are reserved
608             $c->{'reserved'} = $c->{'reserved'} || $k->{'reserved'};
609
610             # a slot is import-export if all of the child slots are import_export
611             $c->{'import_export'} = $c->{'import_export'} && $k->{'import_export'};
612
613             # barcodes, slots, and loaded_in are lists
614             push @{$c->{'slot'}}, $k->{'slot'};
615             push @{$c->{'barcode'}}, $k->{'barcode'};
616             push @{$c->{'loaded_in'}}, $k->{'loaded_in'};
617         }
618     }
619
620     # now post-process the slots, barcodes, and loaded_in into braced-alternates notation
621     my $i;
622     for ($i = 0; $i < @combined; $i++) {
623         my $c = $combined[$i];
624
625         delete $c->{'label_mismatch'} if $c->{'label_mismatch'};
626         delete $c->{'label_set'} if $c->{'label_set'};
627
628         $c->{'slot'} = collapse_braced_alternates([ @{$c->{'slot'}} ]);
629
630         if (grep { !defined $_ } @{$c->{'barcode'}}) {
631             delete $c->{'barcode'};
632         } else {
633             $c->{'barcode'} = collapse_braced_alternates([ @{$c->{'barcode'}} ]);
634         }
635
636         if (grep { !defined $_ } @{$c->{'loaded_in'}}) {
637             delete $c->{'loaded_in'};
638         } else {
639             $c->{'loaded_in'} = collapse_braced_alternates([ @{$c->{'loaded_in'}} ]);
640         }
641     }
642
643     return [ @combined ];
644 }
645
646 package Amanda::Changer::rait::Reservation;
647
648 use Amanda::Util qw( :alternates );
649 use vars qw( @ISA );
650 @ISA = qw( Amanda::Changer::Reservation );
651
652 # utility function to act like 'map', but pass "ERROR" straight through
653 # (this has to appear before it is used, because it has a prototype)
654 sub errmap (&@) {
655     my $sub = shift;
656     return map { ($_ ne "ERROR")? $sub->($_) : "ERROR" } @_;
657 }
658
659 sub new {
660     my $class = shift;
661     my ($child_reservations, $rait_device) = @_;
662     my $self = Amanda::Changer::Reservation::new($class);
663
664     # note that $child_reservations may contain "ERROR" in place of a reservation
665
666     $self->{'child_reservations'} = $child_reservations;
667
668     $self->{'device'} = $rait_device;
669
670     my @slot_names;
671     @slot_names = errmap { "" . $_->{'this_slot'} } @$child_reservations;
672     $self->{'this_slot'} = collapse_braced_alternates(\@slot_names);
673
674     return $self;
675 }
676
677 sub do_release {
678     my $self = shift;
679     my %params = @_;
680     my $remaining = @{$self->{'child_reservations'}};
681     my @outer_errors;
682
683     my $maybe_finished = sub {
684         my ($err) = @_;
685         push @outer_errors, $err if ($err);
686         return if (--$remaining);
687
688         my $errstr;
689         if (@outer_errors) {
690             $errstr = join("; ", @outer_errors);
691         }
692
693         # unref the device, for good measure
694         $self->{'device'} = undef;
695
696         $params{'finished_cb'}->($errstr) if $params{'finished_cb'};
697     };
698
699     for my $res (@{$self->{'child_reservations'}}) {
700         # short-circuit an "ERROR" reservation
701         if ($res eq "ERROR") {
702             $maybe_finished->(undef);
703             next;
704         }
705         $res->release(%params, finished_cb => $maybe_finished);
706     }
707 }
708