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

BEGIN
{
    $::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : '/opt/xcat';
}
use lib "$::XCATROOT/lib/perl";


use IO::Socket;
use Data::Dumper;
use xCAT::NodeRange;
use xCAT::Utils;
use Sys::Syslog;
use Expect;
use Storable;
use strict;

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

=head1  xCAT::MellanoxIB
=head2    Package Description
  It handles Mellanox IB switch related function. It used the CLI interface of
  Mellanox IB switch

=cut

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

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

=head3    getConfigure
      It queries the info from the given swithes.
    Arguments:
        noderange-- an array ref to switches.
        callback -- pointer for writes response back to the client.
        suncommand --- attribute to query about.
    Returns:
        0 --- sucessful
        none 0 --- unsuccessful.
=cut

#--------------------------------------------------------------------------------
sub getConfig {
    my $noderange = shift;
    if ($noderange =~ /xCAT::MellanoxIB/) {
        $noderange = shift;
    }
    my $callback   = shift;
    my $subreq     = shift;
    my $subcommand = shift;

    #handle sshcfg with expect script
    if ($subcommand eq "sshcfg") {
        return querySSHcfg($noderange, $callback, $subreq);
    }

    #get the username and the password
    my $swstab = xCAT::Table->new('switches', -create => 1);
    my $sws_hash = $swstab->getNodesAttribs($noderange, ['sshusername']);

    my $passtab = xCAT::Table->new('passwd');
    my $ent;
    ($ent) = $passtab->getAttribs({ key => "switch" }, qw(username));

    foreach my $node (@$noderange) {
        my $cmd;
        my $username;
        if ($sws_hash->{$node}->[0]) {
            $username = $sws_hash->{$node}->[0]->{sshusername};
        }
        if (!$username) {
            if ($ent) {
                $username = $ent->{username};
            }
        }
        if (!$username) {
            $username = "xcat";
        }

        if (($subcommand eq "alert") || ($subcommand eq "snmpcfg") || ($subcommand eq "community") || ($subcommand eq "snmpdest")) {
            $cmd = 'show snmp';
        } elsif ($subcommand eq "logdest") {
            $cmd = 'show logging';
        }
        else {
            my $rsp = {};
            $rsp->{error}->[0] = "Unsupported subcommand: $subcommand";
            $callback->($rsp);
            return;
        }

        #now goto the switch and get the output
        my $output = xCAT::Utils->runxcmd({ command => ["xdsh"], node => [$node], arg => [ "--devicetype", "IBSwitch::Mellanox", "$cmd" ], env => ["DSH_TO_USERID=$username"] }, $subreq, -1, 1);
        if ($output) {
            my $result = parseOutput($node, $subcommand, $output);
            my $rsp    = {};
            my $i      = -1;
            foreach my $o (@$result) {
                $i++;
                $rsp->{data}->[$i] = $o;
            }
            $callback->($rsp);
        }

    }    #end foreach
}

sub parseOutput {
    my $node       = shift;
    my $subcommand = shift;
    my $input      = shift;    #an array pointer

    my $output;
    if ($subcommand eq "alert") {
        my $done = 0;
        foreach my $tmpstr1 (@$input) {
            my @b = split("\n", $tmpstr1);
            foreach my $tmpstr (@b) {
                if ($tmpstr =~ /Traps enabled:/) {
                    if ($tmpstr =~ /yes/) {
                        $output = ["$node: Switch Alerting enabled"];
                    } else {
                        $output = ["$node: Switch Alerting disabled"];
                    }
                    $done = 1;
                    last;
                }
            }
            if ($done) { last; }
        }
        if ($output) { return $output; }
    } elsif ($subcommand eq "snmpcfg") {
        my $done = 0;
        foreach my $tmpstr1 (@$input) {
            my @b = split("\n", $tmpstr1);
            foreach my $tmpstr (@b) {
                if ($tmpstr =~ /SNMP enabled:/) {
                    if ($tmpstr =~ /yes/) {
                        $output = ["$node: SNMP enabled"];
                    } else {
                        $output = ["$node: SNMP disabled"];
                    }
                    $done = 1;
                    last;
                }
            }
            if ($done) { last; }
        }
        if ($output) { return $output; }
    } elsif ($subcommand eq "snmpdest") {
        my $found = 0;
        my $j     = 0;
        my $done  = 0;
        foreach my $tmpstr1 (@$input) {
            my @b = split("\n", $tmpstr1);
            foreach my $tmpstr (@b) {
                if ((!$found) && ($tmpstr =~ /Trap sinks:/)) {
                    $found = 1;
                    $output->[0] = "$node: SNMP Destination:";
                    next;
                }

                if ($tmpstr =~ /Events for which/) {
                    if (!$found) {
                        next;
                    } else {
                        $done = 1;
                        last;
                    }
                }
                if ($found) {
                    $tmpstr =~ s/$node: //g;
                    $output->[ ++$j ] = $tmpstr;
                }
            }
            if ($done) { last; }
        }
        if ($output) { return $output; }
    } elsif ($subcommand eq "community") {
        my $done = 0;
        foreach my $tmpstr1 (@$input) {
            my @b = split("\n", $tmpstr1);
            foreach my $tmpstr (@b) {
                if ($tmpstr =~ /Read-only community:/) {
                    my @a = split(':', $tmpstr);
                    my $c_str;
                    if (@a > 2) {
                        $c_str = $a[2];
                    }
                    $output = ["$node: SNMP Community: $c_str"];
                    $done   = 1;
                    last;
                }
            }
            if ($done) { last; }
        }
        if ($output) { return $output; }
    } elsif ($subcommand eq "logdest") {
        foreach my $tmpstr1 (@$input) {
            my @b = split("\n", $tmpstr1);
            foreach my $tmpstr (@b) {
                if ($tmpstr =~ /Remote syslog receiver:/) {
                    my @a = split(':', $tmpstr);
                    my $c_str;
                    if (@a > 2) {
                        for my $i (2 .. $#a) {
                            $c_str .= $a[$i] . ':';
                        }
                        chop($c_str);
                    }
                    if ($output) {
                        push(@$output, "  $c_str");
                    } else {
                        $output = ["$node: Logging destination:\n  $c_str"];
                    }
                }
            }
        }
        if ($output) { return $output; }
    }

    return $input    #an array pointer
}


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

=head3    setConfigure
      It configures the the given swithes.
    Arguments:
        noderange-- an array ref to switches.
        callback -- pointer for writes response back to the client.
        suncommand --- attribute to set.
    Returns:
        0 --- sucessful
        none 0 --- unsuccessful.
=cut

#--------------------------------------------------------------------------------
sub setConfig {
    my $noderange = shift;
    if ($noderange =~ /xCAT::MellanoxIB/) {
        $noderange = shift;
    }
    my $callback   = shift;
    my $subreq     = shift;
    my $subcommand = shift;
    my $argument   = shift;

    #handle sshcfg with expect script
    if ($subcommand eq "sshcfg") {
        if ($argument eq "on" or $argument =~ /^en/ or $argument =~ /^enable/) {
            return setSSHcfg($noderange, $callback, $subreq, 1);
        }
        elsif ($argument eq "off" or $argument =~ /^dis/ or $argument =~ /^disable/) {
            return setSSHcfg($noderange, $callback, $subreq, 0);
        } else {
            my $rsp = {};
            $rsp->{error}->[0] = "Unsupported argument for sshcfg: $argument";
            $callback->($rsp);
            return;
        }
    }

    #get the username and the password
    my $swstab = xCAT::Table->new('switches', -create => 1);
    my $sws_hash = $swstab->getNodesAttribs($noderange, ['sshusername']);

    my $passtab = xCAT::Table->new('passwd');
    my $ent;
    ($ent) = $passtab->getAttribs({ key => "switch" }, qw(username));

    foreach my $node (@$noderange) {
        my @cfgcmds;
        my $username;
        if ($sws_hash->{$node}->[0]) {
            $username = $sws_hash->{$node}->[0]->{sshusername};
        }
        if (!$username) {
            if ($ent) {
                $username = $ent->{username};
            }
        }
        if (!$username) {
            $username = "xcat";    #default ssh username
        }

        if ($subcommand eq "alert") {
            if ($argument eq "on" or $argument =~ /^en/ or $argument =~ /^enable/) {
                $cfgcmds[0] = "snmp-server enable traps";
            }
            elsif ($argument eq "off" or $argument =~ /^dis/ or $argument =~ /^disable/) {
                $cfgcmds[0] = "no snmp-server enable traps";
            } else {
                my $rsp = {};
                $rsp->{error}->[0] = "Unsupported argument for $subcommand: $argument";
                $callback->($rsp);
                return;
            }
        }
        elsif ($subcommand eq "snmpcfg") {
            if ($argument eq "on" or $argument =~ /^en/ or $argument =~ /^enable/) {
                $cfgcmds[0] = "snmp-server enable";
            }
            elsif ($argument eq "off" or $argument =~ /^dis/ or $argument =~ /^disable/) {
                $cfgcmds[0] = "no snmp-server enable";
            } else {
                my $rsp = {};
                $rsp->{error}->[0] = "Unsupported argument for $subcommand: $argument";
                $callback->($rsp);
                return;
            }
        }
        elsif ($subcommand eq "community") {
            $cfgcmds[0] = "snmp-server community $argument";
        }
        elsif ($subcommand eq "snmpdest") {
            my @a = split(' ', $argument);
            if (@a > 1) {
                if ($a[1] eq 'remove') {
                    $cfgcmds[0] = "no snmp-server host $a[0]";
                } else {
                    my $rsp = {};
                    $rsp->{error}->[0] = "Unsupported action for $subcommand: $a[1]\nThe valide action is: remove.";
                    $callback->($rsp);
                    return;
                }
            } else {
                $cfgcmds[0] = "snmp-server host $a[0] traps version 2c public";
            }
        }
        elsif ($subcommand eq "logdest") {

            #one can run rspconfig <switch> logdest=<ip> level
            # where level can be:
            #    remove           Remove this ip from receiving logging
            #    none             Disable logging
            #    emerg            Emergency: system is unusable
            #    alert            Action must be taken immediately
            #    crit             Critical conditions
            #    err              Error conditions
            #    warning          Warning conditions
            #    notice           Normal but significant condition
            #    info             Informational messages
            #    debug            Debug-level messages

            my @a = split(' ', $argument);
            if ((@a > 1) && ($a[1] eq 'remove')) {
                $cfgcmds[0] = "no logging $a[0]";
            } else {
                if (@a > 1) {
                    if ($a[1] eq "none" ||
                        $a[1] eq "emerg"   ||
                        $a[1] eq "alert"   ||
                        $a[1] eq "crit"    ||
                        $a[1] eq "err"     ||
                        $a[1] eq "warning" ||
                        $a[1] eq "notice"  ||
                        $a[1] eq "info"    ||
                        $a[1] eq "debug") {
                        $cfgcmds[0] = "logging $a[0] trap $a[1]";
                    } else {
                        my $rsp = {};
                        $rsp->{error}->[0] = "Unsupported loging level for $subcommand: $a[1].\nThe valid levels are: emerg, alert, crit, err, warning, notice, info, debug, none, remove";
                        $callback->($rsp);
                        return;
                    }
                } else {
                    $cfgcmds[0] = "logging $a[0]";
                }
            }
        }
        else {
            my $rsp = {};
            $rsp->{error}->[0] = "Unsupported subcommand: $subcommand";
            $callback->($rsp);
            return;
        }

        #now do the real bussiness
        my $cmd = "enable;configure terminal";
        foreach (@cfgcmds) {
            $cmd .= ";$_";
        }
        my $output = xCAT::Utils->runxcmd({ command => ["xdsh"], node => [$node], arg => [ "--devicetype", "IBSwitch::Mellanox", "$cmd" ], env => ["DSH_TO_USERID=$username"] }, $subreq, -1, 1);

        #only print out the error
        if ($::RUNCMD_RC != 0) {
            if ($output) {
                my $rsp = {};
                my $i   = -1;
                foreach my $o (@$output) {
                    $i++;
                    $rsp->{data}->[$i] = $o;
                }
                $callback->($rsp);
            }
        }

        #now qerry
        return getConfig($noderange, $callback, $subreq, $subcommand);
    }
}



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

=head3    querySSHcfg
      It checks if the current host can ssh to the given switches without password.
    Arguments:
        noderange-- an array ref to switches.
        callback -- pointer for writes response back to the client.
    Returns:
        0 --- sucessful
        none 0 --- unsuccessful.
=cut

#--------------------------------------------------------------------------------
sub querySSHcfg {

    my $noderange = shift;
    if ($noderange =~ /xCAT::MellanoxIB/) {
        $noderange = shift;
    }
    my $callback = shift;
    my $subreq   = shift;

    #get the username and the password
    my $swstab = xCAT::Table->new('switches', -create => 1);
    my $sws_hash = $swstab->getNodesAttribs($noderange, ['sshusername']);

    my $passtab = xCAT::Table->new('passwd');
    my $ent;
    ($ent) = $passtab->getAttribs({ key => "switch" }, qw(username));

    #get the ssh public key from this host
    my $fname = ((xCAT::Utils::isAIX()) ? "/.ssh/" : "/root/.ssh/") . "id_rsa.pub";
    unless (open(FH, "<$fname")) {
        $callback->({ error => ["Error opening file $fname."], errorcode => [1] });
        return 1;
    }
    my ($sshkey) = <FH>;
    close(FH);
    chomp($sshkey);

    my $cmd = "enable;show ssh client";
    foreach my $node (@$noderange) {
        my $username;
        if ($sws_hash->{$node}->[0]) {
            $username = $sws_hash->{$node}->[0]->{sshusername};
        }
        if (!$username) {
            if ($ent) {
                $username = $ent->{username};
            }
        }
        if (!$username) {
            $username = "xcat";
        }


        #now goto the switch and get the output
        my $output = xCAT::Utils->runxcmd({ command => ["xdsh"], node => [$node], arg => [ "--devicetype", "IBSwitch::Mellanox", "$cmd" ], env => ["DSH_TO_USERID=$username"] }, $subreq, -1, 1);
        if ($output) {
            my $keys = getMatchingKeys($node, $username, $output, $sshkey);
            my $rsp = {};
            if (@$keys > 0) {
                $rsp->{data}->[0] = "$node: SSH enabled";
            } else {
                $rsp->{data}->[0] = "$node: SSH disabled";
            }
            $callback->($rsp);
        }
    }    #end foreach node
}


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

=head3   getMatchingKeys
      It checks if the given outout contians the given ssh key for the given user.

    Returns:
        An array pointer to the matching keys.
=cut

#--------------------------------------------------------------------------------
sub getMatchingKeys {
    my $node     = shift;
    my $username = shift;
    my $output   = shift;
    my $sshkey   = shift;

    my @keys       = ();
    my $user_found = 0;
    my $start      = 0;
    my $end        = 0;
    foreach my $tmpstr1 (@$output) {
        my @b = split("\n", $tmpstr1);
        foreach my $o (@b) {

            #print "o=$o\n";
            $o =~ s/$node: //g;
            if ($o =~ /SSH authorized keys:/) {
                $start = 1;
                next;
            }
            if ($start) {
                if ($o =~ /User $username:/) {
                    $user_found = 1;
                    next;
                }

                if ($user_found) {
                    if ($o =~ /Key (\d+): (.*)$/) {
                        my $key       = $1;
                        my $key_value = $2;

                        #print "key=$key\n";
                        #print "key_value=$key_value\n";
                        chomp($key_value);
                        if ("$sshkey" eq "$key_value") {
                            push(@keys, $key);
                        }
                        next;
                    } elsif ($o =~ /^(\s*)$/) {
                        next;
                    }
                    else {
                        $end = 1;
                    }
                }
            }
        }
        if ($end) { last; }
    }

    return \@keys;
}


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

=head3    setSSHcfg
      It enables/diables the current host to ssh to the given switches without password.
    Arguments:
        noderange-- an array ref to switches.
        callback -- pointer for writes response back to the client.
    Returns:
        0 --- sucessful
        none 0 --- unsuccessful.
=cut

#--------------------------------------------------------------------------------
sub setSSHcfg {
    my $noderange = shift;
    if ($noderange =~ /xCAT::MellanoxIB/) {
        $noderange = shift;
    }
    my $callback = shift;
    my $subreq   = shift;
    my $enable   = shift;

    my $mysw;
    my $enable_cmd = "enable\r";
    my $config_cmd = "configure terminal\r";
    my $exit_cmd   = "exit\r";
    my $init_cmd   = "no\r";

    my $pwd_prompt    = "Password: ";
    my $sw_prompt     = "^.*\] > ";
    my $enable_prompt = "^.*\] \#";
    my $config_prompt = "^.*\\\(config\\\) \#";
    my $init_prompt   = "Do you want to use the wizard for initial configuration?";


    my $debug = 0;
    if ($::VERBOSE)
    {
        $debug = 1;
    }

    #get the username and the password
    my $swstab = xCAT::Table->new('switches', -create => 1);
    my $sws_hash = $swstab->getNodesAttribs($noderange, [ 'sshusername', 'sshpassword' ]);

    my $passtab = xCAT::Table->new('passwd');
    my $ent;
    ($ent) = $passtab->getAttribs({ key => "switch" }, qw(username password));

    #get the ssh public key from this host
    my $fname = ((xCAT::Utils::isAIX()) ? "/.ssh/" : "/root/.ssh/") . "id_rsa.pub";
    unless (open(FH, "<$fname")) {
        $callback->({ error => ["Error opening file $fname."], errorcode => [1] });
        return 1;
    }
    my ($sshkey) = <FH>;
    close(FH);

    #remove the userid@host part
    #my @tmpa=split(' ', $sshkey);
    #if (@tmpa > 2) {
    #	$sshkey=$tmpa[0] . ' ' . $tmpa[1];
    #}

    foreach my $node (@$noderange) {
        my $username;
        my $passwd;
        if ($sws_hash->{$node}->[0]) {

            #print "got to switches table\n";
            $username = $sws_hash->{$node}->[0]->{sshusername};
            $passwd   = $sws_hash->{$node}->[0]->{sshpassword};
        }
        if (!$username) {

            #print "got to passwd table\n";
            if ($ent) {
                $username = $ent->{username};
                $passwd   = $ent->{password};
            }
        }

        unless ($username) {
            $callback->({ error => ["Unable to get the username and the password for node $node. Please fill the switches table or the password table."], errorcode => [1] });
            next;
        }



        #print "username=$username, password=$passwd\n";

        if ($enable > 0) {
            $mysw = new Expect;
            $mysw->exp_internal($debug);
            #
            # log_stdout(0) prevent the program's output from being shown.
            #  turn on if debugging error
            $mysw->log_stdout($debug);

            my @cfgcmds = ();
            $cfgcmds[0] = "ssh client user $username authorized-key sshv2 \"$sshkey\"\r";
            my $login_cmd  = "ssh -l $username $node\r";
            my $passwd_cmd = "$passwd\r";
            unless ($mysw->spawn($login_cmd))
            {
                $mysw->soft_close();
                my $rsp;
                $rsp->{data}->[0] = "Unable to run $login_cmd.";
                xCAT::MsgUtils->message("I", $rsp, $callback);
                next;
            }

            my @result = $mysw->expect(
                10,
                [
                    $pwd_prompt,
                    sub {
                        $mysw->clear_accum();
                        $mysw->send($passwd_cmd);

                        #print "$node: password sent\n";
                        $mysw->exp_continue();
                    }
                ],
                [
                    "-re", $init_prompt,
                    sub {
                        $mysw->clear_accum();
                        $mysw->send($init_cmd);
                        $mysw->exp_continue();
                    }
                ],
                [
                    "-re", $sw_prompt,
                    sub {
                        #print "$node: sending command: $enable_cmd\n";
                        $mysw->clear_accum();
                        $mysw->send($enable_cmd);
                        $mysw->exp_continue();
                    }
                ],
                [
                    "-re", $enable_prompt,
                    sub {
                        #print "$node: sending command: $config_cmd\n";
                        $mysw->clear_accum();
                        $mysw->send($config_cmd);
                        $mysw->exp_continue();
                    }
                ],
                [
                    "-re", $config_prompt,
                    sub {
                        #print "$node: sending command: $cfgcmds[0]\n";
                        $mysw->clear_accum();
                        $mysw->send($cfgcmds[0]);
                        sleep 1;
                        $mysw->send($exit_cmd);
                    }
                ],
            );

            if (defined($result[1]))
            {
                my $errmsg = $result[1];
                $mysw->soft_close();
                my $rsp;
                $rsp->{data}->[0] = "$node: command error: $result[1]";
                $callback->($rsp);
                next;

            }
            $mysw->soft_close();
        } else {

            #now goto the switch and get the matching keys
            my $output = xCAT::Utils->runxcmd({ command => ["xdsh"], node => [$node], arg => [ "--devicetype", "IBSwitch::Mellanox", "enable;show ssh client" ], env => ["DSH_TO_USERID=$username"] }, $subreq, -1, 1);
            if ($output) {
                chomp($sshkey);
                my $keys = getMatchingKeys($node, $username, $output, $sshkey);
                if (@$keys > 0) {
                    my $cmd = "enable;configure terminal";
                    foreach my $key (@$keys) {
                        $cmd .= ";no ssh client user admin authorized-key sshv2 $key";
                    }

                    #now remove the keys
                    $output = xCAT::Utils->runxcmd({ command => ["xdsh"], node => [$node], arg => [ "--devicetype", "IBSwitch::Mellanox", $cmd ], env => ["DSH_TO_USERID=$username"] }, $subreq, -1, 1);
                }
            }
        }

        #now query again
        querySSHcfg([$node], $callback, $subreq);
    }
}


1;
