524 lines
15 KiB
Plaintext
524 lines
15 KiB
Plaintext
|
#!/usr/bin/perl -w
|
||
|
|
||
|
#----------------------------------------------------------------------
|
||
|
# copyright (C) 2006-2007 Jean-Paul Leclere <jean-paul@leclere.org>
|
||
|
# copyright (C) 2007 Charlie Brady <charlieb@e-smith.com>
|
||
|
#
|
||
|
# 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
|
||
|
#
|
||
|
#----------------------------------------------------------------------
|
||
|
|
||
|
use strict;
|
||
|
use Errno;
|
||
|
use esmith::util;
|
||
|
use esmith::templates;
|
||
|
use File::Copy;
|
||
|
use File::Path qw(make_path remove_tree);
|
||
|
use File::Find;
|
||
|
use POSIX qw(:sys_wait_h strftime);
|
||
|
use File::Glob qw(bsd_glob);
|
||
|
use esmith::ConfigDB;
|
||
|
use esmith::Backup;
|
||
|
use esmith::BlockDevices;
|
||
|
|
||
|
sub ldie;
|
||
|
sub start_dar_killer;
|
||
|
sub run_backup;
|
||
|
|
||
|
my $job = shift || 'DailyBackup';
|
||
|
my $confdb = esmith::ConfigDB->open;
|
||
|
my $backupwk = $confdb->get('backupwk') or die "No backupwk db entry found\n";
|
||
|
my $bkname = strftime '%Y%m%d%H%M%S', localtime;
|
||
|
my $dow = strftime '%w', localtime;
|
||
|
my $ref = "";
|
||
|
|
||
|
my $id = $backupwk->prop('Id') ||
|
||
|
$confdb->get('SystemName')->value . "." . $confdb->get('DomainName')->value;
|
||
|
my $internalinterface = $confdb->get('InternalInterface') or die "No internalinterface db entry found\n";
|
||
|
my $ether = $internalinterface->prop('Name');
|
||
|
my $smbhost = $backupwk->prop('SmbHost');
|
||
|
my $smbshare = $backupwk->prop('SmbShare');
|
||
|
my $smbv1 = $backupwk->prop('SmbV1') || 'disabled';
|
||
|
my $smbhostmac = $backupwk->prop('SmbHostMAC');
|
||
|
my $smbhostdelay = $backupwk->prop('SmbHostDelay') || 300;
|
||
|
my $login = $backupwk->prop('Login');
|
||
|
my $password = $backupwk->prop('Password');
|
||
|
my $setsmax = $backupwk->prop('SetsMax') || 1;
|
||
|
my $daysinset = $backupwk->prop('DaysInSet') || 1;
|
||
|
my $setnum = $backupwk->prop('SetNum'); $setnum = $setsmax unless defined $setnum;
|
||
|
my $incnum = $backupwk->prop('IncNum'); $incnum = ($daysinset-1) unless defined $incnum;
|
||
|
my $timeout = (($backupwk->prop('Timeout') * 3600) - 30) || '88500';
|
||
|
my $inconly = $backupwk->prop('IncOnlyTimeout') || 'no';
|
||
|
my $VFSType = $backupwk->prop('VFSType') || 'cifs';
|
||
|
my $fullday = $backupwk->prop('FullDay'); $fullday = 7 unless defined $fullday;
|
||
|
my $mail = $backupwk->prop('MailNotify') || 'yes';
|
||
|
my $frommail = $backupwk->prop('FromMail') || 'admin-backup';
|
||
|
my $tomail = $backupwk->prop('ToMail') || 'admin';
|
||
|
my $mntdir = $backupwk->prop('Mount') || '/mnt/smb';
|
||
|
$mntdir = "\/$smbshare" if ($VFSType eq 'usb'); # ToDo change to $backupwk->prop('Mount')
|
||
|
my $deleteearly = $backupwk->prop('DeleteEarly') || 'false';
|
||
|
my @backup_excludes = esmith::Backup->excludes;
|
||
|
@backup_excludes = map "\t/$_\n", sort @backup_excludes;
|
||
|
|
||
|
my $report = "From: $frommail\n";
|
||
|
$report .= "To: $tomail\n";
|
||
|
$report .= "Subject: Daily Backup Report: $id\n\n";
|
||
|
$report .= "================================== \n";
|
||
|
$report .= "DAILY BACKUP TO WORKSTATION REPORT \n";
|
||
|
$report .= "================================== \n";
|
||
|
$report .= "Backup of $id started at " .localtime() . "\n";
|
||
|
$report .= "================================== \n";
|
||
|
|
||
|
if (@backup_excludes) {
|
||
|
$report .= "Some parts are excluded of your backup: \n";
|
||
|
$report .= "@backup_excludes";
|
||
|
$report .= "================================== \n";
|
||
|
}
|
||
|
|
||
|
# ping the SMB Host to see if it is awake
|
||
|
$report .= wol ($ether,$smbhost,$smbhostmac,$smbhostdelay);
|
||
|
|
||
|
# mount backup
|
||
|
bmount($mntdir,$smbhost,$smbshare,$VFSType,$smbv1);
|
||
|
|
||
|
# rotating backup indicators
|
||
|
$incnum++;
|
||
|
$incnum = 0 if ($dow == $fullday && $incnum > $daysinset-7) ||
|
||
|
($fullday == 7 && $incnum >= $daysinset);
|
||
|
if ($incnum == 0)
|
||
|
{
|
||
|
$setnum %= $setsmax;
|
||
|
$setnum++;
|
||
|
}
|
||
|
|
||
|
# if no set directory, make it
|
||
|
my $setname = "set$setnum";
|
||
|
my $setdirname = "$mntdir/$id/$setname";
|
||
|
createTree ($setdirname);
|
||
|
|
||
|
$report .= "Destination //$smbhost/$smbshare/$id/$setname\n";
|
||
|
|
||
|
if ( $incnum == 0 )
|
||
|
{
|
||
|
$bkname = "full-" . $bkname;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
# if $incnum <> 0 backup should be incremental
|
||
|
# we find correct reference backup for incremental
|
||
|
my $file;
|
||
|
opendir(DIR, $setdirname) or ldie("Can't open dir $setdirname $!");
|
||
|
while (defined($file = readdir(DIR)))
|
||
|
{
|
||
|
next if $file =~ /^\.\.?$/;
|
||
|
if ($file =~ /dar$/)
|
||
|
{
|
||
|
$ref = $file;
|
||
|
}
|
||
|
}
|
||
|
closedir (DIR);
|
||
|
# if no reference do full backup
|
||
|
if ($ref eq "")
|
||
|
{
|
||
|
$incnum = 0;
|
||
|
$report .= "No existing reference backup, will make full backup \n";
|
||
|
$bkname = "full-" . $bkname;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
# removing .dar extension
|
||
|
$ref =~ s/\..*\.dar$//;
|
||
|
$ref = "--ref|" . $setdirname . "/" . $ref; # | will be used to split this string in run_backup()
|
||
|
$bkname = "inc-" . sprintf("%03d", $incnum) . "-". $bkname;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
unless ( ( $incnum != 0 ) || ( $fullday == 7 ) || ( $dow == $fullday ) )
|
||
|
{
|
||
|
my $delay = ($fullday - $dow) % 7;
|
||
|
ldie("Not a permitted day for full backup. Aborting...\nNext full backup in $delay days.\n");
|
||
|
}
|
||
|
|
||
|
$report .= "Basename $bkname\n";
|
||
|
|
||
|
# calculate real timeout if we timeout incrementals only.
|
||
|
# timeout of 88500 is a security for aborting backup within 24h
|
||
|
|
||
|
if ( ($ref eq "") && ($inconly eq "yes"))
|
||
|
{
|
||
|
$timeout = 88500;
|
||
|
}
|
||
|
$report .= "Starting the backup with a timeout of ". int ($timeout/(60*60)). " hours\n";
|
||
|
|
||
|
# Expand backup configuration file template
|
||
|
processTemplate ({TEMPLATE_PATH => "/etc/dar/$job.dcf"});
|
||
|
|
||
|
# If this is a new set and delete before backup is in use
|
||
|
# then empty target directory of any .dar files
|
||
|
if (($deleteearly eq 'true') && (($incnum == 0)))
|
||
|
{
|
||
|
foreach my $file (glob("$setdirname/*.dar"))
|
||
|
{
|
||
|
unlink $file or ldie("Error deleting old backup files in $setdirname : $!")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
my $destination = ($deleteearly ne 'true') ? "$mntdir/$id/$bkname" : "$setdirname/$bkname";
|
||
|
|
||
|
# Perform the actual backup
|
||
|
my $rc = run_backup($destination);
|
||
|
|
||
|
if ($rc != 0 && $rc != 11)
|
||
|
{
|
||
|
ldie("Error while running dar: $rc");
|
||
|
}
|
||
|
|
||
|
if ($deleteearly ne 'true') # Not DeleteEarly so move backup to $setdirname
|
||
|
{
|
||
|
if ($incnum == 0) # If this is a new set then empty target directory of any .dar files
|
||
|
{
|
||
|
foreach my $file (glob("$setdirname/*.dar"))
|
||
|
{
|
||
|
unlink $file or ldie("Error deleting old backup files in $setdirname : $!")
|
||
|
}
|
||
|
}
|
||
|
foreach (bsd_glob("$mntdir/$id/*.dar")) # Move the backup files to the set directory
|
||
|
{
|
||
|
ldie("Error while moving backup file $_ to $setdirname : $!")
|
||
|
unless move($_, $setdirname);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# update dar_manager catalog
|
||
|
updateDarCatalog ("$mntdir/$id");
|
||
|
|
||
|
# Check free disk space
|
||
|
my $df = qx(/bin/df -Ph \"$mntdir\");
|
||
|
if ($df =~ /(\S+)\s+(\S+)\s+(\S+)\s+(\d*%)/)
|
||
|
{
|
||
|
$report .= "Destination disk usage $2, $4 full, $3 available\n";
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$report .= "Destination disk space not available\n";
|
||
|
}
|
||
|
|
||
|
# unmount shared folder
|
||
|
system("/bin/umount", "-f", "$mntdir") unless ($VFSType eq 'mnt');
|
||
|
|
||
|
# time now to update backup configuration
|
||
|
$backupwk->set_prop('SetNum', $setnum);
|
||
|
$backupwk->set_prop('IncNum', $incnum);
|
||
|
|
||
|
$report .= "Backup successfully terminated at ".localtime()."\n";
|
||
|
|
||
|
# Send the Workstation Backup report
|
||
|
if ($mail eq 'yes') {sendReport ($report);}
|
||
|
|
||
|
exit (0);
|
||
|
|
||
|
sub ldie
|
||
|
{
|
||
|
my $errmsg = shift;
|
||
|
$report =~ s/Report:/Failed:/;
|
||
|
$report .= "*** No backup allowed or error during backup ***\n";
|
||
|
$report .= $errmsg;
|
||
|
if (($mail eq 'yes') || ($mail eq 'error'))
|
||
|
{
|
||
|
sendReport ($report,$errmsg);
|
||
|
}
|
||
|
if (($VFSType ne 'mnt') && (!checkMount ($mntdir)))
|
||
|
{
|
||
|
system("/bin/umount", "-f", "$mntdir");
|
||
|
}
|
||
|
die($errmsg);
|
||
|
}
|
||
|
|
||
|
sub start_dar_killer
|
||
|
{
|
||
|
my ($darpid, $gracetime) = @_;
|
||
|
my $tick = $gracetime/10;
|
||
|
|
||
|
my $killer = fork;
|
||
|
return $killer if $killer;
|
||
|
|
||
|
POSIX::setsid;
|
||
|
chdir '/';
|
||
|
#fork && exit;
|
||
|
|
||
|
# wait for timeout or backup termination
|
||
|
while ($tick > 0)
|
||
|
{
|
||
|
sleep 10;
|
||
|
$tick--;
|
||
|
exit unless (kill(0, $darpid));
|
||
|
}
|
||
|
|
||
|
if (kill(0, $darpid))
|
||
|
{
|
||
|
while (kill('QUIT', $darpid) != 1)
|
||
|
{
|
||
|
warn "Failed to stop $darpid dar process\n";
|
||
|
}
|
||
|
}
|
||
|
warn "Partial backup stored on backup workstation.\n",
|
||
|
"Session cleanly closed by timeout after $timeout seconds.\n",
|
||
|
"Not an error, backup process will continue next night.\n";
|
||
|
|
||
|
exit;
|
||
|
}
|
||
|
|
||
|
sub run_backup
|
||
|
{
|
||
|
my $dest = shift;
|
||
|
my $data = undef;
|
||
|
my $pid = undef;
|
||
|
my $killerpid = undef;
|
||
|
|
||
|
eval
|
||
|
{
|
||
|
($pid = open INPUT, "-|", "/usr/bin/dar", "-Q", "-asecu", "-az", "--create", "$dest", split(/\|/,$ref), "-B", "/etc/dar/$job.dcf") or ldie("cannot start : $!" );
|
||
|
|
||
|
if ($pid)
|
||
|
{
|
||
|
$killerpid = start_dar_killer($pid, $timeout);
|
||
|
}
|
||
|
$data = do { local($/); <INPUT> };
|
||
|
};
|
||
|
$report .= $data;
|
||
|
|
||
|
if ($killerpid && kill(0, $killerpid))
|
||
|
{
|
||
|
while (kill('TERM', $killerpid) != 1)
|
||
|
{
|
||
|
warn "Failed to kill $killerpid killer process\n";
|
||
|
}
|
||
|
waitpid($killerpid, 0);
|
||
|
}
|
||
|
|
||
|
waitpid($pid, 0);
|
||
|
my $code = WEXITSTATUS($?);
|
||
|
close(INPUT);
|
||
|
return $code;
|
||
|
}
|
||
|
|
||
|
# Copied from /etc/e-smith/web/functions/backup
|
||
|
# TODO: Move to a shared module
|
||
|
sub checkMount
|
||
|
{
|
||
|
# check if $mountdir is mounted
|
||
|
my $mountdir = shift;
|
||
|
$|=1; # Auto-flush
|
||
|
|
||
|
my @res = qx( findmnt $mountdir );
|
||
|
return ( !@res );
|
||
|
}
|
||
|
|
||
|
|
||
|
# Copied from /etc/e-smith/web/functions/backup
|
||
|
# TODO: Move to a shared module
|
||
|
sub dmount
|
||
|
{
|
||
|
# mount dar unit according to dar-workstation configuration
|
||
|
# return nothing if mount successfull
|
||
|
|
||
|
my ($host,$share,$mountdir,$login,$password,$VFSType,$smbv1) = @_;
|
||
|
|
||
|
if ($VFSType eq 'cifs')
|
||
|
{
|
||
|
my $opt= ($smbv1 eq "enabled")? ",vers=1.0": "";
|
||
|
return ( qx(/bin/mount -t cifs "//$host/$share" $mountdir -o credentials=/etc/dar/CIFScredentials,nounix$opt 2>&1) );
|
||
|
}
|
||
|
elsif ($VFSType eq 'nfs')
|
||
|
{
|
||
|
return ( qx(/bin/mount -t nfs -o nolock "$host:/$share" $mountdir 2>&1) );
|
||
|
}
|
||
|
elsif ($VFSType eq 'usb')
|
||
|
{
|
||
|
my $device = "";
|
||
|
my $vollbl = "";
|
||
|
|
||
|
my $devices = esmith::BlockDevices->new ('allowmount' => 'disabled');
|
||
|
my ($valid, $invalid) = $devices->checkBackupDrives(0);
|
||
|
|
||
|
if ( ${$valid}[0] ) {
|
||
|
foreach ( @{$valid} ) {
|
||
|
$vollbl = $devices->label($_);
|
||
|
if ( $share eq "media/$vollbl" ) {
|
||
|
$device = "/dev/$_";
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
$devices->destroy;
|
||
|
return ( qx (mount $device /$share 2>&1) );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return ("Error while mounting $host/$share : $VFSType not supported.\n");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
sub removeTree
|
||
|
{
|
||
|
my $tree = shift;
|
||
|
if (-d "$tree")
|
||
|
{
|
||
|
eval {remove_tree("$tree")};
|
||
|
ldie("Error while deleting $tree : $@.\n") if $@;
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
sub createTree
|
||
|
{
|
||
|
my $tree = shift;
|
||
|
if (! -d "$tree")
|
||
|
{
|
||
|
eval {make_path("$tree")};
|
||
|
ldie("Error while creating $tree : $@. Maybe insufficient rights directory.\n") if $@;
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
sub sendReport
|
||
|
{
|
||
|
my $text = shift;
|
||
|
my $error = shift || "";
|
||
|
open (MAIL, "|/var/qmail/bin/qmail-inject")
|
||
|
|| die "Cannot start mail program: $! $error\n";
|
||
|
print MAIL $text;
|
||
|
close(MAIL);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
sub wol
|
||
|
{
|
||
|
my ($ether,$host,$mac,$delay) = @_;
|
||
|
my $output="";
|
||
|
|
||
|
if (defined($mac) && (system("ping -c1 $host > /dev/null") != 0)) {
|
||
|
$output .= "$host might be asleep, attempting to wake\n";
|
||
|
system("ether-wake -i $ether $mac");
|
||
|
$output .= "Waiting $delay seconds...\n";
|
||
|
sleep $delay;
|
||
|
}
|
||
|
return $output;
|
||
|
}
|
||
|
|
||
|
sub updateDarCatalog
|
||
|
{
|
||
|
# update dar_manager catalog
|
||
|
my $mntbkdir = shift;
|
||
|
my $catalog = "$mntbkdir/dar-catalog";
|
||
|
my %backupfiles = (); # hash of backup files found on the disk.
|
||
|
|
||
|
unless ( -e $catalog) # Create an empty catalog if none found
|
||
|
{
|
||
|
system("/usr/bin/dar_manager", "-Q", "-C", "$catalog") == 0
|
||
|
or ldie("Unable to create dar_manager catalog.\n");
|
||
|
sleep 1; # sleep added to ensure the creation of a valid catalog
|
||
|
}
|
||
|
|
||
|
# find available backups for the server
|
||
|
my $setbackuplist = sub {if($_ =~ /([\w|-]+)-(\d+)\..*\.dar/){$backupfiles{$2} = $File::Find::dir."/$1-$2";}};
|
||
|
find { wanted => \&$setbackuplist, untaint => 1, follow => 1 }, $mntbkdir ;
|
||
|
my @deletions;
|
||
|
|
||
|
my $pid = open (DAR_LIST, "-|", "/usr/bin/dar_manager", "-Q", "-B", "$catalog", "-l") or ldie ("Cannot start : $!");
|
||
|
my $catalogIndex; # Dar refers to each backup by an index
|
||
|
my $oldkey=0; # The key for %backupfiles is the timestamp
|
||
|
|
||
|
while (<DAR_LIST>)
|
||
|
{
|
||
|
next unless ($_ =~ /(\d+).*\s[\w|-]+-(\d+)/);
|
||
|
($catalogIndex, $oldkey) = ($1,$2);
|
||
|
|
||
|
if (defined($backupfiles{$oldkey})) # found on disk and in catalog
|
||
|
{
|
||
|
delete $backupfiles{$oldkey}; # so that it will not be added in the next step.
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
push @deletions, $catalogIndex; # Will be deleted from the catalog
|
||
|
}
|
||
|
}
|
||
|
waitpid ($pid, 0);
|
||
|
my $err = WEXITSTATUS($?);
|
||
|
close (DAR_LIST);
|
||
|
ldie ("Catalog error : $err") if $err;
|
||
|
|
||
|
# Delete backups from the catalog that were missing on disk in descending order
|
||
|
while (my $catIndex = pop @deletions)
|
||
|
{
|
||
|
system("/usr/bin/dar_manager", "-Q", "-ai", "-B", "$catalog", "-D", "$catIndex") == 0
|
||
|
or ldie("Failed to delete set $catIndex from catalog. $!\n");
|
||
|
sleep 1; # sleep added to ensure the creation of a valid catalog
|
||
|
}
|
||
|
|
||
|
# Add backups that were missing from the catalog in chronological order
|
||
|
foreach my $key (sort keys %backupfiles)
|
||
|
{
|
||
|
next unless ($key > $oldkey); # prevent adding old backups to the catalog
|
||
|
system("/usr/bin/dar_manager", "-Q", "-ai", "-B", "$catalog", "-A", "$backupfiles{$key}") == 0
|
||
|
or ldie("Failed to add set $backupfiles{$key} to catalog. $!\n");
|
||
|
sleep 1; # sleep added to ensure the creation of a valid catalog
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
sub bmount
|
||
|
{
|
||
|
my ($mntdir,$host,$share,$VFSType,$smbv1) = @_;
|
||
|
# verify backup directory not already mounted
|
||
|
|
||
|
if (!checkMount ($mntdir))
|
||
|
{
|
||
|
return if ($VFSType eq 'mnt');
|
||
|
ldie("Seems backup directory is already mounted. " .
|
||
|
"It should not happen and maybe there is a zombie process " .
|
||
|
"you must kill, or another backup in progress. \n");
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if ($VFSType eq 'mnt')
|
||
|
{
|
||
|
ldie("Seems backup directory is not mounted. " .
|
||
|
"The backup directory must be mounted when using type 'mnt'. \n");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# create the directory mount point if it does not exist
|
||
|
createTree ($mntdir);
|
||
|
|
||
|
# mount the backup directory
|
||
|
my $err = dmount($host,$share,$mntdir,'','',$VFSType,$smbv1);
|
||
|
ldie("Error while mounting <//$smbhost/$smbshare>\n" . $err) if $err;
|
||
|
|
||
|
# verify $mntdir is mounted
|
||
|
if (checkMount ($mntdir))
|
||
|
{
|
||
|
# The mount should have suceeded, but sometimes it needs more time,
|
||
|
# so sleep and then check again.
|
||
|
sleep 5;
|
||
|
if (checkMount ($mntdir))
|
||
|
{
|
||
|
ldie("Seems backup directory is not really mounted. It should not happen. \
|
||
|
Verify availability of your backup volume. Stopping the backup now.\n");
|
||
|
}
|
||
|
}
|
||
|
return;
|
||
|
}
|