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

# Plugin to interface with IBM SVC managed storage
#
use strict;

package xCAT_plugin::svc;

use xCAT::SvrUtils qw/sendmsg/;
use xCAT::SSHInteract;
use Getopt::Long;
Getopt::Long::Configure("bundling");
Getopt::Long::Configure("pass_through");

my $callback;
my $dorequest;
my %controllersessions;

sub handled_commands {
    return {
        mkstorage     => "storage:type",
        lsstorage     => "storage:type",
        detachstorage => "storage:type",
        rmstorage     => "storage:type",
        lspool        => "storage:type",
      }
}

sub detachstorage {
    my $request = shift;
    my @nodes   = @{ $request->{node} };
    my $controller;
    @ARGV = @{ $request->{arg} };
    unless (GetOptions(
            'controller=s' => \$controller,
        )) {
        foreach (@nodes) {
            sendmsg([ 1, "Error parsing arguments" ], $callback, $_);
        }
    }
    my $storagetab = xCAT::Table->new('storage');
    my $storents = $storagetab->getNodesAttribs(\@nodes, [qw/controller/]);
    unless ($controller) {
        $controller = assure_identical_table_values(\@nodes, $storents, 'controller');
    }
    my @volnames = @ARGV;
    my $wwns     = get_wwns(@nodes);
    use Data::Dumper;
    my %namemap = makehosts($wwns, controller => $controller, cfg => $storents);
    foreach my $node (keys %namemap) {
        my $host = $namemap{$node};
        my $session = establish_session(controller => $controller);
        foreach my $volname (@volnames) {
            my @rets = $session->cmd("rmvdiskhostmap -host $host $volname");
            my $ret  = $rets[0];
            if ($ret =~ m/^CMMVC5842E/) {
                sendmsg([ 1, "Node not attached to $volname" ], $callback, $node);
            }
        }
    }
}

sub rmstorage {
    my $request = shift;
    my @nodes   = @{ $request->{node} };
    my $controller;
    @ARGV = @{ $request->{arg} };
    unless (GetOptions(
            'controller=s' => \$controller,
        )) {
        foreach (@nodes) {
            sendmsg([ 1, "Error parsing arguments" ], $callback, $_);
        }
    }
    my @volnames   = @ARGV;
    my $storagetab = xCAT::Table->new('storage');
    my $storents   = $storagetab->getNodesAttribs(\@nodes, [qw/controller/]);
    unless ($controller) {
        $controller = assure_identical_table_values(\@nodes, $storents, 'controller');
    }
    detachstorage($request);
    my $session = establish_session(controller => $controller);
    foreach my $volname (@volnames) {
        my @info = $session->cmd("rmvdisk $volname");
        my $ret  = $info[0];
        if ($ret =~ m/^CMMVC5753E/) {
            foreach my $node (@nodes) {
                sendmsg([ 1, "Disk $volname does not exist" ], $callback, @nodes);
            }
        } elsif ($ret =~ m/^CMMVC5840E/) {
            foreach my $node (@nodes) {
                sendmsg([ 1, "Disk $volname is mapped to other nodes and/or busy" ], $callback, @nodes);
            }
        }
    }
}


sub lsstorage {
    my $request    = shift;
    my @nodes      = @{ $request->{node} };
    my $storagetab = xCAT::Table->new("storage", -create => 0);
    unless ($storagetab) { return; }
    my $storents = $storagetab->getNodesAttribs(\@nodes, [qw/controller/]);
    my $wwns = get_wwns(@nodes);
    foreach my $node (@nodes) {
        if ($storents and $storents->{$node} and $storents->{$node}->[0]->{controller}) {
            my $ctls = $storents->{$node}->[0]->{controller};
            foreach my $ctl (split /,/, $ctls) { # TODO: scan all controllers at once
                my $session = establish_session(controller => $ctl);
                my %namemap = makehosts($wwns, controller => $ctl, cfg => $storents);
                my @vdisks = hashifyoutput($session->cmd("lsvdisk -delim :"));
                foreach my $vdisk (@vdisks) {
                    my @maps = hashifyoutput($session->cmd("lsvdiskhostmap -delim : " . $vdisk->{'id'}));
                    foreach my $map (@maps) {
                        if ($map->{host_name} eq $namemap{$node}) {
                            sendmsg($vdisk->{name} . ': size: ' . $vdisk->{capacity} . ' id: ' . $vdisk->{vdisk_UID}, $callback, $node);
                            last;
                        }
                    }
                }
            }
        }
    }
}

sub mkstorage {
    my $request = shift;
    my @nodes   = @{ $request->{node} };
    my $shared  = 0;
    my $controller;
    my $pool;
    my $size;
    my $boot   = 0;
    my $format = 0;
    unless (ref $request->{arg}) {
        die "TODO: usage";
    }
    my $name;
    @ARGV = @{ $request->{arg} };
    unless (GetOptions(
            'format'       => \$format,
            'shared'       => \$shared,
            'controller=s' => \$controller,
            'boot'         => \$boot,
            'size=f'       => \$size,
            'name=s'       => \$name,
            'pool=s'       => \$pool,
        )) {
        foreach (@nodes) {
            sendmsg([ 1, "Error parsing arguments" ], $callback, $_);
        }
    }
    if ($shared and $boot) {
        foreach (@nodes) {
            sendmsg([ 1, "Storage can not be both shared and boot" ], $callback, $_);
        }
    }
    my $storagetab = xCAT::Table->new('storage');
    my $storents   = $storagetab->getNodesAttribs(\@nodes,
        [qw/controller storagepool size/]);
    if ($shared) {
        unless ($size) {
            foreach (@nodes) {
                sendmsg([ 1,
"Size for shared volumes must be specified as an argument"
                ], $callback, $_);
            }
        }
        unless ($pool) {
            $pool = assure_identical_table_values(\@nodes, $storents, 'storagepool');
        }
        unless ($controller) {
            $controller = assure_identical_table_values(\@nodes, $storents, 'controller');
        }
        unless (defined $pool and defined $controller) {
            return;
        }
        my %lunargs = (controller => $controller, size => $size, pool => $pool);
        if ($name) { $lunargs{name} = $name; }
        my $lun = create_lun(%lunargs);
        sendmsg($lun->{name} . ": id: " . $lun->{wwn}, $callback);
        my $wwns = get_wwns(@nodes);
        my %namemap = makehosts($wwns, controller => $controller, cfg => $storents);
        my @names = values %namemap;
        bindhosts(\@names, $lun, controller => $controller);

        if ($format) {
            my %request = (
                node    => [ $nodes[0] ],
                command => ['formatdisk'],
                arg     => [ '--id', $lun->{wwn}, '--name', $lun->{name} ]
            );
            $dorequest->(\%request, $callback);
            %request = (
                node    => \@nodes,
                command => ['rescansan'],
            );
            $dorequest->(\%request, $callback);
        }
    } else {
        foreach my $node (@nodes) {
            mkstorage_single(node => $node, size => $size, pool => $pool,
                boot => $boot, name => $name, controller => $controller,
                cfg => $storents->{$node});
        }
    }
}

sub hashifyoutput {
    my @svcoutput = @_;
    my $hdr       = shift @svcoutput;
    my @columns   = split /:/, $hdr;
    my @ret;
    foreach my $line (@svcoutput) {
        my $index  = 0;
        my %record = ();
        my $keyname;
        foreach my $datum (split /:/, $line) {
            $keyname = $columns[$index];
            $record{$keyname} = $datum;
            $index += 1;
        }
        push @ret, \%record;
    }
    pop @ret;    # discard data from prompt
    return @ret;
}

sub bindhosts {
    my $nodes   = shift;
    my $lun     = shift;
    my %args    = @_;
    my $session = establish_session(%args);
    foreach my $node (@$nodes) {

        #TODO: get what failure looks like... somehow...
        #I guess I could make something with mismatched name and see how it
        #goes
        $session->cmd("mkvdiskhostmap -force -host $node " . $lun->{id});
    }
}

sub fixup_host {
    my $session = shift;
    my $wwnlist = shift;
    my @hosts   = hashifyoutput($session->cmd("lshost -delim :"));
    my %wwnmap;
    my %hostmap;
    foreach my $host (@hosts) {
        my @hostd = $session->cmd("lshost -delim : " . $host->{name});
        foreach my $hdatum (@hostd) {
            if ($hdatum =~ m/^WWPN:(.*)$/) {
                $wwnmap{$1} = $host->{name};
                $hostmap{ $host->{name} }->{$1} = 1;
            }
        }
    }
    my $name;
    foreach my $wwn (@$wwnlist) {
        $wwn =~ s/://g;
        $wwn = uc($wwn);
        if (defined $wwnmap{$wwn}) {    # found the matching host
                #we want to give the host all the ports that may be relevant
            $name = $wwnmap{$wwn};
            foreach my $mwwn (@$wwnlist) {
                $mwwn =~ s/://g;
                $mwwn = uc($mwwn);
                if (not defined $hostmap{$name}->{$mwwn}) {
                    $session->cmd("addhostport -hbawwpn $mwwn -force $name");
                }
            }
            return $name;
        }
    }
    die "unable to find host to fixup";
}

sub makehosts {
    my $wwnmap  = shift;
    my %args    = @_;
    my $session = establish_session(%args);
    my $stortab = xCAT::Table->new('storage');
    my %nodenamemap;
    foreach my $node (keys %$wwnmap) {
        my $wwnstr = "";
        foreach my $wwn (@{ $wwnmap->{$node} }) {
            $wwn =~ s/://g;
            $wwnstr .= $wwn . ":";
        }
        chop($wwnstr);

        #TODO: what if the given wwn exists, but *not* as the nodename we want
        #the correct action is to look at hosts, see if one exists, and reuse,
        #create, or warn depending
        my @hostres = $session->cmd("mkhost -name $node -hbawwpn $wwnstr -force");
        my $result = $hostres[0];
        if ($result =~ m/^CMM/) {    # we have some exceptional case....
            if ($result =~ m/^CMMVC6035E/) {    #duplicate name and/or wwn..
                    #need to finde the host and massage it to being viable
                $nodenamemap{$node} = fixup_host($session, $wwnmap->{$node});
            } else {
                die $result . " while trying to create host";
            }
        } else {
            $nodenamemap{$node} = $node;
        }
        my @currentcontrollers = split /,/, $args{cfg}->{$node}->[0]->{controller};
        if ($args{cfg}->{$node}->[0] and $args{cfg}->{$node}->[0]->{controller}) {
            @currentcontrollers = split /,/, $args{cfg}->{$node}->[0]->{controller};
        } else {
            @currentcontrollers = ();
        }
        if (grep { $_ eq $args{controller} } @currentcontrollers) {
            next;
        }
        unshift @currentcontrollers, $args{controller};
        my $ctrstring = join ",", @currentcontrollers;
        $stortab->setNodeAttribs($node, { controller => $ctrstring });
    }
    return %nodenamemap;
}

my %wwnmap;

sub got_wwns {
    my $rsp = shift;
    foreach my $ndata (@{ $rsp->{node} }) {
        my $nodename = $ndata->{name}->[0];
        my @wwns     = ();
        foreach my $data (@{ $ndata->{data} }) {
            push @{ $wwnmap{$nodename} }, $data->{contents}->[0];
        }
    }
}

sub get_wwns {
    %wwnmap = ();
    my @nodes = @_;
    foreach my $node (@nodes) {
        $wwnmap{$node} = [];
    }
    my %request = (
        node    => \@nodes,
        command => ['rinv'],
        arg     => ['wwn']
    );
    $dorequest->(\%request, \&got_wwns);
    return \%wwnmap;
}

my $globaluser;
my $globalpass;

sub get_svc_creds {
    my $controller = shift;
    if ($globaluser and $globalpass) {
        return { 'user' => $globaluser, 'pass' => $globalpass }
    }
    my $passtab = xCAT::Table->new('passwd', -create => 0);
    my $passent = $passtab->getAttribs({ key => 'svc' }, qw/username password/);
    $globaluser = $passent->{username};
    $globalpass = $passent->{password};
    return { 'user' => $globaluser, 'pass' => $globalpass }
}

sub establish_session {
    my %args       = @_;
    my $controller = $args{controller};
    if ($controllersessions{$controller}) {
        return $controllersessions{$controller};
    }

    #need to establish a new session
    my $cred = get_svc_creds($controller);
    my $sess = new xCAT::SSHInteract(-username => $cred->{user},
        -password                => $cred->{pass},
        -host                    => $controller,
        -output_record_separator => "\r",

        #Errmode=>"return",
        #Input_Log=>"/tmp/svcdbgl",
        Prompt => '/>$/');
    unless ($sess and $sess->atprompt) { die "TODO: cleanly handle bad login" }
    $controllersessions{$controller} = $sess;
    return $sess;
}

sub create_lun {
    my %args    = @_;
    my $session = establish_session(%args);
    my $pool    = $args{pool};
    my $size    = $args{size};
    my $cmd     = "mkvdisk -iogrp io_grp0 -mdiskgrp $pool -size $size -unit gb";
    if ($args{name}) {
        $cmd .= " -name " . $args{name};
    }
    my @result = $session->cmd($cmd);
    if ($result[0] =~ m/Virtual Disk, id \[(\d*)\], successfully created/) {
        my $diskid = $1;
        my $name;
        my $wwn;
        @result = $session->cmd("lsvdisk $diskid");
        foreach (@result) {
            chomp;
            if (/^name (.*)\z/) {
                $name = $1;
            } elsif (/^vdisk_UID (.*)\z/) {
                $wwn = $1;
            }
        }
        return { name => $name, id => $diskid, wwn => $wwn };
    }
}

sub assure_identical_table_values {
    my $nodes     = shift;
    my $storents  = shift;
    my $attribute = shift;
    my $lastval;
    foreach my $node (@$nodes) {
        my $sent = $storents->{$node}->[0];
        unless ($sent) {
            sendmsg([ 1, "No $attribute in arguments or table" ],
                $callback, $node);
            return undef;
        }
        my $currval = $sent->{$attribute};
        unless ($currval) {
            sendmsg([ 1, "No $attribute in arguments or table" ],
                $callback, $node);
            return undef;
        }
        if ($lastval and $currval ne $lastval) {
            sendmsg([ 1,
"$attribute mismatch in table config, try specifying as argument" ],
                $callback, $node);
            return undef;
        }
        if (not defined $lastval) { $lastval = $currval; }
    }
    return $lastval;
}

sub mkstorage_single {
    my %args = @_;
    my $size;
    my $cfg  = $args{cfg};
    my $node = $args{node};
    my $pool;
    my $controller;
    if (defined $args{size}) {
        $size = $args{size};
    } elsif ($cfg->{size}) {
        $size = $cfg->{size};
    } else {
        sendmsg([ 1, "Size not provided via argument or storage.size" ],
            $callback, $node);
    }
    if (defined $args{pool}) {
        $pool = $args{pool};
    } elsif ($cfg->{storagepool}) {
        $pool = $cfg->{storagepool};
    } else {
        sendmsg([ 1, "Pool not provided via argument or storage.storagepool" ],
            $callback, $node);
    }
    if (defined $args{controller}) {
        $controller = $args{controller};
    } elsif ($cfg->[0]->{controller}) {
        $controller = $cfg->[0]->{controller};
        $controller =~ s/.*,//;
    }
    my %lunargs = (controller => $controller, size => $size, pool => $pool);
    if ($args{name}) {
        $lunargs{name} = $args{name} . "-" . $node;
    }
    my $lun = create_lun(%lunargs);
    sendmsg($lun->{name} . ": id: " . $lun->{wwn}, $callback, $node);
    my $wwns = get_wwns($node);
    my %namemap = makehosts($wwns, controller => $controller, cfg => { $node => $cfg });
    my @names = values %namemap;
    bindhosts(\@names, $lun, controller => $controller);
}

sub process_request {
    my $request = shift;
    $callback  = shift;
    $dorequest = shift;
    if ($request->{command}->[0] eq 'mkstorage') {
        mkstorage($request);
    } elsif ($request->{command}->[0] eq 'lsstorage') {
        lsstorage($request);
    } elsif ($request->{command}->[0] eq 'rmstorage') {
        rmstorage($request);
    } elsif ($request->{command}->[0] eq 'detachstorage') {
        detachstorage($request);
    } elsif ($request->{command}->[0] eq 'lspool') {
        lsmdiskgrp($request);
    }
    foreach (values %controllersessions) {
        $_->close();
    }
}

sub lsmdiskgrp {
    my $req = shift;
    foreach my $node (@{ $req->{node} }) {
        my $session = establish_session(controller => $node);
        my @pools = hashifyoutput($session->cmd("lsmdiskgrp -delim :"));
        foreach my $pool (@pools) {
            sendmsg($pool->{name} . " available capacity: " . $pool->{free_capacity}, $callback, $node);
            sendmsg($pool->{name} . " total capacity: " . $pool->{capacity}, $callback, $node);
        }
    }
}

1;