2 # Copyright (c) 2010-2012 Zmanda, Inc. All Rights Reserved.
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.
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
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
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20 use lib '@amperldir@';
29 package main::ClientService;
30 use base 'Amanda::ClientService';
35 use Amanda::Debug qw( debug info warning );
36 use Amanda::Util qw( :constants );
38 use Amanda::Config qw( :init :getconf config_dir_relative );
42 use Amanda::Util qw( match_disk match_host );
44 # Note that this class performs its control IO synchronously. This is adequate
45 # for this service, as it never receives unsolicited input from the remote
51 $self->{'my_features'} = Amanda::Feature::Set->mine();
52 $self->{'their_features'} = Amanda::Feature::Set->old();
54 $self->setup_streams();
60 # always started from amandad.
61 my $req = $self->get_req();
63 # make some sanity checks
65 if (defined $req->{'options'}{'auth'} and defined $self->amandad_auth()
66 and $req->{'options'}{'auth'} ne $self->amandad_auth()) {
67 my $reqauth = $req->{'options'}{'auth'};
68 my $amauth = $self->amandad_auth();
69 push @$errors, "recover program requested auth '$reqauth', " .
70 "but amandad is using auth '$amauth'";
71 $main::exit_status = 1;
74 # and pull out the features, if given
75 if (defined($req->{'features'})) {
76 $self->{'their_features'} = $req->{'features'};
79 $self->send_rep(['CTL' => 'rw'], $errors);
80 return $self->quit() if (@$errors);
82 $self->{'ctl_stream'} = 'CTL';
84 $self->read_command();
90 if (defined $self->{'config'}) {
91 $self->sendctlline("ERROR duplicate CONFIG command");
96 config_init($CONFIG_INIT_EXPLICIT_NAME, $config);
97 my ($cfgerr_level, @cfgerr_errors) = config_errors();
98 if ($cfgerr_level >= $CFGERR_ERRORS) {
99 $self->sendctlline("ERROR configuration errors; aborting connection");
100 $self->{'abort'} = 1;
103 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER_PREFERRED);
106 my $diskfile = Amanda::Config::config_dir_relative(getconf($CNF_DISKFILE));
107 $cfgerr_level = Amanda::Disklist::read_disklist('filename' => $diskfile);
108 if ($cfgerr_level >= $CFGERR_ERRORS) {
109 $self->sendctlline("ERROR Errors processing disklist");
110 $self->{'abort'} = 1;
113 $self->{'config'} = $config;
121 $self->{'their_features'} = Amanda::Feature::Set->from_string($features);
123 my $featurestr = $self->{'my_features'}->as_string();
124 $featreply = "FEATURES $featurestr";
126 $self->sendctlline($featreply);
132 if (!defined $self->{'config'}) {
133 $self->sendctlline("CONFIG must be set before listing the disk");
137 for my $disk (@{$self->{'host'}->{'disks'}}) {
138 $self->sendctlline(Amanda::Util::quote_string($disk));
140 $self->sendctlline("ENDLIST");
145 my $qdiskname = shift;
146 my $diskname = Amanda::Util::unquote_string($qdiskname);
147 if (!defined $self->{'config'}) {
148 $self->sendctlline("CONFIG must be set before setting the disk");
152 for my $disk (@{$self->{'host'}->{'disks'}}) {
153 if ($disk eq $diskname) {
154 push @{$self->{'disk'}}, $diskname;
155 $self->sendctlline("DISK $diskname added");
164 if (!defined $self->{'config'}) {
165 $self->sendctlline("CONFIG must be set before doing a backup");
169 my $logdir = config_dir_relative(getconf($CNF_LOGDIR));
170 if (-f "$logdir/log" || -f "$logdir/amdump" || -f "$logdir/amflush") {
171 $self->sendctlline("BUSY Amanda is busy, retry later");
175 $self->sendctlline("DUMPING");
176 my @command = ("$sbindir/amdump", "--no-taper", "--from-client", $self->{'config'}, $self->{'host'}->{'hostname'});
177 if (defined $self->{'disk'}) {
178 @command = (@command, @{$self->{'disk'}});
181 debug("command: @command");
184 my $pid = open3($amdump_in, $amdump_out, $amdump_out, @command);
186 while (<$amdump_out>) {
188 $self->sendctlline($_);
190 $self->sendctlline("ENDDUMP");
196 if (!defined $self->{'config'}) {
197 $self->sendctlline("CONFIG must be set before doing a backup");
201 my $logdir = config_dir_relative(getconf($CNF_LOGDIR));
202 if (-f "$logdir/log" || -f "$logdir/amdump" || -f "$logdir/amflush") {
203 $self->sendctlline("BUSY Amanda is busy, retry later");
207 $self->sendctlline("CHECKING");
208 my @command = ("$sbindir/amcheck", "-c", $self->{'config'}, $self->{'host'}->{'hostname'});
209 if (defined $self->{'disk'}) {
210 @command = (@command, @{$self->{'disk'}});
213 debug("command: @command");
216 my $pid = open3($amcheck_in, $amcheck_out, $amcheck_out, @command);
218 while (<$amcheck_out>) {
220 $self->sendctlline($_);
222 $self->sendctlline("ENDCHECK");
227 my $ctl_stream = $self->{'ctl_stream'};
228 my $command = $self->{'command'} = {};
230 my @known_commands = qw(
231 CONFIG DUMP FEATURES LIST DISK);
232 while (!$self->{'abort'} and ($_ = $self->getline($ctl_stream))) {
238 if (/^CONFIG (.*)$/) {
239 $self->cmd_config($1);
240 } elsif (/^FEATURES (.*)$/) {
241 $self->cmd_features($1);
244 } elsif (/^DISK (.*)$/) {
246 } elsif (/^CHECK$/) {
251 $self->{'abort'} = 1;
253 $self->sendctlline("invalid command '$_'");
261 my @hosts = Amanda::Disklist::all_hosts();
262 my $peer = $ENV{'AMANDA_AUTHENTICATED_PEER'};
264 if (!defined($peer)) {
265 debug("no authenticated peer name is available; rejecting request.");
266 $self->sendctlline("no authenticated peer name is available; rejecting request.");
270 # try to find the host that match the connection
272 for my $host (@hosts) {
273 if (lc($peer) eq lc($host->{'hostname'})) {
275 $self->{'host'} = $host;
281 debug("The peer host '$peer' doesn't match a host in the disklist.");
282 $self->sendctlline("The peer host '$peer' doesn't match a host in the disklist.");
283 $self->{'abort'} = 1;
292 my $buf = Amanda::Util::full_read($self->rfd('main'), 1024);
296 # we've read main to EOF, so close it
297 $self->close('main', 'r');
299 return $self->{'req'} = $self->parse_req($req_str);
304 my ($streams, $errors) = @_;
307 # first, if there were errors in the REQ, report them
309 for my $err (@$errors) {
310 $rep .= "ERROR $err\n";
313 my $connline = $self->connect_streams(@$streams);
314 $rep .= "$connline\n";
316 # rep needs a empty-line terminator, I think
319 # write the whole rep packet, and close main to signal the end of the packet
320 $self->senddata('main', $rep);
321 $self->close('main', 'w');
324 # helper function to get a line, including the trailing '\n', from a stream. This
325 # reads a character at a time to ensure that no extra characters are consumed. This
326 # could certainly be more efficient! (TODO)
330 my $fd = $self->rfd($stream);
335 my $a = POSIX::read($fd, $c, 1);
343 $chopped =~ s/[\r\n]*$//g;
344 debug("CTL << $chopped");
352 # helper function to write a data to a stream. This does not add newline characters.
355 my ($stream, $data) = @_;
356 my $fd = $self->wfd($stream);
358 Amanda::Util::full_write($fd, $data, length($data))
359 or die "writing to $stream: $!";
362 # send a line on the control stream, or just log it if the ctl stream is gone;
363 # async callback is just like for senddata
368 if ($self->{'ctl_stream'}) {
369 debug("CTL >> $msg");
370 return $self->senddata($self->{'ctl_stream'}, $msg . "\n");
372 debug("not sending CTL message as CTL is closed >> $msg");
380 use Amanda::Debug qw( debug );
381 use Amanda::Util qw( :constants );
382 use Amanda::Config qw( :init );
384 our $exit_status = 0;
387 Amanda::Util::setup_application("amdumpd", "server", $CONTEXT_DAEMON);
388 config_init(0, undef);
389 Amanda::Debug::debug_dup_stderr_to_debug();
391 my $cs = main::ClientService->new();
394 debug("exiting with $exit_status");
395 Amanda::Util::finish_application();