#!/bin/perl #============================================================= -*-perl-*- # # BackupPC_restore: Restore files to a client. # # DESCRIPTION # # Usage: BackupPC_restore # # AUTHOR # Craig Barratt # # COPYRIGHT # Copyright (C) 2001-2003 Craig Barratt # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # #======================================================================== # # Version 3.0.0beta2, released 11 Nov 2006. # # See http://backuppc.sourceforge.net. # #======================================================================== use strict; no utf8; use lib "/usr/local/BackupPC/lib"; use BackupPC::Lib; use BackupPC::FileZIO; use BackupPC::Xfer::Smb; use BackupPC::Xfer::Tar; use BackupPC::Xfer::Rsync; use BackupPC::Xfer::BackupPCd; use Socket; use File::Path; use Getopt::Std; use vars qw( %RestoreReq ); ########################################################################### # Initialize ########################################################################### die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) ); my $TopDir = $bpc->TopDir(); my $BinDir = $bpc->BinDir(); my %Conf = $bpc->Conf(); my $NeedPostCmd; my($hostIP, $host, $client, $reqFileName, %stat); $bpc->ChildInit(); if ( @ARGV != 3 ) { print("usage: $0 \n"); exit(1); } $hostIP = $1 if ( $ARGV[0] =~ /(.+)/ ); $client = $1 if ( $ARGV[1] =~ /(.+)/ ); if ( $ARGV[2] !~ /^([\w.]+)$/ ) { print("$0: bad reqFileName (arg #3): $ARGV[2]\n"); exit(1); } $reqFileName = $1; my $startTime = time(); my $Hosts = $bpc->HostInfoRead($client); my $Dir = "$TopDir/pc/$client"; my @xferPid = (); my $tarPid = -1; # # Catch various signals # $SIG{INT} = \&catch_signal; $SIG{ALRM} = \&catch_signal; $SIG{TERM} = \&catch_signal; $SIG{PIPE} = \&catch_signal; $SIG{STOP} = \&catch_signal; $SIG{TSTP} = \&catch_signal; $SIG{TTIN} = \&catch_signal; my $Pid = $$; mkpath($Dir, 0, 0777) if ( !-d $Dir ); if ( !-f "$Dir/LOCK" ) { open(LOCK, ">", "$Dir/LOCK") && close(LOCK); } my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); my $logPath = sprintf("$Dir/LOG.%02d%04d", $mon + 1, $year + 1900); if ( !-f $logPath ) { # # Compress and prune old log files # my $lastLog = $Conf{MaxOldPerPCLogFiles} - 1; foreach my $file ( $bpc->sortedPCLogFiles($client) ) { if ( $lastLog <= 0 ) { unlink($file); next; } $lastLog--; next if ( $file =~ /\.z$/ || !$Conf{CompressLevel} ); BackupPC::FileZIO->compressCopy($file, "$file.z", undef, $Conf{CompressLevel}, 1); } } open(LOG, ">>", $logPath); select(LOG); $| = 1; select(STDOUT); # # Read the request file # if ( !(my $ret = do "$Dir/$reqFileName") ) { my $err; if ( $@ ) { $err = "couldn't parse $Dir/$reqFileName: $@"; } elsif ( !defined($ret) ) { $err = "couldn't do $Dir/$reqFileName: $!"; } else { $err = "couldn't run $Dir/$reqFileName"; } $stat{hostError} = $err; exit(RestoreCleanup($client)); } # # Re-read config file, so we can include the PC-specific config # if ( defined(my $error = $bpc->ConfigRead($client)) ) { $stat{hostError} = "Can't read PC's config file: $error"; exit(RestoreCleanup($client)); } %Conf = $bpc->Conf(); # # Make sure we eventually timeout if there is no activity from # the data transport program. # alarm($Conf{ClientTimeout}); # # See if the host name is aliased # if ( $Conf{ClientNameAlias} ne "" ) { $host = $Conf{ClientNameAlias}; } else { $host = $client; } # # Find its IP address # if ( $hostIP !~ /\d+\.\d+\.\d+\.\d+/ ) { if ( !defined(gethostbyname($host)) ) { # # Ok, NS doesn't know about it. Maybe it is a NetBios name # instead. # if ( !defined($hostIP = $bpc->NetBiosHostIPFind($host)) ) { $stat{hostError} = "Can't find host $host"; exit(RestoreCleanup($client)); } } else { $hostIP = $host; } } # # Check if $host is alive # my $delay = $bpc->CheckHostAlive($hostIP); if ( $delay < 0 ) { $stat{hostError} = "no ping response from $host ($hostIP)"; exit(RestoreCleanup($client)); } elsif ( $delay > $Conf{PingMaxMsec} ) { $stat{hostError} = sprintf("ping too slow: %.4gmsec (max is %gmsec)\n", $delay, $Conf{PingMaxMsec}); exit(RestoreCleanup($client)); } # # Make sure it is really the machine we expect # if ( (my $errMsg = CorrectHostCheck($hostIP, $host)) ) { $stat{hostError} = $errMsg; exit(RestoreCleanup($client)); } # # Setup file extension for compression and open RestoreLOG output file # if ( $Conf{CompressLevel} && !BackupPC::FileZIO->compOk ) { $stat{hostError} = "Compress:Zlib not found"; exit(RestoreCleanup($client)); } my $fileExt = $Conf{CompressLevel} > 0 ? ".z" : ""; my $RestoreLOG = BackupPC::FileZIO->open("$Dir/RestoreLOG$fileExt", 1, $Conf{CompressLevel}); my $tarCreateFileCnt = 0; my $tarCreateByteCnt = 0; my $tarCreateDirCnt = 0; my $tarCreateErrCnt = 1; # assume not ok until we learn otherwise my $tarCreateErr; my($logMsg, $xfer); $stat{xferOK} = $stat{hostAbort} = undef; $stat{hostError} = $stat{lastOutputLine} = undef; local(*RH, *WH); # # Run an optional pre-restore command # UserCommandRun("RestorePreUserCmd"); if ( $? && $Conf{UserCmdCheckStatus} ) { $stat{hostError} = "RestorePreUserCmd returned error status $?"; exit(RestoreCleanup($client)); } $NeedPostCmd = 1; if ( $Conf{XferMethod} eq "tar" ) { # # Use tar (eg: tar/ssh) as the transport program. # $xfer = BackupPC::Xfer::Tar->new($bpc); } elsif ( $Conf{XferMethod} eq "rsync" || $Conf{XferMethod} eq "rsyncd" ) { # # Use rsync as the transport program. # if ( !defined($xfer = BackupPC::Xfer::Rsync->new($bpc)) ) { my $errStr = BackupPC::Xfer::Rsync->errStr; UserCommandRun("RestorePostUserCmd") if ( $NeedPostCmd ); $stat{hostError} = $errStr; exit(RestoreCleanup($client)); } } elsif ( $Conf{XferMethod} eq "backuppcd" ) { # # Use backuppcd as the transport program. # if ( !defined($xfer = BackupPC::Xfer::BackupPCd->new($bpc)) ) { my $errStr = BackupPC::Xfer::BackupPCd->errStr; UserCommandRun("RestorePostUserCmd") if ( $NeedPostCmd ); $stat{hostError} = $errStr; exit(RestoreCleanup($client)); } } else { # # Default is to use smbclient (smb) as the transport program. # $xfer = BackupPC::Xfer::Smb->new($bpc); } my $useTar = $xfer->useTar; if ( $useTar ) { # # Create a socketpair to connect BackupPC_tarCreate to the transport # program (smbclient, tar, etc). # WH is the write handle for writing, provided to BackupPC_tarCreate # and RH is the other end of the pipe for reading provided to the # transport program. # if ( socketpair(RH, WH, AF_UNIX, SOCK_STREAM, PF_UNSPEC) ) { shutdown(RH, 1); # no writing to this socket shutdown(WH, 0); # no reading from this socket setsockopt(RH, SOL_SOCKET, SO_RCVBUF, 8 * 65536); setsockopt(WH, SOL_SOCKET, SO_SNDBUF, 8 * 65536); } else { # # Default to pipe() if socketpair() doesn't work. # pipe(RH, WH); } } # # Run the transport program, which reads from RH and extracts the data. # my @Backups = $bpc->BackupInfoRead($RestoreReq{hostSrc}); my $xferArgs = { client => $client, host => $host, hostIP => $hostIP, type => "restore", shareName => $RestoreReq{shareDest}, pipeRH => *RH, pipeWH => *WH, XferLOG => $RestoreLOG, XferMethod => $Conf{XferMethod}, logLevel => $Conf{XferLogLevel}, bkupSrcHost => $RestoreReq{hostSrc}, bkupSrcShare => $RestoreReq{shareSrc}, bkupSrcNum => $RestoreReq{num}, backups => \@Backups, pathHdrSrc => $RestoreReq{pathHdrSrc}, pathHdrDest => $RestoreReq{pathHdrDest}, fileList => $RestoreReq{fileList}, pidHandler => \&pidHandler, }; $xfer->args($xferArgs); if ( !defined($logMsg = $xfer->start()) ) { UserCommandRun("RestorePostUserCmd") if ( $NeedPostCmd ); $stat{hostError} = "xfer start failed: ", $xfer->errStr; exit(RestoreCleanup($client)); } if ( $useTar ) { # # Now do the restore by running BackupPC_tarCreate # # The parent must close the read handle since the transport program # is using it. # close(RH); # # fork a child for BackupPC_tarCreate. TAR is a file handle # on which we (the parent) read the stderr from BackupPC_tarCreate. # my @tarPathOpts; if ( defined($RestoreReq{pathHdrDest}) && $RestoreReq{pathHdrDest} ne $RestoreReq{pathHdrSrc} ) { @tarPathOpts = ("-r", $RestoreReq{pathHdrSrc}, "-p", $RestoreReq{pathHdrDest} ); } my @tarArgs = ( "-h", $RestoreReq{hostSrc}, "-n", $RestoreReq{num}, "-s", $RestoreReq{shareSrc}, "-t", @tarPathOpts, @{$RestoreReq{fileList}}, ); my $runMsg = "Running: " . $bpc->execCmd2ShellCmd("$BinDir/BackupPC_tarCreate", @tarArgs) . "\n"; $RestoreLOG->write(\$runMsg); if ( !defined($tarPid = open(TAR, "-|")) ) { close(WH); # FIX: need to cleanup xfer UserCommandRun("RestorePostUserCmd") if ( $NeedPostCmd ); $stat{hostError} = "Can't fork to run tar"; exit(RestoreCleanup($client)); } binmode(TAR); if ( !$tarPid ) { # # This is the tarCreate child. Clone STDERR to STDOUT, # STDOUT to WH, and then exec BackupPC_tarCreate. # setpgrp 0,0; close(STDERR); open(STDERR, ">&STDOUT"); close(STDOUT); open(STDOUT, ">&WH"); alarm(0); exec("$BinDir/BackupPC_tarCreate", @tarArgs); print(LOG $bpc->timeStamp, "can't exec $BinDir/BackupPC_tarCreate\n"); # FIX: need to cleanup xfer exit(0); } # # The parent must close the write handle since BackupPC_tarCreate # is using it. # close(WH); @xferPid = $xfer->xferPid; print(LOG $bpc->timeStamp, $logMsg, "\n"); print("started_restore\n"); pidHandler(@xferPid); # # Parse the output of the transfer program and BackupPC_tarCreate # while they run. Since we are reading from two or more children # we use a select. # my($FDread, $tarOut, $mesg); vec($FDread, fileno(TAR), 1) = 1; $xfer->setSelectMask(\$FDread); SCAN: while ( 1 ) { my $ein = $FDread; last if ( $FDread =~ /^\0*$/ ); alarm($Conf{ClientTimeout}); select(my $rout = $FDread, undef, $ein, undef); if ( vec($rout, fileno(TAR), 1) ) { if ( sysread(TAR, $mesg, 8192) <= 0 ) { vec($FDread, fileno(TAR), 1) = 0; if ( !close(TAR) ) { $tarCreateErrCnt = 1; $tarCreateErr = "BackupPC_tarCreate failed"; } } else { $tarOut .= $mesg; } } while ( $tarOut =~ /(.*?)[\n\r]+(.*)/s ) { $_ = $1; $tarOut = $2; $RestoreLOG->write(\"tarCreate: $_\n"); if ( /^Done: (\d+) files, (\d+) bytes, (\d+) dirs, (\d+) specials, (\d+) errors/ ) { $tarCreateFileCnt = $1; $tarCreateByteCnt = $2; $tarCreateDirCnt = $3; $tarCreateErrCnt = $5; } } last if ( !$xfer->readOutput(\$FDread, $rout) ); while ( my $str = $xfer->logMsgGet ) { print(LOG $bpc->timeStamp, "xfer: $str\n"); } if ( $xfer->getStats->{fileCnt} == 1 ) { # # Make sure it is still the machine we expect. We do this while # the transfer is running to avoid a potential race condition if # the ip address was reassigned by dhcp just before we started # the transfer. # if ( my $errMsg = CorrectHostCheck($hostIP, $host) ) { $stat{hostError} = $errMsg; last SCAN; } } } } else { # # otherwise the xfer module does everything for us # print(LOG $bpc->timeStamp, $logMsg . "\n"); print("started_restore\n"); ($tarCreateFileCnt, $tarCreateByteCnt, $tarCreateErrCnt, $tarCreateErr) = $xfer->run(); } alarm(0); # # Merge the xfer status (need to accumulate counts) # my $newStat = $xfer->getStats; foreach my $k ( (keys(%stat), keys(%$newStat)) ) { next if ( !defined($newStat->{$k}) ); if ( $k =~ /Cnt$/ ) { $stat{$k} += $newStat->{$k}; delete($newStat->{$k}); next; } if ( !defined($stat{$k}) ) { $stat{$k} = $newStat->{$k}; delete($newStat->{$k}); next; } } exit(RestoreCleanup($client)); ########################################################################### # Subroutines ########################################################################### sub CorrectHostCheck { my($hostIP, $host) = @_; return if ( $hostIP eq $host && !$Conf{FixedIPNetBiosNameCheck} || $Conf{NmbLookupCmd} eq "" ); my($netBiosHost, $netBiosUser) = $bpc->NetBiosInfoGet($hostIP); return "host $host has mismatching netbios name $netBiosHost" if ( $netBiosHost ne $host ); return; } sub catch_signal { my $signame = shift; # # Children quit quietly on ALRM # exit(1) if ( $Pid != $$ && $signame eq "ALRM" ); # # Ignore signals in children # return if ( $Pid != $$ ); # # Note: needs to be tested for each kind of XferMethod # print(LOG $bpc->timeStamp, "cleaning up after signal $signame\n"); $SIG{$signame} = 'IGNORE'; $RestoreLOG->write(\"exiting after signal $signame\n"); $stat{xferOK} = 0; if ( $signame eq "INT" ) { $stat{hostError} = "aborted by user (signal=$signame)"; } else { $stat{hostError} = "aborted by signal=$signame"; } exit(RestoreCleanup($client)); } # # Cleanup and update the restore status # sub RestoreCleanup { my($client) = @_; $stat{xferOK} = 0 if ( $stat{hostError} || $stat{hostAbort} || $tarCreateErr ); if ( !$stat{xferOK} ) { # # kill off the tranfer program, first nicely then forcefully # if ( @xferPid ) { kill($bpc->sigName2num("INT"), @xferPid); sleep(1); kill($bpc->sigName2num("KILL"), @xferPid); } # # kill off the tar process, first nicely then forcefully # if ( $tarPid > 0 ) { kill($bpc->sigName2num("INT"), $tarPid); sleep(1); kill($bpc->sigName2num("KILL"), $tarPid); } } my $lastNum = -1; my @Restores; # # Do one last check to make sure it is still the machine we expect. # if ( $stat{xferOK} && (my $errMsg = CorrectHostCheck($hostIP, $host)) ) { $stat{hostError} = $errMsg; $stat{xferOK} = 0; } @Restores = $bpc->RestoreInfoRead($client); for ( my $i = 0 ; $i < @Restores ; $i++ ) { $lastNum = $Restores[$i]{num} if ( $lastNum < $Restores[$i]{num} ); } $lastNum++; # # Run an optional post-restore command # if ( $NeedPostCmd ) { UserCommandRun("RestorePostUserCmd"); if ( $? && $Conf{UserCmdCheckStatus} ) { $stat{hostError} = "RestorePostUserCmd returned error status $?"; $stat{xferOK} = 0; } } rename("$Dir/RestoreLOG$fileExt", "$Dir/RestoreLOG.$lastNum$fileExt"); rename("$Dir/$reqFileName", "$Dir/RestoreInfo.$lastNum"); my $endTime = time(); # # If the restore failed, clean up # if ( !$stat{xferOK} ) { # # wait a short while and see if the system is still alive # $stat{hostError} ||= $tarCreateErr if ( $tarCreateErr ne "" ); $stat{hostError} = $stat{lastOutputLine} if ( $stat{hostError} eq "" ); sleep(2); if ( $bpc->CheckHostAlive($hostIP) < 0 ) { $stat{hostAbort} = 1; } if ( $stat{hostAbort} && $stat{hostError} eq "" ) { $stat{hostError} = "lost network connection during restore"; } $RestoreLOG->write(\"restore failed: $stat{hostError}\n") if ( defined($RestoreLOG) ); } $RestoreLOG->close() if ( defined($RestoreLOG) ); # # Add the new restore information to the restore file # @Restores = $bpc->RestoreInfoRead($client); my $i = @Restores; $Restores[$i]{num} = $lastNum; $Restores[$i]{startTime} = $startTime; $Restores[$i]{endTime} = $endTime; $Restores[$i]{result} = $stat{xferOK} ? "ok" : "failed"; $Restores[$i]{errorMsg} = $stat{hostError}; $Restores[$i]{nFiles} = $tarCreateFileCnt; $Restores[$i]{size} = $tarCreateByteCnt; $Restores[$i]{tarCreateErrs} = $tarCreateErrCnt; $Restores[$i]{xferErrs} = $stat{xferErrCnt} || 0; while ( @Restores > $Conf{RestoreInfoKeepCnt} ) { my $num = $Restores[0]{num}; unlink("$Dir/RestoreLOG.$num.z"); unlink("$Dir/RestoreLOG.$num"); unlink("$Dir/RestoreInfo.$num"); shift(@Restores); } $bpc->RestoreInfoWrite($client, @Restores); if ( !$stat{xferOK} ) { print(LOG $bpc->timeStamp, "restore failed ($stat{hostError})\n"); print("restore failed: $stat{hostError}\n"); return 1; } else { $stat{xferErrCnt} ||= 0; print(LOG $bpc->timeStamp, "restore $lastNum complete" . " ($tarCreateFileCnt files, $tarCreateByteCnt bytes," . " $tarCreateDirCnt dirs, $stat{xferErrCnt} xferErrs)\n"); print("restore complete\n"); return; } } # # The Xfer method might tell us from time to time about processes # it forks. We tell BackupPC about this (for status displays) and # keep track of the pids in case we cancel the backup # sub pidHandler { @xferPid = @_; @xferPid = grep(/./, @xferPid); return if ( !@xferPid && $tarPid < 0 ); my @pids = @xferPid; push(@pids, $tarPid) if ( $tarPid > 0 ); my $str = join(",", @pids); $RestoreLOG->write(\"Xfer PIDs are now $str\n") if ( defined($RestoreLOG) ); print("xferPids $str\n"); } # # Run an optional pre- or post-dump command # sub UserCommandRun { my($cmdType) = @_; return if ( !defined($Conf{$cmdType}) ); my $vars = { xfer => $xfer, client => $client, host => $host, hostIP => $hostIP, share => $RestoreReq{shareDest}, XferMethod => $Conf{XferMethod}, sshPath => $Conf{SshPath}, LOG => *LOG, user => $Hosts->{$client}{user}, moreUsers => $Hosts->{$client}{moreUsers}, XferLOG => $RestoreLOG, stat => \%stat, xferOK => $stat{xferOK} || 0, hostError => $stat{hostError}, type => "restore", bkupSrcHost => $RestoreReq{hostSrc}, bkupSrcShare => $RestoreReq{shareSrc}, bkupSrcNum => $RestoreReq{num}, backups => \@Backups, pathHdrSrc => $RestoreReq{pathHdrSrc}, pathHdrDest => $RestoreReq{pathHdrDest}, fileList => $RestoreReq{fileList}, cmdType => $cmdType, }; my $cmd = $bpc->cmdVarSubstitute($Conf{$cmdType}, $vars); $RestoreLOG->write(\"Executing $cmdType: @$cmd\n"); # # Run the user's command, dumping the stdout/stderr into the # Xfer log file. Also supply the optional $vars and %Conf in # case the command is really perl code instead of a shell # command. # $bpc->cmdSystemOrEval($cmd, sub { $RestoreLOG->write(\$_[0]); }, $vars, \%Conf); }