54a7a865b82b7b77c2254dae240c1ffc7c1bb546
[BackupPC.git] / bin / BackupPC_restore
1 #!/bin/perl -T
2 #============================================================= -*-perl-*-
3 #
4 # BackupPC_restore: Restore files to a client.
5 #
6 # DESCRIPTION
7 #
8 #   Usage: BackupPC_restore <hostIP> <host> <reqFileName>
9 #
10 # AUTHOR
11 #   Craig Barratt  <cbarratt@users.sourceforge.net>
12 #
13 # COPYRIGHT
14 #   Copyright (C) 2001  Craig Barratt
15 #
16 #   This program is free software; you can redistribute it and/or modify
17 #   it under the terms of the GNU General Public License as published by
18 #   the Free Software Foundation; either version 2 of the License, or
19 #   (at your option) any later version.
20 #
21 #   This program is distributed in the hope that it will be useful,
22 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
23 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24 #   GNU General Public License for more details.
25 #
26 #   You should have received a copy of the GNU General Public License
27 #   along with this program; if not, write to the Free Software
28 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
29 #
30 #========================================================================
31 #
32 # Version 1.5.0, released 2 Aug 2002.
33 #
34 # See http://backuppc.sourceforge.net.
35 #
36 #========================================================================
37
38 use strict;
39 use lib "__INSTALLDIR__/lib";
40 use BackupPC::Lib;
41 use BackupPC::FileZIO;
42 use BackupPC::Xfer::Smb;
43 use BackupPC::Xfer::Tar;
44
45 use File::Path;
46 use Getopt::Std;
47
48 use vars qw( %RestoreReq );
49
50 ###########################################################################
51 # Initialize
52 ###########################################################################
53
54 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
55 my $TopDir = $bpc->TopDir();
56 my $BinDir = $bpc->BinDir();
57 my %Conf   = $bpc->Conf();
58
59 my($hostIP, $host, $reqFileName);
60
61 $bpc->ChildInit();
62
63 if ( @ARGV != 3 ) {
64     print("usage: $0 <hostIP> <host> <reqFileName>\n");
65     exit(1);
66 }
67 $hostIP = $1 if ( $ARGV[0] =~ /(.+)/ );
68 $host   = $1 if ( $ARGV[1] =~ /(.+)/ );
69 if ( $ARGV[2] !~ /^([\w.]+)$/ ) {
70     print("$0: bad reqFileName (arg #3): $ARGV[2]\n");
71     exit(1);
72 }
73 $reqFileName = $1;
74
75 my $Hosts = $bpc->HostInfoRead();
76
77 #
78 # Re-read config file, so we can include the PC-specific config
79 #
80 $bpc->ConfigRead($host);
81 %Conf = $bpc->Conf();
82
83 my $Dir     = "$TopDir/pc/$host";
84 my $xferPid = -1;
85 my $tarPid  = -1;
86
87 #
88 # Catch various signals
89 #
90 $SIG{INT}  = \&catch_signal;
91 $SIG{ALRM} = \&catch_signal;
92 $SIG{TERM} = \&catch_signal;
93
94 #
95 # Read the request file
96 #
97 if ( !(my $ret = do "$Dir/$reqFileName") ) {
98    die "couldn't parse $Dir/$reqFileName: $@" if $@;
99    die "couldn't do $Dir/$reqFileName: $!"    unless defined $ret;
100    die "couldn't run $Dir/$reqFileName";
101 }
102
103 #
104 # Make sure we eventually timeout if there is no activity from
105 # the data transport program.
106 #
107 alarm($Conf{SmbClientTimeout});
108
109 mkpath($Dir, 0, 0777) if ( !-d $Dir );
110 if ( !-f "$Dir/LOCK" ) {
111     open(LOCK, ">$Dir/LOCK") && close(LOCK);
112 }
113 open(LOG, ">>$Dir/LOG");
114 select(LOG); $| = 1; select(STDOUT);
115
116 #
117 # Check if $host is alive
118 #
119 my $delay = $bpc->CheckHostAlive($hostIP);
120 if ( $delay < 0 ) {
121     print(LOG $bpc->timeStamp, "no ping response\n");
122     print("no ping response\n");
123     exit(1);
124 } elsif ( $delay > $Conf{PingMaxMsec} ) {
125     printf(LOG "%sping too slow: %.4gmsec\n", $bpc->timeStamp, $delay);
126     printf("ping too slow: %.4gmsec (threshold is %gmsec)\n",
127                     $delay, $Conf{PingMaxMsec});
128     exit(1);
129 }
130
131 #
132 # Make sure it is really the machine we expect
133 #
134 if ( (my $errMsg = CorrectHostCheck($hostIP, $host)) ) {
135     print(LOG $bpc->timeStamp, "restore failed: $errMsg\n");
136     print("restore failed: $errMsg\n");
137     exit(1);
138 }
139
140 #
141 # Setup file extension for compression and open RestoreLOG output file
142 #
143 $Conf{CompressLevel} = 0 if ( !BackupPC::FileZIO->compOk );
144 my $fileExt = $Conf{CompressLevel} > 0 ? ".z" : "";
145 my $RestoreLOG = BackupPC::FileZIO->open("$Dir/RestoreLOG$fileExt", 1,
146                                      $Conf{CompressLevel});
147 my $startTime = time();
148
149 my $tarCreateFileCnt = 0;
150 my $tarCreateByteCnt = 0;
151 my $tarCreateErrCnt  = 1;       # assume not ok until we learn otherwise
152 my $tarCreateErr;
153 my($logMsg, %stat, $xfer);
154
155 #
156 # Now do the restore
157 #
158 local(*RH, *WH);
159
160 $stat{xferOK} = $stat{hostAbort} = undef;
161 $stat{hostError} = $stat{lastOutputLine} = undef;
162
163 #
164 # Create a pipe to connect BackupPC_tarCreate to the transport program
165 # (smbclient, tar, etc).
166 # WH is the write handle for writing, provided to BackupPC_tarCreate
167 # and RH is the other end of the pipe for reading provided to the
168 # transport program.
169 #
170 pipe(RH, WH);
171
172 #
173 # Run the transport program, which reads from RH and extracts the data.
174 #
175 my $xferArgs = {
176     host      => $host,
177     hostIP    => $hostIP,
178     type      => "restore",
179     shareName => $RestoreReq{shareDest},
180     pipeRH    => *RH,
181     pipeWH    => *WH,
182     XferLOG   => $RestoreLOG,
183 };
184 if ( $Conf{XferMethod} eq "tar" ) {
185     #
186     # Use tar (eg: tar/ssh) as the transport program.
187     #
188     $xfer = BackupPC::Xfer::Tar->new($bpc, $xferArgs);
189 } else {
190     #
191     # Default is to use smbclient (smb) as the transport program.
192     #
193     $xfer = BackupPC::Xfer::Smb->new($bpc, $xferArgs);
194 }
195 if ( !defined($logMsg = $xfer->start()) ) {
196     print(LOG $bpc->timeStamp, $xfer->errStr, "\n");
197     print($xfer->errStr, "\n");
198     exit(1);
199 }
200 #
201 # The parent must close the read handle since the transport program
202 # is using it.
203 #
204 close(RH);
205
206 #
207 # fork a child for BackupPC_tarCreate.  TAR is a file handle
208 # on which we (the parent) read the stderr from BackupPC_tarCreate.
209 #
210 my @tarPathOpts;
211 if ( defined($RestoreReq{pathHdrDest})
212             && $RestoreReq{pathHdrDest} ne $RestoreReq{pathHdrSrc} ) {
213     @tarPathOpts = ("-r", $RestoreReq{pathHdrSrc},
214                     "-p", $RestoreReq{pathHdrDest}
215             );
216 }
217 my @tarArgs = (
218          "-h", $RestoreReq{hostSrc},
219          "-n", $RestoreReq{num},
220          "-s", $RestoreReq{shareSrc},
221          "-t",
222          @tarPathOpts,
223          @{$RestoreReq{fileList}},
224 );
225 my $logMsg = "Running: $BinDir/BackupPC_tarCreate "
226                   . join(" ", @tarArgs) . "\n";
227 $RestoreLOG->write(\$logMsg);
228 if ( !defined($tarPid = open(TAR, "-|")) ) {
229     print(LOG $bpc->timeStamp, "can't fork to run tar\n");
230     print("can't fork to run tar\n");
231     close(WH);
232     # FIX: need to cleanup xfer
233     exit(0);
234 }
235 if ( !$tarPid ) {
236     #
237     # This is the tarCreate child.  Clone STDERR to STDOUT,
238     # STDOUT to WH, and then exec BackupPC_tarCreate.
239     #
240     setpgrp 0,0;
241     close(STDERR);
242     open(STDERR, ">&STDOUT");
243     close(STDOUT);
244     open(STDOUT, ">&WH");
245     exec("$BinDir/BackupPC_tarCreate", @tarArgs);
246     print(LOG $bpc->timeStamp, "can't exec $BinDir/BackupPC_tarCreate\n");
247     # FIX: need to cleanup xfer
248     exit(0);
249 }
250 #
251 # The parent must close the write handle since BackupPC_tarCreate
252 # is using it.
253 #
254 close(WH);
255
256 $xferPid = $xfer->xferPid;
257 print(LOG $bpc->timeStamp, $logMsg, " (tarPid=$tarPid, xferPid=$xferPid)\n");
258 print("started restore, tarPid=$tarPid, xferPid=$xferPid\n");
259
260 #
261 # Parse the output of the transfer program and BackupPC_tarCreate
262 # while they run.  Since we are reading from two or more children
263 # we use a select.
264 #
265 my($FDread, $tarOut, $mesg);
266 vec($FDread, fileno(TAR), 1) = 1;
267 $xfer->setSelectMask(\$FDread);
268
269 SCAN: while ( 1 ) {
270     my $ein = $FDread;
271     last if ( $FDread =~ /^\0*$/ );
272     select(my $rout = $FDread, undef, $ein, undef);
273     if ( vec($rout, fileno(TAR), 1) ) {
274         if ( sysread(TAR, $mesg, 8192) <= 0 ) {
275             vec($FDread, fileno(TAR), 1) = 0;
276             if ( !close(TAR) ) {
277                 $tarCreateErrCnt  = 1;
278                 $tarCreateErr = "BackupPC_tarCreate failed";
279             }
280         } else {
281             $tarOut .= $mesg;
282         }
283     }
284     while ( $tarOut =~ /(.*?)[\n\r]+(.*)/s ) {
285         $_ = $1;
286         $tarOut = $2;
287         $RestoreLOG->write(\"tarCreate: $_\n");
288         if ( /^Done: (\d+) files, (\d+) bytes, (\d+) dirs, (\d+) specials, (\d+) errors/ ) {
289             $tarCreateFileCnt = $1;
290             $tarCreateByteCnt = $2;
291             $tarCreateErrCnt  = $5;
292         }
293     }
294     last if ( !$xfer->readOutput(\$FDread, $rout) );
295     while ( my $str = $xfer->logMsgGet ) {
296         print(LOG $bpc->timeStamp, "xfer: $str\n");
297     }
298     if ( $xfer->getStats->{fileCnt} == 1 ) {
299         #
300         # Make sure it is still the machine we expect.  We do this while
301         # the transfer is running to avoid a potential race condition if
302         # the ip address was reassigned by dhcp just before we started
303         # the transfer.
304         #
305         if ( my $errMsg = CorrectHostCheck($hostIP, $host) ) {
306             $stat{hostError} = $errMsg;
307             last SCAN;
308         }
309     }
310 }
311
312 #
313 # Merge the xfer status (need to accumulate counts)
314 #
315 my $newStat = $xfer->getStats;
316 foreach my $k ( (keys(%stat), keys(%$newStat)) ) {
317     next if ( !defined($newStat->{$k}) );
318     if ( $k =~ /Cnt$/ ) {
319         $stat{$k} += $newStat->{$k};
320         delete($newStat->{$k});
321         next;
322     }
323     if ( !defined($stat{$k}) ) {
324         $stat{$k} = $newStat->{$k};
325         delete($newStat->{$k});
326         next;
327     }
328 }
329 $RestoreLOG->close();
330 $stat{xferOK} = 0 if ( $stat{hostError} || $stat{hostAbort} || $tarCreateErr );
331
332 if ( !$stat{xferOK} ) {
333     #
334     # kill off the tranfer program, first nicely then forcefully
335     #
336     kill(2, $xferPid);
337     sleep(1);
338     kill(9, $xferPid);
339     #
340     # kill off the tar process, first nicely then forcefully
341     #
342     kill(2, $tarPid);
343     sleep(1);
344     kill(9, $tarPid);
345 }
346
347 my $lastNum  = -1;
348 my @Restores;
349
350 #
351 # Do one last check to make sure it is still the machine we expect.
352 #
353 if ( $stat{xferOK} && (my $errMsg = CorrectHostCheck($hostIP, $host)) ) {
354     $stat{hostError} = $errMsg;
355     $stat{xferOK} = 0;
356 }
357 @Restores = $bpc->RestoreInfoRead($host);
358 for ( my $i = 0 ; $i < @Restores ; $i++ ) {
359     $lastNum = $Restores[$i]{num} if ( $lastNum < $Restores[$i]{num} );
360 }
361 $lastNum++;
362 rename("$Dir/RestoreLOG$fileExt", "$Dir/RestoreLOG.$lastNum$fileExt");
363 rename("$Dir/$reqFileName", "$Dir/RestoreInfo.$lastNum");
364 my $endTime = time();
365
366 #
367 # If the restore failed, clean up
368 #
369 if ( !$stat{xferOK} ) {
370     #
371     # wait a short while and see if the system is still alive
372     #
373     $stat{hostError} ||= $tarCreateErr if ( $tarCreateErr ne "" );
374     $stat{hostError} = $stat{lastOutputLine} if ( $stat{hostError} eq "" );
375     if ( $stat{hostError} ) {
376         print(LOG $bpc->timeStamp,
377                   "Got fatal error during xfer ($stat{hostError})\n");
378     }
379     sleep(2);
380     if ( $bpc->CheckHostAlive($hostIP) < 0 ) {
381         $stat{hostAbort} = 1;
382     }
383     if ( $stat{hostAbort} ) {
384         $stat{hostError} = "lost network connection during restore";
385     }
386 }
387
388 #
389 # Add the new restore information to the restore file
390 #
391 @Restores = $bpc->RestoreInfoRead($host);
392 my $i = @Restores;
393 $Restores[$i]{num}           = $lastNum;
394 $Restores[$i]{startTime}     = $startTime;
395 $Restores[$i]{endTime}       = $endTime;
396 $Restores[$i]{result}        = $stat{xferOK} ? "ok" : "failed";
397 $Restores[$i]{errorMsg}      = $stat{hostError};
398 $Restores[$i]{nFiles}        = $tarCreateFileCnt;
399 $Restores[$i]{size}          = $tarCreateByteCnt;
400 $Restores[$i]{tarCreateErrs} = $tarCreateErrCnt;
401 $Restores[$i]{xferErrs}      = $stat{xferErrCnt} || 0;
402
403 while ( @Restores > $Conf{RestoreInfoKeepCnt} ) {
404     my $num = $Restores[0]{num};
405     unlink("$Dir/RestoreLOG.$num.z");
406     unlink("$Dir/RestoreLOG.$num");
407     unlink("$Dir/RestoreInfo.$num");
408     shift(@Restores);
409 }
410 $bpc->RestoreInfoWrite($host, @Restores);
411
412 if ( !$stat{xferOK} ) {
413     print(LOG $bpc->timeStamp, "Restore aborted ($stat{hostError})\n");
414     print("restore failed: $stat{hostError}\n");
415 } else {
416     print("restore complete\n");
417 }
418
419 ###########################################################################
420 # Subroutines
421 ###########################################################################
422
423 sub CorrectHostCheck
424 {
425     my($hostIP, $host) = @_;
426     return if ( $hostIP eq $host && !$Conf{FixedIPNetBiosNameCheck} );
427     my($netBiosHost, $netBiosUser) = $bpc->NetBiosInfoGet($hostIP);
428     return "host $host has mismatching netbios name $netBiosHost"
429             if ( $netBiosHost ne $host );
430     return;
431 }