43648dcc0d13f44d1c6bd44020eb8149ffc07cff
[debian/amanda] / server-src / amidxtaped.pl
1 #! @PERL@
2 # Copyright (c) 2010 Zmanda, Inc.  All Rights Reserved.
3 #
4 # This program is free software; you can redistribute it and/or modify it
5 # under the terms of the GNU General Public License version 2 as published
6 # by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11 # for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 #
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19
20 use lib '@amperldir@';
21 use strict;
22 use warnings;
23
24 ##
25 # Interactive class
26
27 package main::Interactive;
28 use base 'Amanda::Interactive';
29 use Amanda::Util qw( weaken_ref );
30 use Amanda::MainLoop;
31 use Amanda::Feature;
32 use Amanda::Debug qw( debug );
33 use Amanda::Config qw( :getconf );
34 use Amanda::Recovery::Scan qw( $DEFAULT_CHANGER );
35
36 sub new {
37     my $class = shift;
38     my %params = @_;
39
40     my $self = {
41         clientservice => $params{'clientservice'},
42     };
43
44     # (weak ref here to eliminate reference loop)
45     weaken_ref($self->{'clientservice'});
46
47     return bless ($self, $class);
48 }
49
50 sub abort() {
51     my $self = shift;
52
53     debug("ignoring spurious Amanda::Recovery::Scan abort call");
54 }
55
56 sub user_request {
57     my $self = shift;
58     my %params = @_;
59     my $buffer = "";
60
61     my $steps = define_steps
62         cb_ref => \$params{'finished_cb'};
63
64     step send_message => sub {
65         if ($params{'err'}) {
66             $self->{'clientservice'}->sendmessage("$params{err}");
67         }
68
69         $steps->{'check_fe_feedme'}->();
70     };
71
72     step check_fe_feedme => sub {
73         # note that fe_amrecover_FEEDME implies fe_amrecover_splits
74         if (!$self->{'clientservice'}->{'their_features'}->has(
75                                     $Amanda::Feature::fe_amrecover_FEEDME)) {
76             return $params{'finished_cb'}->("remote cannot prompt for volumes", undef);
77         }
78         $steps->{'send_feedme'}->();
79     };
80
81     step send_feedme => sub {
82         $self->{'clientservice'}->sendctlline("FEEDME $params{label}\r\n", $steps->{'read_response'});
83     };
84
85     step read_response => sub {
86         my ($err, $written) = @_;
87         return $params{'finished_cb'}->($err, undef) if $err;
88
89         $self->{'clientservice'}->getline_async(
90                 $self->{'clientservice'}->{'ctl_stream'}, $steps->{'got_response'});
91     };
92
93     step got_response => sub {
94         my ($err, $line) = @_;
95         return $params{'finished_cb'}->($err, undef) if $err;
96
97         if ($line eq "OK\r\n") {
98             return $params{'finished_cb'}->(undef, undef); # carry on as you were
99         } elsif ($line =~ /^TAPE (.*)\r\n$/) {
100             my $tape = $1;
101             if ($tape eq getconf($CNF_AMRECOVER_CHANGER)) {
102                 $tape = $Amanda::Recovery::Scan::DEFAULT_CHANGER;
103             }
104             return $params{'finished_cb'}->(undef, $tape); # use this device
105         } else {
106             return $params{'finished_cb'}->("got invalid response from remote", undef);
107         }
108     };
109 };
110
111 ##
112 # Clerk Feedback class
113
114 package main::Feedback;
115 use Amanda::Recovery::Clerk;
116 use Amanda::Util qw( weaken_ref );
117 use base 'Amanda::Recovery::Clerk::Feedback';
118
119 sub new {
120     my $class = shift;
121     my %params = @_;
122
123     my $self = bless {
124         clientservice => $params{'clientservice'}
125     }, $class;
126
127     # (weak ref here to eliminate reference loop)
128     weaken_ref($self->{'clientservice'});
129
130     return $self;
131 }
132
133 sub part_notif {
134     my $self = shift;
135
136     my ($label, $filenum, $hdr) = @_;
137     $self->{'clientservice'}->sendmessage("restoring part $hdr->{'partnum'} " .
138           "from '$label' file $filenum");
139 }
140
141 sub holding_notif {
142     my $self = shift;
143
144     my ($holding_file, $hdr) = @_;
145     $self->{'clientservice'}->sendmessage("restoring from holding " .
146                 "file $holding_file");
147 }
148
149 ##
150 # ClientService class
151
152 package main::ClientService;
153 use base 'Amanda::ClientService';
154
155 use Amanda::Debug qw( debug info warning );
156 use Amanda::Util qw( :constants );
157 use Amanda::Feature;
158 use Amanda::Config qw( :init :getconf );
159 use Amanda::Changer;
160 use Amanda::Recovery::Scan;
161 use Amanda::Xfer qw( :constants );
162 use Amanda::Cmdline;
163 use Amanda::Recovery::Clerk;
164 use Amanda::Recovery::Planner;
165 use Amanda::Recovery::Scan;
166 use Amanda::DB::Catalog;
167
168 # Note that this class performs its control IO synchronously.  This is adequate
169 # for this service, as it never receives unsolicited input from the remote
170 # system.
171
172 sub run {
173     my $self = shift;
174
175     $self->{'my_features'} = Amanda::Feature::Set->mine();
176     $self->{'their_features'} = Amanda::Feature::Set->old();
177
178     $self->setup_streams();
179 }
180
181 sub setup_streams {
182     my $self = shift;
183
184     # get started checking security for inetd or processing the REQ/REP
185     # for amandad
186     if ($self->from_inetd()) {
187         if (!$self->check_inetd_security('main')) {
188             $main::exit_status = 1;
189             return $self->quit();
190         }
191         $self->{'ctl_stream'} = 'main';
192         $self->{'data_stream'} = undef; # no data stream yet
193     } else {
194         my $req = $self->get_req();
195
196         # make some sanity checks
197         my $errors = [];
198         if (defined $req->{'options'}{'auth'} and defined $self->amandad_auth()
199                 and $req->{'options'}{'auth'} ne $self->amandad_auth()) {
200             my $reqauth = $req->{'options'}{'auth'};
201             my $amauth = $self->amandad_auth();
202             push @$errors, "recover program requested auth '$reqauth', " .
203                            "but amandad is using auth '$amauth'";
204             $main::exit_status = 1;
205         }
206
207         # and pull out the features, if given
208         if (defined($req->{'features'})) {
209             $self->{'their_features'} = $req->{'features'};
210         }
211
212         $self->send_rep(['CTL' => 'rw', 'DATA' => 'w'], $errors);
213         return $self->quit() if (@$errors);
214
215         $self->{'ctl_stream'} = 'CTL';
216         $self->{'data_stream'} = 'DATA';
217     }
218
219     $self->read_command();
220 }
221
222 sub read_command {
223     my $self = shift;
224     my $ctl_stream = $self->{'ctl_stream'};
225     my $command = $self->{'command'} = {};
226
227     my @known_commands = qw(
228         HOST DISK DATESTAMP LABEL DEVICE FSF HEADER
229         FEATURES CONFIG );
230     while (1) {
231         $_ = $self->getline($ctl_stream);
232         $_ =~ s/\r?\n$//g;
233
234         last if /^END$/;
235         last if /^[0-9]+$/;
236
237         if (/^([A-Z]+)(=(.*))?$/) {
238             my ($cmd, $val) = ($1, $3);
239             if (!grep { $_ eq $cmd } @known_commands) {
240                 $self->sendmessage("invalid command '$cmd'");
241                 return $self->quit();
242             }
243             if (exists $command->{$cmd}) {
244                 warning("got duplicate command key '$cmd' from remote");
245             } else {
246                 $command->{$cmd} = $val || 1;
247             }
248         }
249
250         # features are handled specially.  This is pretty weird!
251         if (/^FEATURES=/) {
252             my $featreply;
253             my $featurestr = $self->{'my_features'}->as_string();
254             if ($self->from_amandad) {
255                 $featreply = "FEATURES=$featurestr\r\n";
256             } else {
257                 $featreply = $featurestr;
258             }
259
260             $self->senddata($ctl_stream, $featreply);
261         }
262     }
263
264     # process some info from the command
265     if ($command->{'FEATURES'}) {
266         $self->{'their_features'} = Amanda::Feature::Set->from_string($command->{'FEATURES'});
267     }
268
269     if ($command->{'CONFIG'}) {
270         config_init($CONFIG_INIT_EXPLICIT_NAME, $command->{'CONFIG'});
271         my ($cfgerr_level, @cfgerr_errors) = config_errors();
272         if ($cfgerr_level >= $CFGERR_ERRORS) {
273             die "configuration errors; aborting connection";
274         }
275         Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER_PREFERRED);
276     }
277
278     $self->setup_data_stream();
279 }
280
281 sub setup_data_stream {
282     my $self = shift;
283
284     # if we're using amandad, then this is ready to roll - it's only inetd mode
285     # that we need to fix
286     if ($self->from_inetd()) {
287         if ($self->{'their_features'}->has($Amanda::Feature::fe_recover_splits)) {
288             # remote side is expecting CONNECT
289             my $port = $self->connection_listen('DATA', 0);
290             $self->senddata($self->{'ctl_stream'}, "CONNECT $port\n");
291             $self->connection_accept('DATA', 30, sub { $self->got_connection(@_); });
292         } else {
293             $self->{'ctl_stream'} = undef; # don't use this for ctl anymore
294             $self->{'data_stream'} = 'main';
295             $self->make_plan();
296         }
297     } else {
298         $self->make_plan();
299     }
300 }
301
302 sub got_connection {
303     my $self = shift;
304     my ($err) = @_;
305
306     if ($err) {
307         $self->sendmessage("$err");
308         return $self->quit();
309     }
310
311     if (!$self->check_inetd_security('DATA')) {
312         $main::exit_status = 1;
313         return $self->quit();
314     }
315     $self->{'data_stream'} = 'DATA';
316
317     $self->make_plan();
318 }
319
320 sub make_plan {
321     my $self = shift;
322
323     # put together a dumpspec
324     my $spec;
325     if (exists $self->{'command'}{'HOST'}
326      || exists $self->{'command'}{'DISK'}
327      || exists $self->{'command'}{'DATESTAMP'}) {
328         my $disk = $self->{'command'}{'DISK'};
329         if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_correct_disk_quoting)) {
330             debug("ignoring specified DISK, as it may be badly quoted");
331             $disk = undef;
332         }
333         $spec = Amanda::Cmdline::dumpspec_t->new(
334             $self->{'command'}{'HOST'},
335             $disk,
336             $self->{'command'}{'DATESTAMP'},
337             undef); # amidxtaped protocol does not provide a level (!?)
338     }
339
340     # figure out if this is a holding-disk recovery
341     my $is_holding = 0;
342     if (!exists $self->{'command'}{'LABEL'} and exists $self->{'command'}{'DEVICE'}) {
343         $is_holding = 1;
344     }
345
346     my $chg;
347     if ($is_holding) {
348         # for holding, give the clerk a null; it won't touch it
349         $chg = Amanda::Changer->new("chg-null:");
350     } else {
351         # if not doing a holding-disk recovery, then we will need a changer.
352         # If we're using the "default" changer, instantiate that.  There are
353         # several ways the user can specify the default changer:
354         my $use_default = 0;
355         if (!exists $self->{'command'}{'DEVICE'}) {
356             $use_default = 1;
357         } elsif ($self->{'command'}{'DEVICE'} eq getconf($CNF_AMRECOVER_CHANGER)) {
358             $use_default = 1;
359         }
360
361         if ($use_default) {
362             $chg = Amanda::Changer->new();
363         } else {
364             $chg = Amanda::Changer->new($self->{'command'}{'DEVICE'});
365         }
366
367         # if we got a bogus changer, log it to the debug log, but allow the
368         # scan algorithm to find a good one later.
369         if ($chg->isa("Amanda::Changer::Error")) {
370             warning("$chg");
371             $chg = Amanda::Changer->new("chg-null:");
372         }
373     }
374     my $inter = main::Interactive->new(clientservice => $self);
375
376     my $scan = Amanda::Recovery::Scan->new(
377                         chg => $chg,
378                         interactive => $inter);
379     # XXX temporary
380     $scan->{'scan_conf'}->{'driveinuse'} = Amanda::Recovery::Scan::SCAN_ASK;
381     $scan->{'scan_conf'}->{'volinuse'} = Amanda::Recovery::Scan::SCAN_ASK;
382     $scan->{'scan_conf'}->{'notfound'} = Amanda::Recovery::Scan::SCAN_ASK;
383
384     $self->{'clerk'} = Amanda::Recovery::Clerk->new(
385         feedback => main::Feedback->new($chg, undef),
386         scan => $scan);
387
388     if ($is_holding) {
389         # if this is a holding recovery, then the plan is pretty easy.  The holding
390         # file is given to us in the aptly-named DEVICE command key, with a :0 suffix
391         my $holding_file_tapespec = $self->{'command'}{'DEVICE'};
392         my $holding_file = $self->tapespec_to_holding($holding_file_tapespec);
393
394         return Amanda::Recovery::Planner::make_plan(
395             holding_file => $holding_file,
396             $spec? (dumpspec => $spec) : (),
397             plan_cb => sub { $self->plan_cb(@_); });
398     } else {
399         my $filelist = Amanda::Util::unmarshal_tapespec($self->{'command'}{'LABEL'});
400
401         # if LABEL was just a label, then FSF should contain the filenum we want to
402         # start with.
403         if ($filelist->[1][0] == 0) {
404             if (exists $self->{'command'}{'FSF'}) {
405                 $filelist->[1][0] = 0+$self->{'command'}{'FSF'};
406                 # note that if this is a split dump, make_plan will helpfully find the
407                 # remaining parts and include them in the restore.  Pretty spiffy.
408             } else {
409                 # we have only a label and (hopefully) a dumpspec, so let's see if the
410                 # catalog can find a dump for us.
411                 $filelist = $self->try_to_find_dump(
412                         $self->{'command'}{'LABEL'},
413                         $spec);
414                 if (!$filelist) {
415                     return $self->quit();
416                 }
417             }
418         }
419
420         return Amanda::Recovery::Planner::make_plan(
421             filelist => $filelist,
422             $spec? (dumpspec => $spec) : (),
423             plan_cb => sub { $self->plan_cb(@_); });
424     }
425 }
426
427 sub plan_cb {
428     my $self = shift;
429     my ($err, $plan) = @_;
430
431     if ($err) {
432         $self->sendmessage("$err");
433         return $self->quit();
434     }
435
436     if (@{$plan->{'dumps'}} > 1) {
437         $self->sendmessage("multiple matching dumps; cannot recover");
438         return $self->quit();
439     }
440
441     if (!$self->{'their_features'}->has($Amanda::Feature::fe_recover_splits)) {
442         # if we have greater than one volume, we may need to prompt for a new
443         # volume in mid-recovery.  Sadly, we have no way to inform the client of
444         # this.  In hopes that this will "just work", we just issue a warning.
445         my @vols = $plan->get_volume_list();
446         warning("client does not support split dumps; restore may fail if " .
447                 "interaction is necessary");
448     }
449
450     # now set up the transfer
451     $self->{'clerk'}->get_xfer_src(
452         dump => $plan->{'dumps'}[0],
453         xfer_src_cb => sub { $self->xfer_src_cb(@_); });
454 }
455
456 sub xfer_src_cb {
457     my $self = shift;
458     my ($errors, $header, $xfer_src, $directtcp_supported) = @_;
459
460     if ($errors) {
461         for (@$errors) {
462             $self->sendmessage("$_");
463         }
464         return $self->quit();
465     }
466
467     $self->{'xfer_src'} = $xfer_src;
468     $self->{'xfer_src_supports_directtcp'} = $directtcp_supported;
469     $self->{'header'} = $header;
470
471     debug("recovering from " . $header->summary());
472
473     # set up any filters that need to be applied, decryption first
474     my @filters;
475     if ($header->{'encrypted'}) {
476         if ($header->{'srv_encrypt'}) {
477             push @filters,
478                 Amanda::Xfer::Filter::Process->new(
479                     [ $header->{'srv_encrypt'}, $header->{'srv_decrypt_opt'} ], 0);
480         } elsif ($header->{'clnt_encrypt'}) {
481             push @filters,
482                 Amanda::Xfer::Filter::Process->new(
483                     [ $header->{'clnt_encrypt'}, $header->{'clnt_decrypt_opt'} ], 0);
484         } else {
485             $self->sendmessage("could not decrypt encrypted dump: no program specified");
486             return $self->quit();
487         }
488
489         $header->{'encrypted'} = 0;
490         $header->{'srv_encrypt'} = '';
491         $header->{'srv_decrypt_opt'} = '';
492         $header->{'clnt_encrypt'} = '';
493         $header->{'clnt_decrypt_opt'} = '';
494         $header->{'encrypt_suffix'} = 'N';
495     }
496
497     if ($header->{'compressed'}) {
498         # need to uncompress this file
499         debug("..with decompression applied");
500
501         if ($header->{'srvcompprog'}) {
502             # TODO: this assumes that srvcompprog takes "-d" to decrypt
503             push @filters,
504                 Amanda::Xfer::Filter::Process->new(
505                     [ $header->{'srvcompprog'}, "-d" ], 0);
506         } elsif ($header->{'clntcompprog'}) {
507             # TODO: this assumes that clntcompprog takes "-d" to decrypt
508             push @filters,
509                 Amanda::Xfer::Filter::Process->new(
510                     [ $header->{'clntcompprog'}, "-d" ], 0);
511         } else {
512             push @filters,
513                 Amanda::Xfer::Filter::Process->new(
514                     [ $Amanda::Constants::UNCOMPRESS_PATH,
515                       $Amanda::Constants::UNCOMPRESS_OPT ], 0);
516         }
517
518         # adjust the header
519         $header->{'compressed'} = 0;
520         $header->{'uncompress_cmd'} = '';
521     }
522     $self->{'xfer_filters'} = [ @filters ];
523
524     # only send the header if requested
525     if ($self->{'command'}{'HEADER'}) {
526         $self->send_header();
527     } else {
528         $self->expect_datapath();
529     }
530 }
531
532 sub send_header {
533     my $self = shift;
534
535     my $header = $self->{'header'};
536
537     # filter out some things the remote might not be able to process
538     if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_dle_in_header)) {
539         $header->{'dle_str'} = undef;
540     }
541     if (!$self->{'their_features'}->has($Amanda::Feature::fe_amrecover_origsize_in_header)) {
542         $header->{'orig_size'} = 0;
543     }
544
545     # even with fe_amrecover_splits, amrecover doesn't like F_SPLIT_DUMPFILE.
546     $header->{'type'} = $Amanda::Header::F_DUMPFILE;
547
548     my $hdr_str = $header->to_string(32768, 32768);
549     Amanda::Util::full_write($self->wfd($self->{'data_stream'}), $hdr_str, length($hdr_str))
550         or die "writing to $self->{data_stream}: $!";
551
552     $self->expect_datapath();
553 }
554
555 sub expect_datapath {
556     my $self = shift;
557
558     $self->{'datapath'} = 'none';
559
560     # short-circuit this if amrecover doesn't support datapaths
561     if (!$self->{'their_features'}->has($Amanda::Feature::fe_amidxtaped_datapath)) {
562         return $self->start_xfer();
563     }
564
565     my $line = $self->getline($self->{'ctl_stream'});
566     if ($line eq "ABORT\r\n") {
567         return Amanda::MainLoop::quit();
568     }
569     my ($dpspec) = ($line =~ /^AVAIL-DATAPATH (.*)\r\n$/);
570     die "bad AVAIL-DATAPATH line" unless $dpspec;
571     my @avail_dps = split / /, $dpspec;
572
573     if (grep /^DIRECT-TCP$/, @avail_dps) {
574         # remote can handle a directtcp transfer .. can we?
575         if ($self->{'xfer_src_supports_directtcp'}) {
576             $self->{'datapath'} = 'directtcp';
577         } else {
578             $self->{'datapath'} = 'amanda';
579         }
580     } else {
581         # remote can at least handle AMANDA
582         die "remote cannot handle AMANDA datapath??"
583             unless grep /^AMANDA$/, @avail_dps;
584         $self->{'datapath'} = 'amanda';
585     }
586
587     $self->start_xfer();
588 }
589
590 sub start_xfer {
591     my $self = shift;
592
593     # create the appropriate destination based on our datapath
594     my $xfer_dest;
595     if ($self->{'datapath'} eq 'directtcp') {
596         $xfer_dest = Amanda::Xfer::Dest::DirectTCPListen->new();
597     } else {
598         $xfer_dest = Amanda::Xfer::Dest::Fd->new(
599                 $self->wfd($self->{'data_stream'})),
600     }
601
602     if ($self->{'datapath'} eq 'amanda') {
603         $self->sendctlline("USE-DATAPATH AMANDA\r\n");
604         my $dpline = $self->getline($self->{'ctl_stream'});
605         if ($dpline ne "DATAPATH-OK\r\n") {
606             die "expected DATAPATH-OK";
607         }
608     }
609
610     # create and start the transfer
611     $self->{'xfer'} = Amanda::Xfer->new([
612         $self->{'xfer_src'},
613         @{$self->{'xfer_filters'}},
614         $xfer_dest,
615     ]);
616     $self->{'xfer'}->start(sub { $self->handle_xmsg(@_); });
617     debug("started xfer; datapath=$self->{datapath}");
618
619     # send the data-path response, if we have a datapath
620     if ($self->{'datapath'} eq 'directtcp') {
621         my $addrs = $xfer_dest->get_addrs();
622         $addrs = [ map { $_->[0] . ":" . $_->[1] } @$addrs ];
623         $addrs = join(" ", @$addrs);
624         $self->sendctlline("USE-DATAPATH DIRECT-TCP $addrs\r\n");
625         my $dpline = $self->getline($self->{'ctl_stream'});
626         if ($dpline ne "DATAPATH-OK\r\n") {
627             die "expected DATAPATH-OK";
628         }
629     }
630
631     # and let the clerk know
632     $self->{'clerk'}->start_recovery(
633         xfer => $self->{'xfer'},
634         recovery_cb => sub { $self->recovery_cb(@_); });
635 }
636
637 sub handle_xmsg {
638     my $self = shift;
639     my ($src, $msg, $xfer) = @_;
640
641     $self->{'clerk'}->handle_xmsg($src, $msg, $xfer);
642     if ($msg->{'elt'} != $self->{'xfer_src'}) {
643         if ($msg->{'type'} == $XMSG_ERROR) {
644             $self->sendmessage("$msg->{message}");
645         }
646     }
647 }
648
649 sub recovery_cb {
650     my $self = shift;
651     my %params = @_;
652
653     debug("recovery complete");
654     if (@{$params{'errors'}}) {
655         for (@{$params{'errors'}}) {
656             $self->sendmessage("$_");
657         }
658         return $self->quit();
659     }
660
661     # note that the amidxtaped protocol has no way to indicate successful
662     # completion of a transfer
663     if ($params{'result'} ne 'DONE') {
664         warning("NOTE: transfer failed, but amrecover does not know that");
665     }
666
667     $self->finish();
668 }
669
670 sub finish {
671     my $self = shift;
672
673     # close the data fd for writing to signal EOF
674     $self->close($self->{'data_stream'}, 'w');
675
676     $self->quit();
677 }
678
679 sub quit {
680     my $self = shift;
681
682     if ($self->{'clerk'}) {
683         $self->{'clerk'}->quit(finished_cb => sub {
684             my ($err) = @_;
685             if ($err) {
686                 # it's *way* too late to report this to amrecover now!
687                 warning("while quitting clerk: $err");
688             }
689             Amanda::MainLoop::quit();
690         });
691     } else {
692         Amanda::MainLoop::quit();
693     }
694 }
695
696 ## utilities
697
698 sub check_inetd_security {
699     my $self = shift;
700     my ($stream) = @_;
701
702     my $firstline = $self->getline($stream);
703     if ($firstline !~ /^SECURITY (.*)\n/) {
704         warning("did not get security line");
705         print "ERROR did not get security line\r\n";
706         return 0;
707     }
708
709     my $errmsg = $self->check_bsd_security($stream, $1);
710     if ($errmsg) {
711         print "ERROR $errmsg\r\n";
712         return 0;
713     }
714
715     return 1;
716 }
717
718 sub get_req {
719     my $self = shift;
720
721     my $req_str = '';
722     while (1) {
723         my $buf = Amanda::Util::full_read($self->rfd('main'), 1024);
724         last unless $buf;
725         $req_str .= $buf;
726     }
727     # we've read main to EOF, so close it
728     $self->close('main', 'r');
729
730     return $self->{'req'} = $self->parse_req($req_str);
731 }
732
733 sub send_rep {
734     my $self = shift;
735     my ($streams, $errors) = @_;
736     my $rep = '';
737
738     # first, if there were errors in the REQ, report them
739     if (@$errors) {
740         for my $err (@$errors) {
741             $rep .= "ERROR $err\n";
742         }
743     } else {
744         my $connline = $self->connect_streams(@$streams);
745         $rep .= "$connline\n";
746     }
747     # rep needs a empty-line terminator, I think
748     $rep .= "\n";
749
750     # write the whole rep packet, and close main to signal the end of the packet
751     $self->senddata('main', $rep);
752     $self->close('main', 'w');
753 }
754
755 # helper function to get a line, including the trailing '\n', from a stream.  This
756 # reads a character at a time to ensure that no extra characters are consumed.  This
757 # could certainly be more efficient! (TODO)
758 sub getline {
759     my $self = shift;
760     my ($stream) = @_;
761     my $fd = $self->rfd($stream);
762     my $line = '';
763
764     while (1) {
765         my $c;
766         POSIX::read($fd, $c, 1)
767             or last;
768         $line .= $c;
769         last if $c eq "\n";
770     }
771
772     my $chopped = $line;
773     $chopped =~ s/[\r\n]*$//g;
774     debug("CTL << $chopped");
775
776     return $line;
777 }
778
779 # like getline, but async; TODO:
780 #  - make all uses of getline async
781 #  - use buffering to read more than one character at a time
782 sub getline_async {
783     my $self = shift;
784     my ($stream, $async_read_cb) = @_;
785     my $fd = $self->rfd($stream);
786
787     my $data_in;
788     my $buf = '';
789
790     $data_in = sub {
791         my ($err, $data) = @_;
792
793         return $async_read_cb->($err, undef) if $err;
794
795         $buf .= $data;
796         if ($buf =~ /\r\n$/) {
797             my $chopped = $buf;
798             $chopped =~ s/[\r\n]*$//g;
799             debug("CTL << $chopped");
800
801             $async_read_cb->(undef, $buf);
802         } else {
803             Amanda::MainLoop::async_read(fd => $fd, size => 1, async_read_cb => $data_in);
804         }
805     };
806     Amanda::MainLoop::async_read(fd => $fd, size => 1, async_read_cb => $data_in);
807 }
808
809 # helper function to write a data to a stream.  This does not add newline characters.
810 # If the callback is given, this is async (TODO: all calls should be async)
811 sub senddata {
812     my $self = shift;
813     my ($stream, $data, $async_write_cb) = @_;
814     my $fd = $self->wfd($stream);
815
816     if (defined $async_write_cb) {
817         return Amanda::MainLoop::async_write(
818                 fd => $fd,
819                 data => $data,
820                 async_write_cb => $async_write_cb);
821     } else {
822         Amanda::Util::full_write($fd, $data, length($data))
823             or die "writing to $stream: $!";
824     }
825 }
826
827 # send a line on the control stream, or just log it if the ctl stream is gone;
828 # async callback is just like for senddata
829 sub sendctlline {
830     my $self = shift;
831     my ($msg, $async_write_cb) = @_;
832
833     my $chopped = $msg;
834     $chopped =~ s/[\r\n]*$//g;
835
836     if ($self->{'ctl_stream'}) {
837         debug("CTL >> $chopped");
838         return $self->senddata($self->{'ctl_stream'}, $msg, $async_write_cb);
839     } else {
840         debug("not sending CTL message as CTL is closed >> $chopped");
841         if (defined $async_write_cb) {
842             $async_write_cb->(undef, length($msg));
843         }
844     }
845 }
846
847 # send a MESSAGE on the CTL stream, but only if the remote has
848 # fe_amrecover_message
849 sub sendmessage {
850     my $self = shift;
851     my ($msg) = @_;
852
853     if ($self->{'their_features'}->has($Amanda::Feature::fe_amrecover_message)) {
854         $self->sendctlline("MESSAGE $msg\r\n");
855     } else {
856         warning("remote does not understand MESSAGE; not sent: MESSAGE $msg");
857     }
858 }
859
860 # covert a tapespec to a holding filename
861 sub tapespec_to_holding {
862     my $self = shift;
863     my ($tapespec) = @_;
864
865     my $filelist = Amanda::Util::unmarshal_tapespec($tapespec);
866
867     # $filelist should have the form [ $holding_file => [ 0 ] ]
868     die "invalid holding tapespec" unless @$filelist == 2;
869     die "invalid holding tapespec" unless @{$filelist->[1]} == 1;
870     die "invalid holding tapespec" unless $filelist->[1][0] == 0;
871
872     return $filelist->[0];
873 }
874
875 # amrecover didn't give us much to go on, but see if we can find a dump that
876 # will make it happy.
877 sub try_to_find_dump {
878     my $self = shift;
879     my ($label, $spec) = @_;
880
881     # search the catalog; get_dumps cannot search by labels, so we have to use
882     # get_parts instead
883     my @parts = Amanda::DB::Catalog::get_parts(
884         label => $label,
885         dumpspecs => [ $spec ]);
886
887     if (!@parts) {
888         $self->sendmessage("could not find any matching dumps on volume '$label'");
889         return undef;
890     }
891
892     # (note that if there is more than one dump in @parts, the planner will
893     # catch it later)
894
895     # sort the parts by their order on each volume.  This sorts the volumes
896     # lexically by label, but the planner will straighten it out.
897     @parts = Amanda::DB::Catalog::sort_dumps([ "label", "filenum" ], @parts);
898
899     # loop over the parts for the dump and make a filelist.
900     my $last_label = '';
901     my $last_filenums = undef;
902     my $filelist = [];
903     for my $part (@parts) {
904         next unless defined $part; # skip part number 0
905         if ($part->{'label'} ne $last_label) {
906             $last_label = $part->{'label'};
907             $last_filenums = [];
908             push @$filelist, $last_label, $last_filenums;
909         }
910         push @$last_filenums, $part->{'filenum'};
911     }
912
913     return $filelist;
914 }
915
916 ##
917 # main driver
918
919 package main;
920 use Amanda::Debug qw( debug );
921 use Amanda::Util qw( :constants );
922 use Amanda::Config qw( :init );
923
924 our $exit_status = 0;
925
926 sub main {
927     Amanda::Util::setup_application("amidxtaped", "server", $CONTEXT_DAEMON);
928     config_init(0, undef);
929     Amanda::Debug::debug_dup_stderr_to_debug();
930
931     my $cs = main::ClientService->new();
932     Amanda::MainLoop::call_later(sub { $cs->run(); });
933     Amanda::MainLoop::run();
934
935     debug("exiting with $exit_status");
936     Amanda::Util::finish_application();
937 }
938
939 main();
940 exit($exit_status);