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