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

=head1
    xCAT plugin package to handle rinstall and winstall

    Supported command:
        rinstall - runs nodeset, rsetboot, rpower commands
        winstall - also opens the console

=cut

#-------------------------------------------------------
package xCAT_plugin::rinstall;
use strict;

require xCAT::Utils;
require xCAT::MsgUtils;
use xCAT::NodeRange;
use xCAT::Table;
use xCAT::Usage;

use Data::Dumper;
use Getopt::Long;

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

=head3  handled_commands

    Return list of commands handled by this plugin

=cut

#-------------------------------------------------------
sub handled_commands {
    return {
        rinstall => "rinstall",
        winstall => "rinstall",
    };
}

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

=head3  Process the command

=cut

#-------------------------------------------------------
sub process_request {
    my $request  = shift;
    my $callback = shift;
    my $subreq   = shift;

    rinstall($request, $callback, $subreq);
}

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

=head3  rinstall

    Wrapper around nodeset, rsetboot, rpower for the admin convenience

=cut

#-------------------------------------------------------
sub rinstall {
    my ($req, $callback, $subreq) = @_;
    $::CALLBACK = $callback;
    my $CONSOLE;
    my $OSIMAGE;
    my $STATES;
    my $ignorekernelchk;
    my $noupdateinitrd;
    my $VERBOSE;
    my $HELP;
    my $VERSION;
    my $UEFIMODE;

    # Could be rinstall or winstall
    my $command = $req->{command}->[0];

    my $nodes;
    my @nodes;
    my %nodes;
    my $rsp = {};

    # There are nodes
    if (defined($req->{node})) {
        $nodes = $req->{node};
        @nodes = @$nodes;
    }

    my $args;

    # There are arguments
    if (defined($req->{arg})) {
        $args = $req->{arg};
        @ARGV = @{$args};
    }

    if (($command =~ /rinstall/) or ($command =~ /winstall/)) {
        my $ret=xCAT::Usage->validateArgs($command,@ARGV);
        if ($ret->[0]!=0) {
             $rsp->{error}->[0] = $ret->[1];
             $rsp->{errorcode}->[0] = $ret->[0];
             xCAT::MsgUtils->message("E", $rsp, $callback);
             &usage($command,$callback);
             return;
        }

        my $state = $ARGV[0];
        chomp($state);
        if ($state =~ /^osimage=(\S+)/) {
           $OSIMAGE = $1; # osimage was specified
           xCAT::MsgUtils->message("I", $rsp, $callback);
        }
        elsif ($state =~ /^boot$|^shell$|^osimage$|^runcmd=|^runimage=/) {
           # the rest are valid actions, just pass to nodeset
           $STATES=$state;
        }
        elsif ($state =~ /^-/) {
           # if starts with dash, let GetOptions below to process
        }
        else {
           if ($state) {
               $rsp->{errorcode}->[0]=1;
               $rsp->{error}->[0]="Invalid option $state";
               xCAT::MsgUtils->message("E",$rsp,$callback);
               &usage($command, $callback);
               return 1;
           }
        }

        Getopt::Long::Configure("bundling");
        Getopt::Long::Configure("no_pass_through");
        unless (
            GetOptions(
                'ignorekernelchk' => \$ignorekernelchk,
                'noupdateinitrd'  => \$noupdateinitrd,
                'V|verbose'       => \$VERBOSE,
                'h|help'          => \$HELP,
                'v|version'       => \$VERSION,
                'u|uefimode'      => \$UEFIMODE,
                'c|console'       => \$CONSOLE)
          ) {
            &usage($command, $callback);
            return 1;
        }
    }
    if ($HELP) {
        &usage($command, $callback);
        return 0;
    }
    if ($VERSION) {
        my $version = xCAT::Utils->Version();
        $rsp->{data}->[0] = "$version";
        xCAT::MsgUtils->message("I", $rsp, $callback);
        return 0;
    }
    if (scalar(@nodes) == 0) {
        &usage($command, $callback);
        return 1;
    }

    if($command eq "rinstall" and scalar(@nodes) > 1 and $CONSOLE){
       $rsp->{errorcode}->[0]=1;
       $rsp->{error}->[0]="rinstall -c/--console can only be run against one node! Please use winstall -c/--console for multiple nodes.";
       xCAT::MsgUtils->message("E",$rsp,$callback);
       return 1;
    }

    my $rc = 0;
    my @parameter;

    my $nodehmtable = xCAT::Table->new("nodehm");
    my $nodehmcache = $nodehmtable->getNodesAttribs(\@nodes, ['mgt']);
    $nodehmtable->close();

    if ($OSIMAGE) {

        # if osimage=<imagename> is specified,
        # call "nodeset ... osimage= ..." to set the boot state of the noderange to the specified osimage,
        # "nodeset" will handle the updating of node attributes such as os,arch,profile,provmethod.

        my $noderestable = xCAT::Table->new("noderes");
        my $noderescache = $noderestable->getNodesAttribs(\@nodes, ['netboot']);
        $noderestable->close();
        my $nodetypetable = xCAT::Table->new("nodetype");
        my $nodetypecache = $nodetypetable->getNodesAttribs(\@nodes, ['arch']);
        $nodetypetable->close();
        my $osimagetable = xCAT::Table->new("osimage");
        (my $ref) = $osimagetable->getAttribs({ imagename => $OSIMAGE }, 'osvers', 'osarch', 'imagetype');
        $osimagetable->close();

        unless ($ref) {
            # Nothing was returned from getAttrbs for the specified image
            $rsp->{data}->[0] = "Cannot find the OS image $OSIMAGE in the osimage table.";
            xCAT::MsgUtils->message("E", $rsp, $callback);
            return 1;
        }
        unless (defined($ref->{osarch})) {
            $rsp->{error}->[0] = "$OSIMAGE 'osarch' attribute not defined in 'osimage' table.";
            $rsp->{errorcode}->[0] = 1;
            xCAT::MsgUtils->message("E", $rsp, $callback);
            return 1;
        }
        my $osimagearch = $ref->{osarch};
        my $netbootval = xCAT::Utils->lookupNetboot($ref->{osvers}, $ref->{osarch}, $ref->{imagetype});
        my @validnodes;
        foreach my $node (@nodes) {
            unless ($noderescache)  { next; }
            unless ($nodetypecache) { next; }
            unless ($nodehmcache)   { next; }
            my $noderesattribs  = $noderescache->{$node}->[0];
            my $nodetypeattribs = $nodetypecache->{$node}->[0];
            my $nodehmattribs   = $nodehmcache->{$node}->[0];
            unless (defined($noderesattribs) and defined($noderesattribs->{'netboot'})) {
                $rsp->{error}->[0] = "$node: Missing the 'netboot' attribute.";
                $rsp->{errorcode}->[0] = 1;
                xCAT::MsgUtils->message("E", $rsp, $callback);
                next;
            }
            else {
                unless ($netbootval =~ /$noderesattribs->{'netboot'}/i) {
                    $callback->({ warning => [ $node . ": $noderesattribs->{'netboot'} might be invalid when provisioning $OSIMAGE,valid options: \"$netbootval\". For more details see the 'netboot' description in the output of \"tabdump -d noderes\"." ] });
                    next;
                }
            }

            unless (defined($nodetypeattribs) and defined($nodetypeattribs->{'arch'})) {
                $rsp->{error}->[0] = "$node: 'arch' attribute not defined in 'nodetype' table.";
                $rsp->{errorcode}->[0] = 1;
                xCAT::MsgUtils->message("E", $rsp, $callback);
                next;
            }
            my $nodetypearch = $nodetypeattribs->{'arch'};
            if ($nodetypearch ne $osimagearch) {
	        unless(($nodetypearch =~ /^ppc64(le|el)?$/i) and ($osimagearch =~ /^ppc64(le|el)?$/i)){
                    $rsp->{error}->[0] = "$node: The value of 'arch' attribute of node does not match the 'osarch' attribute of osimage.";
                    $rsp->{errorcode}->[0] = 1;
                    xCAT::MsgUtils->message("E", $rsp, $callback);
                    next;
                }
            }

            unless (defined($nodehmattribs) and defined($nodehmattribs->{'mgt'})) {
                $rsp->{error}->[0] = "$node: 'mgt' attribute not defined in 'nodehm' table.";
                $rsp->{errorcode}->[0] = 1;
                xCAT::MsgUtils->message("E", $rsp, $callback);
                next;
            }
            push @validnodes, $node;
        }

        #only provision the normal nodes
        @nodes = @validnodes;

        push @parameter, "osimage=$OSIMAGE";

    }
    elsif ($STATES) {
        push @parameter, "$STATES";
    }
    else {

        # No osimage specified, set the boot state of each node based on the nodetype.provmethod:
        # 1) if nodetype.provmethod = [install/netboot/statelite],
        #  then output error message.
        # 2) if nodetype.provmethod = <osimage>,
        #  then call "nodeset ... osimage"

        # Group the nodes according to the nodetype.provmethod
        my %tphash;
        my $nodetypetable = xCAT::Table->new("nodetype");
        my $nodetypecache = $nodetypetable->getNodesAttribs(\@nodes, ['provmethod']);
        $nodetypetable->close();
        foreach my $node (@nodes) {
            unless ($nodetypecache) { next; }
            my $nodetypeattribs = $nodetypecache->{$node}->[0];
            unless (defined($nodetypeattribs) and defined($nodetypeattribs->{'provmethod'})) {
                $rsp->{error}->[0] = "$node: 'provmethod' attribute not defined in 'nodetype' table.";
                $rsp->{errorcode}->[0] = 1;
                xCAT::MsgUtils->message("E", $rsp, $callback);
                next;
            }
            else {
                push(@{ $tphash{ $nodetypeattribs->{'provmethod'} } }, $node);
            }
        }

        # Now for each group based on provmethod
        my @validnodes;
        foreach my $key (keys %tphash) {
            $::RUNCMD_RC = 0;
            my @pnnodes = @{ $tphash{$key} };

            # If nodetype.provmethod = [install|netboot|statelite]
            if ($key =~ /^(install|netboot|statelite)$/) {
                my $rsp = {};
                $rsp->{error}->[0] = "@pnnodes: The options 'install', 'netboot', and 'statelite' have been deprecated, use 'nodeset <noderange> osimage=<imagename>' instead.";
                $rsp->{errorcode}->[0] = 1;
                xCAT::MsgUtils->message("E", $rsp, $callback);
                next;
            }

            # If nodetype.provmethod != [install|netboot|statelite]
            else {
                push @validnodes, @pnnodes;
            }
        }

        #only provision the normal nodes
        @nodes = @validnodes;
        push @parameter, "osimage";

    }

    if ( grep( /osimage/, @parameter ) ) {
        # Task is osimage, check --ignorekernelchk and --noupdateinitrd flags and set if needed

        if ($ignorekernelchk) {
            push @parameter, "--ignorekernelchk";
        }
        if ($noupdateinitrd) {
            push @parameter, "--noupdateinitrd";
        }
    }

    if (scalar(@nodes) == 0) {
        $rsp->{error}->[0]     = "No available nodes for provision.";
        $rsp->{errorcode}->[0] = 1;
        xCAT::MsgUtils->message("E", $rsp, $callback);
        return 1;
    }
    else {
        $rsp->{data}->[0] = "Provision node(s): @nodes";
        xCAT::MsgUtils->message("I", $rsp, $callback);
    }

    %nodes = map { $_, 1 } @nodes;

    # Run nodeset $noderange $parameter
    my $res =
      xCAT::Utils->runxcmd(
        {
            command => ["nodeset"],
            node    => \@nodes,
            arg     => \@parameter
        },
        $subreq, -1, 1);

    $rc = $::RUNCMD_RC;
    my $rsp = {};
    if ($VERBOSE) {
        my @cmd = "Run command: nodeset @nodes @parameter";
        push @{ $rsp->{data} }, @cmd;
        push @{ $rsp->{data} }, @$res;
        xCAT::MsgUtils->message("D", $rsp, $callback);
    }

    unless ($rc == 0) {
        # We got an error with the nodeset
        my @successnodes;
        my @failurenodes;
        # copy into a temporary variable to avoid of circular reference
        my @lines = @$res;
        foreach my $line (@lines) {
            $rsp->{data}->[0] = $line;
            if($line =~ /The (\S+) can not be resolved/){
                push @failurenodes,$1;
            }
            if ($line =~ /dhcp server is not running/) {
                my $rsp = {};
                $rsp->{error}->[0]     = "Fatal error: dhcp server is not running";
                $rsp->{errorcode}->[0] = 1;
                xCAT::MsgUtils->message("E", $rsp, $callback);
                return 1;
            }
            if ($line =~ /Cannot wget/) {
                # If nodeset returns error that runimage can not be downloaded by wget,
                # display the error from nodeset (if not alredy displayed by VERBOSE above), stop processing and return.
                unless ($VERBOSE) {
                    xCAT::MsgUtils->message("I", $rsp, $callback);
                }
                return 1;
            }
            xCAT::MsgUtils->message("I", $rsp, $callback);
        }

        foreach my $node (@failurenodes) {
            delete $nodes{$node};
        }

        if (0+@failurenodes > 0) {
            $rsp->{error}->[0] = "Failed to run 'nodeset' against the following nodes: @failurenodes";
            $rsp->{errorcode}->[0] = 1;
            xCAT::MsgUtils->message("E", $rsp, $callback);
        }
        @nodes = keys %nodes;
    }

    # Group the nodes according to the nodehm.mgt
    my %hmhash;
    foreach my $node (@nodes) {
        unless ($nodehmcache) { next; }
        my $nodehmattribs = $nodehmcache->{$node}->[0];
        push(@{ $hmhash{ $nodehmattribs->{'mgt'} } }, $node);
    }

    # Now for each group based on mgt
    foreach my $hmkey (keys %hmhash) {
        $::RUNCMD_RC = 0;
        my @nodes = @{ $hmhash{$hmkey} };
        unless ($hmkey =~ /^(ipmi|blade|hmc|ivm|fsp|kvm|esx|rhevm|openbmc)$/)  {
            $rsp->{error}->[0] = "@nodes: rinstall only support nodehm.mgt type 'ipmi', 'blade', 'hmc', 'ivm', 'fsp', 'kvm', 'esx', 'rhevm', 'openbmc'.";
            $rsp->{errorcode}->[0] = 1;
            xCAT::MsgUtils->message("E", $rsp, $callback);
            next;
        }
        if (($hmkey =~ /^ivm$/) or ($hmkey =~ /^fsp$/) or ($hmkey =~ /^hmc$/)) {
            %nodes = map { $_, 1 } @nodes;

            # Run rnetboot $noderange
            my $res =
              xCAT::Utils->runxcmd(
                {
                    command => ["rnetboot"],
                    node    => \@nodes
                },
                $subreq, -1, 1);

            $rc = $::RUNCMD_RC;
            if ($VERBOSE) {
                my @cmd = "Run command: rnetboot @nodes";
                push @{ $rsp->{data} }, @cmd;
                push @{ $rsp->{data} }, @$res;
                xCAT::MsgUtils->message("D", $rsp, $callback);
            }
            unless ($rc == 0) {

                # We got an error with the rnetboot
                my @failurenodes;
                # copy into a temporary variable to avoid of circular reference
                my @lines = @$res;
                foreach my $line (@lines) {
                    $rsp->{data}->[0] = $line;
                    if ($line =~ /: Success/) {
                        my $successnode;
                        my $restline;
                        ($successnode, $restline) = split(/:/, $line, 2);
                        $nodes{$successnode} = 0;
                    }
                    xCAT::MsgUtils->message("I", $rsp, $callback);
                }
                foreach my $node (@nodes) {
                    if ($nodes{$node} == 1) {
                        push @failurenodes, $node;
                    }
                }
                if (0+@failurenodes > 0) {
                    $rsp->{error}->[0] = "Failed to run 'rnetboot' against the following nodes: @failurenodes";
                    $rsp->{errorcode}->[0] = 1;
                    xCAT::MsgUtils->message("E", $rsp, $callback);
                }
            }
        }
        else {
            # Call "rsetboot" to set the boot order of the nodehm.mgt=ipmi/openbmc nodes
            if ($hmkey =~ /^(ipmi|openbmc)$/) {
                %nodes = map { $_, 1 } @nodes;

                # Run rsetboot $noderange net
                my @rsetbootarg;
                push @rsetbootarg, "net";
                if ($UEFIMODE) {
                    push @rsetbootarg, "-u";
                }

                my %req=(
                        command => ["rsetboot"],
                        node    => \@nodes,
                        arg     => \@rsetbootarg
                    );

                #TODO: When OPENBMC support is finished, this line should be removed
                if($hmkey =~ /^openbmc$/){
                    $req{environment}{XCAT_OPENBMC_DEVEL}= "YES";
                }

                my $res =
                  xCAT::Utils->runxcmd(
                    \%req,
                    $subreq, -1, 1);


                $rc = $::RUNCMD_RC;
                my $rsp = {};
                if ($VERBOSE) {
                    my @cmd = "Run command: rsetboot @nodes @rsetbootarg";
                    push @{ $rsp->{data} }, @cmd;
                    push @{ $rsp->{data} }, @$res;
                    xCAT::MsgUtils->message("D", $rsp, $callback);
                }
                unless ($rc == 0) {
                    # We got an error with the rsetboot
                    my @successnodes;
                    my @failurenodes;
                    # copy into a temporary variable to avoid of circular reference
                    my @lines = @$res;
                    foreach my $line (@lines) {
                        $rsp->{data}->[0] = $line;
                        if ($line =~ /: Network/) {
                            my $successnode;
                            my $restline;
                            ($successnode, $restline) = split(/:/, $line, 2);
                            $nodes{$successnode} = 0;
                            push @successnodes, $successnode;
                        }
                        xCAT::MsgUtils->message("I", $rsp, $callback);
                    }
                    foreach my $node (@nodes) {
                        if ($nodes{$node} == 1) {
                            push @failurenodes, $node;
                        }
                    }
                    my $rsp = {};
                    if (0+@failurenodes > 0) {
                        $rsp->{error}->[0] = "Failed to run 'rsetboot' against the following nodes: @failurenodes";
                        $rsp->{errorcode}->[0] = 1;
                        xCAT::MsgUtils->message("E", $rsp, $callback);
                    }
                    @nodes = @successnodes;
                }
            }

            # Call "rpower" to start the node provision process
            %nodes = map { $_, 1 } @nodes;

            # Run rpower $noderange boot
            my @rpowerarg;
            push @rpowerarg, "boot";
            my %req=(
                    command => ["rpower"],
                    node    => \@nodes,
                    arg     => \@rpowerarg
            );

            my $res =
              xCAT::Utils->runxcmd(
                \%req,
                $subreq, -1, 1);

            $rc = $::RUNCMD_RC;
            if ($VERBOSE) {
                my @cmd = "Run command: rpower @nodes @rpowerarg";
                push @{ $rsp->{data} }, @cmd;
                push @{ $rsp->{data} }, @$res;
                xCAT::MsgUtils->message("D", $rsp, $callback);
            }
            unless ($rc == 0) {
                # We got an error with the rpower
                my @failurenodes;
                # copy into a temporary variable to avoid of circular reference
                my @lines = @$res;
                foreach my $line (@lines) {
                    $rsp->{data}->[0] = $line;
                    if (($line =~ /: on reset/) or ($line =~ /: off on/)) {
                        my $successnode;
                        my $restline;
                        ($successnode, $restline) = split(/:/, $line, 2);
                        $nodes{$successnode} = 0;
                    }
                    xCAT::MsgUtils->message("I", $rsp, $callback);
                }
                foreach my $node (@nodes) {
                    if ($nodes{$node} == 1) {
                        push @failurenodes, $node;
                    }
                }
                my $rsp = {};
                if (0+@failurenodes > 0) {
                    $rsp->{error}->[0] = "Failed to run 'rpower' against the following nodes: @failurenodes";
                    $rsp->{errorcode}->[0] = 1;
                    xCAT::MsgUtils->message("E", $rsp, $callback);
                }
            }
        }
    }

    return 0;
}

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

=head3  Usage


=cut

#-------------------------------------------------------
sub usage {
    my $command  = shift;
    my $callback = shift;
    my $rsp      = {};
    $rsp->{data}->[0] = "Usage:";
    $rsp->{data}->[1] = "   $command <noderange> [boot | shell | runcmd=<command>] [-c|--console] [-u|--uefimode] [-V|--verbose]";
    $rsp->{data}->[2] = "   $command <noderange> [osimage[=<imagename>]] [--noupdateinitrd] [--ignorekernelchk] [-c|--console] [-u|--uefimode] [-V|--verbose]";
    $rsp->{data}->[3] = "   $command <noderange> runimage=<task>";
    $rsp->{data}->[4] = "   $command [-h|--help|-v|--version]";
    xCAT::MsgUtils->message("I", $rsp, $callback);
}



1;
