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

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


use IO::Socket;
use Data::Dumper;
use xCAT::MacMap;
use xCAT::NodeRange;
use Sys::Syslog;
use xCAT::Usage;
use Storable;
use xCAT::MellanoxIB;
require xCAT::TableUtils;
require xCAT::ServiceNodeUtils;

my $macmap;

sub handled_commands {
    return {
        findme      => 'switch',
        findmac     => 'switch',
        switchprobe => 'switch',
        rspconfig   => 'nodehm:mgt',
    };
}

sub preprocess_request {
    my $request = shift;
    if (defined $request->{_xcatpreprocessed}->[0] and $request->{_xcatpreprocessed}->[0] == 1) { return [$request]; }

    my $callback = shift;
    my @requests;

    my $noderange = $request->{node};
    my $command   = $request->{command}->[0];

    if ($command eq "rspconfig") {
        my $extrargs = $request->{arg};
        my @exargs   = ($request->{arg});
        if (ref($extrargs)) {
            @exargs = @$extrargs;
        }
        my $usage_string = xCAT::Usage->parseCommand($command, @exargs);
        if ($usage_string) {
            $callback->({ data => $usage_string });
            $request = {};
            return;
        }
        if (!$noderange) {
            $usage_string = xCAT::Usage->getUsage($command);
            $callback->({ data => $usage_string });
            $request = {};
            return;
        }

        #make sure all the nodes are switches
        my $switchestab = xCAT::Table->new('switches', -create => 0);
        my @all_switches;
        my @tmp = $switchestab->getAllAttribs(('switch'));
        if (@tmp && (@tmp > 0)) {
            foreach (@tmp) {
                my @switches_tmp = noderange($_->{switch});
                if (@switches_tmp == 0) { push @switches_tmp, $_->{switch}; }
                foreach my $switch (@switches_tmp) {
                    push @all_switches, $switch;
                }
            }
        }

        #print "all switches=@all_switches\n";
        my @wrong_nodes;
        foreach my $node (@$noderange) {
            if (!grep /^$node$/, @all_switches) {
                push @wrong_nodes, $node;
            }
        }
        if (@wrong_nodes > 0) {
            my $rsp = {};
            $rsp->{error}->[0] = "The following nodes are not defined in the switches table:\n  @wrong_nodes.";
            $callback->($rsp);
            return;
        }

        # find service nodes for requested switch
        # build an individual request for each service node
        my $service = "xcat";
        my $sn = xCAT::ServiceNodeUtils->get_ServiceNode($noderange, $service, "MN");

        # build each request for each service node
        foreach my $snkey (keys %$sn)
        {
            #print "snkey=$snkey\n";
            my $reqcopy = {%$request};
            $reqcopy->{node}                   = $sn->{$snkey};
            $reqcopy->{'_xcatdest'}            = $snkey;
            $reqcopy->{_xcatpreprocessed}->[0] = 1;
            push @requests, $reqcopy;
        }
        return \@requests;
    }
    elsif ($command eq 'switchprobe') {
        @ARGV = ();
        if (ref($request->{arg})) {
            @ARGV = @{ $request->{arg} };
        }
        use Getopt::Long;
        $Getopt::Long::ignorecase = 0;
        Getopt::Long::Configure("bundling");
        my $verbose = undef;
        my $check   = undef;
        my $help    = undef;
        unless (GetOptions('h|help' => \$help, 'V|verbose' => \$verbose, 'c|check' => \$check)) {
            $callback->({ error => ["Parse args failed"], errorcode => 1 });
            return;
        }
        if (@ARGV) {
            $callback->({ error => [ "Option @ARGV not supported.\n" . xCAT::Usage->getUsage($command) ], errorcode => 1 });
            return;
        }
        if (defined($help)) {
            $callback->({ data => xCAT::Usage->getUsage($command) });
            return;
        }
        if (defined($verbose)) {
            $request->{opt}->{verbose} = $verbose;
        }
        if (defined($check)) {
            $request->{opt}->{check} = $check;
        }
        my $switchestab = xCAT::Table->new('switches', -create => 0);
        my $swhash = undef;
        if ($switchestab) {
            $swhash = $switchestab->getAllNodeAttribs(['switch'], 1);
            if (!defined($swhash)) {
                $callback->({ error => ["Get attributes from table 'switches' failed"], errorcode => 1 });
                return;
            }
        }
        else {
            $callback->({ error => ["Open table 'switches' failed"], errorcode => 1 });
            return;
        }
        if (defined($noderange)) {
            my $nthash = undef;
            my $nodetypetab = xCAT::Table->new('nodetype', -create => 0);
            if ($nodetypetab) {
                $nthash = $nodetypetab->getNodesAttribs($noderange, ['nodetype']);
                if (!defined($nthash)) {
                    $callback->({ error => ["Get attributes from table 'nodetype' failed"], errorcode => 1 });
                    return;
                }
            }
            else {
                $callback->({ error => ["Open table 'nodetype' failed"], errorcode => 1 });
                return;
            }
            my @switchnode = ();
            my @errswnode  = ();
            my @errornode  = ();
            foreach my $node (@$noderange) {
                if (!defined($nthash->{$node}) or $nthash->{$node}->[0]->{nodetype} ne 'switch') {
                    push @errornode, $node;
                }
                elsif (!defined($swhash->{$node}) or !defined($swhash->{$node}->[0])) {
                    push @errswnode, $node;
                }
                else {
                    push @switchnode, $node;
                }
            }
            if (@errornode) {
                $callback->({ error => [ "The nodetype is not 'switch' for nodes: " . join(",", @errornode) ], errorcode => 1 });
            }
            if (@errswnode) {
                $callback->({ error => [ "No switch configuration info find for " . join(",", @errswnode) ], errorcode => 1 });
            }
            if (@switchnode) {
                @{ $request->{node} } = @switchnode;
                return [$request];
            }
            return;
        }
        else {
            if (!scalar(keys %$swhash)) {
                $callback->({ error => ["No switch configuration info get from 'switches' table"], errorcode => 1 });
                return;
            }
        }
    }
    return [$request];
}

sub process_request {
    my $req   = shift;
    my $cb    = shift;
    my $doreq = shift;
    unless ($macmap) {
        $macmap = xCAT::MacMap->new();
    }
    my $node;
    my $mac = '';
    if ($req->{command}->[0] eq 'findmac') {
        $mac = $req->{arg}->[0];
        $node = $macmap->find_mac($mac, 0);
        $cb->({ node => [ { name => $node, data => $mac } ] });
        return;
    } elsif ($req->{command}->[0] eq 'rspconfig') {
        return process_switch_config($req, $cb, $doreq);
    } elsif ($req->{command}->[0] eq 'switchprobe') {
        my $macinfo = $macmap->dump_mac_info($req, $cb);
        if ($macinfo and ref($macinfo) eq 'HASH') {
            my $switch_name_length = 0;
            my $port_name_length   = 0;
            foreach my $switch (keys %$macinfo) {
                if (length($switch) > $switch_name_length) {
                    $switch_name_length = length($switch);
                }
                if (defined($macinfo->{$switch}->{ErrorStr})) {
                    next;
                }
                foreach my $portname (keys %{ $macinfo->{$switch} }) {
                    if (length($portname) > $port_name_length) {
                        $port_name_length = length($portname) + 10;
                    }
                }
            }
            my $format = "%-" . $switch_name_length . "s  %-" . $port_name_length . "s  %-26s  %s";
            my %failed_switches = ();
            my $header = sprintf($format, "Switch", "Port(MTU)", "MAC address(VLAN)", "Node");
            if (!defined($req->{opt}->{check}) and $port_name_length) {
                $cb->({ data => $header });
                $cb->({ data => "--------------------------------------------------------------------------------------" })
            }
            foreach my $switch (keys %$macinfo) {
                if (defined($macinfo->{$switch}->{ErrorStr})) {
                    if (defined($req->{opt}->{check})) {
                        $cb->({ node => [ { name => $switch, error => [ $macinfo->{$switch}->{ErrorStr} ], errorcode => 1 } ] });
                    }
                    else {
                        $failed_switches{$switch} = "$macinfo->{$switch}->{ErrorStr}";
                    }
                    next;
                }
                elsif (defined($req->{opt}->{check})) {
                    $cb->({ node => [ { name => $switch, data => ["PASS"] } ] });
                    next;
                }
                foreach my $port (map{$_->[0]}sort{$a->[1] cmp $b->[1] || $a->[2] <=> $b->[2]}map{[$_, /^(.*?)(\d+)?$/]} keys %{ $macinfo->{$switch} }) {
                    my $node = '';
                    if (defined($macinfo->{$switch}->{$port}->{Node})) {
                        $node = $macinfo->{$switch}->{$port}->{Node};
                    }

                    my $mtu = '';
                    my $vlanid = '';
                    my @vlans = ();
                    if (defined($macinfo->{$switch}->{$port}->{Vlanid})) {
                        @vlans = @{ $macinfo->{$switch}->{$port}->{Vlanid} };
                    }

                    my @macarrary = ();
                    if (defined($macinfo->{$switch}->{$port}->{MACaddress})) {
                        @macarray = @{ $macinfo->{$switch}->{$port}->{MACaddress} };
                        my $ind = 0;
                        foreach (@macarray) {
                            my $mac = $_;
                            $vlanid = $vlans[$ind];
                            my $mac_vlan;
                            if (!$mac) {
                                $mac_vlan="N/A";
                            } elsif ($vlanid) {
                                $mac_vlan = "$mac($vlanid)"; 
                            } else {
                                $mac_vlan = $mac;
                            }
                            my $port_mtu = $port;
                            if (defined($macinfo->{$switch}->{$port}->{Mtu})) {
                                $mtu = $macinfo->{$switch}->{$port}->{Mtu}->[0];
                                $port_mtu = "$port($mtu)";
                            }
                            my $data = sprintf($format, $switch, $port_mtu, $mac_vlan, $node);
                            $cb->({ data => $data });
                            $ind++;

                            #$cb->({node=>[{name=>$switch,data=>$data}]});
                        }
                    }
                }
            }
            if (!defined($req->{opt}->{check}) and $port_name_length) {
                $cb->({ data => "--------------------------------------------------------------------------------------" })
            }
            foreach (keys %failed_switches) {
                $cb->({ node => [ { name => $_, error => [ $failed_switches{$_} ], errorcode => 1 } ] });
            }
        }
        return;
    } elsif ($req->{command}->[0] eq 'findme') {
        if (defined($req->{discoverymethod}) and defined($req->{discoverymethod}->[0]) and ($req->{discoverymethod}->[0] ne 'undef')) {

            # The findme request had been processed by other module, just return
            return;
        }
        $mac = $req->{_xcat_clientmac}->[0];
        if (defined $req->{nodetype} and $req->{nodetype}->[0] eq 'virtual') {

            #Don't attempt switch discovery of a  VM Guest
            #TODO: in this case, we could/should find the host system
            #and then ask it what node is associated with the mac
            #Either way, it would be kinda weird since xCAT probably made up the mac addy
            #anyway, however, complex network topology function may be aided by
            #discovery working.  Food for thought.
            return;
        }
        my $discoverswitch = 0;
        if (defined $req->{nodetype} and $req->{nodetype}->[0] eq 'switch') {
            $discoverswitch = 1;
        }
        my $firstpass = 1;
        if ($mac) {
            $node = $macmap->find_mac($mac, $req->{cacheonly}->[0], $discoverswitch);
            $firstpass = 0;
        }
        if (not $node) {    # and $req->{checkallmacs}->[0]) {
            foreach (@{ $req->{mac} }) {
                /.*\|.*\|([\dABCDEFabcdef:]+)(\||$)/;
                $node = $macmap->find_mac($1, $firstpass, $discoverswitch);
                $firstpass = 0;
                if ($node) { last; }
            }
        }
        my $bmc_node = undef;
        my @bmc_nodes = ();
        if ($req->{'mtm'}->[0] and $req->{'serial'}->[0]) {
            my $mtms      = $req->{'mtm'}->[0] . "*" . $req->{'serial'}->[0];
            my $tmp_nodes = $::XCATVPDHASH{$mtms};
            foreach (@$tmp_nodes) {
                if ($::XCATMPHASH{$_}) {
                    push @bmc_nodes, $_;
                }
            }
        }

        if ($req->{'bmcmac'}->[0]) {
            my $bmcmac = lc($req->{'bmcmac'}->[0]);
            $bmcmac =~ s/\://g;
            my $tmp_node = "node-$bmcmac";
            push @bmc_nodes, $tmp_node if ($::XCATMPHASH{$tmp_node});
        }

        $bmc_node = join(",", @bmc_nodes);
        if ($node) {
            xCAT::MsgUtils->message("S", "xcat.discovery.switch: ($req->{_xcat_clientmac}->[0]) Found node: $node");

            # No need to write mac table here, 'discovered' command will write
            # my $mactab = xCAT::Table->new('mac',-create=>1);
            # $mactab->setNodeAttribs($node,{mac=>$mac});
            # $mactab->close();
            #my %request = (
            #  command => ['makedhcp'],
            #  node => [$node]
            #);
            #$doreq->(\%request);
            $req->{discoverymethod}->[0] = 'switch';
            my $request = {%$req};
            $request->{command}   = ['discovered'];
            $request->{noderange} = [$node];
            $request->{bmc_node}  = [$bmc_node];
            $doreq->($request);
            %{$request} = ();    #Clear req structure, it's done..
            undef $mactab;
        } else {
            xCAT::MsgUtils->message("S", "xcat.discovery.switch: ($req->{_xcat_clientmac}->[0]) Warning: Could not find any nodes using switch-based discovery");
        }
    }
}

sub process_switch_config {
    my $request   = shift;
    my $callback  = shift;
    my $subreq    = shift;
    my $noderange = $request->{node};
    my $command   = $request->{command}->[0];
    my $extrargs  = $request->{arg};
    my @exargs    = ($request->{arg});
    if (ref($extrargs)) {
        @exargs = @$extrargs;
    }

    my $subcommand = join(' ', @exargs);
    my $argument;
    ($subcommand, $argument) = split(/=/, $subcommand);
    if (!$subcommand) {
        my $rsp = {};
        $rsp->{error}->[0] = "No subcommand specified.";
        $callback->($rsp);
        return;
    }


    #decide what kind of swith it is
    my $sw_types = getSwitchType($noderange);    #hash {type=>[node1,node1...]}
    foreach my $t (keys(%$sw_types)) {
        my $nodes = $sw_types->{$t};
        if (@$nodes > 0) {
            if ($t =~ /Mellanox/i) {
                if (!$argument) {
                    xCAT::MellanoxIB::getConfig($nodes, $callback, $subreq, $subcommand);
                } else {
                    xCAT::MellanoxIB::setConfig($nodes, $callback, $subreq, $subcommand, $argument);
                }
            } else {
                my $rsp = {};
                $rsp->{error}->[0] = "The following '$t' switches are unsuppored:\n@$nodes";
                $callback->($rsp);
            }
        }
    }
}

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

=head3    getSwitchType
      It determins the swtich vendor and model for the given swith.      
    Arguments:
        noderange-- an array ref to switches.
    Returns:
        a hash ref. the key is the switch type string and the value is an array ref to the swithces t
=cut

#--------------------------------------------------------------------------------
sub getSwitchType {
    my $noderange = shift;
    if ($noderange =~ /xCAT_plugin::switch/) {
        $noderange = shift;
    }

    my $ret = {};
    my $switchestab = xCAT::Table->new('switches', -create => 1);
    my $switches_hash = $switchestab->getNodesAttribs($noderange, ['switchtype']);
    foreach my $node (@$noderange) {
        my $type = "EtherNet";
        if ($switches_hash) {
            if ($switches_hash->{$node} - [0]) {
                $type = $switches_hash->{$node}->[0]->{switchtype};
            }
        }
        if (exists($ret->{$type})) {
            $pa = $ret->{$type};
            push @$pa, $node;
        } else {
            $ret->{$type} = [$node];
        }
    }

    return $ret;
}

1;