#!/usr/bin/perl -w -t
# #
# Software subject to following license(s):
#   The Apache Software License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt)
#   null
#

# #
# Current developer(s):
#   Luis Fernando Muñoz Mejías <Luis.Munoz@UGent.be>
#

# #
# Author(s): Germán Cancio Meliá, Marco Emilio Poleggi
#

# #
# ncm-ncd, 15.4.0-rc13, rc13_1, 2015-06-03T09:57:32Z
#
#
use lib '/usr/lib/perl';

#------------------------------------------------------------
# Application
#------------------------------------------------------------

package ncd;

use strict;
use warnings;
use parent qw(CAF::Application CAF::Reporter);
use LC::Exception qw (SUCCESS throw_error);
use EDG::WP4::CCM::CacheManager;
use CAF::Lock qw(FORCE_IF_STALE FORCE_ALWAYS);

#
# Public Methods/Functions for CAF
#

sub app_options()
{
    # these options complement the ones defined in CAF::Application
    my @array;
    push(
        @array,

        {
            NAME    => 'configure',
            HELP    => 'run the configure method on the components',
            DEFAULT => undef
        },

        {
            NAME    => 'all',
            HELP    => 'used with --configure to run on all components',
            DEFAULT => undef
        },

        {
            NAME    => 'unconfigure',
            HELP    => 'run the unconfigure method on the component',
            DEFAULT => undef
        },

        {
            NAME    => 'logdir=s',
            HELP    => 'log directory to use for ncd log files',
            DEFAULT => '/var/log/ncm'
        },

        {
            NAME    => 'cache_root:s',
            HELP    => 'CCM cache root directory (optional, otherwise CCM default taken)',
            DEFAULT => undef
        },

        {
            NAME    => 'cfgfile=s',
            HELP    => 'configuration file for ncd defaults',
            DEFAULT => '/etc/ncm-ncd.conf'
        },

        {
            NAME    => 'multilog',
            HELP    => 'use separate component log files in log directory',
            DEFAULT => 1
        },

        {
            NAME    => 'noaction',
            HELP    => 'do not actually perform operations',
            DEFAULT => undef
        },

        {
            NAME    => 'retries=i',
            HELP    => 'number of retries if ncd is locked',
            DEFAULT => 10
        },

        {
            NAME    => 'state=s',
            HELP    => 'where to find state files',
            DEFAULT => undef
        },

        {
            NAME    => 'timeout=i',
            HELP    => 'maximum time in seconds between retries',
            DEFAULT => 30
        },

        {
            NAME => 'ignorelock',
            HELP => 'ignore application lock. Use with care.'
        },

        {
            NAME => 'forcelock',
            HELP => 'take over application lock. Use with care.'
        },

        {
            NAME    => 'useprofile:s',
            HELP    => 'profile to use as configuration profile (optional, otherwise latest)',
            DEFAULT => undef
        },

        {
            NAME    => 'nodeps',
            HELP    => 'ignore broken (pre/post) dependencies in configure',
            DEFAULT => undef
        },

        {
            NAME    => 'skip:s',
            HELP    => 'skip one component (only to be used with --all)',
            DEFAULT => undef
        },

        {
            NAME    => 'autodeps',
            HELP    => 'expand missing pre/post dependencies in configure',
            DEFAULT => 1
        },

        {
            NAME    => 'allowbrokencomps',
            HELP    => 'Do not stop overall execution if broken components are found',
            DEFAULT => 1
        },

        {
            NAME    => 'list',
            HELP    => 'list existing components and exit',
            DEFAULT => undef
        },

        {
            NAME    => 'facility=s',
            HELP    => 'facility name for syslog',
            DEFAULT => 'local1'
        },

        {
            NAME    => "template-path=s",
            HELP    => 'store for Template Toolkit files',
            DEFAULT => '/usr/share/templates/quattor'
        },

        {
            NAME    => "include=s",
            HELP    => 'a colon-seperated list of directories to include in search path',
            DEFAULT => undef
        },

        {
            NAME => "pre-hook=s",
            HELP => "Command line to run as pre-hook"
        },
        {
            NAME    => "pre-hook-timeout=i",
            HELP    => "Time out for the pre hook, in seconds",
            DEFAULT => 300
        },
        {
            NAME => "post-hook=s",
            HELP => "Command line to run as post hook"
        },
        {
            NAME    => "post-hook-timeout=i",
            HELP    => "Time out for hte post hook, in seconds",
            DEFAULT => 300
        },
        {
            NAME    => "chroot=s",
            HELP    => "Chroot to the the directory given as an argument",
            DEFAULT => undef
        },
    );

    return \@array;

}

# public methods

#
# setLockedCCMConfig($cacheroot,$profileID): boolean
#

sub setLockCCMConfig
{
    my ($self, $cacheroot, $profileID) = @_;

    $self->verbose('accessing CCM cache manager..');

    $self->{'CACHEMGR'} = EDG::WP4::CCM::CacheManager->new($cacheroot);
    unless (defined $self->{'CACHEMGR'}) {
        throw_error('cannot access cache manager');
        return undef;
    }

    my $cred = undef;    # not defined yet in CCM

    $self->verbose('getting locked CCM configuration..');

    $self->{'CCM_CONFIG'} = $self->{'CACHEMGR'}->getLockedConfiguration($cred, $profileID);
    unless (defined $self->{'CCM_CONFIG'}) {
        throw_error('cannot get configuration via CCM');
        return undef;
    }

    return SUCCESS;
}

#
# getCCMConfig(): ref(EDG::WP4::CCM::Configuration)
# returns the CCM config instance
#

sub getCCMConfig
{
    my $self = shift;

    return $self->{'CCM_CONFIG'};
}

#
# Other relevant methods
#

sub lock
{
    my $self = shift;

    # /var/lock can be volatile
    mkdir('/var/lock/quattor');
    $self->{LOCK} = CAF::Lock->new('/var/lock/quattor/ncm-ncd');
    my $lock_flag = FORCE_IF_STALE;
    $lock_flag = FORCE_ALWAYS if ($self->option("forcelock"));
    unless ($self->{LOCK}->set_lock($self->option("retries"), $self->option("timeout"), $lock_flag))
    {
        return undef;
    }
    return SUCCESS;
}

sub finish
{
    my ($self, $ret) = @_;
    $self->{LOCK}->unlock() if ($self->{LOCK} && $self->{LOCK}->is_set());
    exit($ret);
}

sub _initialize
{
    my $self = shift;
    #
    # define application specific data.
    #
    # external version number
    $self->{'VERSION'} = '15.4.0-rc13';

    # show setup text
    $self->{'USAGE'} =
          "Usage: ncm-ncd --configure   [options] [<component1,2..>] or\n"
        . "       ncm-ncd --unconfigure [options] <component>\n";
    #
    # start initialization of CAF::Application
    #
    unless ($self->SUPER::_initialize(@_)) {
        return undef;
    }

    # ensure allowed to run
    if ($>) {
        $self->error("Sorry " . $self->username() . ", this program must be run by root");
        exit(-1);
    }

    $self->{'NCD_LOGFILE'} = $self->option("logdir") . '/ncd.log';
    $self->{'LOG'} = CAF::Log->new($self->{'NCD_LOGFILE'}, 'at');
    return undef unless (defined $self->{'LOG'});

    # start using log file
    $self->set_report_logfile($self->{'LOG'});
    return SUCCESS;
}

#############################################################
# ncd main program
#############################################################

package main;

use strict;
use LC::Exception qw (SUCCESS throw_error);
use NCD::ComponentProxyList;
use vars qw($this_app %SIG);

my $ec = LC::Exception::Context->new->will_store_errors;
$LC::Exception::Reporter = \&main::error_reporter;

# fix umask
umask(022);

# minimal Path
$ENV{"PATH"} = "/bin:/sbin:/usr/bin:/usr/sbin";

# unbuffer STDOUT & STDERR
autoflush STDOUT 1;
autoflush STDERR 1;

#------------------------------------------------------------
# Functions in the main program
#------------------------------------------------------------

sub signal_handler
{
    my $signal = shift;

    # ignore further signals
    $SIG{'INT'}  = 'IGNORE';
    $SIG{'TERM'} = 'IGNORE';
    $SIG{'QUIT'} = 'IGNORE';
    $SIG{'USR2'} = 'IGNORE';
    $SIG{'HUP'}  = 'IGNORE';
    $this_app->warn('signal handler: received signal: ' . $signal);
    unless ($this_app->option('noaction')) {
        #
        # handle the signal.
        #
        $this_app->error('ncd exiting gracefully after signal hit.');
        $this_app->finish(-1);
    }
    $this_app->finish(0);
}

#
# report exceptions here in CAF compatible way
#

sub error_reporter
{
    my ($err, $uncaught) = @_;
    my ($stack, $depth, $frame);
    my $report = 'error';
    $report = 'warn' unless ($err->is_error);
    if ($uncaught) {
        $this_app->$report("Uncaught exception!");
        if ($err->is_error || $this_app->option('debug') || $this_app->option('verbose')) {
            $this_app->$report("Calling stack is:");
            $stack = $err->stack;
            $depth = 0;
            while ($frame = $stack->[$depth]) {
                $this_app->report("\t", $frame->subroutine, " called at ",
                    $frame->filename, " line ", $frame->line, "\n");
                $depth++;
            }
        }
    }
    $this_app->$report($err->format, "\n");
    die("finishing...") if $err->is_error;
}

#------------------------------------------------------------
# main loop
#------------------------------------------------------------

#
# initialize the ncd application
#
unless ($this_app = 'ncd'->new($0, @ARGV)) {
    die("cannot start application");
    exit(1);
}

# ensure allowed to run
if ($>) {
    $this_app->error("Sorry " . $this_app->username() . ", this program must be run by root");
    exit(-1);
}

#
# Handle signals properly
#
$SIG{'INT'}  = \&signal_handler;
$SIG{'TERM'} = \&signal_handler;
$SIG{'QUIT'} = \&signal_handler;
$SIG{'USR2'} = \&signal_handler;
$SIG{'HUP'}  = 'IGNORE';

#
# process command line options before proceeding.
#

$this_app->report();
$this_app->log('------------------------------------------------------------');
$this_app->info('NCM-NCD version '
        . $this_app->version()
        . ' started by '
        . $this_app->username() . ' at: '
        . scalar(localtime));

$this_app->info('Dry run, no changes will be performed (--noaction flag set)')
    if ($this_app->option('noaction'));

unless ($this_app->option('configure')
    || $this_app->option('unconfigure')
    || $this_app->option('list'))
{
    $this_app->error('please specify either configure, unconfigure or list as options');
    $this_app->finish(-1);
}

if ($this_app->option('configure') && $this_app->option('unconfigure')) {
    $this_app->error('configure and unconfigure options cannot be used simultaneously');
    $this_app->finish(-1);
}

# add include directories to perl include search path
if ($this_app->option('include')) {
    unshift(@INC, split(/:+/, $this_app->option('include')));
}

# set local NVA API lock TBD
unless (
    $this_app->setLockCCMConfig($this_app->option('cache_root'), $this_app->option('useprofile')))
{
    $this_app->error("cannot get locked CCM configuration");
    $this_app->finish(-1);
}

if ($this_app->option('list')) {
    my $compList = NCD::ComponentProxyList->new($this_app->getCCMConfig());
    unless (defined $compList) {
        $ec->ignore_error();
        $this_app->error("cannot get component(s)");
        $this_app->finish(-1);
    }
    $compList->reportComponents();
    $this_app->finish(0);
}

#
# now, do either configure or unconfigure. Set the application lock
#

$this_app->verbose('checking for ncm-ncd locks...');
unless ($this_app->option("ignorelock")) {
    $this_app->lock() or $this_app->finish(-1);
}

my ($method, $msg);
if ($this_app->option('unconfigure')) {
    #
    # UNCONFIGURE option
    #
    unless (scalar @ARGV) {
        $this_app->error("unconfigure requires a component as argument");
        $this_app->finish(-1);
    }
    unless (scalar @ARGV == 1) {
        $this_app->error('more than one components cannot be unconfigured at a time');
        $this_app->finish(-1);
    }
    $method = 'executeUnconfigComponent';
    $msg    = 'unconfigure';
} else {
    #
    # CONFIGURE option
    #
    $this_app->info('Ignoring broken pre/post dependencies (--nodeps flag set)')
        if ($this_app->option('nodeps'));
    $method = 'executeConfigComponents';
    $msg    = 'configure';
}

# remove duplicates and sort
my @component_names = sort(keys( %{ {map {$_ => 1} @ARGV} } ));
$this_app->verbose("Sorted unique components ", join(',', @component_names), 
                   " from commandline ", join(',', @ARGV));

unless ($this_app->option('all') || scalar(@component_names)) {
    $this_app->error("Please provide component names as parameters, or use --all");
    $this_app->finish(-1);
}

my $skip = $this_app->option('skip');

if (defined $skip && !$this_app->option('all')) {
    $this_app->error("--skip option requires --all option to be set");
    $this_app->finish(-1);
}

unless (scalar(@component_names)) {
    $this_app->info('No components specified, getting all active ones.');
}

my $compList = NCD::ComponentProxyList->new($this_app->getCCMConfig(), $skip, @component_names);

my @args;

unless (defined $compList && defined($compList->{CLIST})) {
    $ec->ignore_error();
    $this_app->error("No components to dispatch.");
    $this_app->finish(-1);
}

@args = (
    $this_app->option("pre-hook"),  $this_app->option("pre-hook-timeout"),
    $this_app->option("post-hook"), $this_app->option("post-hook-timeout")
);

if ($this_app->option("chroot")) {
    chroot($this_app->option("chroot")) or die "Unable to chroot to ", $this_app->option("chroot");
}

chdir('/tmp');
my $ret  = $compList->$method(@args);
my $fun  = 'OK';
my $exit = 0;
if ($ret->{'ERRORS'}) {
    $fun  = 'error';
    $exit = -1;
} elsif ($ret->{'WARNINGS'}) {
    $fun = 'warn';
}
$this_app->report();
$this_app->report('=========================================================');
$this_app->report();

# Get the list of components with errors

my $arrayref        = $ret->{'ERR_COMPS'};
my $err_comp_string = "";
foreach my $err_comp (keys %$arrayref) {
    $err_comp_string .= "$err_comp ($arrayref->{$err_comp}) ";
}
chop($err_comp_string);

# Get the list of components with warnings

my $arrayrefw        = $ret->{'WARN_COMPS'};
my $warn_comp_string = "";
foreach my $warn_comp (keys %$arrayrefw) {
    $warn_comp_string .= "$warn_comp ($arrayrefw->{$warn_comp}) ";
}
chop($warn_comp_string);

if ($ret->{'ERRORS'} > 0) {
    $this_app->info("Errors while configuring $err_comp_string");
}

if ($ret->{'WARNINGS'} > 0) {
    $this_app->info("Warnings while configuring $warn_comp_string");

}

$this_app->$fun($ret->{'ERRORS'}, ' errors, ', $ret->{'WARNINGS'}, ' warnings ', "executing $msg");

$this_app->finish($exit);
