smeserver-backup/root/etc/e-smith/events/actions/workstation-backup-dar

524 lines
15 KiB
Perl

#!/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;
}