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

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 $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 $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";

# ping the SMB Host to see if it is awake
$report .= wol ($ether,$smbhost,$smbhostmac,$smbhostdelay);

# mount backup
bmount($mntdir,$smbhost,$smbshare,$VFSType);

# 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", "--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;
}


sub checkMount
{
    # check if $mountdir is mounted
    my $mountdir = shift;
    return system("/bin/mountpoint", "-q", "$mountdir");
}


# dmount is copied from /etc/e-smith/web/functions/backup
# it could become a common function later
sub dmount
{
    # mount dar unit according to dar-workstation configuration
    # return nothing if mount successfull

    my ($host,$share,$mountdir,$login,$password,$VFSType) = @_;

    if ($VFSType eq 'cifs')
    {
        return ( qx(/bin/mount -t cifs "//$host/$share" $mountdir -o credentials=/etc/dar/CIFScredentials,nounix 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 $blkdev = "";
        my $vollbl = "";
        foreach my $udi (qx(hal-find-by-property --key volume.fsusage --string filesystem))
        {
            $udi =~ m/^(\S+)/;
            my $is_mounted = qx(hal-get-property --udi $1 --key volume.is_mounted);

            if ($is_mounted eq "false\n")
            {
                $blkdev = qx(hal-get-property --udi $1 --key block.device);
                if ($blkdev =~ m/^(\S+)/) {$blkdev = $1;}
            }
            if ($is_mounted eq "false\n")
            {
                $vollbl = qx(hal-get-property --udi $1 --key volume.label);
                $vollbl =~ m/^(\S+)/;
                if ($vollbl =~ /^\s/) {$vollbl = 'nolabel';}
            }

            chomp $vollbl;
            chomp $blkdev;
            $vollbl = "media/$vollbl";
            if ($vollbl eq $share) {$device = $blkdev;}
        }
        return ( qx(/bin/mount $device "/$share" 2>&1) );
    }
    else
    {
        return ("Mount of type $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 }, $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", "-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", "-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) = @_;
    # 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);
    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;
}
