#! /usr/bin/perl -w

# Copyright 2008-2014 Cumulus Systems Incorporated.
# All Rights Reserved.

# Runs on a target node, a host on which our tools are being installed or un-installed.
# install.pl is initially installed on the target node by running the script install-on-host.
#
# Syntax: perl install.pl [-iuab] [-d directory path]

# Switch on warnings. It is used so that we can switch-off warning in Getopts() call.
$^W = 1;

# Flush output after every write. Unbuffered output is usually desired.
$| = 1;

use strict;

# Used to read script arguments.
use Getopt::Std;

# Home directory.
my $marsHome = "";

# Installation flag.
$::opt_i = undef;

# Un-installation flag.
$::opt_u = undef;

# Start data collection flag.
$::opt_a = undef;

# Stop data collection flag.
$::opt_b = undef;

# Installation directory.
$::opt_d = undef;

# Help.
$::opt_h = undef;

# Installation directory.
my $installDir = undef;

# Initialize the environment.
my $status = init();
if (0 != $status)
{
    logMsg("Error: Call to init() failed.");
    goto EXIT;
}

# Initialize the environment.
callTasks();

EXIT:

exit $status;

# This function is used to initialize the environment to run the Perl script correctly.
#
# @affected global variables :
#    $::opt_l
#    $installDir
#    $ISCRIPT
#    @clients
#
# @return :
#   0 if Success
#   1 if Error
#
sub init
{
    # This variable is used to store the return value of function calls.
    my $retVal = 0;

    # If first argument is not an option then it is an error.
    if (@ARGV && $ARGV[0] !~ "^-.+")
    {
        logMsg("[Error]: First argument is not an option.");
        $retVal = 1;

        goto EXIT;
    }
    else
    {
        # Suppress annoying undef warnings.
        local($^W) = 0;

        &getopts("hiuabd:");
    }

    # Give help menu if user passed -h.
    if ($::opt_h)
    {
        giveHelp();
        exit;
    }

    # One of the following operations should be specified in argument list.
    if (! (defined($::opt_i) || defined($::opt_u) || defined($::opt_a) || defined($::opt_b)))
    {
        logMsg("[Error]: Must use one of the following options [ -i , -u , -a , -b].");
        $retVal = 1;

        goto EXIT;
    }

    # Set installation path.
    $installDir = $::opt_d;

    # If install directory is not present then exit.
    if (! defined $installDir)
    {
        logMsg("[Error]: Installation directory is not given with -d option.");

        $retVal = 1;
        goto EXIT;
    }

    # Add some standard execution directory to our PATH.
    $ENV{'PATH'} = '/usr/sbin:/sbin:/usr/bin:/bin:'.$ENV{'PATH'};

    # Set MARS home path.
    $marsHome = $installDir."/probe";
    if (!(-e $marsHome))
    {
        logMsg("[Error]: Home directory does not exist [$marsHome].");
        $retVal = 1;

        goto EXIT;
    }

    logMsg("Debug: Exiting from init().");

EXIT:

    return $retVal;
}

# This function is used to call functions specific to task for which user has invoked this script.
#
# @param:
#    None.
#
# @return:
#    Nothing.
sub callTasks
{
    # Do installation.
    if ($::opt_i)
    {
        doInstall();
    }
    # Do un-installation.
    elsif ($::opt_u)
    {
        doUninstall();
    }
    # Start probe.
    elsif ($::opt_a)
    {
        doStart();
    }
    # Stop probe.
    elsif ($::opt_b)
    {
        doStop();
    }
    else
    {
        # It should not come here.
        die logMsg("[Error]: No valid operation type provided.");
    }
}

# Used to install Linux probe software on the system.
#
# @param:
#    None.
#
# @return:
#    Nothing.
sub doInstall
{
    # Following (umask 022) ensure that files or directories created will have permission 0755.
    umask(022);

    # If installation directory is symbolic link then return.
    if (-l $installDir)
    {
        die logMsg("[Error]: '$installDir' is a symlink. So exiting installation to prevent any error later.");
    }

    # Delete existing runtime.properties file.
    if (! unlink("$marsHome/conf/runtime.properties"))
    {
        die logMsg("[Error]: Failed to delete [$marsHome/conf/runtime.properties]: [$!]");
    }

    # Copy new runtime.properties file.
    if (system("cp $marsHome/conf/runtime.properties.new $marsHome/conf/runtime.properties"))
    {
        die logMsg("[Error]: Can't copy [$marsHome/conf/runtime.properties.new] to [$marsHome/conf/runtime.properties] :$!");
    }

    # Copy MARS RSA file for current user.
    my $home = '$HOME';
    my $userHome = `echo $home`;
    chomp $userHome;

    # Create .ssh folder.
    if (system("mkdir -p $userHome/.ssh"))
    {
        die logMsg("[Error]: Can't create [$userHome/.ssh].");
    }

    # Copy RSA file in .ssh folder.
    if (system("cp $marsHome/conf/rsa $userHome/.ssh/rsa"))
    {
        die logMsg("[Error]: Can't copy [$marsHome/conf/rsa] to ['$userHome/.ssh']");
    }

    # Read data collection properties from runtime.properties file.
    my ($INTERVAL, $SAMPLES, $XFER, $MAINCRONFREQ, $SIZE_LIMIT_OF_BACKLOG_FILES) = parseRuntimeConfFile("$marsHome/conf/runtime.properties");

    # Adjust CRON line for data collection parameters obtained above.
    my $freq = $MAINCRONFREQ;

    # Adjust CRON line for maximum limit of backlog files.
    my $size = $SIZE_LIMIT_OF_BACKLOG_FILES;

    # CRON file path.
    my $cronFile = $marsHome.'/bin/cronJobs';

    # Replace "/" with "\\/" to escape "/" character in sed command.
    my $shellEscapedMarsHome = $marsHome;
    $shellEscapedMarsHome =~ s!/!\\/!g;

    my $shellEscapedScriptDir = $shellEscapedMarsHome."\\/bin";
    my $shellEscapedLogDir = $shellEscapedMarsHome."\\/logs";

    # Update frequency in CRON file.
    if (system("sed -i -e 's/%FREQ%/$freq/g' $cronFile"))
    {
        die logMsg("[Error]: Could not update frequency in cronJobs .Error: [$!].");
    }

    # Update script directory in CRON file.
    if (system("sed -i -e 's/%SCRIPTDIR%/$shellEscapedScriptDir/g' $cronFile"))
    {
        die logMsg("[Error]:[sed -i -e 's/%SCRIPTDIR%/$shellEscapedScriptDir/g' $cronFile] could not update script directory in cronJobs. Error: [$!].");
    }

    # Update installation directory in CRON file.
    if (system("sed -i -e 's/%INSTALLDIR%/$shellEscapedMarsHome/g' $cronFile"))
    {
        die logMsg("[Error]:[sed -i -e 's/%INSTALLDIR%/$shellEscapedMarsHome/g' $cronFile] could not update installation directory in cronJobs. Error: [$!].");
    }

    # Update log directory in CRON file.
    if (system("sed -i -e 's/%LOGDIR%/$shellEscapedLogDir/g' $cronFile"))
    {
        die logMsg("[Error]:[sed -i -e 's/%LOGDIR%/$shellEscapedLogDir/g' $cronFile] could not update log directory in cronJobs. Error: [$!].");
    }

    # Update backlog file size in CRON file.
    if (system("sed -i -e 's/%SIZE_LIMIT_OF_BACKLOG_FILES%/$size/g' $cronFile"))
    {
        die logMsg("[Error]:[sed -i -e 's/%SIZE_LIMIT_OF_BACKLOG_FILES%/$size/g' $cronFile] could not update max backlog file size in cronJobs. Error: [$!].");
    }

    # Change permission of MARS related files.
    foreach ("$marsHome/bin/triggerDataCollection.pl",
             "$marsHome/bin/cleanupOldFiles.sh",
             "$marsHome/bin/sendRawData.pl",
             "$marsHome/lib/collectConfInfo.sh",
             "$marsHome/lib/collectIoStats.sh",
             "$marsHome/lib/collectMpStats.sh",
             "$marsHome/lib/collectNetStats.sh",
             "$marsHome/lib/collectVmStats.sh",
             "$marsHome/bin/inotifywatch",
             "$marsHome/conf/runtime.properties")
    {
        system("chmod a+x $_");
    }
}

# Un-install Linux probe application from system. It first kill all Cumulus related processes and then deletes the installation folder.
#
# @param
#    None.
#
# @return:
#    Nothing.
sub doUninstall
{
    # Get current user.
    my $user = `whoami`;
    chomp $user;

    my @patternsToKill = ("sendRawData.pl",
                          "lib/collectConfInfo.sh",
                          "lib/collectIoStats.sh",
                          "lib/collectMpStats.sh",
                          "lib/collectNetStats.sh",
                          "lib/collectVmStats.sh",
                          "$marsHome/bin/inotifywatch");

    # Kill above mentioned processes which belongs to Linux probe.
    foreach my $patternToKill (@patternsToKill)
    {
        logMsg("[Info]: Process patterns to kill [$patternToKill].");

        if (system("ps -u $user -f | grep \"$patternToKill\" | awk '{print \$2}' | xargs kill -9"))
        {
            warn logMsg("[Warn]: Could not kill linux probe process with pattern [$patternToKill].");
        }
    }

    # TODO: .ssh/authorized_keys. We should be removing entry that we added and not the whole file.
    # Skipped adding support for it for now.

    # Clean up installation directory.
    if (system("rm -rf $marsHome"))
    {
        warn logMsg("[Warn]: Could not remove installation directory [$marsHome]. Error: [$!].");
    }
}

# Start data collection by adding cumulus CRON entries in crontab.
#
# @param:
#    None.
#
# @return:
#    Nothing.
sub doStart
{
    # Open System CRON file.
    open(USER_CRON, "crontab -l |") or warn logMsg("[Warn]: Can not list user's crontab. Error: [$!].");

    # Open Cumulus CRON file.
    open(CUMULUS_CRON, "<", $marsHome."/bin/cronJobs") or die logMsg("[Error]: Can not open [$marsHome/bin/cronJobs]. Error: [$!]");

    # List of user CRON jobs.
    my @userCronJobs = ();

    # List of Linux probe CRON jobs.
    my @cumulusCronJobs = ();

    my $emptyLine = 0;
    
    # Read System CRON entries.
    while (<USER_CRON>)
    {
        # This safer version of chop removes any trailing string that corresponds to the current value of $_ (also known as $INPUT_RECORD_SEPARATOR).
        chomp;
        
        # Skip consecutive empty lines.
        if (/^$/)
        {
            $emptyLine++;
            if ($emptyLine >= 2)
            {
                next;
            }
        }
        else
        {
            $emptyLine = 0;
        }

        push(@userCronJobs, $_);
    }

    # If no user cron jobs found then directly add our cron jobs and return.
    if (scalar @userCronJobs == 0)
    {
        if (system("crontab $marsHome/bin/cronJobs"))
        {
            die logMsg("[ERROR]: Failed to add cronJobs to user's crontab, error: [$!].");
        }
        
        return;
    }
    
    # Read Cumulus CRON entries and push them into user cron tab.
    while (<CUMULUS_CRON>)
    {
        chomp;

        push(@userCronJobs, $_);
    }

    close(USER_CRON);
    close(CUMULUS_CRON);

    # Create a temporary file for storing CRON entries.
    open(TEMP_CRON_FILE, ">", $marsHome."/bin/tmp.user.cron") or die logMsg("[Error]: Can not create temporary user cron for writing. Error: [$!].");

    # Add all CRON entries in the temp file which are not in Cumulus CRON list.
    foreach (@userCronJobs)
    {
        print(TEMP_CRON_FILE $_."\n");
    }

    close(TEMP_CRON_FILE);

    # Update System CRON job with Cumulus cron entries.
    if (system("crontab $marsHome/bin/tmp.user.cron"))
    {
        warn logMsg("[Warn]: Failed to install updated user crontab. Error: [$!].");
    }

    # Delete temporary CRON file.
    if (-e $marsHome."/bin/tmp.user.cron")
    {
        if (! unlink $marsHome."/bin/tmp.user.cron")
        {
            logMsg("[Error]: Failed to delete file [$marsHome/bin/tmp.user.cron]. Error: [$!].");
        }
    }
    else
    {
        warn logMsg("[Warn]: Failed to get file [$marsHome/bin/tmp.user.cron]. Error: [$!].");
    }
}

# Used to stop data collection. It removes CRON entries to stop data collection.
#
# @param:
#    None.
#
# @return:
#    Nothing.
sub doStop
{
    # Open System CRON file.
    open(USER_CRON, "crontab -l |") or warn logMsg("[Warn]: Can not list user's crontab. Error: [$!].");

    # Open Cumulus CRON file.
    open(CUMULUS_CRON, "<", $marsHome."/bin/cronJobs") or die logMsg("[Error]: Can not open [$marsHome/bin/cronJobs]. Error: [$!]");

    # List of user CRON jobs.
    my @userCronJobs = ();

    # List of Linux probe CRON jobs.
    my @cumulusCronJobs = ();

    # Read System CRON entries.
    while (<USER_CRON>)
    {
        # This safer version of chop removes any trailing string that corresponds to the current value of $_ (also known as $INPUT_RECORD_SEPARATOR).
        chomp;

        push(@userCronJobs, $_);
    }

    # Read Cumulus CRON entries.
    while (<CUMULUS_CRON>)
    {
        chomp;

        # Ignore empty lines.
        if (/^$/)
        {
            next;
        }

        push(@cumulusCronJobs, $_);
    }

    close(USER_CRON);
    close(CUMULUS_CRON);

    # This list is used to store those CRON entries which are not in Cumulus CRON list.
    my @updatedUserCrons = ();

    # Loop for each System CRON list.
    foreach my $userCronEntry (@userCronJobs)
    {
        my $found = "false";

        # Try to find any System CRON entry which is also in Cumulus CRON list.
        foreach my $cumulusCronEntry (@cumulusCronJobs)
        {
            if ($userCronEntry eq $cumulusCronEntry)
            {
                $found = "true";
                last;
            }
        }

        # If not found System CRON entry in Cumulus CRON list then store it.
        if ($found ne "true")
        {
            push(@updatedUserCrons, $userCronEntry);
        }
    }

    # If there are no CRON entries in system except for Cumulus then clear all CRON entries.
    if (scalar @updatedUserCrons == 0)
    {
        if (system("crontab -r"))
        {
            die logMsg("[Error]: Failed to remove user crontab. Error: [$!].");
        }

        return;
    }

    # Create a temporary file for storing CRON entries.
    open(TEMP_CRON_FILE, ">", $marsHome."/bin/tmp.user.cron") or die logMsg("[Error]: Can not create temporary user cron for writing. Error: [$!].");

    # Add all CRON entries in the temp file which are not in Cumulus CRON list.
    foreach (@updatedUserCrons)
    {
        print(TEMP_CRON_FILE $_."\n");
    }

    close(TEMP_CRON_FILE);

    # Update System CRON job with only those CRON entries which are not used by Cumulus.
    if (system("crontab $marsHome/bin/tmp.user.cron"))
    {
        warn logMsg("[Warn]: Failed to install updated user crontab. Error: [$!].");
    }

    # Delete temporary CRON file.
    if (-e $marsHome."/bin/tmp.user.cron")
    {
        if (! unlink $marsHome."/bin/tmp.user.cron")
        {
            logMsg("[Error]: Failed to delete file [$marsHome/bin/tmp.user.cron]. Error: [$!].");
        }
    }
    else
    {
        warn logMsg("[Warn]: Failed to get file [$marsHome/bin/tmp.user.cron]. Error: [$!].");
    }
}

# Parse runtime configuration file to get data collection details.
#
# @param:
#    $_[0]: [In] Runtime configuration file.
#
# @return:
#    Returns a list ($INTERVAL, $SAMPLES, $XFER, $MAIN_CRON_FREQ, $SIZE_LIMIT_OF_BACKLOG_FILES) of information.
sub parseRuntimeConfFile
{
    my ($confFile) = @_;

    my($INTERVAL, $SAMPLES, $XFER, $MAIN_CRON_FREQ, $SIZE_LIMIT_OF_BACKLOG_FILES) = (undef, undef, undef, undef, undef);

    logMsg("[Debug]: Inside parseRuntimeConfFile(). Runtime configuration file name [$confFile].");

    if (! open(RUNTIME, '<', $confFile))
    {
        die logMsg("[Error]: Can't read runtime configuration file: [$confFile]. Error: [$!].");
    }

    while (<RUNTIME>)
    {
        if (/^INTERVAL=(\d+)/)
        {
            $INTERVAL = $1;
        }
        elsif (/^SAMPLES=(\d+)/)
        {
            $SAMPLES = $1;
        }
        elsif (/^XFER=(\d+)/)
        {
            $XFER = $1;
        }
        elsif (/^MAINCRONFREQ=(\d+)/)
        {
            $MAIN_CRON_FREQ = $1;
        }
        elsif (/^SIZE_LIMIT_OF_BACKLOG_FILES=(\d+)/)
        {
            $SIZE_LIMIT_OF_BACKLOG_FILES = $1;
        }
        else
        {
            # Skip the line.
        }
    }

    close(RUNTIME);

    return ($INTERVAL, $SAMPLES, $XFER, $MAIN_CRON_FREQ, $SIZE_LIMIT_OF_BACKLOG_FILES);
}

# Prints message to STDOUT.
#
# @param:
#    $_[0]: [In] Message to print.
#
# @return:
#    Nothing.
sub logMsg
{
    my (@msg) = @_;

    my $daytime = formatTime(time);

    print "[$daytime][install.pl][$$]: @msg\n";
}

# Return formatted time from current $time as: yyyymmdd-hhmmss.
#
# @param:
#    $_[0]: [In] Current time.
#
# @return:
#    Formatted time.
sub formatTime
{
    my ($time) = @_;

    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($time);

    return sprintf "%4u%02u%02u-%02u%02u%02u", $year + 1900, $mon + 1,, $mday, $hour, $min, $sec;
}

# Used to dump help instructions.
#
# @param:
#    None.
#
# @return:
#    Nothing.
sub giveHelp
{
   print <<EOF;

Command: Perl install.pl [-iuab] [-d Installation Directory path]

    -d <Directory Path> : List of Users required for execution.

    Provide any one option from follwoing:

    -i : Install Linux probe.
    -u : Un-install Linux probe.
    -a : Start data collection.
    -b : Stop data collection.
EOF
}