Imported Upstream version 3.3.3
[debian/amanda] / server-src / amdumpd.pl
1 #! @PERL@
2 # Copyright (c) 2010-2012 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
21 use lib '@amperldir@';
22 use strict;
23 use warnings;
24
25 use Data::Dumper;
26
27 ##
28 # ClientService class
29
30 package main::ClientService;
31 use base 'Amanda::ClientService';
32
33 use Symbol;
34 use IPC::Open3;
35
36 use Amanda::Debug qw( debug info warning );
37 use Amanda::Util qw( :constants );
38 use Amanda::Feature;
39 use Amanda::Config qw( :init :getconf config_dir_relative );
40 use Amanda::Cmdline;
41 use Amanda::Paths;
42 use Amanda::Disklist;
43 use 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
49 sub 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
58 sub 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
88 sub 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
118 sub 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
130 sub 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
144 sub 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
162 sub 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
194 sub 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
226 sub 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
259 sub 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
288 sub 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
303 sub 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)
328 sub 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.
354 sub 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
365 sub 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
380 package main;
381 use Amanda::Debug qw( debug );
382 use Amanda::Util qw( :constants );
383 use Amanda::Config qw( :init );
384
385 our $exit_status = 0;
386
387 sub 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
399 main();
400 exit($exit_status);