2 # Copyright (c) 2010-2012 Zmanda, Inc. All Rights Reserved.
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
19 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
21 use lib '@amperldir@';
28 package main::Interactivity;
29 use base 'Amanda::Interactivity';
30 use Amanda::Util qw( weaken_ref );
33 use Amanda::Debug qw( debug );
34 use Amanda::Config qw( :getconf );
35 use Amanda::Recovery::Scan qw( $DEFAULT_CHANGER );
42 clientservice => $params{'clientservice'},
45 # (weak ref here to eliminate reference loop)
46 weaken_ref($self->{'clientservice'});
48 return bless ($self, $class);
54 debug("ignoring spurious Amanda::Recovery::Scan abort call");
62 my $steps = define_steps
63 cb_ref => \$params{'request_cb'};
65 step send_message => sub {
67 $self->{'clientservice'}->sendmessage("$params{err}");
70 $steps->{'check_fe_feedme'}->();
73 step check_fe_feedme => sub {
74 # note that fe_amrecover_FEEDME implies fe_amrecover_splits
75 if (!$self->{'clientservice'}->{'their_features'}->has(
76 $Amanda::Feature::fe_amrecover_FEEDME)) {
77 return $params{'request_cb'}->("remote cannot prompt for volumes", undef);
79 $steps->{'send_feedme'}->();
82 step send_feedme => sub {
83 $self->{'clientservice'}->sendctlline("FEEDME $params{label}\r\n", $steps->{'read_response'});
86 step read_response => sub {
87 my ($err, $written) = @_;
88 return $params{'request_cb'}->($err, undef) if $err;
90 $self->{'clientservice'}->getline_async(
91 $self->{'clientservice'}->{'ctl_stream'}, $steps->{'got_response'});
94 step got_response => sub {
95 my ($err, $line) = @_;
96 return $params{'request_cb'}->($err, undef) if $err;
98 if ($line eq "OK\r\n") {
99 return $params{'request_cb'}->(undef, undef); # carry on as you were
100 } elsif ($line =~ /^TAPE (.*)\r\n$/) {
102 if ($tape eq getconf($CNF_AMRECOVER_CHANGER)) {
103 $tape = $Amanda::Recovery::Scan::DEFAULT_CHANGER;
105 return $params{'request_cb'}->(undef, $tape); # use this device
107 return $params{'request_cb'}->("got invalid response from remote", undef);
113 # ClientService class
115 package main::ClientService;
116 use base 'Amanda::ClientService';
120 use Amanda::Debug qw( debug info warning );
121 use Amanda::MainLoop qw( :GIOCondition );
122 use Amanda::Util qw( :constants match_disk match_host );
124 use Amanda::Config qw( :init :getconf );
126 use Amanda::Recovery::Scan;
127 use Amanda::Xfer qw( :constants );
129 use Amanda::Recovery::Clerk;
130 use Amanda::Recovery::Planner;
131 use Amanda::Recovery::Scan;
132 use Amanda::DB::Catalog;
133 use Amanda::Disklist;
135 # Note that this class performs its control IO synchronously. This is adequate
136 # for this service, as it never receives unsolicited input from the remote
142 $self->{'my_features'} = Amanda::Feature::Set->mine();
143 $self->{'their_features'} = Amanda::Feature::Set->old();
144 $self->{'all_filter'} = {};
146 $self->setup_streams();
152 # get started checking security for inetd or processing the REQ/REP
154 if ($self->from_inetd()) {
155 if (!$self->check_inetd_security('main')) {
156 $main::exit_status = 1;
157 return $self->quit();
159 $self->{'ctl_stream'} = 'main';
160 $self->{'data_stream'} = undef; # no data stream yet
162 my $req = $self->get_req();
164 # make some sanity checks
166 if (defined $req->{'options'}{'auth'} and defined $self->amandad_auth()
167 and $req->{'options'}{'auth'} ne $self->amandad_auth()) {
168 my $reqauth = $req->{'options'}{'auth'};
169 my $amauth = $self->amandad_auth();
170 push @$errors, "recover program requested auth '$reqauth', " .
171 "but amandad is using auth '$amauth'";
172 $main::exit_status = 1;
175 # and pull out the features, if given
176 if (defined($req->{'features'})) {
177 $self->{'their_features'} = $req->{'features'};
180 $self->send_rep(['CTL' => 'rw', 'DATA' => 'w'], $errors);
181 return $self->quit() if (@$errors);
183 $self->{'ctl_stream'} = 'CTL';
184 $self->{'data_stream'} = 'DATA';
187 $self->read_command();
192 my $ctl_stream = $self->{'ctl_stream'};
193 my $command = $self->{'command'} = {};
195 my @known_commands = qw(
196 HOST DISK DATESTAMP LABEL DEVICE FSF HEADER
199 $_ = $self->getline($ctl_stream);
205 if (/^([A-Z]+)(=(.*))?$/) {
206 my ($cmd, $val) = ($1, $3);
207 if (!grep { $_ eq $cmd } @known_commands) {
208 $self->sendmessage("invalid command '$cmd'");
209 return $self->quit();
211 if (exists $command->{$cmd}) {
212 warning("got duplicate command key '$cmd' from remote");
214 $command->{$cmd} = $val || 1;
218 # features are handled specially. This is pretty weird!
221 my $featurestr = $self->{'my_features'}->as_string();
222 if ($self->from_amandad) {
223 $featreply = "FEATURES=$featurestr\r\n";
225 $featreply = $featurestr;
228 $self->senddata($ctl_stream, $featreply);
232 # process some info from the command
233 if ($command->{'FEATURES'}) {
234 $self->{'their_features'} = Amanda::Feature::Set->from_string($command->{'FEATURES'});
237 # load the configuration
238 if (!$command->{'CONFIG'}) {
239 die "no CONFIG line given";
241 config_init($CONFIG_INIT_EXPLICIT_NAME, $command->{'CONFIG'});
242 my ($cfgerr_level, @cfgerr_errors) = config_errors();
243 if ($cfgerr_level >= $CFGERR_ERRORS) {
244 die "configuration errors; aborting connection";
246 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER_PREFERRED);
249 my $diskfile = Amanda::Config::config_dir_relative(getconf($CNF_DISKFILE));
250 $cfgerr_level = Amanda::Disklist::read_disklist('filename' => $diskfile);
251 if ($cfgerr_level >= $CFGERR_ERRORS) {
252 die "Errors processing disklist";
255 $self->setup_data_stream();
258 sub setup_data_stream {
261 # if we're using amandad, then this is ready to roll - it's only inetd mode
262 # that we need to fix
263 if ($self->from_inetd()) {
264 if ($self->{'their_features'}->has($Amanda::Feature::fe_recover_splits)) {
265 # remote side is expecting CONNECT
266 my $port = $self->connection_listen('DATA', 0);
267 $self->senddata($self->{'ctl_stream'}, "CONNECT $port\n");
268 $self->connection_accept('DATA', 30, sub { $self->got_connection(@_); });
270 $self->{'ctl_stream'} = undef; # don't use this for ctl anymore
271 $self->{'data_stream'} = 'main';
284 $self->sendmessage("$err");
285 return $self->quit();
288 if (!$self->check_inetd_security('DATA')) {
289 $main::exit_status = 1;
290 return $self->quit();
292 $self->{'data_stream'} = 'DATA';
300 # put together a dumpspec
302 if (exists $self->{'command'}{'HOST'}
303 || exists $self->{'command'}{'DISK'}
304 || exists $self->{'command'}{'DATESTAMP'}) {
305 my $disk = $self->{'command'}{'DISK'};
306 if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_correct_disk_quoting)) {
307 debug("ignoring specified DISK, as it may be badly quoted");
310 $spec = Amanda::Cmdline::dumpspec_t->new(
311 $self->{'command'}{'HOST'},
313 $self->{'command'}{'DATESTAMP'},
314 undef, # amidxtaped protocol does not provide a level (!?)
315 undef); # amidxtaped protocol does not provide a write timestamp
318 # figure out if this is a holding-disk recovery
320 if (!exists $self->{'command'}{'LABEL'} and exists $self->{'command'}{'DEVICE'}) {
326 # for holding, give the clerk a null; it won't touch it
327 $chg = Amanda::Changer->new("chg-null:");
329 # if not doing a holding-disk recovery, then we will need a changer.
330 # If we're using the "default" changer, instantiate that. There are
331 # several ways the user can specify the default changer:
333 if (!exists $self->{'command'}{'DEVICE'}) {
335 } elsif ($self->{'command'}{'DEVICE'} eq getconf($CNF_AMRECOVER_CHANGER)) {
339 my $tlf = Amanda::Config::config_dir_relative(getconf($CNF_TAPELIST));
340 my $tl = Amanda::Tapelist->new($tlf);
342 $chg = Amanda::Changer->new(undef, tapelist => $tl);
344 $chg = Amanda::Changer->new($self->{'command'}{'DEVICE'}, tapelist => $tl);
347 # if we got a bogus changer, log it to the debug log, but allow the
348 # scan algorithm to find a good one later.
349 if ($chg->isa("Amanda::Changer::Error")) {
351 $chg = Amanda::Changer->new("chg-null:");
354 $self->{'chg'} = $chg;
356 my $interactivity = main::Interactivity->new(clientservice => $self);
358 my $scan = Amanda::Recovery::Scan->new(
360 interactivity => $interactivity);
361 $self->{'scan'} = $scan;
364 $scan->{'scan_conf'}->{'driveinuse'} = Amanda::Recovery::Scan::SCAN_ASK;
365 $scan->{'scan_conf'}->{'volinuse'} = Amanda::Recovery::Scan::SCAN_ASK;
366 $scan->{'scan_conf'}->{'notfound'} = Amanda::Recovery::Scan::SCAN_ASK;
368 $self->{'clerk'} = Amanda::Recovery::Clerk->new(
369 # note that we don't have any use for clerk_notif's, so we don't pass
374 # if this is a holding recovery, then the plan is pretty easy. The holding
375 # file is given to us in the aptly-named DEVICE command key, with a :0 suffix
376 my $holding_file_tapespec = $self->{'command'}{'DEVICE'};
377 my $holding_file = $self->tapespec_to_holding($holding_file_tapespec);
379 return Amanda::Recovery::Planner::make_plan(
380 holding_file => $holding_file,
381 $spec? (dumpspec => $spec) : (),
382 plan_cb => sub { $self->plan_cb(@_); });
384 my $filelist = Amanda::Util::unmarshal_tapespec($self->{'command'}{'LABEL'});
386 # if LABEL was just a label, then FSF should contain the filenum we want to
388 if ($filelist->[1][0] == 0) {
389 if (exists $self->{'command'}{'FSF'}) {
390 $filelist->[1][0] = 0+$self->{'command'}{'FSF'};
391 # note that if this is a split dump, make_plan will helpfully find the
392 # remaining parts and include them in the restore. Pretty spiffy.
394 # we have only a label and (hopefully) a dumpspec, so let's see if the
395 # catalog can find a dump for us.
396 $filelist = $self->try_to_find_dump(
397 $self->{'command'}{'LABEL'},
400 return $self->quit();
405 return Amanda::Recovery::Planner::make_plan(
406 filelist => $filelist,
408 $spec? (dumpspec => $spec) : (),
409 plan_cb => sub { $self->plan_cb(@_); });
415 my ($err, $plan) = @_;
418 $self->sendmessage("$err");
419 return $self->quit();
422 if (@{$plan->{'dumps'}} > 1) {
423 $self->sendmessage("multiple matching dumps; cannot recover");
424 return $self->quit();
427 # check that the request-limit for this DLE allows this recovery. because
428 # of the bass-ackward way that amrecover specifies the dump to us, we can't
429 # check the results until *after* the plan was created.
430 my $dump = $plan->{'dumps'}->[0];
431 my $dle = Amanda::Disklist::get_disk($dump->{'hostname'}, $dump->{'diskname'});
433 if ($dle && dumptype_seen($dle->{'config'}, $DUMPTYPE_RECOVERY_LIMIT)) {
434 debug("using DLE recovery limit");
435 $recovery_limit = dumptype_getconf($dle->{'config'}, $DUMPTYPE_RECOVERY_LIMIT);
436 } elsif (getconf_seen($CNF_RECOVERY_LIMIT)) {
437 debug("using global recovery limit as default");
438 $recovery_limit = getconf($CNF_RECOVERY_LIMIT);
440 my $peer = $ENV{'AMANDA_AUTHENTICATED_PEER'};
441 if (defined $recovery_limit) { # undef -> no recovery limit
443 warning("a recovery limit is specified for this DLE, but no authenticated ".
444 "peer name is available; rejecting request.");
445 $self->sendmessage("No matching dumps found");
446 return $self->quit();
449 for my $rl (@$recovery_limit) {
450 if ($rl eq $Amanda::Config::LIMIT_SAMEHOST) {
451 # handle same-host with a case-insensitive string compare, not match_host
452 if (lc($peer) eq lc($dump->{'hostname'})) {
456 } elsif ($rl eq $Amanda::Config::LIMIT_SERVER) {
457 # handle server with a case-insensitive string compare, not match_host
458 my $myhostname = hostname;
459 debug("myhostname: $myhostname");
460 if (lc($peer) eq lc($myhostname)) {
465 # otherwise use match_host to allow match expressions
466 if (match_host($rl, $peer)) {
473 warning("authenticated peer '$peer' did not match recovery-limit ".
474 "config; rejecting request");
475 $self->sendmessage("No matching dumps found");
476 return $self->quit();
480 if (!$self->{'their_features'}->has($Amanda::Feature::fe_recover_splits)) {
481 # if we have greater than one volume, we may need to prompt for a new
482 # volume in mid-recovery. Sadly, we have no way to inform the client of
483 # this. In hopes that this will "just work", we just issue a warning.
484 my @vols = $plan->get_volume_list();
485 warning("client does not support split dumps; restore may fail if " .
486 "interaction is necessary");
489 # now set up the transfer
490 $self->{'dump'} = $plan->{'dumps'}[0];
491 $self->{'clerk'}->get_xfer_src(
492 dump => $self->{'dump'},
493 xfer_src_cb => sub { $self->xfer_src_cb(@_); });
498 my ($errors, $header, $xfer_src, $directtcp_supported) = @_;
502 $self->sendmessage("$_");
504 return $self->quit();
507 $self->{'xfer_src'} = $xfer_src;
508 $self->{'xfer_src_supports_directtcp'} = $directtcp_supported;
509 $self->{'header'} = $header;
511 debug("recovering from " . $header->summary());
513 # set up any filters that need to be applied, decryption first
515 if ($header->{'encrypted'}) {
516 if ($header->{'srv_encrypt'}) {
518 Amanda::Xfer::Filter::Process->new(
519 [ $header->{'srv_encrypt'}, $header->{'srv_decrypt_opt'} ], 0);
520 $header->{'encrypted'} = 0;
521 $header->{'srv_encrypt'} = '';
522 $header->{'srv_decrypt_opt'} = '';
523 $header->{'clnt_encrypt'} = '';
524 $header->{'clnt_decrypt_opt'} = '';
525 $header->{'encrypt_suffix'} = 'N';
526 } elsif ($header->{'clnt_encrypt'}) {
527 if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_receive_unfiltered)) {
529 Amanda::Xfer::Filter::Process->new(
530 [ $header->{'clnt_encrypt'},
531 $header->{'clnt_decrypt_opt'} ], 0);
532 $header->{'encrypted'} = 0;
533 $header->{'srv_encrypt'} = '';
534 $header->{'srv_decrypt_opt'} = '';
535 $header->{'clnt_encrypt'} = '';
536 $header->{'clnt_decrypt_opt'} = '';
537 $header->{'encrypt_suffix'} = 'N';
539 debug("Not decrypting client encrypted stream");
542 $self->sendmessage("could not decrypt encrypted dump: no program specified");
543 return $self->quit();
548 if ($header->{'compressed'}) {
549 # need to uncompress this file
550 debug("..with decompression applied");
552 if ($header->{'srvcompprog'}) {
553 # TODO: this assumes that srvcompprog takes "-d" to decrypt
555 Amanda::Xfer::Filter::Process->new(
556 [ $header->{'srvcompprog'}, "-d" ], 0);
558 $header->{'compressed'} = 0;
559 $header->{'uncompress_cmd'} = '';
560 $header->{'srvcompprog'} = '';
561 } elsif ($header->{'clntcompprog'}) {
562 if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_receive_unfiltered)) {
563 # TODO: this assumes that clntcompprog takes "-d" to decrypt
565 Amanda::Xfer::Filter::Process->new(
566 [ $header->{'clntcompprog'}, "-d" ], 0);
568 $header->{'compressed'} = 0;
569 $header->{'uncompress_cmd'} = '';
570 $header->{'clntcompprog'} = '';
573 my $dle = $header->get_dle();
575 (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_receive_unfiltered) ||
576 $dle->{'compress'} == $Amanda::Config::COMP_SERVER_FAST ||
577 $dle->{'compress'} == $Amanda::Config::COMP_SERVER_BEST)) {
579 Amanda::Xfer::Filter::Process->new(
580 [ $Amanda::Constants::UNCOMPRESS_PATH,
581 $Amanda::Constants::UNCOMPRESS_OPT ], 0);
583 $header->{'compressed'} = 0;
584 $header->{'uncompress_cmd'} = '';
589 $self->{'xfer_filters'} = [ @filters ];
591 # only send the header if requested
592 if ($self->{'command'}{'HEADER'}) {
593 $self->send_header();
595 $self->expect_datapath();
602 my $header = $self->{'header'};
604 # filter out some things the remote might not be able to process
605 if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_dle_in_header)) {
606 $header->{'dle_str'} = undef;
608 $header->{'dle_str'} =
609 Amanda::Disklist::clean_dle_str_for_client($header->{'dle_str'},
610 Amanda::Feature::am_features($self->{'their_features'}));
612 if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_origsize_in_header)) {
613 $header->{'orig_size'} = 0;
616 # even with fe_amrecover_splits, amrecover doesn't like F_SPLIT_DUMPFILE.
617 $header->{'type'} = $Amanda::Header::F_DUMPFILE;
619 my $hdr_str = $header->to_string(32768, 32768);
620 Amanda::Util::full_write($self->wfd($self->{'data_stream'}), $hdr_str, length($hdr_str))
621 or die "writing to $self->{data_stream}: $!";
623 $self->expect_datapath();
626 sub expect_datapath {
629 $self->{'datapath'} = 'none';
631 # short-circuit this if amrecover doesn't support datapaths
632 if (!$self->{'their_features'}->has($Amanda::Feature::fe_amidxtaped_datapath)) {
633 return $self->start_xfer();
636 my $line = $self->getline($self->{'ctl_stream'});
637 if ($line eq "ABORT\r\n") {
638 return Amanda::MainLoop::quit();
640 my ($dpspec) = ($line =~ /^AVAIL-DATAPATH (.*)\r\n$/);
641 die "bad AVAIL-DATAPATH line" unless $dpspec;
642 my @avail_dps = split / /, $dpspec;
644 if (grep /^DIRECT-TCP$/, @avail_dps) {
645 # remote can handle a directtcp transfer .. can we?
646 if ($self->{'xfer_src_supports_directtcp'}) {
647 $self->{'datapath'} = 'directtcp';
649 $self->{'datapath'} = 'amanda';
652 # remote can at least handle AMANDA
653 die "remote cannot handle AMANDA datapath??"
654 unless grep /^AMANDA$/, @avail_dps;
655 $self->{'datapath'} = 'amanda';
664 # create the appropriate destination based on our datapath
666 if ($self->{'datapath'} eq 'directtcp') {
667 $xfer_dest = Amanda::Xfer::Dest::DirectTCPListen->new();
669 $xfer_dest = Amanda::Xfer::Dest::Fd->new(
670 $self->wfd($self->{'data_stream'})),
673 if ($self->{'datapath'} eq 'amanda') {
674 $self->sendctlline("USE-DATAPATH AMANDA\r\n");
675 my $dpline = $self->getline($self->{'ctl_stream'});
676 if ($dpline ne "DATAPATH-OK\r\n") {
677 die "expected DATAPATH-OK";
681 # start reading all filter stderr
682 foreach my $filter (@{$self->{'xfer_filters'}}) {
683 my $fd = $filter->get_stderr_fd();
686 my $src = Amanda::MainLoop::fd_source($fd,
687 $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
689 $self->{'all_filter'}{$src} = 1;
690 $src->set_callback( sub {
692 my $n_read = POSIX::read($fd, $b, 1);
693 if (!defined $n_read) {
695 } elsif ($n_read == 0) {
696 delete $self->{'all_filter'}->{$src};
699 if (!%{$self->{'all_filter'}} and $self->{'fetch_done'}) {
700 Amanda::MainLoop::quit();
706 #print STDERR "filter stderr: $line";
708 $self->sendmessage("filter stderr: $line");
709 debug("filter stderr: $line");
716 # create and start the transfer
717 $self->{'xfer'} = Amanda::Xfer->new([
719 @{$self->{'xfer_filters'}},
723 $size = $self->{'dump'}->{'bytes'} if exists $self->{'dump'}->{'bytes'};
724 $self->{'xfer'}->start(sub { $self->handle_xmsg(@_); }, 0, $size);
725 debug("started xfer; datapath=$self->{datapath}");
727 # send the data-path response, if we have a datapath
728 if ($self->{'datapath'} eq 'directtcp') {
729 my $addrs = $xfer_dest->get_addrs();
730 $addrs = [ map { $_->[0] . ":" . $_->[1] } @$addrs ];
731 $addrs = join(" ", @$addrs);
732 $self->sendctlline("USE-DATAPATH DIRECT-TCP $addrs\r\n");
733 my $dpline = $self->getline($self->{'ctl_stream'});
734 if ($dpline ne "DATAPATH-OK\r\n") {
735 die "expected DATAPATH-OK";
739 # and let the clerk know
740 $self->{'clerk'}->start_recovery(
741 xfer => $self->{'xfer'},
742 recovery_cb => sub { $self->recovery_cb(@_); });
747 my ($src, $msg, $xfer) = @_;
749 $self->{'clerk'}->handle_xmsg($src, $msg, $xfer);
750 if ($msg->{'elt'} != $self->{'xfer_src'}) {
751 if ($msg->{'type'} == $XMSG_ERROR) {
752 $self->sendmessage("$msg->{message}");
761 debug("recovery complete");
762 if (@{$params{'errors'}}) {
763 for (@{$params{'errors'}}) {
764 $self->sendmessage("$_");
766 return $self->quit();
769 # note that the amidxtaped protocol has no way to indicate successful
770 # completion of a transfer
771 if ($params{'result'} ne 'DONE') {
772 warning("NOTE: transfer failed, but amrecover does not know that");
781 # close the data fd for writing to signal EOF
782 $self->close($self->{'data_stream'}, 'w');
790 if ($self->{'clerk'}) {
791 $self->{'clerk'}->quit(finished_cb => sub {
793 $self->{'chg'}->quit() if defined $self->{'chg'};
795 # it's *way* too late to report this to amrecover now!
796 warning("while quitting clerk: $err");
801 $self->{'scan'}->quit() if defined $self->{'scan'};
802 $self->{'chg'}->quit() if defined $self->{'chg'};
811 $self->{'fetch_done'} = 1;
812 if (!%{$self->{'all_filter'}}) {
813 Amanda::MainLoop::quit();
819 sub check_inetd_security {
823 my $firstline = $self->getline($stream);
824 if ($firstline !~ /^SECURITY (.*)\n/) {
825 warning("did not get security line");
826 print "ERROR did not get security line\r\n";
830 my $errmsg = $self->check_bsd_security($stream, $1);
832 print "ERROR $errmsg\r\n";
844 my $buf = Amanda::Util::full_read($self->rfd('main'), 1024);
848 # we've read main to EOF, so close it
849 $self->close('main', 'r');
851 return $self->{'req'} = $self->parse_req($req_str);
856 my ($streams, $errors) = @_;
859 # first, if there were errors in the REQ, report them
861 for my $err (@$errors) {
862 $rep .= "ERROR $err\n";
865 my $connline = $self->connect_streams(@$streams);
866 $rep .= "$connline\n";
868 # rep needs a empty-line terminator, I think
871 # write the whole rep packet, and close main to signal the end of the packet
872 $self->senddata('main', $rep);
873 $self->close('main', 'w');
876 # helper function to get a line, including the trailing '\n', from a stream. This
877 # reads a character at a time to ensure that no extra characters are consumed. This
878 # could certainly be more efficient! (TODO)
882 my $fd = $self->rfd($stream);
887 POSIX::read($fd, $c, 1)
894 $chopped =~ s/[\r\n]*$//g;
895 debug("CTL << $chopped");
900 # like getline, but async; TODO:
901 # - make all uses of getline async
902 # - use buffering to read more than one character at a time
905 my ($stream, $async_read_cb) = @_;
906 my $fd = $self->rfd($stream);
912 my ($err, $data) = @_;
914 return $async_read_cb->($err, undef) if $err;
917 if ($buf =~ /\r\n$/) {
919 $chopped =~ s/[\r\n]*$//g;
920 debug("CTL << $chopped");
922 $async_read_cb->(undef, $buf);
924 Amanda::MainLoop::async_read(fd => $fd, size => 1, async_read_cb => $data_in);
927 Amanda::MainLoop::async_read(fd => $fd, size => 1, async_read_cb => $data_in);
930 # helper function to write a data to a stream. This does not add newline characters.
931 # If the callback is given, this is async (TODO: all calls should be async)
934 my ($stream, $data, $async_write_cb) = @_;
935 my $fd = $self->wfd($stream);
937 if (defined $async_write_cb) {
938 return Amanda::MainLoop::async_write(
941 async_write_cb => $async_write_cb);
943 Amanda::Util::full_write($fd, $data, length($data))
944 or die "writing to $stream: $!";
948 # send a line on the control stream, or just log it if the ctl stream is gone;
949 # async callback is just like for senddata
952 my ($msg, $async_write_cb) = @_;
955 $chopped =~ s/[\r\n]*$//g;
957 if ($self->{'ctl_stream'}) {
958 debug("CTL >> $chopped");
959 return $self->senddata($self->{'ctl_stream'}, $msg, $async_write_cb);
961 debug("not sending CTL message as CTL is closed >> $chopped");
962 if (defined $async_write_cb) {
963 $async_write_cb->(undef, length($msg));
968 # send a MESSAGE on the CTL stream, but only if the remote has
969 # fe_amrecover_message
974 if ($self->{'their_features'}->has($Amanda::Feature::fe_amrecover_message)) {
975 $self->sendctlline("MESSAGE $msg\r\n");
977 warning("remote does not understand MESSAGE; not sent: MESSAGE $msg");
981 # covert a tapespec to a holding filename
982 sub tapespec_to_holding {
986 my $filelist = Amanda::Util::unmarshal_tapespec($tapespec);
988 # $filelist should have the form [ $holding_file => [ 0 ] ]
989 die "invalid holding tapespec" unless @$filelist == 2;
990 die "invalid holding tapespec" unless @{$filelist->[1]} == 1;
991 die "invalid holding tapespec" unless $filelist->[1][0] == 0;
993 return $filelist->[0];
996 # amrecover didn't give us much to go on, but see if we can find a dump that
997 # will make it happy.
998 sub try_to_find_dump {
1000 my ($label, $spec) = @_;
1002 # search the catalog; get_dumps cannot search by labels, so we have to use
1004 my @parts = Amanda::DB::Catalog::get_parts(
1006 dumpspecs => [ $spec ]);
1009 $self->sendmessage("could not find any matching dumps on volume '$label'");
1013 # (note that if there is more than one dump in @parts, the planner will
1016 # sort the parts by their order on each volume. This sorts the volumes
1017 # lexically by label, but the planner will straighten it out.
1018 @parts = Amanda::DB::Catalog::sort_dumps([ "label", "filenum" ], @parts);
1020 # loop over the parts for the dump and make a filelist.
1021 my $last_label = '';
1022 my $last_filenums = undef;
1024 for my $part (@parts) {
1025 next unless defined $part; # skip part number 0
1026 if ($part->{'label'} ne $last_label) {
1027 $last_label = $part->{'label'};
1028 $last_filenums = [];
1029 push @$filelist, $last_label, $last_filenums;
1031 push @$last_filenums, $part->{'filenum'};
1041 use Amanda::Debug qw( debug );
1042 use Amanda::Util qw( :constants );
1043 use Amanda::Config qw( :init );
1045 our $exit_status = 0;
1048 Amanda::Util::setup_application("amidxtaped", "server", $CONTEXT_DAEMON);
1049 config_init(0, undef);
1050 Amanda::Debug::debug_dup_stderr_to_debug();
1052 my $cs = main::ClientService->new();
1053 Amanda::MainLoop::call_later(sub { $cs->run(); });
1054 Amanda::MainLoop::run();
1056 debug("exiting with $exit_status");
1057 Amanda::Util::finish_application();