f0590bcb63c021248a1ff8571d185e9cb8dfdd8c
[BackupPC.git] / lib / BackupPC / Lib.pm
1 #============================================================= -*-perl-*-
2 #
3 # BackupPC::Lib package
4 #
5 # DESCRIPTION
6 #
7 #   This library defines a BackupPC::Lib class and a variety of utility
8 #   functions used by BackupPC.
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.6.0_CVS, released 10 Dec 2002.
33 #
34 # See http://backuppc.sourceforge.net.
35 #
36 #========================================================================
37
38 package BackupPC::Lib;
39
40 use strict;
41
42 use vars qw(%Conf %Lang);
43 use Fcntl qw/:flock/;
44 use Carp;
45 use DirHandle ();
46 use File::Path;
47 use File::Compare;
48 use Socket;
49 use Cwd;
50 use Digest::MD5;
51
52 sub new
53 {
54     my $class = shift;
55     my($topDir, $installDir) = @_;
56
57     my $bpc = bless {
58         TopDir  => $topDir || '/data/BackupPC',
59         BinDir  => $installDir || '/usr/local/BackupPC',
60         LibDir  => $installDir || '/usr/local/BackupPC',
61         Version => '1.6.0_CVS',
62         BackupFields => [qw(
63                     num type startTime endTime
64                     nFiles size nFilesExist sizeExist nFilesNew sizeNew
65                     xferErrs xferBadFile xferBadShare tarErrs
66                     compress sizeExistComp sizeNewComp
67                     noFill fillFromNum mangle xferMethod level
68                 )],
69         RestoreFields => [qw(
70                     num startTime endTime result errorMsg nFiles size
71                     tarCreateErrs xferErrs
72                 )],
73     }, $class;
74     $bpc->{BinDir} .= "/bin";
75     $bpc->{LibDir} .= "/lib";
76     #
77     # Clean up %ENV and setup other variables.
78     #
79     delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
80     $bpc->{PoolDir}  = "$bpc->{TopDir}/pool";
81     $bpc->{CPoolDir} = "$bpc->{TopDir}/cpool";
82     if ( defined(my $error = $bpc->ConfigRead()) ) {
83         print(STDERR $error, "\n");
84         return;
85     }
86     return $bpc;
87 }
88
89 sub TopDir
90 {
91     my($bpc) = @_;
92     return $bpc->{TopDir};
93 }
94
95 sub BinDir
96 {
97     my($bpc) = @_;
98     return $bpc->{BinDir};
99 }
100
101 sub Version
102 {
103     my($bpc) = @_;
104     return $bpc->{Version};
105 }
106
107 sub Conf
108 {
109     my($bpc) = @_;
110     return %{$bpc->{Conf}};
111 }
112
113 sub Lang
114 {
115     my($bpc) = @_;
116     return $bpc->{Lang};
117 }
118
119 sub adminJob
120 {
121     return " admin ";
122 }
123
124 sub trashJob
125 {
126     return " trashClean ";
127 }
128
129 sub timeStamp
130 {
131     my($bpc, $t, $noPad) = @_;
132     my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)
133               = localtime($t || time);
134     $year += 1900;
135     $mon++;
136     return "$year/$mon/$mday " . sprintf("%02d:%02d:%02d", $hour, $min, $sec)
137             . ($noPad ? "" : " ");
138 }
139
140 #
141 # An ISO 8601-compliant version of timeStamp.  Needed by the
142 # --newer-mtime argument to GNU tar in BackupPC::Xfer::Tar.
143 # Also see http://www.w3.org/TR/NOTE-datetime.
144 #
145 sub timeStampISO
146 {
147     my($bpc, $t, $noPad) = @_;
148     my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)
149               = localtime($t || time);
150     $year += 1900;
151     $mon++;
152     return sprintf("%04d-%02d-%02d ", $year, $mon, $mday)
153          . sprintf("%02d:%02d:%02d", $hour, $min, $sec)
154          . ($noPad ? "" : " ");
155 }
156
157 sub BackupInfoRead
158 {
159     my($bpc, $host) = @_;
160     local(*BK_INFO, *LOCK);
161     my(@Backups);
162
163     flock(LOCK, LOCK_EX) if open(LOCK, "$bpc->{TopDir}/pc/$host/LOCK");
164     if ( open(BK_INFO, "$bpc->{TopDir}/pc/$host/backups") ) {
165         while ( <BK_INFO> ) {
166             s/[\n\r]+//;
167             next if ( !/^(\d+\t(incr|full)[\d\t]*$)/ );
168             $_ = $1;
169             @{$Backups[@Backups]}{@{$bpc->{BackupFields}}} = split(/\t/);
170         }
171         close(BK_INFO);
172     }
173     close(LOCK);
174     return @Backups;
175 }
176
177 sub BackupInfoWrite
178 {
179     my($bpc, $host, @Backups) = @_;
180     local(*BK_INFO, *LOCK);
181     my($i);
182
183     flock(LOCK, LOCK_EX) if open(LOCK, "$bpc->{TopDir}/pc/$host/LOCK");
184     unlink("$bpc->{TopDir}/pc/$host/backups.old")
185                 if ( -f "$bpc->{TopDir}/pc/$host/backups.old" );
186     rename("$bpc->{TopDir}/pc/$host/backups",
187            "$bpc->{TopDir}/pc/$host/backups.old")
188                 if ( -f "$bpc->{TopDir}/pc/$host/backups" );
189     if ( open(BK_INFO, ">$bpc->{TopDir}/pc/$host/backups") ) {
190         for ( $i = 0 ; $i < @Backups ; $i++ ) {
191             my %b = %{$Backups[$i]};
192             printf(BK_INFO "%s\n", join("\t", @b{@{$bpc->{BackupFields}}}));
193         }
194         close(BK_INFO);
195     }
196     close(LOCK);
197 }
198
199 sub RestoreInfoRead
200 {
201     my($bpc, $host) = @_;
202     local(*RESTORE_INFO, *LOCK);
203     my(@Restores);
204
205     flock(LOCK, LOCK_EX) if open(LOCK, "$bpc->{TopDir}/pc/$host/LOCK");
206     if ( open(RESTORE_INFO, "$bpc->{TopDir}/pc/$host/restores") ) {
207         while ( <RESTORE_INFO> ) {
208             s/[\n\r]+//;
209             next if ( !/^(\d+.*)/ );
210             $_ = $1;
211             @{$Restores[@Restores]}{@{$bpc->{RestoreFields}}} = split(/\t/);
212         }
213         close(RESTORE_INFO);
214     }
215     close(LOCK);
216     return @Restores;
217 }
218
219 sub RestoreInfoWrite
220 {
221     my($bpc, $host, @Restores) = @_;
222     local(*RESTORE_INFO, *LOCK);
223     my($i);
224
225     flock(LOCK, LOCK_EX) if open(LOCK, "$bpc->{TopDir}/pc/$host/LOCK");
226     unlink("$bpc->{TopDir}/pc/$host/restores.old")
227                 if ( -f "$bpc->{TopDir}/pc/$host/restores.old" );
228     rename("$bpc->{TopDir}/pc/$host/restores",
229            "$bpc->{TopDir}/pc/$host/restores.old")
230                 if ( -f "$bpc->{TopDir}/pc/$host/restores" );
231     if ( open(RESTORE_INFO, ">$bpc->{TopDir}/pc/$host/restores") ) {
232         for ( $i = 0 ; $i < @Restores ; $i++ ) {
233             my %b = %{$Restores[$i]};
234             printf(RESTORE_INFO "%s\n",
235                         join("\t", @b{@{$bpc->{RestoreFields}}}));
236         }
237         close(RESTORE_INFO);
238     }
239     close(LOCK);
240 }
241
242 sub ConfigRead
243 {
244     my($bpc, $host) = @_;
245     my($ret, $mesg, $config, @configs);
246
247     $bpc->{Conf} = ();
248     push(@configs, "$bpc->{TopDir}/conf/config.pl");
249     push(@configs, "$bpc->{TopDir}/conf/$host.pl")
250             if ( $host ne "config" && -f "$bpc->{TopDir}/conf/$host.pl" );
251     push(@configs, "$bpc->{TopDir}/pc/$host/config.pl")
252             if ( defined($host) && -f "$bpc->{TopDir}/pc/$host/config.pl" );
253     foreach $config ( @configs ) {
254         %Conf = ();
255         if ( !defined($ret = do $config) && ($! || $@) ) {
256             $mesg = "Couldn't open $config: $!" if ( $! );
257             $mesg = "Couldn't execute $config: $@" if ( $@ );
258             $mesg =~ s/[\n\r]+//;
259             return $mesg;
260         }
261         %{$bpc->{Conf}} = ( %{$bpc->{Conf} || {}}, %Conf );
262     }
263     return if ( !defined($bpc->{Conf}{Language}) );
264     if ( defined($bpc->{Conf}{PerlModuleLoad}) ) {
265         #
266         # Load any user-specified perl modules.  This is for
267         # optional user-defined extensions.
268         #
269         $bpc->{Conf}{PerlModuleLoad} = [$bpc->{Conf}{PerlModuleLoad}]
270                     if ( ref($bpc->{Conf}{PerlModuleLoad}) ne "ARRAY" );
271         foreach my $module ( @{$bpc->{Conf}{PerlModuleLoad}} ) {
272             eval("use $module;");
273         }
274     }
275     my $langFile = "$bpc->{LibDir}/BackupPC/Lang/$bpc->{Conf}{Language}.pm";
276     if ( !defined($ret = do $langFile) && ($! || $@) ) {
277         $mesg = "Couldn't open language file $langFile: $!" if ( $! );
278         $mesg = "Couldn't execute language file $langFile: $@" if ( $@ );
279         $mesg =~ s/[\n\r]+//;
280         return $mesg;
281     }
282     $bpc->{Lang} = \%Lang;
283     return;
284 }
285
286 #
287 # Return the mtime of the config file
288 #
289 sub ConfigMTime
290 {
291     my($bpc) = @_;
292     return (stat("$bpc->{TopDir}/conf/config.pl"))[9];
293 }
294
295 #
296 # Returns information from the host file in $bpc->{TopDir}/conf/hosts.
297 # With no argument a ref to a hash of hosts is returned.  Each
298 # hash contains fields as specified in the hosts file.  With an
299 # argument a ref to a single hash is returned with information
300 # for just that host.
301 #
302 sub HostInfoRead
303 {
304     my($bpc, $host) = @_;
305     my(%hosts, @hdr, @fld);
306     local(*HOST_INFO);
307
308     if ( !open(HOST_INFO, "$bpc->{TopDir}/conf/hosts") ) {
309         print(STDERR $bpc->timeStamp,
310                      "Can't open $bpc->{TopDir}/conf/hosts\n");
311         return {};
312     }
313     while ( <HOST_INFO> ) {
314         s/[\n\r]+//;
315         s/#.*//;
316         s/\s+$//;
317         next if ( /^\s*$/ || !/^([\w\.-]+\s+.*)/ );
318         @fld = split(/\s+/, $1);
319         if ( @hdr ) {
320             if ( defined($host) ) {
321                 next if ( lc($fld[0]) ne $host );
322                 @{$hosts{lc($fld[0])}}{@hdr} = @fld;
323                 close(HOST_INFO);
324                 return \%hosts;
325             } else {
326                 @{$hosts{lc($fld[0])}}{@hdr} = @fld;
327             }
328         } else {
329             @hdr = @fld;
330         }
331     }
332     close(HOST_INFO);
333     return \%hosts;
334 }
335
336 #
337 # Return the mtime of the hosts file
338 #
339 sub HostsMTime
340 {
341     my($bpc) = @_;
342     return (stat("$bpc->{TopDir}/conf/hosts"))[9];
343 }
344
345 #
346 # Stripped down from File::Path.  In particular we don't print
347 # many warnings and we try three times to delete each directory
348 # and file -- for some reason the original File::Path rmtree
349 # didn't always completely remove a directory tree on the NetApp.
350 #
351 # Warning: this routine changes the cwd.
352 #
353 sub RmTreeQuiet
354 {
355     my($bpc, $pwd, $roots) = @_;
356     my(@files, $root);
357
358     if ( defined($roots) && length($roots) ) {
359       $roots = [$roots] unless ref $roots;
360     } else {
361       print "RmTreeQuiet: No root path(s) specified\n";
362     }
363     chdir($pwd);
364     foreach $root (@{$roots}) {
365         $root = $1 if ( $root =~ m{(.*?)/*$} );
366         #
367         # Try first to simply unlink the file: this avoids an
368         # extra stat for every file.  If it fails (which it
369         # will for directories), check if it is a directory and
370         # then recurse.
371         #
372         if ( !unlink($root) ) {
373             if ( -d $root ) {
374                 my $d = DirHandle->new($root)
375                   or print "Can't read $pwd/$root: $!";
376                 @files = $d->read;
377                 $d->close;
378                 @files = grep $_!~/^\.{1,2}$/, @files;
379                 $bpc->RmTreeQuiet("$pwd/$root", \@files);
380                 chdir($pwd);
381                 rmdir($root) || rmdir($root);
382             } else {
383                 unlink($root) || unlink($root);
384             }
385         }
386     }
387 }
388
389 #
390 # Move a directory or file away for later deletion
391 #
392 sub RmTreeDefer
393 {
394     my($bpc, $trashDir, $file) = @_;
395     my($i, $f);
396
397     return if ( !-e $file );
398     mkpath($trashDir, 0, 0777) if ( !-d $trashDir );
399     for ( $i = 0 ; $i < 1000 ; $i++ ) {
400         $f = sprintf("%s/%d_%d_%d", $trashDir, time, $$, $i);
401         next if ( -e $f );
402         return if ( rename($file, $f) );
403     }
404     # shouldn't get here, but might if you tried to call this
405     # across file systems.... just remove the tree right now.
406     if ( $file =~ /(.*)\/([^\/]*)/ ) {
407         my($d) = $1;
408         my($f) = $2;
409         my($cwd) = Cwd::fastcwd();
410         $cwd = $1 if ( $cwd =~ /(.*)/ );
411         $bpc->RmTreeQuiet($d, $f);
412         chdir($cwd) if ( $cwd );
413     }
414 }
415
416 #
417 # Empty the trash directory.  Returns 0 if it did nothing.
418 #
419 sub RmTreeTrashEmpty
420 {
421     my($bpc, $trashDir) = @_;
422     my(@files);
423     my($cwd) = Cwd::fastcwd();
424
425     $cwd = $1 if ( $cwd =~ /(.*)/ );
426     return if ( !-d $trashDir );
427     my $d = DirHandle->new($trashDir)
428       or carp "Can't read $trashDir: $!";
429     @files = $d->read;
430     $d->close;
431     @files = grep $_!~/^\.{1,2}$/, @files;
432     return 0 if ( !@files );
433     $bpc->RmTreeQuiet($trashDir, \@files);
434     chdir($cwd) if ( $cwd );
435     return 1;
436 }
437
438 #
439 # Open a connection to the server.  Returns an error string on failure.
440 # Returns undef on success.
441 #
442 sub ServerConnect
443 {
444     my($bpc, $host, $port, $justConnect) = @_;
445     local(*FH);
446
447     return if ( defined($bpc->{ServerFD}) );
448     #
449     # First try the unix-domain socket
450     #
451     my $sockFile = "$bpc->{TopDir}/log/BackupPC.sock";
452     socket(*FH, PF_UNIX, SOCK_STREAM, 0)     || return "unix socket: $!";
453     if ( !connect(*FH, sockaddr_un($sockFile)) ) {
454         my $err = "unix connect: $!";
455         close(*FH);
456         if ( $port > 0 ) {
457             my $proto = getprotobyname('tcp');
458             my $iaddr = inet_aton($host)     || return "unknown host $host";
459             my $paddr = sockaddr_in($port, $iaddr);
460
461             socket(*FH, PF_INET, SOCK_STREAM, $proto)
462                                              || return "inet socket: $!";
463             connect(*FH, $paddr)             || return "inet connect: $!";
464         } else {
465             return $err;
466         }
467     }
468     my($oldFH) = select(*FH); $| = 1; select($oldFH);
469     $bpc->{ServerFD} = *FH;
470     return if ( $justConnect );
471     #
472     # Read the seed that we need for our MD5 message digest.  See
473     # ServerMesg below.
474     #
475     sysread($bpc->{ServerFD}, $bpc->{ServerSeed}, 1024);
476     $bpc->{ServerMesgCnt} = 0;
477     return;
478 }
479
480 #
481 # Check that the server connection is still ok
482 #
483 sub ServerOK
484 {
485     my($bpc) = @_;
486
487     return 0 if ( !defined($bpc->{ServerFD}) );
488     vec(my $FDread, fileno($bpc->{ServerFD}), 1) = 1;
489     my $ein = $FDread;
490     return 0 if ( select(my $rout = $FDread, undef, $ein, 0.0) < 0 );
491     return 1 if ( !vec($rout, fileno($bpc->{ServerFD}), 1) );
492 }
493
494 #
495 # Disconnect from the server
496 #
497 sub ServerDisconnect
498 {
499     my($bpc) = @_;
500     return if ( !defined($bpc->{ServerFD}) );
501     close($bpc->{ServerFD});
502     delete($bpc->{ServerFD});
503 }
504
505 #
506 # Sends a message to the server and returns with the reply.
507 #
508 # To avoid possible attacks via the TCP socket interface, every client
509 # message is protected by an MD5 digest. The MD5 digest includes four
510 # items:
511 #   - a seed that is sent to us when we first connect
512 #   - a sequence number that increments for each message
513 #   - a shared secret that is stored in $Conf{ServerMesgSecret}
514 #   - the message itself.
515 # The message is sent in plain text preceded by the MD5 digest. A
516 # snooper can see the plain-text seed sent by BackupPC and plain-text
517 # message, but cannot construct a valid MD5 digest since the secret in
518 # $Conf{ServerMesgSecret} is unknown. A replay attack is not possible
519 # since the seed changes on a per-connection and per-message basis.
520 #
521 sub ServerMesg
522 {
523     my($bpc, $mesg) = @_;
524     return if ( !defined(my $fh = $bpc->{ServerFD}) );
525     my $md5 = Digest::MD5->new;
526     $md5->add($bpc->{ServerSeed} . $bpc->{ServerMesgCnt}
527             . $bpc->{Conf}{ServerMesgSecret} . $mesg);
528     print($fh $md5->b64digest . " $mesg\n");
529     $bpc->{ServerMesgCnt}++;
530     return <$fh>;
531 }
532
533 #
534 # Do initialization for child processes
535 #
536 sub ChildInit
537 {
538     my($bpc) = @_;
539     close(STDERR);
540     open(STDERR, ">&STDOUT");
541     select(STDERR); $| = 1;
542     select(STDOUT); $| = 1;
543     $ENV{PATH} = $bpc->{Conf}{MyPath};
544 }
545
546 #
547 # Compute the MD5 digest of a file.  For efficiency we don't
548 # use the whole file for big files:
549 #   - for files <= 256K we use the file size and the whole file.
550 #   - for files <= 1M we use the file size, the first 128K and
551 #     the last 128K.
552 #   - for files > 1M, we use the file size, the first 128K and
553 #     the 8th 128K (ie: the 128K up to 1MB).
554 # See the documentation for a discussion of the tradeoffs in
555 # how much data we use and how many collisions we get.
556 #
557 # Returns the MD5 digest (a hex string) and the file size.
558 #
559 sub File2MD5
560 {
561     my($bpc, $md5, $name) = @_;
562     my($data, $fileSize);
563     local(*N);
564
565     $fileSize = (stat($name))[7];
566     return ("", -1) if ( !-f _ );
567     $name = $1 if ( $name =~ /(.*)/ );
568     return ("", 0) if ( $fileSize == 0 );
569     return ("", -1) if ( !open(N, $name) );
570     $md5->reset();
571     $md5->add($fileSize);
572     if ( $fileSize > 262144 ) {
573         #
574         # read the first and last 131072 bytes of the file,
575         # up to 1MB.
576         #
577         my $seekPosn = ($fileSize > 1048576 ? 1048576 : $fileSize) - 131072;
578         $md5->add($data) if ( sysread(N, $data, 131072) );
579         $md5->add($data) if ( sysseek(N, $seekPosn, 0)
580                                 && sysread(N, $data, 131072) );
581     } else {
582         #
583         # read the whole file
584         #
585         $md5->add($data) if ( sysread(N, $data, $fileSize) );
586     }
587     close(N);
588     return ($md5->hexdigest, $fileSize);
589 }
590
591 #
592 # Compute the MD5 digest of a buffer (string).  For efficiency we don't
593 # use the whole string for big strings:
594 #   - for files <= 256K we use the file size and the whole file.
595 #   - for files <= 1M we use the file size, the first 128K and
596 #     the last 128K.
597 #   - for files > 1M, we use the file size, the first 128K and
598 #     the 8th 128K (ie: the 128K up to 1MB).
599 # See the documentation for a discussion of the tradeoffs in
600 # how much data we use and how many collisions we get.
601 #
602 # Returns the MD5 digest (a hex string).
603 #
604 sub Buffer2MD5
605 {
606     my($bpc, $md5, $fileSize, $dataRef) = @_;
607
608     $md5->reset();
609     $md5->add($fileSize);
610     if ( $fileSize > 262144 ) {
611         #
612         # add the first and last 131072 bytes of the string,
613         # up to 1MB.
614         #
615         my $seekPosn = ($fileSize > 1048576 ? 1048576 : $fileSize) - 131072;
616         $md5->add(substr($$dataRef, 0, 131072));
617         $md5->add(substr($$dataRef, $seekPosn, 131072));
618     } else {
619         #
620         # add the whole string
621         #
622         $md5->add($$dataRef);
623     }
624     return $md5->hexdigest;
625 }
626
627 #
628 # Given an MD5 digest $d and a compress flag, return the full
629 # path in the pool.
630 #
631 sub MD52Path
632 {
633     my($bpc, $d, $compress, $poolDir) = @_;
634
635     return if ( $d !~ m{(.)(.)(.)(.*)} );
636     $poolDir = ($compress ? $bpc->{CPoolDir} : $bpc->{PoolDir})
637                     if ( !defined($poolDir) );
638     return "$poolDir/$1/$2/$3/$1$2$3$4";
639 }
640
641 #
642 # For each file, check if the file exists in $bpc->{TopDir}/pool.
643 # If so, remove the file and make a hardlink to the file in
644 # the pool.  Otherwise, if the newFile flag is set, make a
645 # hardlink in the pool to the new file.
646 #
647 # Returns 0 if a link should be made to a new file (ie: when the file
648 #    is a new file but the newFile flag is 0).
649 # Returns 1 if a link to an existing file is made,
650 # Returns 2 if a link to a new file is made (only if $newFile is set)
651 # Returns negative on error.
652 #
653 sub MakeFileLink
654 {
655     my($bpc, $name, $d, $newFile, $compress) = @_;
656     my($i, $rawFile);
657
658     return -1 if ( !-f $name );
659     for ( $i = -1 ; ; $i++ ) {
660         return -2 if ( !defined($rawFile = $bpc->MD52Path($d, $compress)) );
661         $rawFile .= "_$i" if ( $i >= 0 );
662         if ( -f $rawFile ) {
663             if ( !compare($name, $rawFile) ) {
664                 unlink($name);
665                 return -3 if ( !link($rawFile, $name) );
666                 return 1;
667             }
668         } elsif ( $newFile && -f $name && (stat($name))[3] == 1 ) {
669             my($newDir);
670             ($newDir = $rawFile) =~ s{(.*)/.*}{$1};
671             mkpath($newDir, 0, 0777) if ( !-d $newDir );
672             return -4 if ( !link($name, $rawFile) );
673             return 2;
674         } else {
675             return 0;
676         }
677     }
678 }
679
680 sub CheckHostAlive
681 {
682     my($bpc, $host) = @_;
683     my($s, $pingCmd);
684
685     my $args = {
686         pingPath => $bpc->{Conf}{PingPath},
687         host     => $host,
688     };
689     $pingCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{PingCmd}, $args);
690
691     #
692     # Do a first ping in case the PC needs to wakeup
693     #
694     $s = $bpc->cmdSystemOrEval($pingCmd, undef, $args);
695     return -1 if ( $? );
696
697     #
698     # Do a second ping and get the round-trip time in msec
699     #
700     $s = $bpc->cmdSystemOrEval($pingCmd, undef, $args);
701     return -1 if ( $? );
702     return $1 if ( $s =~ /time=([\d\.]+)\s*ms/i );
703     return $1/1000 if ( $s =~ /time=([\d\.]+)\s*usec/i );
704     return 0;
705 }
706
707 sub CheckFileSystemUsage
708 {
709     my($bpc) = @_;
710     my($topDir) = $bpc->{TopDir};
711     my($s, $dfCmd);
712
713     my $args = {
714         dfPath   => $bpc->{Conf}{DfPath},
715         topDir   => $bpc->{TopDir},
716     };
717     $dfCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{DfCmd}, $args);
718     $s = $bpc->cmdSystemOrEval($dfCmd, undef, $args);
719     return 0 if ( $? || $s !~ /(\d+)%/s );
720     return $1;
721 }
722
723 sub NetBiosInfoGet
724 {
725     my($bpc, $host) = @_;
726     my($netBiosHostName, $netBiosUserName);
727     my($s, $nmbCmd);
728
729     my $args = {
730         nmbLookupPath => $bpc->{Conf}{NmbLookupPath},
731         host          => $host,
732     };
733     $nmbCmd = $bpc->cmdVarSubstitute($bpc->{Conf}{NmbLookupCmd}, $args);
734     foreach ( split(/[\n\r]+/, $bpc->cmdSystemOrEval($nmbCmd, undef, $args)) ) {
735         next if ( !/^\s*([\w\s-]+?)\s*<(\w{2})\> - .*<ACTIVE>/i );
736         $netBiosHostName ||= $1 if ( $2 eq "00" );  # host is first 00
737         $netBiosUserName   = $1 if ( $2 eq "03" );  # user is last 03
738     }
739     return if ( !defined($netBiosHostName) );
740     return (lc($netBiosHostName), lc($netBiosUserName));
741 }
742
743 sub fileNameEltMangle
744 {
745     my($bpc, $name) = @_;
746
747     return "" if ( $name eq "" );
748     $name =~ s{([%/\n\r])}{sprintf("%%%02x", ord($1))}eg;
749     return "f$name";
750 }
751
752 #
753 # We store files with every name preceded by "f".  This
754 # avoids possible name conflicts with other information
755 # we store in the same directories (eg: attribute info).
756 # The process of turning a normal path into one with each
757 # node prefixed with "f" is called mangling.
758 #
759 sub fileNameMangle
760 {
761     my($bpc, $name) = @_;
762
763     $name =~ s{/([^/]+)}{"/" . $bpc->fileNameEltMangle($1)}eg;
764     $name =~ s{^([^/]+)}{$bpc->fileNameEltMangle($1)}eg;
765     return $name;
766 }
767
768 #
769 # This undoes FileNameMangle
770 #
771 sub fileNameUnmangle
772 {
773     my($bpc, $name) = @_;
774
775     $name =~ s{/f}{/}g;
776     $name =~ s{^f}{};
777     $name =~ s{%(..)}{chr(hex($1))}eg;
778     return $name;
779 }
780
781 #
782 # Escape shell meta-characters with backslashes.
783 # This should be applied to each argument seperately, not an
784 # entire shell command.
785 #
786 sub shellEscape
787 {
788     my($bpc, $cmd) = @_;
789
790     $cmd =~ s/([][;&()<>{}|^\n\r\t *\$\\'"`?])/\\$1/g;
791     return $cmd;
792 }
793
794 #
795 # Do variable substitution prior to execution of a command.
796 #
797 sub cmdVarSubstitute
798 {
799     my($bpc, $template, $vars) = @_;
800     my(@cmd);
801
802     #
803     # Return without any substitution if the first entry starts with "&",
804     # indicating this is perl code.
805     #
806     if ( (ref($template) eq "ARRAY" ? $template->[0] : $template) =~ /^\&/ ) {
807         return $template;
808     }
809     $template = [split(/\s+/, $template)] if ( ref($template) ne "ARRAY" );
810     #
811     # Merge variables into @tarClientCmd
812     #
813     foreach my $arg ( @$template ) {
814         #
815         # Replace scalar variables first
816         #
817         $arg =~ s{\$(\w+)(\+?)}{
818             defined($vars->{$1}) && ref($vars->{$1}) ne "ARRAY"
819                 ? ($2 eq "+" ? $bpc->shellEscape($vars->{$1}) : $vars->{$1})
820                 : "\$$1"
821         }eg;
822         #
823         # Now replicate any array arguments; this just works for just one
824         # array var in each argument.
825         #
826         if ( $arg =~ m{(.*)\$(\w+)(\+?)(.*)} && ref($vars->{$2}) eq "ARRAY" ) {
827             my $pre  = $1;
828             my $var  = $2;
829             my $esc  = $3;
830             my $post = $4;
831             foreach my $v ( @{$vars->{$var}} ) {
832                 $v = $bpc->shellEscape($v) if ( $esc eq "+" );
833                 push(@cmd, "$pre$v$post");
834             }
835         } else {
836             push(@cmd, $arg);
837         }
838     }
839     return \@cmd;
840 }
841
842 #
843 # Exec or eval a command.  $cmd is either a string on an array ref.
844 #
845 # @args are optional arguments for the eval() case; they are not used
846 # for exec().
847 #
848 sub cmdExecOrEval
849 {
850     my($bpc, $cmd, @args) = @_;
851     
852     if ( (ref($cmd) eq "ARRAY" ? $cmd->[0] : $cmd) =~ /^\&/ ) {
853         $cmd = join(" ", $cmd) if ( ref($cmd) eq "ARRAY" );
854         eval($cmd)
855     } else {
856         $cmd = [split(/\s+/, $cmd)] if ( ref($cmd) ne "ARRAY" );
857         exec(@$cmd);
858     }
859 }
860
861 #
862 # System or eval a command.  $cmd is either a string on an array ref.
863 # $stdoutCB is a callback for output generated by the command.  If it
864 # is undef then output is returned.  If it is a code ref then the function
865 # is called with each piece of output as an argument.  If it is a scalar
866 # ref the output is appended to this variable.
867 #
868 # @args are optional arguments for the eval() case; they are not used
869 # for system().
870 #
871 # Also, $? should be set when the CHILD pipe is closed.
872 #
873 sub cmdSystemOrEval
874 {
875     my($bpc, $cmd, $stdoutCB, @args) = @_;
876     my($pid, $out);
877     local(*CHILD);
878     
879     if ( (ref($cmd) eq "ARRAY" ? $cmd->[0] : $cmd) =~ /^\&/ ) {
880         $cmd = join(" ", $cmd) if ( ref($cmd) eq "ARRAY" );
881         my $out = eval($cmd);
882         $$stdoutCB .= $out if ( ref($stdoutCB) eq 'SCALAR' );
883         &$stdoutCB($out)   if ( ref($stdoutCB) eq 'CODE' );
884         return $out        if ( !defined($stdoutCB) );
885         return;
886     } else {
887         $cmd = [split(/\s+/, $cmd)] if ( ref($cmd) ne "ARRAY" );
888         if ( !defined($pid = open(CHILD, "-|")) ) {
889             my $err = "Can't fork to run @$cmd\n";
890             $? = 1;
891             $$stdoutCB .= $err if ( ref($stdoutCB) eq 'SCALAR' );
892             &$stdoutCB($err)   if ( ref($stdoutCB) eq 'CODE' );
893             return $err        if ( !defined($stdoutCB) );
894             return;
895         }
896         if ( !$pid ) {
897             #
898             # This is the child
899             #
900             close(STDERR);
901             open(STDERR, ">&STDOUT");
902             exec(@$cmd);
903         }
904         #
905         # The parent gathers the output from the child
906         #
907         while ( <CHILD> ) {
908             $$stdoutCB .= $_ if ( ref($stdoutCB) eq 'SCALAR' );
909             &$stdoutCB($_)   if ( ref($stdoutCB) eq 'CODE' );
910             $out .= $_       if ( !defined($stdoutCB) );
911         }
912         $? = 0;
913         close(CHILD);
914     }
915     return $out;
916 }
917
918 1;