Imported Upstream version 3.3.2
[debian/amanda] / perl / Amanda / Changer / multi.pm
1 # Copyright (c) 2008-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::multi;
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;
30 use Amanda::Changer;
31 use Amanda::MainLoop;
32 use Amanda::Device qw( :constants );
33
34 =head1 NAME
35
36 Amanda::Changer::multi
37
38 =head1 DESCRIPTION
39
40 This changer operates with a list of device, specified in the tpchanger
41 string.
42
43 See the amanda-changers(7) manpage for usage information.
44
45 =cut
46
47 # STATE
48 #
49 # The device state is shared between all changers accessing the same changer.
50 # It is a hash with keys:
51 #   current_slot - the unaliased device name of the current slot
52 #   slots - see below
53 #
54 # The 'slots' key is a hash, with unaliased device name as keys and hashes
55 # as values.  Each slot's hash has keys:
56 #   pid           - the pid that reserved that slot.
57 #   state         - SLOT_FULL/SLOT_EMPTY/SLOT_UNKNOWN
58 #   device_status - the status of the device after the open or read_label
59 #   device_error  - error message from the device
60 #   f_type        - the F_TYPE of the fileheader.
61 #   label         - the label, if known, of the volume in this slot
62
63 # $self is a hash with keys:
64 #   slot           : slot number of the current slot
65 #   slots          : An array with all slot names
66 #   unaliased      : A hash with slot number as keys and unaliased device name
67 #                    as value
68 #   slot_name      : A hash with slot number as keys and device name as value
69 #   number         : A hash with unaliased device name as keys and slot number
70 #                    as value
71 #   config         : The Amanda::Changer::Config for this changer
72 #   state_filename : The filename of the state file
73 #   first_slot     : The number of the first slot
74 #   last_slot      : The number of the last slot + 1
75
76 sub new {
77     my $class = shift;
78     my ($config, $tpchanger) = @_;
79     my $devices = $tpchanger;
80     $devices =~ s/^chg-multi://g;
81     my (@slots) = Amanda::Util::expand_braced_alternates($devices);
82
83     unless (scalar @slots != 0) {
84         return Amanda::Changer->make_error("fatal", undef,
85             message => "no devices specified");
86     }
87
88     my $properties = $config->{'properties'};
89     my $first_slot = 1;
90     if (exists $properties->{'first-slot'}) {
91         $first_slot = @{$properties->{'first-slot'}->{'values'}}[0];
92     }
93
94     my %number = ();
95     my %unaliased = ();
96     my %slot_name = ();
97     my $last_slot = $first_slot;
98     foreach my $slot_name (@slots) {
99         my $unaliased_name = Amanda::Device::unaliased_name($slot_name);
100         $number{$unaliased_name} = $last_slot;
101         $unaliased{$last_slot} = $unaliased_name;
102         $slot_name{$last_slot} = $slot_name;
103         $last_slot++;
104     }
105
106     if (!defined $config->{changerfile} ||
107         $config->{changerfile} eq "") {
108         return Amanda::Changer->make_error("fatal", undef,
109             reason => "invalid",
110             message => "no changerfile specified for changer '$config->{name}'");
111     }
112
113     my $state_filename = Amanda::Config::config_dir_relative($config->{'changerfile'});
114     Amanda::Debug::debug("Using state file: $state_filename");
115
116     my $self = {
117         slots => \@slots,
118         unaliased => \%unaliased,
119         slot_name => \%slot_name,
120         number => \%number,
121         config => $config,
122         state_filename => $state_filename,
123         first_slot => $first_slot,
124         last_slot => $last_slot,
125     };
126
127     bless ($self, $class);
128     return $self;
129 }
130
131 sub load {
132     my $self = shift;
133     my %params = @_;
134     my $old_res_cb = $params{'res_cb'};
135     my $state;
136
137     $self->validate_params('load', \%params);
138
139     return if $self->check_error($params{'res_cb'});
140
141     $self->with_locked_state($self->{'state_filename'},
142                                      $params{'res_cb'}, sub {
143         my ($state, $res_cb) = @_;
144
145         $params{'state'} = $state;
146         # overwrite the callback for _load_by_xxx
147         $params{'res_cb'} = $res_cb;
148
149         if (exists $params{'slot'} or exists $params{'relative_slot'}) {
150             $self->_load_by_slot(%params);
151         } elsif (exists $params{'label'}) {
152             $self->_load_by_label(%params);
153         }
154     });
155 }
156
157 sub info_key {
158     my $self = shift;
159     my ($key, %params) = @_;
160     my %results;
161
162     return if $self->check_error($params{'info_cb'});
163
164     # no need for synchronization -- all of these values are static
165
166     if ($key eq 'num_slots') {
167         $results{$key} = $self->{last_slot} - $self->{first_slot};
168     } elsif ($key eq 'vendor_string') {
169         $results{$key} = 'chg-multi'; # mostly just for testing
170     } elsif ($key eq 'fast_search') {
171         $results{$key} = 0;
172     }
173
174     $params{'info_cb'}->(undef, %results) if $params{'info_cb'};
175 }
176
177 sub reset {
178     my $self = shift;
179     my %params = @_;
180
181     return if $self->check_error($params{'finished_cb'});
182
183     $self->with_locked_state($self->{'state_filename'},
184                                      $params{'finished_cb'}, sub {
185         my ($state, $finished_cb) = @_;
186         my $slot;
187
188         $params{state} = $state;
189         $slot = $self->{first_slot};
190         $self->{slot} = $slot;
191         $self->_set_current($state, $slot);
192
193         $finished_cb->();
194     });
195 }
196
197 sub eject {
198     my $self = shift;
199     my %params = @_;
200     my $slot;
201
202     return if $self->check_error($params{'finished_cb'});
203
204     $self->with_locked_state($self->{'state_filename'},
205                                      $params{'finished_cb'}, sub {
206         my ($state, $finished_cb) = @_;
207         my $drive;
208
209         $params{state} = $state;
210         if (!exists $params{'drive'}) {
211             $drive = $self->_get_current($params{state});
212         } else {
213             $drive = $params{'drive'};
214         }
215         if (!defined $self->{unaliased}->{$drive}) {
216             return $self->make_error("failed", $finished_cb,
217                 reason => "invalid",
218                 message => "Invalid slot '$drive'");
219         }
220
221         Amanda::Debug::debug("ejecting drive $drive");
222         my $device = Amanda::Device->new($self->{slot_name}->{$drive});
223         if ($device->status() != $DEVICE_STATUS_SUCCESS) {
224             return $self->make_error("failed", $finished_cb,
225                 reason => "device",
226                 message => $device->error_or_status);
227         }
228         if (my $err = $self->{'config'}->configure_device($device)) {
229             return $self->make_error("failed", $params{'res_cb'},
230                         reason => "device",
231                         message => $err);
232         }
233         $device->eject();
234         if ($device->status() != $DEVICE_STATUS_SUCCESS) {
235             return $self->make_error("failed", $finished_cb,
236                 reason => "invalid",
237                 message => $device->error_or_status);
238         }
239         undef $device;
240
241         $finished_cb->();
242     });
243 }
244
245 sub update {
246     my $self = shift;
247     my %params = @_;
248     my @slots_to_check;
249     my $state;
250     my $set_to_unknown = 0;
251
252     my $user_msg_fn = $params{'user_msg_fn'};
253     $user_msg_fn ||= sub { Amanda::Debug::info("chg-multi: " . $_[0]); };
254
255     my $steps = define_steps
256         cb_ref => \$params{'finished_cb'};
257
258     step lock => sub {
259         $self->with_locked_state($self->{'state_filename'},
260                                  $params{'finished_cb'}, sub {
261             my ($state, $finished_cb) = @_;
262
263             $params{state} = $state;
264             $params{'finished_cb'} = $finished_cb;
265
266             $steps->{'handle_assignment'}->();
267         });
268     };
269
270     step handle_assignment => sub {
271         $state = $params{state};
272         # check for the SL=LABEL format, and handle it here
273         if (exists $params{'changed'} and
274             $params{'changed'} =~ /^\d+=\S+$/) {
275             my ($slot, $label) = ($params{'changed'} =~ /^(\d+)=(\S+)$/);
276
277             # let's list the reasons we *can't* do what the user has asked
278             my $whynot;
279             if (!exists $self->{unaliased}->{$slot}) {
280                 $whynot = "slot $slot does not exist";
281             }
282
283             if ($whynot) {
284                 return $self->make_error("failed", $params{'finished_cb'},
285                         reason => "unknown", message => $whynot);
286             }
287
288             $user_msg_fn->("recording volume '$label' in slot $slot");
289             # ok, now erase all knowledge of that label
290             while (my ($sl, $inf) = each %{$state->{'slots'}}) {
291                 if ($inf->{'label'} and $inf->{'label'} eq $label) {
292                     $inf->{'label'} = undef;
293                 }
294             }
295
296             # and add knowledge of the label to the given slot
297             my $unaliased = $self->{unaliased}->{$slot};
298             $state->{'slots'}->{$unaliased}->{'label'} = $label;
299
300             # that's it -- no changer motion required
301             return $params{'finished_cb'}->(undef);
302         } elsif (exists $params{'changed'} and
303                $params{'changed'} =~ /^(.+)=$/) {
304             $params{'changed'} = $1;
305             $set_to_unknown = 1;
306             $steps->{'calculate_slots'}->();
307         } else {
308             $steps->{'calculate_slots'}->();
309         }
310     };
311
312     step calculate_slots => sub {
313         if (exists $params{'changed'}) {
314             # parse the string just like use-slots, using a hash for uniqueness
315             my %changed;
316             for my $range (split ',', $params{'changed'}) {
317                 my ($first, $last) = ($range =~ /(\d+)(?:-(\d+))?/);
318                 $last = $first unless defined($last);
319                 for ($first .. $last) {
320                     $changed{$_} = undef;
321                 }
322             }
323
324             @slots_to_check = keys %changed;
325             @slots_to_check = grep { exists $self->{'unaliased'}->{$_} } @slots_to_check;
326         } else {
327             @slots_to_check = keys %{ $self->{unaliased} };
328         }
329
330         # sort them so we don't confuse the user with a "random" order
331         @slots_to_check = sort @slots_to_check;
332
333         $steps->{'update_slot'}->();
334     };
335
336     # TODO: parallelize, we have one drive by slot
337
338     step update_slot => sub {
339         return $steps->{'done'}->() if (!@slots_to_check);
340         my $slot = shift @slots_to_check;
341         if ($self->_is_slot_in_use($state, $slot)) {
342             $user_msg_fn->("Slot $slot is already in use");
343             return $steps->{'update_slot'}->();
344         }
345
346         if ($set_to_unknown == 1) {
347             $user_msg_fn->("removing entry for slot $slot");
348             my $unaliased = $self->{unaliased}->{$slot};
349             delete $state->{slots}->{$unaliased};
350             return $steps->{'update_slot'}->();
351         } else {
352             $user_msg_fn->("scanning slot $slot");
353             $params{'slot'} = $slot;
354             $params{'res_cb'} = $steps->{'slot_loaded'};
355             $self->_load_by_slot(%params);
356         }
357     };
358
359     step slot_loaded => sub {
360         my ($err, $res) = @_;
361         if ($err) {
362             return $params{'finished_cb'}->($err);
363         }
364
365         my $slot = $res->{'this_slot'};
366         my $dev = $res->{device};
367         $self->_update_slot_state(state => $state, dev => $dev, slot =>$slot);
368         if ($dev->status() == $DEVICE_STATUS_SUCCESS) {
369             my $label = $dev->volume_label;
370             $user_msg_fn->("recording volume '$label' in slot $slot");
371         } else {
372             my $status = $dev->error_or_status;
373             $user_msg_fn->("recording device error '" . $status . "' in slot $slot");
374         }
375         $res->release(
376             finished_cb => $steps->{'released'},
377             unlocked => 1,
378             state => $state);
379     };
380
381     step released => sub {
382         my ($err) = @_;
383         if ($err) {
384             return $params{'finished_cb'}->($err);
385         }
386
387         $steps->{'update_slot'}->();
388     };
389
390     step done => sub {
391         $params{'finished_cb'}->(undef);
392     };
393 }
394
395 sub inventory {
396     my $self = shift;
397     my %params = @_;
398
399     return if $self->check_error($params{'inventory_cb'});
400
401     $self->with_locked_state($self->{'state_filename'},
402                              $params{'inventory_cb'}, sub {
403         my ($state, $inventory_cb) = @_;
404
405         my @inventory;
406         my $current = $self->_get_current($state);
407         foreach ($self->{first_slot} .. ($self->{last_slot} - 1)) {
408             my $slot = "$_";
409             my $unaliased = $self->{unaliased}->{$slot};
410             my $s = { slot => $slot,
411                       state => $state->{slots}->{$unaliased}->{state} || Amanda::Changer::SLOT_UNKNOWN,
412                       reserved => $self->_is_slot_in_use($state, $slot) };
413             if (defined $state->{slots}->{$unaliased} and
414                 exists $state->{slots}->{$unaliased}->{device_status}) {
415                 $s->{'device_status'} =
416                               $state->{slots}->{$unaliased}->{device_status};
417                 if ($s->{'device_status'} != $DEVICE_STATUS_SUCCESS) {
418                     $s->{'device_error'} =
419                               $state->{slots}->{$unaliased}->{device_error};
420                 } else {
421                     $s->{'device_error'} = undef;
422                 }
423                 $s->{'f_type'} = $state->{slots}->{$unaliased}->{f_type};
424                 $s->{'label'} = $state->{slots}->{$unaliased}->{label};
425             } else {
426                 $s->{'device_status'} = undef;
427                 $s->{'device_error'} = undef;
428                 $s->{'f_type'} = undef;
429                 $s->{'label'} = undef;
430             }
431             if ($slot eq $current) {
432                 $s->{'current'} = 1;
433             }
434             push @inventory, $s;
435         }
436         $inventory_cb->(undef, \@inventory);
437     })
438 }
439
440 sub _load_by_slot {
441     my $self = shift;
442     my %params = @_;
443     my $slot;
444
445     if (exists $params{'relative_slot'}) {
446         if ($params{'relative_slot'} eq "current") {
447             $slot = $self->_get_current($params{state});
448         } elsif ($params{'relative_slot'} eq "next") {
449             if (exists $params{'slot'}) {
450                 $slot = $params{'slot'};
451             } else {
452                 $slot = $self->_get_current($params{state});
453             }
454             $slot = $self->_get_next($slot);
455             $self->{slot} = $slot if ($params{'set_current'});
456             $self->_set_current($params{state}, $slot) if ($params{'set_current'});
457         } else {
458             return $self->make_error("failed", $params{'res_cb'},
459                 reason => "invalid",
460                 message => "Invalid relative slot '$params{relative_slot}'");
461         }
462     } else {
463         $slot = $params{'slot'};
464     }
465
466     if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
467         return $self->make_error("failed", $params{'res_cb'},
468             reason => "notfound",
469             message => "all slots have been loaded");
470     }
471
472     if (!$self->_slot_exists($slot)) {
473         return $self->make_error("failed", $params{'res_cb'},
474             reason => "notfound",
475             message => "Slot $slot not defined");
476     }
477
478     if ($self->_is_slot_in_use($params{state}, $slot)) {
479         my $unaliased = $self->{unaliased}->{$slot};
480         return $self->make_error("failed", $params{'res_cb'},
481             reason => "volinuse",
482             slot => $slot,
483             message => "Slot $slot is already in use by process '$params{state}->{slots}->{$unaliased}->{pid}'");
484     }
485
486     $self->{slot} = $slot if ($params{'set_current'});
487     $self->_set_current($params{state}, $slot) if ($params{'set_current'});
488
489     $self->_make_res($params{state}, $params{'res_cb'}, $slot);
490 }
491
492 sub _load_by_label {
493     my $self = shift;
494     my %params = @_;
495     my $label = $params{'label'};
496     my $slot;
497     my $slot_name;
498     my $state = $params{state};
499
500     foreach $slot (keys %{$state->{slots}}) {
501         if (defined $state->{slots}->{$slot} &&
502             $state->{slots}->{$slot}->{label} &&
503             $state->{slots}->{$slot}->{label} eq $label) {
504             $slot_name = $slot;
505             last;
506         }
507     }
508
509     if (defined $slot_name &&
510         $state->{slots}->{$slot_name}->{label} eq $label) {
511
512         $slot = $self->{number}->{$slot_name};
513         delete $params{'label'};
514         $params{'slot'} = $slot;
515         $self->_load_by_slot(%params);
516     } else {
517         return $self->make_error("failed", $params{'res_cb'},
518                                 reason => "notfound",
519                                 message => "Label '$label' not found");
520     }
521 }
522
523
524 sub _make_res {
525     my $self = shift;
526     my ($state, $res_cb, $slot) = @_;
527     my $res;
528
529     my $unaliased = $self->{unaliased}->{$slot};
530     my $slot_name = $self->{slot_name}->{$slot};
531     my $device = Amanda::Device->new($slot_name);
532     if ($device->status != $DEVICE_STATUS_SUCCESS) {
533         return $self->make_error("failed", $res_cb,
534                 reason => "device",
535                 message => "opening '$slot': " . $device->error_or_status());
536     }
537
538     if (my $err = $self->{'config'}->configure_device($device)) {
539         return $self->make_error("failed", $res_cb,
540                 reason => "device",
541                 message => $err);
542     }
543
544     $res = Amanda::Changer::multi::Reservation->new($self, $device, $slot);
545     $state->{slots}->{$unaliased}->{pid} = $$;
546     $device->read_label();
547
548     $self->_update_slot_state(state => $state, dev => $res->{device}, slot => $slot);
549     $res_cb->(undef, $res);
550 }
551
552
553 # Internal function to determine whether a slot exists.
554 sub _slot_exists {
555     my ($self, $slot) = @_;
556
557     return 1 if defined $self->{unaliased}->{$slot};
558     return 0;
559 }
560
561 sub _update_slot_state {
562     my $self = shift;
563     my %params = @_;
564     my $state = $params{state};
565     my $dev = $params{dev};
566     my $slot = $params{slot};
567     my $unaliased = $self->{unaliased}->{$slot};
568     $state->{slots}->{$unaliased}->{device_status} = "".scalar($dev->status);
569     if ($dev->status != $DEVICE_STATUS_SUCCESS) {
570         $state->{slots}->{$unaliased}->{device_error} = $dev->error;
571     } else {
572         $state->{slots}->{$unaliased}->{device_error} = undef;
573     }
574     my $label = $dev->volume_label;
575     $state->{slots}->{$unaliased}->{state} = Amanda::Changer::SLOT_FULL;
576     $state->{slots}->{$unaliased}->{label} = $label;
577     my $volume_header = $dev->volume_header;
578     if (defined $volume_header) {
579         $state->{slots}->{$unaliased}->{f_type} = "".scalar($volume_header->{type});
580     } else {
581         delete $state->{slots}->{$unaliased}->{f_type};
582     }
583 }
584 # Internal function to determine if a slot (specified by number) is in use by a
585 # drive, and return the path for that drive if so.
586 sub _is_slot_in_use {
587     my ($self, $state, $slot) = @_;
588
589     return 0 if !defined $state;
590     return 0 if !defined $state->{slots};
591     return 0 if !defined $self->{unaliased}->{$slot};
592     my $unaliased = $self->{unaliased}->{$slot};
593     return 0 if !defined $state->{slots}->{$unaliased};
594     return 0 if !defined $state->{slots}->{$unaliased}->{pid};
595
596     #check if PID is still alive
597     my $pid = $state->{slots}->{$unaliased}->{pid};
598     if (Amanda::Util::is_pid_alive($pid) == 1) {
599         return 1;
600     }
601
602     delete $state->{slots}->{$unaliased}->{pid};
603     return 0;
604 }
605
606 # Internal function to get the next slot after $slot.
607 # skip over except_slot and slot in use.
608 sub _get_next {
609     my ($self, $slot, $except_slot) = @_;
610     my $next_slot;
611
612     $next_slot = $slot + 1;
613     $next_slot = $self->{'first_slot'} if $next_slot >= $self->{'last_slot'};
614
615     return $next_slot;
616 }
617
618 # Get the 'current' slot
619 sub _get_current {
620     my ($self, $state) = @_;
621
622     return $self->{slot} if defined $self->{slot};
623     if (defined $state->{current_slot}) {
624         my $slot = $self->{number}->{$state->{current_slot}};
625         # return the slot if it exist.
626         return $slot if $slot >= $self->{'first_slot'} && $slot < $self->{'last_slot'};
627         Amanda::Debug::debug("statefile current_slot is not configured");
628     }
629     # return the first slot
630     return $self->{first_slot};
631 }
632
633 # Set the 'current' slot
634 sub _set_current {
635     my ($self, $state, $slot) = @_;
636
637     $self->{slot} = $slot;
638     $state->{current_slot} = $self->{unaliased}->{$slot};
639 }
640
641 package Amanda::Changer::multi::Reservation;
642 use vars qw( @ISA );
643 @ISA = qw( Amanda::Changer::Reservation );
644 use Amanda::Device qw( :constants );
645
646 sub new {
647     my $class = shift;
648     my ($chg, $device, $slot) = @_;
649     my $self = Amanda::Changer::Reservation::new($class);
650
651     $self->{'chg'} = $chg;
652     $self->{'device'} = $device;
653     $self->{'this_slot'} = $slot;
654
655     return $self;
656 }
657
658 sub set_label {
659     my $self = shift;
660     my %params = @_;
661
662     my $chg = $self->{chg};
663     $chg->with_locked_state($chg->{'state_filename'},
664                             $params{'finished_cb'}, sub {
665         my ($state, $finished_cb) = @_;
666         my $label = $params{'label'};
667         my $slot = $self->{'this_slot'};
668         my $unaliased = $chg->{unaliased}->{$slot};
669         my $dev = $self->{'device'};
670
671         $state->{slots}->{$unaliased}->{label} =  $label;
672         $state->{slots}->{$unaliased}->{device_status} =
673                                 "".$dev->status;
674         if ($dev->status != $DEVICE_STATUS_SUCCESS) {
675             $state->{slots}->{$unaliased}->{device_error} = $dev->error;
676         } else {
677             $state->{slots}->{$unaliased}->{device_error} = undef;
678         }
679         my $volume_header = $dev->volume_header;
680         if (defined $volume_header) {
681             $state->{slots}->{$unaliased}->{f_type} =
682                                 "".$volume_header->{type};
683         } else {
684             $state->{slots}->{$unaliased}->{f_type} = undef;
685         }
686         $finished_cb->();
687     });
688 }
689
690 sub do_release {
691     my $self = shift;
692     my %params = @_;
693
694     # if we're in global cleanup and the changer is already dead,
695     # then never mind
696     return unless $self->{'chg'};
697
698     $self->{'device'}->eject() if (exists $self->{'device'} and
699                                    exists $params{'eject'} and
700                                    $params{'eject'});
701
702     # unref the device, for good measure
703     $self->{'device'} = undef;
704
705     if (exists $params{'unlocked'}) {
706         my $state = $params{state};
707         my $slot = $self->{'this_slot'};
708         my $unaliased = $self->{chg}->{unaliased}->{$slot};
709         delete $state->{slots}->{$unaliased}->{pid};
710         return $params{'finished_cb'}->();
711     }
712
713     $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
714                                     $params{'finished_cb'}, sub {
715         my ($state, $finished_cb) = @_;
716         $params{state} = $state;
717         my $slot = $self->{'this_slot'};
718         my $unaliased = $self->{chg}->{unaliased}->{$slot};
719         delete $state->{slots}->{$unaliased}->{pid};
720         $finished_cb->();
721     });
722 }