#!/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 Time::localtime;
use File::Copy;
use File::Path;
use POSIX ":sys_wait_h";
use esmith::ConfigDB;

sub ldie;
sub start_dar_killer;
sub run_backup;

my $job = shift || 'DailyBackup';
my $report;

my $confdb = esmith::ConfigDB->open;
my $backupwk = $confdb->get('backupwk') or die "No backupwk db entry found\n";

my $tm = localtime(time);
my $bkname = $tm->year+1900;

$bkname .= "0" if ($tm->mon < 9);
$bkname .= $tm->mon + 1;

$bkname .= "0" if ($tm->mday < 10);
$bkname .= $tm->mday;

$bkname .= "0" if ($tm->hour < 10);
$bkname .= $tm->hour;

$bkname .= "0" if ($tm->min < 10);
$bkname .= $tm->min;

my $dow = $tm->wday;
my $id = $backupwk->prop('Id') ||
    $confdb->get('SystemName')->value . "." . $confdb->get('DomainName')->value;
my $err;
my $ref = "";
my $mntdone = 0;
my $tim = ctime();

my $smbhost = $backupwk->prop('SmbHost');
my $smbshare = $backupwk->prop('SmbShare');
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-1) 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') || 7;
my $mail = $backupwk->prop('MailNotify') || 'yes';
my $mntdir = $backupwk->prop('Mount') || '/mnt/smb';
my $tmpdir = $mntdir . "/tmp_dir";

$report .= "From: admin-backup\n";
$report .= "To: admin\n";
$report .= "Subject: Daily Backup Report\n\n";
$report .= "================================== \n";
$report .= "DAILY BACKUP TO WORKSTATION REPORT \n";
$report .= "================================== \n";
$report .= "Backup started at " .$tim . "\n";
$report .= "Backup of mysql databases has been done\n";

# mounting backup directory

$report .= "Mounting backup shared directory $smbhost/$smbshare \n";

# verify backup directory not already mounted

open FD, '/proc/mounts';
if ( $VFSType ne 'usb' )
{
    while (<FD>)
    {
    next unless /\s\/$smbhost(.*)\/$smbshare\s/;
    next unless /\s\/$mntdir\s/;
    $err++
    }
}
else 
{
    while (<FD>) 
    {
    next unless /\s\/$smbshare\s/;
    $err++;
    }
}
close FD;
if ($err)
{
    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");
}

# mount the backup dir

if ($VFSType eq 'cifs')
{
    $err = qx(/bin/mount -t cifs "$smbhost:$smbshare" $mntdir -o credentials=/etc/dar/CIFScredentials,nounix 2>&1);
    ldie("Error while mounting $smbhost:$smbshare : \n" . $err) if $err; 
}
elsif ($VFSType eq 'nfs')
{
    $err = qx(/bin/mount -t nfs -o nolock "$smbhost:/$smbshare" $mntdir 2>&1);
    ldie("Error while mounting $smbhost:/$smbshare : \n" . $err) if $err; 
}
elsif ($VFSType eq 'usb')
{
    $err = qx(/bin/mount "/$smbshare" 2>&1);
    if ($err) {ldie("Error while mounting /$smbshare : \n" . $err)};
    $mntdir = "/$smbshare";
}
else
{
    ldie("Error while mounting $smbhost/$smbshare : $VFSType not supported.\n");
}
$mntdone = 1;

# verify $mntdir has an entry in /proc/mounts

$err = 0;
open FD, '/proc/mounts';
while (<FD>)
{
    next unless /\s$mntdir\s/;
    $err++;
}
close FD;
if ($err == 0)
{
    ldie("Seems backup directory is not really mounted. It should not happen. \
    Verify availability of your backup volume. Stopping the backup now.\n")
}

$tmpdir = $mntdir . '/tmp_dir';
if (-d "$tmpdir/$id")
{
    eval {rmtree("$tmpdir/$id")};
    ldie("Error while deleting $tmpdir/$id : $@.\n") if $@;
}
eval {mkpath("$tmpdir/$id")};
ldie("Error while creating $tmpdir/$id : $@. Maybe insufficient rights on backup directory.\n")
    if $@;

# we know right backup directory is in line and we can write on it.
$report .= "Backup temp directory $tmpdir/$id is mounted and is writable \n";

# rotating backup indicators

++$incnum;
$incnum = 0 if ($dow == $fullday && $incnum > $daysinset-7) || 
               ($fullday == 7 && $incnum >= $daysinset);
if ($incnum == 0)
{
    ++$setnum;
    $setnum %= $setsmax;
}

$report .= "Using set number $setnum of $setsmax\n";
if ($incnum == 0)
{
	$report .= "Attempt full backup \n";
}
else
{
	$report .= "Attempt incremental backup number $incnum of $daysinset\n";
}

# if no set directory, make it
my $setname = "set" . $setnum;
my $setdirname = $mntdir . "/$id/" . $setname;

unless ( -d $setdirname )
{
    eval {mkpath($setdirname)};
    ldie("Can't create $setdirname : $@.\n") if $@;
    $report .= "Backup directory $id/$setname created \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;
	$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 .= "Backup base file name is $bkname \n";
$report .= "Making backup in temp directory\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 .= "Using a backup session timeout of : $timeout seconds\n";

# expanding backup configuration file template

processTemplate({
        TEMPLATE_PATH => "/etc/dar/$job.dcf",
    });

# launching dar backup

my $rc = run_backup();
if ($rc != 0 && $rc != 11)
{
	ldie("Error while running dar: $rc");
}

if ($incnum == 0)
{
    $report .= "Rotating backups in a new set $setdirname. \n";
    eval {rmtree($setdirname)};
    ldie("Error while deleting $setdirname : $@.\n") if $@;
    eval {mkpath("$setdirname")};
    ldie("Error while creating $setdirname : $@.\n") if $@;
}

$report .= "Moving backup files to target directory $setdirname \n"; 

foreach (<$tmpdir/$id/$bkname*>)
{
    ldie("Error while moving backup file $_ from temporary dir $tmpdir/$id to $setdirname : $!")
	unless move($_, $setdirname);
}

# unmount shared folder
system("/bin/umount", "-f", "$mntdir");

# time now to update backup configuration

$report .= "Updating backup configuration data \n";
$backupwk->set_prop('SetNum', $setnum);
$backupwk->set_prop('IncNum', $incnum);

$tim = ctime();
$report .= "Backup successfully terminated at : $tim \n";

if ($mail eq 'yes')
{
    open (MAIL, "|/var/qmail/bin/qmail-inject -a admin")
        || die "Cannot start mail program: $!\n";
    print MAIL $report;
    close(MAIL);
}

exit (0);

sub ldie 
{
    my $errmsg = shift;
    $report .= "*** No backup allowed or error during backup ***\n";
    $report .= $errmsg;
    if (($mail eq 'yes') || ($mail eq 'error'))
    {
	open (MAIL, "|/var/qmail/bin/qmail-inject -a admin")
	    || die "Cannot start mail program: $!: message was $errmsg\n";
	print MAIL $report;
	close(MAIL);
    }
    system("/bin/umount", "$mntdir") if $mntdone;
    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 $data = undef;
    my $pid = undef;
    my $killerpid = undef;

    eval {
         		($pid = open INPUT, "-|", "/usr/bin/dar", "-Q", "--create", "$tmpdir/$id/$bkname", split(/\s+/,$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(9, $killerpid) != 1) {
             warn "Failed to kill $killerpid killer process\n";
         }

         waitpid($killerpid, 0);
     }

	waitpid($pid, 0);
	my $code = WEXITSTATUS($?);
	close(INPUT);
	return $code;
}
