# IBM(c) 2007 EPL license http://www.eclipse.org/legal/epl-v10.html
package xCAT_plugin::prescripts;

BEGIN
{
    $::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : '/opt/xcat';
}
use lib "$::XCATROOT/lib/perl";
use strict;
require xCAT::Table;
require xCAT::Utils;
require xCAT::TableUtils;
require xCAT::ServiceNodeUtils;
require xCAT::MsgUtils;
use Getopt::Long;
use Sys::Hostname;
use Time::HiRes qw(gettimeofday sleep);
use POSIX "WNOHANG";


1;

#-------------------------------------------------------

=head3  handled_commands
Return list of commands handled by this plugin
=cut

#-------------------------------------------------------

sub handled_commands
{
    return {
        runbeginpre => "prescripts",
        runendpre   => "prescripts"
    };
}

#-------------------------------------------------------

=head3  preprocess_request
  Check and setup for hierarchy
=cut

#-------------------------------------------------------
sub preprocess_request
{
    my $req = shift;
    my $cb  = shift;

    #>>>>>>>used for trace log start>>>>>>>
    my @args = ();
    my %opt;
    my $verbose_on_off = 0;
    if (ref($req->{arg})) {
        @args = @{ $req->{arg} };
    } else {
        @args = ($req->{arg});
    }
    @ARGV = @args;
    GetOptions('V' => \$opt{V});
    if ($opt{V}) { $verbose_on_off = 1; }

    #>>>>>>>used for trace log end>>>>>>>

    #if already preprocessed, go straight to request
    if ($req->{_xcatpreprocessed}->[0] == 1) { return [$req]; }

    my $req_nodes = $req->{node};
    if (!$req_nodes) { return; }

    my @nodes;
    my $command = $req->{command}->[0];
    my $column;
    if    ($command eq 'runbeginpre') { $column = 'begin'; }
    elsif ($command eq 'runendpre')   { $column = 'end'; }
    else                              { $column = ''; }

    # See if anything in the prescripts table for the nodes.  If not, skip.
    #   Nothing to do.
    my $tab = xCAT::Table->new('prescripts');

    #first check if xcatdefaults entry
    if ($tab->getAttribs({ node => "xcatdefaults" }, $column)) {

        # yes - process all nodes
        @nodes = @$req_nodes;
    } else {

        # no xcatdefaults, check for node entries
        my $tabdata = $tab->getNodesAttribs($req_nodes, [ 'node', $column ]);
        if ($tabdata) {
            foreach my $node (@$req_nodes) {
                if (($tabdata->{$node}) &&
                    ($tabdata->{$node}->[0]) &&
                    ($tabdata->{$node}->[0]->{$column})) {
                    push(@nodes, $node);
                }
            }
        }
    }

    # if no nodes left to process, we are done
    if (!@nodes) {
        xCAT::MsgUtils->trace($verbose_on_off, "d", "prescripts->preprocess_request: no nodes left to process, we are done");
        return;
    }

    my $service = "xcat";
    @args = ();
    if (ref($req->{arg})) {
        @args = @{ $req->{arg} };
    } else {
        @args = ($req->{arg});
    }
    @ARGV = @args;

    #print "prepscripts: preprocess_request get called, args=@args, nodes=@$nodes\n";

    #>>>>>>>used for trace log>>>>>>
    my $str_node = join(" ", @nodes);
    my $str_args = join(" ", @args);
    xCAT::MsgUtils->trace($verbose_on_off, "d", "prescripts->preprocess_request: get called, args='$str_args', nodes='$str_node'");

    #use Getopt::Long;
    Getopt::Long::Configure("bundling");
    Getopt::Long::Configure("pass_through");
    GetOptions('l' => \$::LOCAL);
    my $sn = xCAT::ServiceNodeUtils->getSNformattedhash(\@nodes, $service, "MN");
    my @requests;
    if ($::LOCAL) {    #only handle the local nodes
                       #print "process local nodes: @$nodes\n";
                       #get its own children only
        my @hostinfo = xCAT::NetworkUtils->determinehostname();
        my %iphash   = ();
        foreach (@hostinfo) { $iphash{$_} = 1; }

        my @children = ();
        foreach my $snkey (keys %$sn) {
            if (exists($iphash{$snkey})) {
                my $tmp = $sn->{$snkey};
                @children = (@children, @$tmp);
            }
        }
        if (@children > 0) {
            my $reqcopy = {%$req};
            $reqcopy->{node}                   = \@children;
            $reqcopy->{'_xcatdest'}            = $hostinfo[0];
            $reqcopy->{_xcatpreprocessed}->[0] = 1;
            push @requests, $reqcopy;
            xCAT::MsgUtils->trace($verbose_on_off, "d", "prescripts: handle request in $hostinfo[0]");
            return \@requests;
        }
    } else {   #run on mn and need to dispatch the requests to the service nodes
               #print "dispatch to sn\n";
               # find service nodes for requested nodes
               # build an individual request for each service node
               # find out the names for the Management Node
        foreach my $snkey (keys %$sn)
        {
            my $reqcopy = {%$req};
            $reqcopy->{node}                   = $sn->{$snkey};
            $reqcopy->{'_xcatdest'}            = $snkey;
            $reqcopy->{_xcatpreprocessed}->[0] = 1;
            xCAT::MsgUtils->trace($verbose_on_off, "d", "prescripts: handle request in $snkey");
            push @requests, $reqcopy;

        }      # end foreach
        return \@requests;
    }
    return;
}

#-------------------------------------------------------

=head3  process_request

  Process the command

=cut

#-------------------------------------------------------
sub process_request
{
    my $request  = shift;
    my $callback = shift;
    my $nodes    = $request->{node};
    my $command  = $request->{command}->[0];
    my $args     = $request->{arg};
    my $rsp      = {};

    if ($command eq "runbeginpre")
    {
        runbeginpre($nodes, $request, $callback);
    }
    else
    {
        if ($command eq "runendpre")
        {
            runendpre($nodes, $request, $callback)
        }
        else
        {
            my $rsp = {};
            $rsp->{data}->[0] =
              "Unknown command $command.  Cannot process the command.";
            xCAT::MsgUtils->message("E", $rsp, $callback, 1);
            return;
        }
    }
}

#-------------------------------------------------------

=head3  runbeginpre
   Runs all the begin scripts defined in prescripts.begin column for the give nodes.
=cut

#-------------------------------------------------------
sub runbeginpre
{
    my ($nodes, $request, $callback) = @_;
    my $args          = $request->{arg};
    my $action        = $args->[0];
    my $localhostname = hostname();
    my $installdir    = xCAT::TableUtils->getInstallDir();

    my %script_hash = getprescripts($nodes, $action, "begin");
    foreach my $scripts (keys %script_hash) {
        my $runnodes = $script_hash{$scripts};
        if ($runnodes && (@$runnodes > 0)) {
            my $runnodes_s = join(',', @$runnodes);

            #now run the scripts
            my @script_array = split(',', $scripts);
            foreach my $s (@script_array) {
                my $rsp = {};
                $rsp->{data}->[0] = "$localhostname: Running begin script $s for nodes $runnodes_s.";
                $callback->($rsp);

                #check if the script need to be invoked for each node in parallel.
                #script must contian a line like this in order to be run this way: #xCAT setting: MAX_INSTANCE=4
                #where 4 is the maximum instance at a time
                my $max_instance = 0;
                my $ret = `grep -E '#+xCAT setting: *MAX_INSTANCE=' $installdir/prescripts/$s`;
                if ($? == 0) {
                    $max_instance = `echo "$ret" | cut -d= -f2`;
                    chomp($max_instance);
                }

                if ($max_instance > 0) {

                    #run the script for each node in paralell, no more than max_instance at a time
                    run_script_single_node($installdir, $s, $action, $max_instance, $runnodes, $callback);
                } else {
                    undef $SIG{CHLD};

                    #pass all the nodes to the script, only invoke the script once
                    my $ret = `NODES=$runnodes_s ACTION=$action $installdir/prescripts/$s 2>&1`;
                    my $err_code = $? / 256;
                    if ($err_code != 0) {
                        my $rsp = {};
                        $rsp->{error}->[0] = "$localhostname: $s: return code=$err_code. Error message=$ret";
                        $callback->($rsp);
                        if ($err_code > 1) { return $err_code; }
                    } else {
                        if ($ret) {
                            my $rsp = {};
                            $rsp->{data}->[0] = "$localhostname: $s: $ret";
                            $callback->($rsp);
                        }
                    }
                }
            }
        }
    }
    return;
}

#-------------------------------------------------------

=head3  runendpre
   Runs all the begin scripts defined in prescripts.begin column for the give nodes.
=cut

#-------------------------------------------------------
sub runendpre
{
    my ($nodes, $request, $callback) = @_;

    my $args          = $request->{arg};
    my $action        = $args->[0];
    my $localhostname = hostname();
    my $installdir    = xCAT::TableUtils->getInstallDir();

    my %script_hash = getprescripts($nodes, $action, "end");
    foreach my $scripts (keys %script_hash) {
        my $runnodes = $script_hash{$scripts};
        if ($runnodes && (@$runnodes > 0)) {
            my $runnodes_s = join(',', @$runnodes);
            my %runnodes_hash = ();

            #now run the scripts
            my @script_array = split(',', $scripts);
            foreach my $s (@script_array) {
                my $rsp = {};
                $rsp->{data}->[0] = "$localhostname: Running end script $s for nodes $runnodes_s.";
                $callback->($rsp);

                #check if the script need to be invoked for each node in parallel.
                #script must contian a line like this in order to be run this way: #xCAT setting: MAX_INSTANCE=4
                #where 4 is the maximum instance at a time
                my $max_instance = 0;
                my $ret = `grep -E '#+xCAT setting: *MAX_INSTANCE=' $installdir/prescripts/$s`;
                if ($? == 0) {
                    $max_instance = `echo "$ret" | cut -d= -f2`;
                    chomp($max_instance);
                }

                if ($max_instance > 0) {

                    #run the script for each node in paralell, no more than max_instance at a time
                    run_script_single_node($installdir, $s, $action, $max_instance, $runnodes, $callback);
                } else {
                    undef $SIG{CHLD};
                    my $ret = `NODES=$runnodes_s ACTION=$action $installdir/prescripts/$s 2>&1`;
                    my $err_code = $? / 256;
                    if ($err_code != 0) {
                        my $rsp = {};
                        $rsp->{error}->[0] = "$localhostname: $s: return code=$err_code. Error message=$ret";
                        $callback->($rsp);
                        if ($err_code > 1) { return $err_code; }
                    } else {
                        if ($ret) {
                            my $rsp = {};
                            $rsp->{data}->[0] = "$localhostname: $s: $ret";
                            $callback->($rsp);
                        }
                    }
                }
            }
        }
    }

    return;
}

#-------------------------------------------------------

=head3  getprescripts
   get the prescripts for the given nodes and actions
=cut

#-------------------------------------------------------
sub getprescripts
{
    my ($nodes, $tmp_action, $colname) = @_;
    my @action_a = split('=', $tmp_action);
    my $action = $action_a[0];

    my %ret = ();
    if ($nodes && (@$nodes > 0)) {
        my $tab = xCAT::Table->new('prescripts', -create => 1);

        #first get xcatdefault column
        my $et = $tab->getAttribs({ node => "xcatdefaults" }, $colname);
        my $tmp_def = $et->{$colname};
        my $defscripts;
        if ($tmp_def) {
            $defscripts = parseprescripts($tmp_def, $action);
        }

        #get scripts for the given nodes and
        #add the scripts from xcatdefault in front of the other scripts
        my $tabdata = $tab->getNodesAttribs($nodes, [ 'node', $colname ]);
        foreach my $node (@$nodes) {
            my $scripts_to_save = $defscripts;
            my %lookup = (); #so that we do not have to parse the same scripts more than once
            if ($tabdata && exists($tabdata->{$node})) {
                my $tmp     = $tabdata->{$node}->[0];
                my $scripts = $tmp->{$colname};
                if ($scripts) {

                    #parse the script. it is in the format of netboot:s1,s2|install:s3,s4 or just s1,s2
                    if (!exists($lookup{$scripts})) {
                        my $tmp_s = parseprescripts($scripts, $action);
                        $lookup{$scripts} = $tmp_s;
                        $scripts = $tmp_s;
                    } else {
                        $scripts = $lookup{$scripts};
                    }

                    #add the xcatdefaults
                    if ($scripts_to_save && $scripts) {
                        $scripts_to_save .= ",$scripts";
                    } else {
                        if ($scripts) { $scripts_to_save = $scripts; }
                    }
                }
            }

            #save to the hash
            if ($scripts_to_save) {
                if (exists($ret{$scripts_to_save})) {
                    my $pa = $ret{$scripts_to_save};
                    push(@$pa, $node);
                }
                else {
                    $ret{$scripts_to_save} = [$node];
                }
            }
        }
    }
    return %ret;
}

#-------------------------------------------------------

=head3  parseprescripts
   Parse the prescript string and get the scripts for the given action out
=cut

#-------------------------------------------------------
sub parseprescripts
{
    my $scripts = shift;
    my $action  = shift;
    my $ret;

    if ($scripts) {
        foreach my $token (split(/\|/, $scripts)) {
            if ($token =~ /:/) {
                if ($token =~ /^$action:(.*)/) {
                    $ret .= "$1,";
                }
            } else {
                $ret .= "$token,";
            }
        }
    }

    return $ret;
}


#-------------------------------------------------------

=head3  run_script_single_node

=cut

#-------------------------------------------------------
sub run_script_single_node
{
    my $installdir = shift;    #/install
    my $s          = shift;    #script name
    my $action     = shift;
    my $max        = shift;    #max number of instances to be run at a time
    my $nodes      = shift;    #nodes to be run
    my $callback   = shift;    #callback

    my $children      = 0;
    my $localhostname = hostname();

    foreach my $node (@$nodes) {
        $SIG{CHLD} = sub { my $pid = 0; while (($pid = waitpid(-1, WNOHANG)) > 0) { $children--; } };

        while ($children >= $max) {
            Time::HiRes::sleep(0.5);
            next;
        }

        my $pid = xCAT::Utils->xfork;
        if (!defined($pid)) {

            # Fork error
            my $rsp = {};
            $rsp->{data}->[0] = "$localhostname: Fork error before running script $s for node $node";
            $callback->($rsp);
            return 1;
        }
        elsif ($pid == 0) {

            # Child process
            undef $SIG{CHLD};
            my $ret = `NODES=$node ACTION=$action $installdir/prescripts/$s 2>&1`;
            my $err_code = $?;
            my $rsp      = {};
            if ($err_code != 0) {
                $rsp = {};
                $rsp->{error}->[0] = "$localhostname: $s: node=$node. return code=$err_code. Error message=$ret";
                $callback->($rsp);
            } else {
                if ($ret) {
                    $rsp->{data}->[0] = "$localhostname: $s: node=$node. $ret";
                    $callback->($rsp);
                }
            }
            exit $err_code;
        }
        else {
            # Parent process
            $children++;
        }
    }

    #drain one more time
    while ($children > 0) {
        Time::HiRes::sleep(0.5);

        $SIG{CHLD} = sub { my $pid = 0; while (($pid = waitpid(-1, WNOHANG)) > 0) { $children--; } };
    }
    return 0;
}
