#! /usr/bin/env perl

#
#   Copyright (C) Dr. Heinz-Josef Claes (2004-2012)
#                 hjclaes@web.de
#   
#   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 3 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, see <http://www.gnu.org/licenses/>.
#


$main::STOREBACKUPVERSION = undef;

use strict;
use Net::Ping;
use POSIX;


sub libPath
{
    my $file = shift;

    my $dir;

    # Falls Datei selbst ein symlink ist, solange folgen, bis aufgelöst
    if (-f $file)
    {
	while (-l $file)
	{
	    my $link = readlink($file);

	    if (substr($link, 0, 1) ne "/")
	    {
		$file =~ s/[^\/]+$/$link/;
	    }
	    else
	    {
		$file = $link;
	    }
	}

	($dir, $file) = &splitFileDir($file);
	$file = "/$file";
    }
    else
    {
	print STDERR "<$file> does not exist, exiting!\n";
        POSIX::_exit 2;
    }

    $dir .= "/../lib";           # Pfad zu den Bibliotheken
    my $oldDir = `/bin/pwd`;
    chomp $oldDir;
    if (chdir $dir)
    {
	my $absDir = `/bin/pwd`;
	chop $absDir;
	chdir $oldDir;

	return (&splitFileDir("$absDir$file"));
    }
    else
    {
	print STDERR "<$dir> does not exist, exiting\n";
        POSIX::_exit 2;
    }
}
sub splitFileDir
{
    my $name = shift;

    return ('.', $name) unless ($name =~/\//);    # nur einfacher Dateiname

    my ($dir, $file) = $name =~ /^(.*)\/(.*)$/s;
    $dir = '/' if ($dir eq '');                   # gilt, falls z.B. /filename
    return ($dir, $file);
}
my ($req, $prog) = &libPath($0);
(@INC) = ($req, @INC);

require 'checkParam2.pl';
require 'checkObjPar.pl';
require 'prLog.pl';
require 'forkProc.pl';
require 'dateTools.pl';
require 'version.pl';
require 'tail.pl';

$main::exit = 0;                               # exit status

=head1 NAME

storeBackupMount.pl - runs storeBackup backing up to an nfs mount

=head1 SYNOPSIS

	storeBackupMount.pl -c configFile [-s server] [-l logFile] [-d]
	[-p pathToStoreBackup] [-k killTime] [-m] mountPoints...

=head1 DESCRIPTION

This script does the following:

=over 4

=item - checks an nfs server with ping

=item - mounts that server via a list of mount points

=item - starts storeBackup (with a config file)

=item - umounts that server

=back

=head1 OPTIONS

=over 8

=item B<--server>, B<-s>

    name or ip address of the nfs server
    default is localhost

=item B<--configFile>, B<-c>

    configuration file for storeBackup.
    if option 'logFile' is set in the configration file,
    that log file is read online, if it is different from -l

=item B<--logFile>, B<-l>

    logFile for this process.
    default is STDOUT.
    you can log into the same logfile as storeBackup

=item B<--debug>, B<-d>

    generate some debug messages

=item B<--pathStbu>, B<-p>

    path to executable storeBackup.pl or storeBackup

=item B<--killTime> B<-k>

    time until storeBackup.pl will be killed.
    default is 365 days.
    the time range has to be specified in format 'dhms', e.g.
    10d4h means 10 days and 4 hours

=item B<--keepExistingMounts>, B<-m>

    if a mount already exists, do not umount after
    running storeBackup

=item F<mountPoints>

    List of mount points needed to perform the backup.
    This must be a list of paths which have to be
    defined in /etc/fstab.
    -
    if you add 'ro,' or 'rw,' to the beginning of a mount
    point, you can overwrite that option set in /etc/fstab

    example:
    ro,/filesSystemToRead
       will mount /fileSystemToRead read only, even if the
       corresponding entry in /etc/fstab mounts it read write

    only root is allowed to use this feature!

=back

=head1 EXIT STATUS

=over 4

=item 0 -> everything is ok

=item 1 -> error from storeBackup

=item 2 -> error from storeBackupMount

=item 3 -> error from both programs

=back

=head1 COPYRIGHT

Copyright (c) 2004-2012 by Heinz-Josef Claes (see README).
Published under the GNU General Public License v3 or any later version

=cut

my $Help = join('', grep(!/^\s*$/, `pod2text $0`));
$Help = "cannot find pod2text, see documentation for details\n"
    unless $Help;

&printVersion(\@ARGV, '-V');

my $CheckPar =
    CheckParam->new('-allowLists' => 'yes',
		    '-list' => [Option->new('-name' => 'server',
					    '-cl_option' => '-s',
					    '-cl_alias' => '--server',
					    '-param' => 'yes'),
				Option->new('-name' => 'configFile',
					    '-cl_option' => '-c',
					    '-cl_alias' => '--configFile',
					    '-param' => 'yes',
					    '-must_be' => 'yes'),
				Option->new('-name' => 'logFile',
					    '-cl_option' => '-l',
					    '-cl_alias' => '--logFile',
					    '-param' => 'yes'),
				Option->new('-name' => 'debug',
					    '-cl_option' => '-d',
					    '-cl_alias' => '--debug'),
				Option->new('-name' => 'pathStbu',
					    '-cl_option' => '-p',
					    '-cl_alias' => '--pathStbu',
					    '-param' => 'yes'),
				Option->new('-name' => 'killTime',
					    '-cl_option' => '-k',
					    '-cl_alias' => '--killTime',
					    '-default' => '365d'),
				Option->new('-name' => 'keepExistingMounts',
					    '-cl_option' => '-m',
					    '-cl_alias' => '--keepExistingMounts')
				]);

$CheckPar->check('-argv' => \@ARGV,
                 '-help' => $Help
                 );

my $server = $CheckPar->getOptWithPar('server');
my $configFile = $CheckPar->getOptWithPar('configFile');
my $logFile = $CheckPar->getOptWithPar('logFile');
my $debug = $CheckPar->getOptWithoutPar('debug');
my $pathStbu = $CheckPar->getOptWithPar('pathStbu');
my $kt = $CheckPar->getOptWithPar('killTime');
my $keepExistingMounts = $CheckPar->getOptWithoutPar('keepExistingMounts');
my (@mountPoints) = $CheckPar->getListPar();

my $prLog;
my ($prLogKind) = ['A:BEGIN',
		   'Z:END',
		   'V:VERSION',
		   'I:INFO',
		   'D:DEBUG',
		   'W:WARNING',
		   'E:ERROR'];
if ($logFile)
{
    $prLog = printLog->new('-file' => $logFile,
			   '-multiprint' => 'yes',
			   '-kind' => $prLogKind);
}
else
{
    $prLog = printLog->new('-kind' => $prLogKind);
}

$prLog->print('-kind' => 'A',
	      '-str' => ["starting storeBackup -f $configFile"]);
$prLog->print('-kind' => 'V',
	      '-str' => ["storeBackupMount.pl, $main::STOREBACKUPVERSION"]);
$prLog->print('-kind' => 'E',
	      '-str' => ["cannot open configuration file <$configFile>"],
	      '-exit' => 1)
    unless -r $configFile;


# killTime in seconds:
my $killTime = &dateTools::strToSec('-str' => $kt);
unless (defined $killTime)
{
    $prLog->print('-kind' => 'E',
		  '-str' => ["wrong format of parameter --killTime: <$kt>"]);
    wait;
    POSIX::_exit 2;
}

my $withDotPlFlag;
if ($pathStbu)
{
    if (-x "$pathStbu/storeBackup.pl")
    {
	$withDotPlFlag = 1;
    }
    elsif (-x "$pathStbu/storeBackup")
    {
	$withDotPlFlag = 0;
    }
    else
    {
	$prLog->print('-kind' => 'E',
		      '-str' => ["cannot find executable storeBackup or" .
		      "storeBackup.pl at <$pathStbu>"],
		      '-exit' => 1);
    }
}
else
{
    $pathStbu = `which storeBackup.pl`;
    if ($pathStbu)
    {
	$withDotPlFlag = 1;
    }
    else
    {
	$pathStbu = `which storeBackup`;
	$prLog->print('-kind' => 'E',
		      '-str' => ["cannot find executable storeBackup or" .
		      "storeBackup.pl in \$PATH"],
		      '-exit' => 1)
	    unless $pathStbu;
	$withDotPlFlag = 0;
    }
    chop $pathStbu;
}

#
# read config file of storeBackup.pl
#
my $stbuLogFile = undef;
{
    my $CheckStbuConf =
	CheckParam->new('-allowLists' => 'no',
			'-configFile' => '-f',
			'-list' => [Option->new('-name' => 'configFile',
						'-cl_option' => '-f',
						'-param' => 'yes'),
				    Option->new('-name' => 'logFile',
						'-cf_key' => 'logFile',
						'-param' => 'yes')
			]);
    $CheckStbuConf->check('-argv' => ['-f' => $configFile],
			  '-help' =>
			  "cannot read configuration file <$configFile>\n",
			  '-ignoreAdditionalKeys' => 1);
    $stbuLogFile = $CheckStbuConf->getOptWithPar('logFile');

    $logFile = ::absolutePath($logFile) if $logFile;
    my $_logFile = $logFile ? $logFile : "stdout";
    $stbuLogFile = ::absolutePath($stbuLogFile) if ($logFile);
    my $_stbuLogFile = $stbuLogFile ? $stbuLogFile : "stdout";
    $prLog->print('-kind' => 'D',
		  '-str' =>
		  ["logFile for $prog is set to <$_logFile>",
		   "logFile in <$configFile> is set to <$_stbuLogFile>"])
	if $debug;
    if (not $stbuLogFile or $stbuLogFile eq $logFile)
    {
	$stbuLogFile = undef;  # do not read log file from storeBackup.pl
    }
    else
    {
	$prLog->print('-kind' => 'D',
		      '-str' => ["copying log from storeBackup.pl to my log"]);
    }
}

#
# test ping to server
#
if ($server)
{
    my $p = Net::Ping->new('tcp', 5); # wait a maximum of 5 seconds for response
    my $ret = $p->ping($server);
    if ($ret == 1)
    {
	$prLog->print('-kind' => 'I',
		      '-str' => ["host <$server> reachable via tcp-ping"]);
    }
    else
    {
	$main::exit |= 2;
	$prLog->print('-kind' => 'E',
		      '-str' => ["host <$server> not reachable via tcp-ping"]);
	wait;
	POSIX::_exit $main::exit;
    }
}

#
# checking for already mounted filesystems
#
my (@aM) = `mount`;
my (%alreadyMounted, $m);
foreach $m (@aM)
{
    $m =~ /(.+?) on (.*) type /;
    $alreadyMounted{$2} = 1;
}

#
# mounting the file systems
#
my (@mounted) = ();
my $error = 0;
foreach $m (@mountPoints)
{
    my (@opt) = ();
    if ($m =~ /\A(r[ow]),(.*)/)
    {
	(@opt) = ('-o', $1);
	$m = $2;
    }

    if (exists $alreadyMounted{$m})
    {
	$prLog->print('-kind' => 'I',
		      '-str' => ["<$m> is already mounted"]);
	next;
    }

    $prLog->print('-kind' => 'I',
		  '-str' => ["trying to mount @opt $m"]);
    my $fp = forkProc->new('-exec' => 'mount',
			   '-param' => [@opt, $m],
			   '-outRandom' => '/tmp/doStoreBackup-forkMount-',
			   '-prLog' => $prLog);

    # wait for a maximum of 10 seconds
    foreach (1..10)
    {
	sleep 1;
	if ($fp->processRuns() == 0)
	{
	    last;
	}
	else
	{
	    $prLog->print('-kind' => 'D',
			  '-str' => ["waiting for mount command ..."])
		if $debug;
	}
    }
    my $out1 = $fp->getSTDOUT();
    my $out2 = $fp->getSTDERR();
    $fp->DESTROY();
    if ($fp->get('-what' => 'status') != 0    # mount not successfull
	or @$out2 > 0)
    {
	$main::exit |= 2;
	$error = 1;
	$prLog->print('-kind' => 'E',
		      '-str' => ["could not mount @opt $m"]);
	$fp->signal('-value' => 9);

	&umount(\@mounted, $keepExistingMounts, \%alreadyMounted, $debug);

	$prLog->print('-kind' => 'E',
		      '-str' => ["exiting"]);
	wait;
	POSIX::_exit $main::exit;
    }
    else
    {
	push @mounted, $m;
	$prLog->print('-kind' => 'I',
		      '-str' => ["<mount $m> successfull"]);
    }

    $prLog->print('-kind' => 'W',
		  '-str' => ["STDOUT of <mount $m>:", @$out1])
	if (@$out1 > 0);
    $prLog->print('-kind' => 'E',
		  '-str' => ["STDERR of <mount $m>:", @$out2])
	if (@$out2 > 0);

    if (@$out2)
    {
	$main::exit |= 2;
	$prLog->print('-kind' => 'E',
		      '-str' => ["exiting"]);
	wait;
	POSIX::_exit $main::exit;
    }
}
if ($error == 1)
{
    $prLog->print('-kind' => 'E',
		  '-str' => ["exiting"]);
    wait;
    POSIX::_exit $main::exit;
}

#
# starting storeBackup
#
my $stbuProg = $withDotPlFlag ? 'storeBackup.pl' : 'storeBackup';
my $storeBackup = $pathStbu ? "$pathStbu/$stbuProg" : $stbuProg;

my $tailStbuLogFile = undef;
if ($stbuLogFile)
{
    $tailStbuLogFile = tailOneFile->new('-filename' => $stbuLogFile,
					 '-position' => 'end',
					 '-maxlines' => 100);
#print "tailOneFile!\n";
}

my $stbu = forkProc->new('-exec' => $storeBackup,
			 '-param' => ['-f', $configFile],
			 '-outRandom' => '/tmp/doStoreBackup-stbu-',
			 '-prLog' => $prLog);
$prLog->print('-kind' => 'I',
	      '-str' => ["started <$storeBackup -f $configFile>, pid=" .
			 $stbu->get('-what' => 'pid')]);

$killTime = 3600*24*365;   # set to one year
if ($killTime)
{
    my $ready = 0;
    foreach (1..$killTime)
    {
	if ($stbuLogFile)
	{
	    my ($l, $e) = $tailStbuLogFile->read();
	    chop @$l;
	    $prLog->__reallyPrint($l) if (@$l > 0);
	    $prLog->print('-kind' => 'E',
			  '-str' => [$e]) if $e;
	}
	sleep 1;
	if ($stbu->processRuns() == 0)
	{
	    $ready = 1;
	    last;
	}
    }
    if ($ready == 0)      # duration too long
    {
	$prLog->print('-kind' => 'E',
		      '-str' => ["time limit <$kt> exceeded for " .
				 "<storeBackup -f $configFile>"]);
	$stbu->signal('-value' => 2);     # SIGINT
	$main::exit |= 1;
	sleep 10;          # time for storeBackup to finish
    }
}

if ($stbuLogFile)
{
    my ($l, $e);
    do
    {
	($l, $e) = $tailStbuLogFile->read();
	chop @$l;
	$prLog->__reallyPrint($l) if (@$l > 0);
	$prLog->print('-kind' => 'E',
		      '-str' => [$e]) if $e;
    } while (@$l);
}

if ($stbu->get('-what' => 'status') != 0)
{
    $main::exit |= 1;
    $prLog->print('-kind' => 'E',
		  '-str' => ["storeBackup exit status != 0"]);
}

unless ($stbuLogFile)
{
    my $out1 = $stbu->getSTDOUT();
    $prLog->__reallyPrint($out1) if (@$out1 > 0);
}

my $out2 = $stbu->getSTDERR();
if (@$out2 > 0)
{
    $main::exit |= 1;
    $prLog->print('-kind' => 'E',
		  '-str' => ["STDERR of <storeBackup -f $configFile>:",
			     @$out2]);
}
$stbu->DESTROY();

sleep 2;

&umount(\@mounted, $keepExistingMounts, \%alreadyMounted, $debug);

$prLog->print('-kind' => 'Z',
	      '-str' => ["finished storeBackup -f $configFile"]);

wait;
POSIX::_exit $main::exit;

######################################################################
sub umount
{
    my ($mounted, $keepExistingMounts, $alreadyMounted, $debug) = @_;

    foreach $m (reverse @$mounted)
    {
	if (exists $alreadyMounted{$m})
	{
	    $prLog->print('-kind' => 'I',
			  '-str' =>
			  ["do not umount <$m>, was already mounted"]);
	    next;
	}
	$prLog->print('-kind' => 'I',
		      '-str' => ["trying to <umount $m>"]);
	sleep 5;
	my $um = forkProc->new('-exec' => 'umount',
			       '-param' => [$m],
			       '-outRandom' =>
			       '/tmp/doStoreBackup-forkMount-',
			       '-prLog' => $prLog);

	# wait for a maximum of 60 seconds
	foreach (1..60)
	{
	    sleep 1;
	    if ($um->processRuns() == 0)
	    {
		last;
	    }
	    else
	    {
		$prLog->print('-kind' => 'D',
			      '-str' => ["waiting for umount command ..."])
		    if $debug;
	    }
	}
	$um->DESTROY();
	if ($um->get('-what' => 'status') != 0)    # umount not successfull
	{
	    $prLog->print('-kind' => 'E',
			  '-str' => ["could not <umount $m>"]);
	    $um->signal('-value' => 9);
	    $main::exit |= 2;
	}
	else
	{
	    $prLog->print('-kind' => 'I',
			  '-str' => ["<umount> $m successfull"]);
	}
    }
}
