7e3bba019787be00a38377a7041431215659c8d7
[debian/amanda] / server-src / amdumpd.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 use Data::Dumper;
25
26 ##
27 # ClientService class
28
29 package main::ClientService;
30 use base 'Amanda::ClientService';
31
32 use Symbol;
33 use IPC::Open3;
34
35 use Amanda::Debug qw( debug info warning );
36 use Amanda::Util qw( :constants );
37 use Amanda::Feature;
38 use Amanda::Config qw( :init :getconf config_dir_relative );
39 use Amanda::Cmdline;
40 use Amanda::Paths;
41 use Amanda::Disklist;
42 use Amanda::Util qw( match_disk match_host );
43
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
46 # system.
47
48 sub run {
49     my $self = shift;
50
51     $self->{'my_features'} = Amanda::Feature::Set->mine();
52     $self->{'their_features'} = Amanda::Feature::Set->old();
53
54     $self->setup_streams();
55 }
56
57 sub setup_streams {
58     my $self = shift;
59
60     # always started from amandad.
61     my $req = $self->get_req();
62
63     # make some sanity checks
64     my $errors = [];
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;
72     }
73
74     # and pull out the features, if given
75     if (defined($req->{'features'})) {
76         $self->{'their_features'} = $req->{'features'};
77     }
78
79     $self->send_rep(['CTL' => 'rw'], $errors);
80     return $self->quit() if (@$errors);
81
82     $self->{'ctl_stream'} = 'CTL';
83
84     $self->read_command();
85 }
86
87 sub cmd_config {
88     my $self = shift;
89
90     if (defined $self->{'config'}) {
91         $self->sendctlline("ERROR duplicate CONFIG command");
92         $self->{'abort'} = 1;
93         return;
94     }
95     my $config = $1;
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;
101         return;
102     }
103     Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER_PREFERRED);
104
105     # and the disklist
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;
111         return;
112     }
113     $self->{'config'} = $config;
114     $self->check_host();
115 }
116
117 sub cmd_features {
118     my $self = shift;
119     my $features;
120
121     $self->{'their_features'} = Amanda::Feature::Set->from_string($features);
122     my $featreply;
123     my $featurestr = $self->{'my_features'}->as_string();
124     $featreply = "FEATURES $featurestr";
125
126     $self->sendctlline($featreply);
127 }
128
129 sub cmd_list {
130     my $self = shift;
131
132     if (!defined $self->{'config'}) {
133         $self->sendctlline("CONFIG must be set before listing the disk");
134         return;
135     }
136
137     for my $disk (@{$self->{'host'}->{'disks'}}) {
138         $self->sendctlline(Amanda::Util::quote_string($disk));
139     }
140     $self->sendctlline("ENDLIST");
141 }
142
143 sub cmd_disk {
144     my $self = shift;
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");
149         return;
150     }
151
152     for my $disk (@{$self->{'host'}->{'disks'}}) {
153         if ($disk eq $diskname) {
154             push @{$self->{'disk'}}, $diskname;
155             $self->sendctlline("DISK $diskname added");
156             last;
157         }
158     }
159 }
160
161 sub cmd_dump {
162     my $self = shift;
163
164     if (!defined $self->{'config'}) {
165         $self->sendctlline("CONFIG must be set before doing a backup");
166         return;
167     }
168
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");
172         return;
173     }
174
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'}});
179     }
180
181     debug("command: @command");
182     my $amdump_out;
183     my $amdump_in;
184     my $pid = open3($amdump_in, $amdump_out, $amdump_out, @command);
185     close($amdump_in);
186     while (<$amdump_out>) {
187         chomp;
188         $self->sendctlline($_);
189     }
190     $self->sendctlline("ENDDUMP");
191 }
192
193 sub cmd_check {
194     my $self = shift;
195
196     if (!defined $self->{'config'}) {
197         $self->sendctlline("CONFIG must be set before doing a backup");
198         return;
199     }
200
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");
204         return;
205     }
206
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'}});
211     }
212
213     debug("command: @command");
214     my $amcheck_out;
215     my $amcheck_in;
216     my $pid = open3($amcheck_in, $amcheck_out, $amcheck_out, @command);
217     close($amcheck_in);
218     while (<$amcheck_out>) {
219         chomp;
220         $self->sendctlline($_);
221     }
222     $self->sendctlline("ENDCHECK");
223 }
224
225 sub read_command {
226     my $self = shift;
227     my $ctl_stream = $self->{'ctl_stream'};
228     my $command = $self->{'command'} = {};
229
230     my @known_commands = qw(
231         CONFIG DUMP FEATURES LIST DISK);
232     while (!$self->{'abort'} and ($_ = $self->getline($ctl_stream))) {
233         $_ =~ s/\r?\n$//g;
234
235         last if /^END$/;
236         last if /^[0-9]+$/;
237
238         if (/^CONFIG (.*)$/) {
239             $self->cmd_config($1);
240         } elsif (/^FEATURES (.*)$/) {
241             $self->cmd_features($1);
242         } elsif (/^LIST$/) {
243             $self->cmd_list();
244         } elsif (/^DISK (.*)$/) {
245             $self->cmd_disk($1);
246         } elsif (/^CHECK$/) {
247             $self->cmd_check();
248         } elsif (/^DUMP$/) {
249             $self->cmd_dump();
250         } elsif (/^END$/) {
251             $self->{'abort'} = 1;
252         } else {
253             $self->sendctlline("invalid command '$_'");
254         }
255     }
256 }
257
258 sub check_host {
259     my $self = shift;
260
261     my @hosts = Amanda::Disklist::all_hosts();
262     my $peer = $ENV{'AMANDA_AUTHENTICATED_PEER'};
263
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.");
267         die();
268     }
269
270     # try to find the host that match the connection
271     my $matched = 0;
272     for my $host (@hosts) {
273         if (lc($peer) eq lc($host->{'hostname'})) {
274             $matched = 1;
275             $self->{'host'} = $host;
276             last;
277         }
278     }
279
280     if (!$matched) {
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;
284     }
285 }
286
287 sub get_req {
288     my $self = shift;
289
290     my $req_str = '';
291     while (1) {
292         my $buf = Amanda::Util::full_read($self->rfd('main'), 1024);
293         last unless $buf;
294         $req_str .= $buf;
295     }
296     # we've read main to EOF, so close it
297     $self->close('main', 'r');
298
299     return $self->{'req'} = $self->parse_req($req_str);
300 }
301
302 sub send_rep {
303     my $self = shift;
304     my ($streams, $errors) = @_;
305     my $rep = '';
306
307     # first, if there were errors in the REQ, report them
308     if (@$errors) {
309         for my $err (@$errors) {
310             $rep .= "ERROR $err\n";
311         }
312     } else {
313         my $connline = $self->connect_streams(@$streams);
314         $rep .= "$connline\n";
315     }
316     # rep needs a empty-line terminator, I think
317     $rep .= "\n";
318
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');
322 }
323
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)
327 sub getline {
328     my $self = shift;
329     my ($stream) = @_;
330     my $fd = $self->rfd($stream);
331     my $line = undef;
332
333     while (1) {
334         my $c;
335         my $a = POSIX::read($fd, $c, 1);
336         last if $a != 1;
337         $line .= $c;
338         last if $c eq "\n";
339     }
340
341     if ($line) {
342         my $chopped = $line;
343         $chopped =~ s/[\r\n]*$//g;
344         debug("CTL << $chopped");
345     } else {
346         debug("CTL << EOF");
347     }
348
349     return $line;
350 }
351
352 # helper function to write a data to a stream.  This does not add newline characters.
353 sub senddata {
354     my $self = shift;
355     my ($stream, $data) = @_;
356     my $fd = $self->wfd($stream);
357
358     Amanda::Util::full_write($fd, $data, length($data))
359             or die "writing to $stream: $!";
360 }
361
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
364 sub sendctlline {
365     my $self = shift;
366     my ($msg) = @_;
367
368     if ($self->{'ctl_stream'}) {
369         debug("CTL >> $msg");
370         return $self->senddata($self->{'ctl_stream'}, $msg . "\n");
371     } else {
372         debug("not sending CTL message as CTL is closed >> $msg");
373     }
374 }
375
376 ##
377 # main driver
378
379 package main;
380 use Amanda::Debug qw( debug );
381 use Amanda::Util qw( :constants );
382 use Amanda::Config qw( :init );
383
384 our $exit_status = 0;
385
386 sub main {
387     Amanda::Util::setup_application("amdumpd", "server", $CONTEXT_DAEMON);
388     config_init(0, undef);
389     Amanda::Debug::debug_dup_stderr_to_debug();
390
391     my $cs = main::ClientService->new();
392     $cs->run();
393
394     debug("exiting with $exit_status");
395     Amanda::Util::finish_application();
396 }
397
398 main();
399 exit($exit_status);