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

=head1
  xCAT plugin package to handle xdsh

   Supported command:
         xdsh-> dsh
         xdcp-> dcp

=cut

#-------------------------------------------------------
package xCAT_plugin::xdsh;
use strict;
use Storable qw(dclone);
use File::Basename;
use File::Path;
require xCAT::Table;

require xCAT::Utils;

require xCAT::MsgUtils;
use Getopt::Long;
require xCAT::DSHCLI;
1;

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

=head3  handled_commands

Return list of commands handled by this plugin

=cut

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

sub handled_commands
{
    return {
            xdsh => "xdsh",
            xdcp => "xdsh"
            };
}

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

=head3  preprocess_request

  Check and setup for hierarchy 

=cut

#-------------------------------------------------------
sub preprocess_request
{
    my $req     = shift;
    my $cb      = shift;
    my $sub_req = shift;
    my %sn;
    my $sn;
    my $rc = 0;

    #if already preprocessed, go straight to request
    if (   (defined($req->{_xcatpreprocessed}))
        && ($req->{_xcatpreprocessed}->[0] == 1))
    {
        return [$req];
    }
    my $command = $req->{command}->[0];    # xdsh vs xdcp
    my $nodes   = $req->{node};
    my $service = "xcat";
    my @requests;
    $::tmpsyncsnfile = "/tmp/xcatrf.tmp";
    $::RUNCMD_RC     = 0;
    @::good_SN;
    @::bad_SN;
    my $syncsn = 0;                        # sync service node only if 1

    # read the environment variables for rsync setup
    foreach my $envar (@{$req->{env}})
    {
        my ($var, $value) = split(/=/, $envar, 2);
        if ($var eq "RSYNCSNONLY")
        {    # syncing SN, will change noderange to list of SN
                # we are only syncing the service node ( -s flag)
            $syncsn = 1;
        }
        if ($var eq "DSH_RSYNC_FILE")    # from -F flag
        {    # if hierarchy,need to copy file to the SN
            $::syncsnfile = $value;    # in the new /tmp/xcatrf.tmp
        }
        if ($var eq "DCP_PULL")        # from -P flag
        {
            $::dcppull = 1;            # TBD  handle pull hierarchy
        }
    }

    # there are nodes in the xdsh command, not xdsh  to an image
    if ($nodes)
    {

        # find service nodes for requested nodes
        # build an individual request for each service node
        # find out the names for the Management Node
        my @MNnodeinfo   = xCAT::Utils->determinehostname;
        my $MNnodename   = pop @MNnodeinfo;                  # hostname
        my @MNnodeipaddr = @MNnodeinfo;                      # ipaddresses
        $::mnname = $MNnodeipaddr[0];
        $::SNpath;    # syncfile path on the service node
        $sn = xCAT::Utils->get_ServiceNode($nodes, $service, "MN");
        my @snodes;
        my @snoderange;

        # check to see if service nodes and not just the MN
        # if just MN, then no hierarchy to deal with
        if ($sn)
        {
            foreach my $snkey (keys %$sn)
            {
                if (!grep(/$snkey/, @MNnodeipaddr))
                {     # if not the MN
                    push @snodes, $snkey;
                    $snoderange[0] .= "$snkey,";
                    chop $snoderange[0];

                }
            }
        }

        # if servicenodes and if xdcp and not pull function
        # send command to service nodes first and process errors
        # return an array  of good service nodes
        #
        if (@snodes)    # service nodes
        {

            # if xdcp and not pull function
            if (($command eq "xdcp") && ($::dcppull == 0))
            {
                my $synfiledir;

                # get the directory on the servicenode to put the  files in
                my @syndir = xCAT::Utils->get_site_attribute("SNsyncfiledir");
                if ($syndir[0])
                {
                    $synfiledir = $syndir[0];
                }
                else
                {
                    $synfiledir = "/var/xcat/syncfiles";    # default
                }

                # setup the service node with the files to xdcp to the
                # compute nodes
                $rc =
                  &process_servicenodes($req, $cb, $sub_req, \@snodes,
                                        \@snoderange, $synfiledir);

                # fatal error need to stop
                if ($rc != 0)
                {
                    return;
                }
            }
            else
            {    # command is xdsh or xdcp pull
                @::good_SN = @snodes;    # all good service nodes for now
            }

        }
        else
        {                                # no servicenodes, no hierarchy
                                         # process here on the MN
            &process_request($req, $cb, $sub_req);
            return;

        }

        # if  hierarchical work still to do
        # Note there may still be a mix of nodes that are service from
        # the MN and nodes that are serviced from the SN, for example
        # a dsh to a list of servicenodes and nodes in the noderange.

        if ($syncsn == 0)    # not just syncing (-s) the service nodes
                             # taken care of in process_servicenodes

        {
            foreach my $snkey (keys %$sn)
            {

                # if it is not being service by the MN
                if (!grep(/$snkey/, @MNnodeipaddr))
                {

                    # if it is a good SN, one ready to service the nodes
                    if (grep(/$snkey/, @::good_SN))
                    {
                        my $noderequests = &process_nodes($req, $sn, $snkey);
                        push @requests, $noderequests;    # build request queue

                    }
                }
                else    # serviced by the MN, then
                {       # just run normal dsh dcp
                    my $reqcopy = {%$req};
                    $reqcopy->{node}                   = $sn->{$snkey};
                    $reqcopy->{'_xcatdest'}            = $snkey;
                    $reqcopy->{_xcatpreprocessed}->[0] = 1;
                    push @requests, $reqcopy;

                }
            }    # end foreach
        }    # end syncing  nodes
    }
    else     # no nodes on the command
    {        # running on local image
        return [$req];
    }
    return \@requests;
}

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

=head3  process_servicenodes
  Build the xdcp command to send to the service nodes first 
  Return an array of servicenodes that do not have errors 
  Returns error code:
  if  = 0,  good return continue to process the
	  nodes.
  if  = 1,  global error need to quit

=cut

#-------------------------------------------------------
sub process_servicenodes
{

    my $req        = shift;
    my $callback   = shift;
    my $sub_req    = shift;
    my $sn         = shift;
    my $snrange    = shift;
    my $synfiledir = shift;
    my @snodes     = @$sn;
    my @snoderange = @$snrange;
    my $args;
    $::RUNCMD_RC = 0;
    my $cmd = $req->{command}->[0];

    # if xdcp -F command (input $syncsnfile) and service nodes first need
    #   to be rsync to the
    #  $synfiledir  directory
    if ($::syncsnfile)
    {
        if (!-f $::syncsnfile)
        {    # syncfile does not exist,  quit
            my $rsp = {};
            $rsp->{data}->[0] = "File:$::syncsnfile does not exist.";
            xCAT::MsgUtils->message("E", $rsp, $callback, 1);
            return (1);    # process no service nodes
        }

        # xdcp sync to the service node first
        #change noderange to the service nodes
        # sync each one and check for error
        # if error do not add to good_SN array, add to bad_SN
        foreach my $node (@snodes)
        {

            # run the command to each servicenode
            # xdcp <sn> -s -F <syncfile>
            my @sn = ();
            push @sn, $node;

            # don't use runxcmd, because can go straight to process_request,
            # these are all service nodes. Also servicenode is taken from
            # the noderes table and may not be the same name as in the nodelist
            # table, for example may be an ip address.
            # here on the MN
            my $addreq;
            $addreq->{'_xcatdest'}  = $::mnname;
            $addreq->{node}         = \@sn;
            $addreq->{noderange}    = \@sn;
            $addreq->{arg}->[0]     = "-s";
            $addreq->{arg}->[1]     = "-F";
            $addreq->{arg}->[2]     = $::syncsnfile;
            $addreq->{command}->[0] = $cmd;
            $addreq->{cwd}->[0]     = $req->{cwd}->[0];
            $addreq->{env}          = $req->{env};
            &process_request($addreq, $callback, $sub_req);

            if ($::FAILED_NODES == 0)
            {
                push @::good_SN, $node;
            }
            else
            {
                push @::bad_SN, $node;
            }
        }    # end foreach good servicenode

        # for all the service nodes that are still good
        # need to xdcp rsync file( -F input)
        # to the service node  to the /tmp/xcatrf.tmp file
        my @good_SN2 = @::good_SN;
        @::good_SN = ();
        foreach my $node (@good_SN2)
        {
            my @sn = ();
            push @sn, $node;

            # run the command to each good servicenode
            # xdcp <sn> <syncfile> <tmp/xcatrf.tmp>
            my $addreq;
            $addreq->{'_xcatdest'}  = $::mnname;
            $addreq->{node}         = \@sn;
            $addreq->{noderange}    = \@sn;
            $addreq->{arg}->[0]     = "$::syncsnfile";
            $addreq->{arg}->[1]     = "$::tmpsyncsnfile";
            $addreq->{command}->[0] = $cmd;
            $addreq->{cwd}->[0]     = $req->{cwd}->[0];
            $addreq->{env}          = $req->{env};
            &process_request($addreq, $callback, $sub_req);

            if ($::FAILED_NODES == 0)
            {
                push @::good_SN, $node;
            }
            else
            {
                push @::bad_SN, $node;
            }

        }    # end foreach good service node
    }    # end  xdcp -F
    else
    {

        # if other xdcp commands, and not pull function
        # mk the directory on the SN to hold the files
        # to be sent to the SN.
        # build a command to update the service nodes
        # change the destination to the tmp location on
        # the service node
        # hierarchical support for pull (TBD)

        #make the needed directory on the service node
        # create new directory for path on Service Node
        # xdsh  <sn> mkdir -p $SNdir
        my $frompath = $req->{arg}->[-2];
        $::SNpath = $synfiledir;
        $::SNpath .= $frompath;
        my $SNdir;
        $SNdir = dirname($::SNpath);    # get directory

        foreach my $node (@snodes)
        {
            my @sn = ();
            push @sn, $node;

            # run the command to each servicenode
            # to make the directory under the temporary
            # SNsyncfiledir to hold the files that will be
            # sent to the service nodes
            # xdsh <sn> mkdir -p <SNsyncfiledir>/$::SNpath
            my $addreq;
            $addreq->{'_xcatdest'}  = $::mnname;
            $addreq->{node}         = \@sn;
            $addreq->{noderange}    = \@sn;
            $addreq->{arg}->[0]     = "mkdir ";
            $addreq->{arg}->[1]     = "-p ";
            $addreq->{arg}->[2]     = $SNdir;
            $addreq->{command}->[0] = 'xdsh';
            $addreq->{cwd}->[0]     = $req->{cwd}->[0];
            $addreq->{env}          = $req->{env};
            &process_request($addreq, $callback, $sub_req);

            if ($::FAILED_NODES == 0)
            {
                push @::good_SN, $node;
            }
            else
            {
                push @::bad_SN, $node;
            }
        }    # end foreach good servicenode

        # now xdcp file to the service node to the new
        # tmp path

        # for all the service nodes that are still good
        my @good_SN2 = @::good_SN;
        @::good_SN = ();
        foreach my $node (@good_SN2)
        {
            my @sn;
            push @sn, $node;

            # copy the file to each good servicenode
            # xdcp <sn> <file> <SNsyncfiledir/../file>
            my $addreq = dclone($req);    # get original request
            $addreq->{arg}->[-1] = $SNdir;    # change to tmppath on servicenode
            $addreq->{'_xcatdest'} = $::mnname;
            $addreq->{node}        = \@sn;
            $addreq->{noderange}   = \@sn;
            &process_request($addreq, $callback, $sub_req);

            if ($::FAILED_NODES == 0)
            {
                push @::good_SN, $node;
            }
            else
            {
                push @::bad_SN, $node;
            }

        }    # end foreach good service node
    }

    # report bad service nodes]
    if (@::bad_SN)
    {
        my $rsp = {};
        my $badnodes;
        foreach my $badnode (@::bad_SN)
        {
            $badnodes .= $badnode;
            $badnodes .= ", ";
        }
        chop $badnodes;
        my $msg =
          "\nThe following servicenodes: $badnodes have errors and cannot be updated\n Until the error is fixed, xdcp will not work to nodes serviced by these service nodes. Run xdsh <servicenode,...> -c ,  to clean up the xdcp servicenode directory, and run the command again.";
        $rsp->{data}->[0] = $msg;
        xCAT::MsgUtils->message("D", $rsp, $callback);
    }
    return (0);
}

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

=head3  process_nodes

  Build the  request to send to the nodes, serviced by SN 
  Return the request 

=cut

#-------------------------------------------------------
sub process_nodes
{

    my $req     = shift;
    my $sn      = shift;
    my $snkey   = shift;
    my $command = $req->{command}->[0];
    my @requests;

    # if the -F option to sync the nodes
    # then for a Node
    # change the command to use the -F /tmp/xcatrf.tmp
    # because that is where the file was put on the SN
    #
    my $newSNreq = dclone($req);
    if ($::syncsnfile)    # -F option
    {
        my $args = $newSNreq->{arg};

        my $i = 0;
        foreach my $argument (@$args)
        {

            # find the -F and change the name of the
            # file in the next array entry to the tmp file
            if ($argument eq "-F")
            {
                $i++;
                $newSNreq->{arg}->[$i] = $::tmpsyncsnfile;
                last;
            }
            $i++;
        }
    }
    else
    {    # if other dcp command, change from directory
            # to be the site.SNsyncfiledir
            #	directory on the service node
            # if not pull (-P) pullfunction
            # xdsh and xdcp pull just use the input request
        if (($command eq "xdcp") && ($::dcppull == 0))
        {
            $newSNreq->{arg}->[-2] = $::SNpath;
        }
    }
    $newSNreq->{node}                   = $sn->{$snkey};
    $newSNreq->{'_xcatdest'}            = $snkey;
    $newSNreq->{_xcatpreprocessed}->[0] = 1;

    #push @requests, $newSNreq;

    return $newSNreq;
}

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

=head3  process_request

  Process the command

=cut

#-------------------------------------------------------
sub process_request
{

    my $request  = shift;
    my $callback = shift;
    my $sub_req  = shift;

    my $nodes   = $request->{node};
    my $command = $request->{command}->[0];
    my $args    = $request->{arg};
    my $envs    = $request->{env};
    my $rsp     = {};

    # get the Environment Variables and set them in the current environment
    foreach my $envar (@{$request->{env}})
    {
        my ($var, $value) = split(/=/, $envar, 2);
        $ENV{$var} = $value;
    }
    if ($command eq "xdsh")
    {
        xdsh($nodes, $args, $callback, $command, $request->{noderange}->[0]);
    }
    else
    {
        if ($command eq "xdcp")
        {
            xdcp($nodes, $args, $callback, $command,
                 $request->{noderange}->[0]);
        }
        else
        {
            my $rsp = {};
            $rsp->{data}->[0] =
              "Unknown command $command.  Cannot process the command.";
            xCAT::MsgUtils->message("E", $rsp, $callback, 1);
            return;
        }
    }
}

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

=head3  xdsh

   Parses Builds and runs the dsh


=cut

#-------------------------------------------------------
sub xdsh
{
    my ($nodes, $args, $callback, $command, $noderange) = @_;

    $::FAILED_NODES = 0;

    # parse dsh input, will return $::NUMBER_NODES_FAILED
    my @local_results =
      xCAT::DSHCLI->parse_and_run_dsh($nodes,   $args, $callback,
                                      $command, $noderange);

    my $maxlines = 10000;
    my $arraylen = @local_results;
    my $rsp      = {};
    my $i        = 0;
    my $j;
    while ($i < $arraylen)
    {

        for ($j = 0 ; $j < $maxlines ; $j++)
        {
            if ($i > $arraylen)
            {
                last;
            }
            else
            {
                $rsp->{data}->[$j] = $local_results[$i];    # send  max lines
            }
            $i++;
        }
        xCAT::MsgUtils->message("D", $rsp, $callback);
    }

    # set return code
    $rsp = {};
    $rsp->{errorcode} = $::FAILED_NODES;
    $callback->($rsp);
    return;
}

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

=head3  xdcp

   Parses, Builds and runs the dcp command


=cut

#-------------------------------------------------------
sub xdcp
{
    my ($nodes, $args, $callback, $command, $noderange) = @_;

    $::FAILED_NODES = 0;

    #`touch /tmp/lissadebug`;
    # parse dcp input
    my @local_results =
      xCAT::DSHCLI->parse_and_run_dcp($nodes,   $args, $callback,
                                      $command, $noderange);
    my $rsp = {};
    my $i   = 0;
    ##  process return data
    if (@local_results)
    {
        foreach my $line (@local_results)
        {
            $rsp->{data}->[$i] = $line;
            $i++;
        }

        xCAT::MsgUtils->message("D", $rsp, $callback);
    }
    if (-e "/tmp/xcatrf.tmp")
    {    # used tmp file for -F option
            #`rm /tmp/xcatrf.tmp`;
    }

    # set return code
    $rsp = {};
    $rsp->{errorcode} = $::FAILED_NODES;
    $callback->($rsp);
    return;
}