# IBM(c) 2014 EPL license http://www.eclipse.org/legal/epl-v10.html
# Lenovo(c) 2016
#TODO: delete entries not being refreshed if no noderange
package xCAT_plugin::confluent;
use strict;
use warnings;
use xCAT::PasswordUtils;
use xCAT::Table;
use xCAT::Utils;
use xCAT::TableUtils;
use Getopt::Long;
use Sys::Hostname;
use xCAT::SvrUtils;
use Confluent::Client;

use strict;
my %termservers;    #list of noted termservers

my $usage_string =
  "  makeconfluentcfg [-d|--delete] noderange
  makeconfluentcf [-l|--local]
  makeconfluentcf [-c|--confluent]
  makeconfluentcf
  makeconfluentcf -h|--help
  makeconfluentcf -v|--version
    -c|--confluent   Configure confluent only on the host.
                     The default goes down to all the confluent instances on
                     the server nodes and set them up
    -l|--local       Configure confluent only on the local system.
                     The default goes down to all the confluent instances on
                     the server nodes and set them up
    -d|--delete      Conserver has the relevant entries for the given noderange removed immediately from configuration
    -h|--help        Display this usage statement.
    -V|--verbose     Verbose mode.
    -v|--version     Display the version number.";

my $version_string = xCAT::Utils->Version();

sub handled_commands {
    return {
        makeconfluentcfg => "confluent"
      }
}

sub preprocess_request {
    my $request = shift;

    #if ($request->{_xcatdest}) { return [$request]; }    #exit if preprocessed
    if ($request->{_xcatpreprocessed}->[0] == 1) { return [$request]; }
    my $callback = shift;
    my @requests;
    my $noderange = $request->{node};    #Should be arrayref

    #display usage statement if -h
    my $extrargs = $request->{arg};
    my @exargs   = ($request->{arg});
    if (ref($extrargs)) {
        @exargs = @$extrargs;
    }
    @ARGV = @exargs;

    my $isSN     = xCAT::Utils->isServiceNode();
    my @hostinfo = xCAT::NetworkUtils->determinehostname();
    my %iphash   = ();
    foreach (@hostinfo) { $iphash{$_} = 1; }

    $Getopt::Long::ignorecase = 0;

    #$Getopt::Long::pass_through=1;
    if (!GetOptions(
            'c|confluent' => \$::CONSERVER,
            'l|local'     => \$::LOCAL,
            'h|help'      => \$::HELP,
            'D|debug'     => \$::DEBUG,
            'v|version'   => \$::VERSION,
            'V|verbose'   => \$::VERBOSE)) {
        $request = {};
        return;
    }
    if ($::HELP) {
        $callback->({ data => $usage_string });
        $request = {};
        return;
    }
    if ($::VERSION) {
        $callback->({ data => $version_string });
        $request = {};
        return;
    }
    if ($::LOCAL) {
        if ($noderange && @$noderange > 0) {
            $callback->({ data => "Invalid option -l or --local when there are nodes specified." });
            $request = {};
            return;
        }
    }
    if ($::CONSERVER && $::LOCAL) {
        $callback->({ data => "Can not specify -l or --local together with -c or --confluent." });
        $request = {};
        return;
    }


    # get site master
    my $master = xCAT::TableUtils->get_site_Master();
    if (!$master) { $master = hostname(); }

    my %cons_hash = ();
    my $hmtab     = xCAT::Table->new('nodehm');
    my @items;
    my $allnodes = 1;
    if ($noderange && @$noderange > 0) {
        $allnodes = 0;
        my $hmcache = $hmtab->getNodesAttribs($noderange, [ 'node', 'serialport', 'cons', 'conserver' ]);
        foreach my $node (@$noderange) {
            my $ent = $hmcache->{$node}->[0]; #$hmtab->getNodeAttribs($node,['node', 'serialport','cons', 'conserver']);
            push @items, $ent;
        }
    } else {
        $allnodes = 1;
        @items = $hmtab->getAllNodeAttribs([ 'node', 'serialport', 'cons', 'conserver' ]);
    }

    my @nodes = ();
    foreach (@items) {
        if (((!defined($_->{cons})) || ($_->{cons} eq "")) and !defined($_->{serialport})) { next; } #skip if 'cons' is not defined for this node, unless serialport suggests otherwise
        if (defined($_->{conserver})) { push @{ $cons_hash{ $_->{conserver} }{nodes} }, $_->{node}; }
        else { push @{ $cons_hash{$master}{nodes} }, $_->{node}; }
        push @nodes, $_->{node};
    }

    #send all nodes to the MN
    if (!$isSN && !$::CONSERVER) { #If -c flag is set, do not add the all nodes to the management node
        if ($::VERBOSE) {
            my $rsp;
            $rsp->{data}->[0] = "Configuring nodes in confluent on the management node";
            xCAT::MsgUtils->message("I", $rsp, $callback);
        }
        my $reqcopy = {%$request};
        $reqcopy->{'_xcatdest'} = $master;
        $reqcopy->{_xcatpreprocessed}->[0] = 1;
        $reqcopy->{'_allnodes'} = $allnodes; # the original command comes with nodes or not
        if ($allnodes == 1) { @nodes = (); }
        $reqcopy->{node} = \@nodes;
        push @requests, $reqcopy;
        if ($::LOCAL) { return \@requests; }
    }

    # send to conserver hosts
    foreach my $cons (keys %cons_hash) {

        #print "cons=$cons\n";
        my $doit = 0;
        if ($isSN) {
            if (exists($iphash{$cons})) { $doit = 1; }
        } else {
            if (!exists($iphash{$cons}) || $::CONSERVER) { $doit = 1; }
        }

        if ($doit) {
            my $reqcopy = {%$request};
            $reqcopy->{'_xcatdest'} = $cons;
            $reqcopy->{_xcatpreprocessed}->[0] = 1;
            $reqcopy->{'_allnodes'} = [$allnodes]; # the original command comes with nodes or not
            $reqcopy->{node} = $cons_hash{$cons}{nodes};
            my $no = $reqcopy->{node};

            #print "node=@$no\n";
            push @requests, $reqcopy;
        }    #end if
    }    #end foreach

    if ($::DEBUG) {
        my $rsp;
        $rsp->{data}->[0] = "In preprocess_request, request is " . Dumper(@requests);
        xCAT::MsgUtils->message("I", $rsp, $callback);
    }
    return \@requests;
}

sub process_request {
    my $req = shift;
    my $cb  = shift;
    if ($req->{command}->[0] eq "makeconfluentcfg") {
        makeconfluentcfg($req, $cb);
    }
}

# Read the file, get db info, update the file contents, and then write the file
sub makeconfluentcfg {
    my $req = shift;
    %termservers = ();    #clear hash of existing entries
    my $cb       = shift;
    my $extrargs = $req->{arg};
    my @exargs   = ($req->{arg});
    if (ref($extrargs)) {
        @exargs = @$extrargs;
    }
    @ARGV                     = @exargs;
    $Getopt::Long::ignorecase = 0;

    #$Getopt::Long::pass_through=1;
    my $delmode;
    GetOptions('d|delete' => \$delmode,
    );
    my $nodes  = $req->{node};
    my $svboot = 0;
    if (exists($req->{svboot})) { $svboot = 1; }
    my $confluent = Confluent::Client->new();    # just the local form for now..
    unless ($confluent) {

        # unable to get a connection to confluent
        my $rsp;
        $rsp->{data}->[0] = "Unable to open a connection to confluent, verify that confluent is running.";
        xCAT::MsgUtils->message("E", $rsp, $cb);
        return;
    }

    my $isSN     = xCAT::Utils->isServiceNode();
    my @hostinfo = xCAT::NetworkUtils->determinehostname();
    my %iphash   = ();
    foreach (@hostinfo) { $iphash{$_} = 1; }

    #print "process_request nodes=@$nodes\n";

    # Get db info for the nodes related to console
    my $hmtab      = xCAT::Table->new('nodehm');
    my $nodepostab = xCAT::Table->new('nodepos');
    my $mptab = xCAT::Table->new('mp');
    my @cfgents1; # = $hmtab->getAllNodeAttribs(['cons','serialport','mgt','conserver','termserver','termport']);
    my @cfgents2;
    my @cfgents3;
    my $explicitnodes = 0;
    if (($nodes and @$nodes > 0) or $req->{noderange}->[0]) {
        $explicitnodes = 1;
        @cfgents1 = $hmtab->getNodesAttribs($nodes, [ 'node', 'cons', 'mgt', 'conserver', 'termserver', 'termport', 'consoleondemand' ]);
        @cfgents2 = $nodepostab->getNodesAttribs($nodes, [ 'node', 'rack', 'u', 'chassis', 'slot', 'room' ]);
        @cfgents3 = $mptab->getNodesAttribs($nodes, [ 'node', 'mpa', 'id' ]);

        # Adjust the data structure to make the result consistent with the getAllNodeAttribs() call we make if a noderange was not specified
        my @tmpcfgents1;
        foreach my $ent (@cfgents1)
        {
            foreach my $nodeent (keys %$ent)
            {
                push @tmpcfgents1, $ent->{$nodeent}->[0];
            }
        }
        @cfgents1    = @tmpcfgents1;
        @tmpcfgents1 = ();
        foreach my $ent (@cfgents2)
        {
            foreach my $nodeent (keys %$ent)
            {
                push @tmpcfgents1, $ent->{$nodeent}->[0];
            }
        }
        @cfgents2    = @tmpcfgents1;
        @tmpcfgents1 = ();
        foreach my $ent (@cfgents3)
        {
            foreach my $nodeent (keys %$ent)
            {
                push @tmpcfgents1, $ent->{$nodeent}->[0];
            }
        }
        @cfgents3 = @tmpcfgents1;
    } else {
        @cfgents1 = $hmtab->getAllNodeAttribs([ 'cons', 'serialport', 'mgt', 'conserver', 'termserver', 'termport', 'consoleondemand' ]);
        @cfgents2 = $nodepostab->getAllNodeAttribs([ 'rack', 'u', 'chassis', 'slot', 'room' ]);
        @cfgents3 = $nodepostab->getAllNodeAttribs([ 'mpa', 'id' ]);
    }


    #cfgents1 should now have all the nodes, so we can fill in the cfgents array and cfgenthash one at a time.
    # skip the nodes that do not have 'cons' defined, unless a serialport setting suggests otherwise
    my %cfgenthash;
    foreach (@cfgents1) {
        if ($_->{cons} or defined($_->{'serialport'})) {
            unless ($_->{cons}) { $_->{cons} = $_->{mgt}; } #populate with fallback
            $cfgenthash{ $_->{node} } = $_; # also put the ref to the entry in a hash for quick look up
        } elsif ($explicitnodes) {
            $cfgenthash{ $_->{node} } = $_; # also put the ref to the entry in a hash for quick look up
        }
    }
    foreach my $nent (@cfgents2) {
        foreach (keys %$nent) {
            $cfgenthash{ $nent->{node} }->{$_} = $nent->{$_};
        }
    }
    foreach my $nent (@cfgents3) {
        foreach (keys %$nent) {
            $cfgenthash{ $nent->{node} }->{$_} = $nent->{$_};
        }
    }
    my @cfgents = ();
    foreach (values %cfgenthash) {
        push @cfgents, $_;
    }

    if ($::DEBUG) {
        my $rsp;
        $rsp->{data}->[0] = "In makeconservercf, cfgents is " . Dumper(@cfgents);
        xCAT::MsgUtils->message("I", $rsp, $cb);
    }

    # if nodes defined, it is either on the service node or makeconserver was called with noderange on mn
    if (($nodes and @$nodes > 0) or $req->{noderange}->[0]) {

        # strip all xCAT configured nodes from config if the original command was for all nodes
        #if (($req->{_allnodes}) && ($req->{_allnodes}->[0]==1)) {} #TODO: identify nodes that will be removed
        # call donodeent to add all node entries into the file.  It will return the 1st node in error.
        my $node;
        if ($node = donodeent(\%cfgenthash, $confluent, $delmode, $cb)) {

            #$cb->({node=>[{name=>$node,error=>"Bad configuration, check attributes under the nodehm category",errorcode=>1}]});
            xCAT::SvrUtils::sendmsg([ 1, "Bad configuration, check attributes under the nodehm category" ], $cb, $node);
        }
    } else {    #no nodes specified, do em all up
           #zapcfg(\@filecontent); # strip all xCAT configured nodes from config
           #TODO: identify nodes to be removed

        # get nodetype so we can filter out node types without console support
        my $typetab = xCAT::Table->new('nodetype');
        my %type;

        if (defined($typetab)) {
            my @ents = $typetab->getAllNodeAttribs([qw(node nodetype)]);
            foreach (@ents) {
                $type{ $_->{node} } = $_->{nodetype};
            }
        }

        # remove nodes that arent for this SN or type of node doesnt have console
        foreach (@cfgents) {
            my $keepdoing = 0;
            if ($isSN && $_->{conserver} && exists($iphash{ $_->{conserver} })) {
                $keepdoing = 1; #only hanlde the nodes that use this SN as the conserver
            }
            if (!$isSN) { $keepdoing = 1; }    #handle all for MN
            if ($keepdoing) {
                if ($_->{termserver} and not $termservers{ $_->{termserver} }) {
                    die "confluent does not currently support termserver";
                    $termservers{ $_->{termserver} } = 1; # dont add this one again
                }
                if ($type{$_->{node}} and $type{ $_->{node} } =~ /fsp|bpa|hmc|ivm/) {
                    $keepdoing = 0;    # these types dont have consoles
                }
            }
            if (!$keepdoing) { delete $cfgenthash{ $_->{node} }; } # remove this node from the hash so we dont process it later
        }

        # Now add into the file all the node entries that we kept
        my $node;
        if ($node = donodeent(\%cfgenthash, $confluent, undef, $cb)) {

            # donodeent will return the 1st node in error
            #$cb->({node=>[{name=>$node,error=>"Bad configuration, check attributes under the nodehm category",errorcode=>1}]});
            xCAT::SvrUtils::sendmsg([ 1, "Bad configuration, check attributes under the nodehm category" ], $cb, $node);
        }
    }
}


# Add entries in the file for each node.  This function used to do 1 node at a time, but was changed to do
# all nodes at once for performance reasons.  If there is a problem with a nodes config, this
# function will return that node name as the one in error.
sub donodeent {
    my $cfgenthash = shift;
    my $confluent  = shift;
    my $delmode    = shift;
    my $cb         = shift;
    my $idx        = 0;
    my $toidx      = -1;
    my $skip       = 0;
    my $skipnext   = 0;

    # Delete all the previous stanzas of the nodes specified
    my $isSN = xCAT::Utils->isServiceNode();
    my $curnode;

    # Loop till find the start of a node stanza and remove lines till get to the end of the stanza
    my %currnodes;
    $confluent->read('/nodes/');
    my $listitem = $confluent->next_result();
    while ($listitem) {
        if (exists $listitem->{item}) {
            my $name = $listitem->{item}->{href};
            $name =~ s/\/$//;
            $currnodes{$name} = 1;
        }
        $listitem = $confluent->next_result();
    }
    if ($delmode) {
        foreach my $confnode (keys %currnodes) {
            if ($cfgenthash->{$confnode}) {
                $confluent->delete('/nodes/' . $confnode);
            }
        }
        return;
    }
    my @toconfignodes = keys %{$cfgenthash};
    my $ipmitab       = xCAT::Table->new('ipmi', -create => 0);
    my $ipmientries   = {};
    if ($ipmitab) {
        $ipmientries = $ipmitab->getNodesAttribs(\@toconfignodes,
            [qw/bmc username password/]);
    }
    my $ipmiauthdata = xCAT::PasswordUtils::getIPMIAuth(
        noderange => \@toconfignodes, ipmihash => $ipmientries);
    my $nltab = xCAT::Table->new('nodelist', -create => 0);
    my $groupdata = {};
    if ($nltab) {
        $groupdata = $nltab->getNodesAttribs(\@toconfignodes,
            [qw/groups/]);
    }
    my @cfgroups;
    foreach (keys %$groupdata) {
        push @cfgroups, grep { defined && length } split /,/, $groupdata->{$_}->[0]->{groups};
    }
    $confluent->read('/nodegroups/');
    my $currgroup = $confluent->next_result();
    my %currgroups;
    while ($currgroup) {
        if (exists $currgroup->{item}) {
            my $groupname = $currgroup->{item}->{href};
            $groupname =~ s/\/$//;
            $currgroups{$groupname} = 1;
        }
        $currgroup = $confluent->next_result();
    }
    foreach (@cfgroups) {
        if (not exists $currgroups{$_}) {
            $confluent->create('/nodegroups/', parameters => { name => $_ });
            my $rsp = $confluent->next_result();
            while ($rsp) {
                if (exists $rsp->{error}) {
                    xCAT::SvrUtils::sendmsg([ 1, ":Confluent error: " . $rsp->{error} ], $cb);
                }
                $rsp = $confluent->next_result();
            }
        }
    }

    # Go thru all nodes specified to add them to the file
    foreach my $node (sort keys %$cfgenthash) {
        my $cfgent = $cfgenthash->{$node};
        my $cmeth  = $cfgent->{cons};
        if ($cmeth and $cmeth ne 'ipmi') {
            $cmeth = 'xcat' . $cmeth;
        }
        my %parameters;
        if ($cmeth) { $parameters{'console.method'} = $cmeth; }
        if (not $cmeth or $cmeth eq 'ipmi') {
            $parameters{'secret.hardwaremanagementuser'} =
              $ipmiauthdata->{$node}->{username};
            $parameters{'secret.hardwaremanagementpassword'} =
              $ipmiauthdata->{$node}->{password};
            my $bmc = $ipmientries->{$node}->[0]->{bmc};
            if ($bmc) {
		 $bmc =~ s/,.*//;
            	$parameters{'hardwaremanagement.manager'} = $bmc;
             }
        }
        if (defined($cfgent->{consoleondemand})) {
            if ($cfgent->{consoleondemand} == 'yes') {
                $parameters{'console.logging'} = 'none';
            }
            else {
                $parameters{'console.logging'} = 'full';
            }
        } elsif ($::XCATSITEVALS{'consoleondemand'} and $::XCATSITEVALS{'consoleondemand'} !~ m/^n/) {
            $parameters{'console.logging'} = 'none';
        }
	elsif ($::XCATSITEVALS{'consoleondemand'} and $::XCATSITEVALS{'consoleondemand'} == 'no') {
            $parameters{'console.logging'} = 'full';
        }

        # ok, now for nodepos...
        if (defined $cfgent->{u}) {
            $parameters{'location.u'} = $cfgent->{u};
        }
        if (defined $cfgent->{rack}) {
            $parameters{'location.rack'} = $cfgent->{rack};
        }
        if (defined $cfgent->{room}) {
            $parameters{'location.room'} = $cfgent->{room};
        }
        if (defined $cfgent->{chassis}) {
            $parameters{'enclosure.manager'} = $cfgent->{chassis};
        } elsif (defined $cfgent->{mpa}) {
            $parameters{'enclosure.manager'} = $cfgent->{mpa};
        }
        if (defined $cfgent->{slot}) {
            $parameters{'enclosure.bay'} = $cfgent->{slot};
        } elsif (defined $cfgent->{id}) {
            $parameters{'enclosure.bay'} = $cfgent->{id};
        }
        $parameters{'groups'} = [ grep { defined && length } split /,/, $groupdata->{$node}->[0]->{'groups'} ];
        if (exists $currnodes{$node}) {
            $confluent->update('/nodes/' . $node . '/attributes/current', parameters => \%parameters);
            my $rsp = $confluent->next_result();
            while ($rsp) {
                if (exists $rsp->{error}) {
                    xCAT::SvrUtils::sendmsg([ 1, ":Confluent error: " . $rsp->{error} ], $cb, $node);
                }
                $rsp = $confluent->next_result();
            }
        } else {
            $parameters{name} = $node;
            $confluent->create('/nodes/', parameters => \%parameters);
            my $rsp = $confluent->next_result();
            while ($rsp) {
                if (exists $rsp->{error}) {
                    xCAT::SvrUtils::sendmsg([ 1, ":Confluent error: " . $rsp->{error} ], $cb, $node);
                }
                $rsp = $confluent->next_result();
            }
        }
    }
    return 0;
}

1;