1#! @PERL@
2# Copyright (c) 2010-2013 Zmanda, Inc.  All Rights Reserved.
3#
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.
8#
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
12# for more details.
13#
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
17#
18# Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
19# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20
21use lib '@amperldir@';
22use strict;
23use warnings;
24
25use Data::Dumper;
26
27##
28# ClientService class
29
30package main::ClientService;
31use base 'Amanda::ClientService';
32
33use Symbol;
34use IPC::Open3;
35
36use Amanda::Debug qw( debug info warning );
37use Amanda::Util qw( :constants );
38use Amanda::Feature;
39use Amanda::Config qw( :init :getconf config_dir_relative );
40use Amanda::Cmdline;
41use Amanda::Paths;
42use Amanda::Disklist;
43use Amanda::Util qw( match_disk match_host );
44
45# Note that this class performs its control IO synchronously.  This is adequate
46# for this service, as it never receives unsolicited input from the remote
47# system.
48
49sub run {
50    my $self = shift;
51
52    $self->{'my_features'} = Amanda::Feature::Set->mine();
53    $self->{'their_features'} = Amanda::Feature::Set->old();
54
55    $self->setup_streams();
56}
57
58sub setup_streams {
59    my $self = shift;
60
61    # always started from amandad.
62    my $req = $self->get_req();
63
64    # make some sanity checks
65    my $errors = [];
66    if (defined $req->{'options'}{'auth'} and defined $self->amandad_auth()
67	    and $req->{'options'}{'auth'} ne $self->amandad_auth()) {
68	my $reqauth = $req->{'options'}{'auth'};
69	my $amauth = $self->amandad_auth();
70	push @$errors, "recover program requested auth '$reqauth', " .
71		       "but amandad is using auth '$amauth'";
72	$main::exit_status = 1;
73    }
74
75    # and pull out the features, if given
76    if (defined($req->{'features'})) {
77	$self->{'their_features'} = $req->{'features'};
78    }
79
80    $self->send_rep(['CTL' => 'rw'], $errors);
81    return $self->quit() if (@$errors);
82
83    $self->{'ctl_stream'} = 'CTL';
84
85    $self->read_command();
86}
87
88sub cmd_config {
89    my $self = shift;
90
91    if (defined $self->{'config'}) {
92	$self->sendctlline("ERROR duplicate CONFIG command");
93	$self->{'abort'} = 1;
94	return;
95    }
96    my $config = $1;
97    config_init($CONFIG_INIT_EXPLICIT_NAME, $config);
98    my ($cfgerr_level, @cfgerr_errors) = config_errors();
99    if ($cfgerr_level >= $CFGERR_ERRORS) {
100	$self->sendctlline("ERROR configuration errors; aborting connection");
101	$self->{'abort'} = 1;
102	return;
103    }
104    Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER_PREFERRED);
105
106    # and the disklist
107    my $diskfile = Amanda::Config::config_dir_relative(getconf($CNF_DISKFILE));
108    $cfgerr_level = Amanda::Disklist::read_disklist('filename' => $diskfile);
109    if ($cfgerr_level >= $CFGERR_ERRORS) {
110	$self->sendctlline("ERROR Errors processing disklist");
111	$self->{'abort'} = 1;
112	return;
113    }
114    $self->{'config'} = $config;
115    $self->check_host();
116}
117
118sub cmd_features {
119    my $self = shift;
120    my $features;
121
122    $self->{'their_features'} = Amanda::Feature::Set->from_string($features);
123    my $featreply;
124    my $featurestr = $self->{'my_features'}->as_string();
125    $featreply = "FEATURES $featurestr";
126
127    $self->sendctlline($featreply);
128}
129
130sub cmd_list {
131    my $self = shift;
132
133    if (!defined $self->{'config'}) {
134	$self->sendctlline("CONFIG must be set before listing the disk");
135	return;
136    }
137
138    for my $disk (@{$self->{'host'}->{'disks'}}) {
139	$self->sendctlline(Amanda::Util::quote_string($disk));
140    }
141    $self->sendctlline("ENDLIST");
142}
143
144sub cmd_disk {
145    my $self = shift;
146    my $qdiskname = shift;
147    my $diskname = Amanda::Util::unquote_string($qdiskname);
148    if (!defined $self->{'config'}) {
149	$self->sendctlline("CONFIG must be set before setting the disk");
150	return;
151    }
152
153    for my $disk (@{$self->{'host'}->{'disks'}}) {
154	if ($disk eq $diskname) {
155	    push @{$self->{'disk'}}, $diskname;
156	    $self->sendctlline("DISK $diskname added");
157	    last;
158	}
159    }
160}
161
162sub cmd_dump {
163    my $self = shift;
164
165    if (!defined $self->{'config'}) {
166	$self->sendctlline("CONFIG must be set before doing a backup");
167	return;
168    }
169
170    my $logdir = config_dir_relative(getconf($CNF_LOGDIR));
171    if (-f "$logdir/log" || -f "$logdir/amdump" || -f "$logdir/amflush") {
172	$self->sendctlline("BUSY Amanda is busy, retry later");
173	return;
174    }
175
176    $self->sendctlline("DUMPING");
177    my @command = ("$sbindir/amdump", "--no-taper", "--from-client", $self->{'config'}, $self->{'host'}->{'hostname'});
178    if (defined $self->{'disk'}) {
179	@command = (@command, @{$self->{'disk'}});
180    }
181
182    debug("command: @command");
183    my $amdump_out;
184    my $amdump_in;
185    my $pid = open3($amdump_in, $amdump_out, $amdump_out, @command);
186    close($amdump_in);
187    while (<$amdump_out>) {
188	chomp;
189	$self->sendctlline($_);
190    }
191    $self->sendctlline("ENDDUMP");
192}
193
194sub cmd_check {
195    my $self = shift;
196
197    if (!defined $self->{'config'}) {
198	$self->sendctlline("CONFIG must be set before doing a backup");
199	return;
200    }
201
202    my $logdir = config_dir_relative(getconf($CNF_LOGDIR));
203    if (-f "$logdir/log" || -f "$logdir/amdump" || -f "$logdir/amflush") {
204	$self->sendctlline("BUSY Amanda is busy, retry later");
205	return;
206    }
207
208    $self->sendctlline("CHECKING");
209    my @command = ("$sbindir/amcheck", "-c", $self->{'config'}, $self->{'host'}->{'hostname'});
210    if (defined $self->{'disk'}) {
211	@command = (@command, @{$self->{'disk'}});
212    }
213
214    debug("command: @command");
215    my $amcheck_out;
216    my $amcheck_in;
217    my $pid = open3($amcheck_in, $amcheck_out, $amcheck_out, @command);
218    close($amcheck_in);
219    while (<$amcheck_out>) {
220	chomp;
221	$self->sendctlline($_);
222    }
223    $self->sendctlline("ENDCHECK");
224}
225
226sub read_command {
227    my $self = shift;
228    my $ctl_stream = $self->{'ctl_stream'};
229    my $command = $self->{'command'} = {};
230
231    my @known_commands = qw(
232	CONFIG DUMP FEATURES LIST DISK);
233    while (!$self->{'abort'} and ($_ = $self->getline($ctl_stream))) {
234	$_ =~ s/\r?\n$//g;
235
236	last if /^END$/;
237	last if /^[0-9]+$/;
238
239	if (/^CONFIG (.*)$/) {
240	    $self->cmd_config($1);
241	} elsif (/^FEATURES (.*)$/) {
242	    $self->cmd_features($1);
243	} elsif (/^LIST$/) {
244	    $self->cmd_list();
245	} elsif (/^DISK (.*)$/) {
246	    $self->cmd_disk($1);
247	} elsif (/^CHECK$/) {
248	    $self->cmd_check();
249	} elsif (/^DUMP$/) {
250	    $self->cmd_dump();
251	} elsif (/^END$/) {
252	    $self->{'abort'} = 1;
253	} else {
254	    $self->sendctlline("invalid command '$_'");
255	}
256    }
257}
258
259sub check_host {
260    my $self = shift;
261
262    my @hosts = Amanda::Disklist::all_hosts();
263    my $peer = $ENV{'AMANDA_AUTHENTICATED_PEER'};
264
265    if (!defined($peer)) {
266	debug("no authenticated peer name is available; rejecting request.");
267	$self->sendctlline("no authenticated peer name is available; rejecting request.");
268	die();
269    }
270
271    # try to find the host that match the connection
272    my $matched = 0;
273    for my $host (@hosts) {
274	if (lc($peer) eq lc($host->{'hostname'})) {
275	    $matched = 1;
276	    $self->{'host'} = $host;
277	    last;
278	}
279    }
280
281    if (!$matched) {
282	debug("The peer host '$peer' doesn't match a host in the disklist.");
283	$self->sendctlline("The peer host '$peer' doesn't match a host in the disklist.");
284	$self->{'abort'} = 1;
285    }
286}
287
288sub get_req {
289    my $self = shift;
290
291    my $req_str = '';
292    while (1) {
293	my $buf = Amanda::Util::full_read($self->rfd('main'), 1024);
294	last unless $buf;
295	$req_str .= $buf;
296    }
297    # we've read main to EOF, so close it
298    $self->close('main', 'r');
299
300    return $self->{'req'} = $self->parse_req($req_str);
301}
302
303sub send_rep {
304    my $self = shift;
305    my ($streams, $errors) = @_;
306    my $rep = '';
307
308    # first, if there were errors in the REQ, report them
309    if (@$errors) {
310	for my $err (@$errors) {
311	    $rep .= "ERROR $err\n";
312	}
313    } else {
314	my $connline = $self->connect_streams(@$streams);
315	$rep .= "$connline\n";
316    }
317    # rep needs a empty-line terminator, I think
318    $rep .= "\n";
319
320    # write the whole rep packet, and close main to signal the end of the packet
321    $self->senddata('main', $rep);
322    $self->close('main', 'w');
323}
324
325# helper function to get a line, including the trailing '\n', from a stream.  This
326# reads a character at a time to ensure that no extra characters are consumed.  This
327# could certainly be more efficient! (TODO)
328sub getline {
329    my $self = shift;
330    my ($stream) = @_;
331    my $fd = $self->rfd($stream);
332    my $line = undef;
333
334    while (1) {
335	my $c;
336	my $a = POSIX::read($fd, $c, 1);
337	last if $a != 1;
338	$line .= $c;
339	last if $c eq "\n";
340    }
341
342    if ($line) {
343	my $chopped = $line;
344	$chopped =~ s/[\r\n]*$//g;
345	debug("CTL << $chopped");
346    } else {
347	debug("CTL << EOF");
348    }
349
350    return $line;
351}
352
353# helper function to write a data to a stream.  This does not add newline characters.
354sub senddata {
355    my $self = shift;
356    my ($stream, $data) = @_;
357    my $fd = $self->wfd($stream);
358
359    Amanda::Util::full_write($fd, $data, length($data))
360	    or die "writing to $stream: $!";
361}
362
363# send a line on the control stream, or just log it if the ctl stream is gone;
364# async callback is just like for senddata
365sub sendctlline {
366    my $self = shift;
367    my ($msg) = @_;
368
369    if ($self->{'ctl_stream'}) {
370	debug("CTL >> $msg");
371	return $self->senddata($self->{'ctl_stream'}, $msg . "\n");
372    } else {
373	debug("not sending CTL message as CTL is closed >> $msg");
374    }
375}
376
377##
378# main driver
379
380package main;
381use Amanda::Debug qw( debug );
382use Amanda::Util qw( :constants );
383use Amanda::Config qw( :init );
384
385our $exit_status = 0;
386
387sub main {
388    Amanda::Util::setup_application("amdumpd", "server", $CONTEXT_DAEMON);
389    config_init(0, undef);
390    Amanda::Debug::debug_dup_stderr_to_debug();
391
392    my $cs = main::ClientService->new();
393    $cs->run();
394
395    debug("exiting with $exit_status");
396    Amanda::Util::finish_application();
397}
398
399main();
400exit($exit_status);
401