#!/usr/bin/python

#*******************************************************************************
#
#  @file    taihostctl
#  @brief   This file provides a command line interface to the taihost daemon
#
#  @copyright Copyright (C) 2018 Cumulus Networks, Inc. All rights reserved
#
#  @remark  This software is subject to the Cumulus Networks End User License
#           Agreement available  at the following locations:
#
#  @remark  Internet: https://cumulusnetworks.com/downloads/eula/latest/view/
#
#  @remark  Cumulus Linux systems: /usr/share/cumulus/EULA.txt
#
#*******************************************************************************

#
#   Import the necessary modules
#
try:
    import argparse
    import json
    import os
    import select
    import signal
    import socket
    import sys
    import textwrap
except ImportError, e:
    raise ImportError (str(e) + "- required module not found")
except KeyboardInterrupt:
    exit(-1)

#
#   Define constants
#
_TaiHostCtlVersion      = "0.1.0"
_TaiHostCmdVersion      = "0.1.0"


#
#   Global Variables
#
Parser     = None
TaiHostCmd = None


#-------------------------------------------------------------------------------
#
#   The TaiHostCmdClass class
#
#-------------------------------------------------------------------------------

class TaiHostCmdClass(object):

    """
    Simple class that connects to the taihost daemon's command socket, exported as
    a UNIX_AF socket by taihost, allowing users to interact with taihost, sending
    commands and getting results.  TaiHostCmdClass opens the socket file and issues
    commands via TaiHostCmdClass.run().  The command output is read from the socket and
    returned to the caller.
    """

    version = "1.0"


    def __init__(self, socketname="/var/run/taihost.socket"):

        """
        Constructor:

        socketname - the name of the taihost socket file.
        """

        if not os.access(socketname, os.F_OK):
            raise ValueError("socket %s does not exist" % socketname)
        elif not os.access(socketname, os.R_OK | os.W_OK):
            raise ValueError("missing read/write permissions for %s" % socketname)
        else:
            self.socketname = socketname

        self.socketobj = None
        self.__open__()


    def __del__(self):

        """Destructor: flush and close all files that we opened"""

        try:
            self.close()
        except:
            pass


    def __str__(self):

        """Return object state in human-readable form"""

        s = []
        s.append(repr(self))
        s.append('version: ' + self.version)
        s.append('socketname: ' + str(self.socketname))
        s.append('socketobj: ' + str(self.socketobj))
        return '\n'.join(s)


    def close(self):

        """Close the socket object"""

        if self.socketobj is not None:
            self.socketobj.shutdown(socket.SHUT_RDWR)
            self.socketobj.close()
            self.socketobj = None


    def __open__(self):

        """
        Connect to the a taihost socket. No-op if the socket is already open.
        """

        if self.socketobj is None:
            try:
                self.socketobj = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            except socket.error as msg:
                self.socketobj = None
                raise IOError("Unable to create a socket (%s)" % (msg,))

            try:
                self.socketobj.connect(self.socketname)
            except socket.error as msg:
                self.socketobj.close()
                self.socketobj = None
                raise IOError("Unable to connect to taihost command socket: %s (%s)" % (self.socketname, msg))
            self.buffer=''


    def run(self, cmd, callback=None, keepOpen=False):

        """
        Issue the command to taihost daemon and collect the return data.
        cmd must be a string.
        """

        if type(cmd) is not str:
            raise TypeError("expecting string argument to TaiHostCmdClass.run(cmd)")
        cmd += chr(0)

        self.__open__()

        inputs = [self.socketobj]
        outputs = [self.socketobj]
        runResult = ""

        while inputs + outputs:
            (readable, writable, exceptional) = select.select(inputs, outputs, inputs + outputs)

            for readSock in readable:
                try:
                    cmdResult = readSock.recv(2048)
                except:
                    cmdResult = 0
                if cmdResult:
                    self.buffer += cmdResult
                    while "\n" in self.buffer and callback is not None:
                        cmdEnd = self.buffer.find("\n")
                        cmdStr = self.buffer[:cmdEnd]
                        self.buffer = self.buffer[cmdEnd+1:]
                        callback(cmdStr)
                    if chr(0) in self.buffer:
                        cmdEnd = self.buffer.find(chr(0))
                        runResult = self.buffer[:cmdEnd]
                        self.buffer = self.buffer[cmdEnd+1:]
                        if keepOpen:
                            return runResult
                        if readSock in inputs:
                            inputs.remove(readSock)
                else:
                    if readSock in inputs:
                        inputs.remove(readSock)
                        if readSock == self.socketobj:
                            self.close()

            for writeSock in writable:
                try:
                    numSent = writeSock.send(cmd)
                except:
                    numSent = len(cmd)
                    writeSock.close()
                cmd = cmd[numSent:]
                if not cmd and writeSock in outputs:
                    outputs.remove(writeSock)

            for errSock in exceptional:
                if errSock in inputs:
                    inputs.remove(errSock)
                if errSock in outputs:
                    outputs.remove(errSock)
                if errSock == self.socketobj:
                    self.close()

        return runResult


#-------------------------------------------------------------------------------
#
#   Command Handlers
#
#-------------------------------------------------------------------------------

def HandleEcho():
    '''
    This command simply echoes back the text following the "echo" command. This
    can be used to test the interface to the daemon.
    '''
    jsonStr = TaiHostCmd.run("echo " + " ".join(Parser.args.args))
    try:
        cmdResult = json.loads(jsonStr)
    except ValueError as e:
        print "Invalid output from taihost: %s (%s)\n" % (jsonStr,e)
        return -1
    retVal = -1 if 'errorMsg' in cmdResult else 0
    if Parser.args.json:
        outStr = jsonStr
    else:
        outStr = "taihost returned : " + cmdResult.get('errorMsg', cmdResult.get('reply', ""))
    print outStr
    return retVal

def HandleReload():
    '''
    This command causes the configuration file to be re-read and any differences
    between the file and the hardware configuration to be updated in the
    hardware.
    '''
    jsonStr = TaiHostCmd.run("reload")
    try:
        cmdResult = json.loads(jsonStr)
    except ValueError as e:
        print "Invalid output from taihost: %s (%s)\n" % (jsonStr,e)
        return -1
    retVal = -1 if 'errorMsg' in cmdResult else 0
    if Parser.args.json:
        outStr = jsonStr
    else:
        outStr = cmdResult.get('errorMsg', "")
    print outStr
    return retVal

def HandleLogMessage():
    '''
    This command causes a log message to be output. The message is whatever
    text follows the "logmsg" command.
    '''
    jsonStr = TaiHostCmd.run("logmsg " + " ".join(Parser.args.args))
    try:
        cmdResult = json.loads(jsonStr)
    except ValueError as e:
        print "Invalid output from taihost: %s (%s)\n" % (jsonStr,e)
        return -1
    retVal = -1 if 'errorMsg' in cmdResult else 0
    if Parser.args.json:
        outStr = jsonStr
    else:
        outStr = cmdResult.get('errorMsg', "")
    print outStr
    return retVal

#
#   A Voyager-specific table for determining the Linux interface name for a
#   host index. The modulation format and module location are also used.
#
VoyagerHostIfs = {
    '16-qam' : {
        "1" : ["swpL3s0", "swpL3s1", "swpL4s0", "swpL4s1"],
        "2" : ["swpL1s0", "swpL1s1", "swpL2s0", "swpL2s1"]
        },
    '8-qam' : {
        "1" : ["swpL3s0", "swpL3s1", "swpL3s2", "unused"],
        "2" : ["swpL1s0", "swpL1s1", "swpL1s2", "unused"]
        },
    'pm-qpsk' : {
        "1" : ["swpL3", "unused", "swpL4", "unused"],
        "2" : ["swpL1", "unused", "swpL2", "unused"]
        }
}

def GetHostIfName(module, hostif):
    '''
    Given a host interface and the module it is on, determine the Linux interface
    name. The first step is to determine the type of modulation
    which will be applied to the traffic flowing through that host interface.

    Algorithm is basically find the network interface index from the host
    interface index, go through all of the network interfaces looking for
    the one with that index, and get the modulation format of that interface.

    Then use the table, above, to look up the Linux interface name using the
    modulation format, module location, and host interface index.

    NOTE: This function is specific to Voayger and will need to be modified
    when other platforms are supported.
    '''
    hostIdx = hostif.get('index', 0)
    netIdx = hostIdx / module.get('num_net_ifs', 1)
    modulation = 'unknown'
    for netif in module.get('network_interfaces', []):
        if netif.get('index', -1) == netIdx:
            modulation = netif.get('modulation', 'unknown')
            break
    location = module.get('location', '')
    try:
        ifname = VoyagerHostIfs.get(modulation, {}).get(location, [])[hostIdx]
    except IndexError:
        ifname = "unknown"
    return ifname

#
#   A Voyager-specific table for determining the front panel label for a
#   network interface. The module location is also used.
#
VoyagerNetIfs = {
    "1" : [ "L3", "L4" ],
    "2" : [ "L1", "L2" ]
}

def GetNetIfName(module, netif):
    '''
    Given a network interface, and the module which it is upon, determine the
    name of that interface (label on the front of the box).

    '''
    location = module.get('location', "")
    netifidx = netif.get('index', 0)
    try:
        netifstr = VoyagerNetIfs.get(location, [])[netifidx]
    except IndexError:
        netifstr = "Unknown"
    return netifstr

def HandleHostInterfaceStatus(module):
    '''
    This function creates a string with the status output for all of the
    host interfaces on the supplied module.
    '''
    outStr = ""
    numHostIfs = module.get('num_host_ifs', 0)
    if Parser.args.verbose and numHostIfs:
        outStr += "\n"
        colWidth = 58 / numHostIfs
        outStr += " "*23 + "Host Interfaces".center(58) + "\n"
        outStr += " "*23
        hostifs = module.get('host_interfaces', [])
        for hostif in hostifs:
            outStr += GetHostIfName(module, hostif).center(colWidth)
        outStr += "\n"
        outStr += " "*23
        for hostif in hostifs:
            outStr += "-"*(colWidth - 2) + "  "
        outStr += "\n"
        outStr += "%22s " % ("Lane faults",)
        for hostif in hostifs:
            faults = "None"
            for faultList in hostif.get('lane_fault_status', []):
                if faultList != ["no_faults"]:
                    faults = "faults"
            outStr += faults.ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("TX alignment",)
        for hostif in hostifs:
            txalign = "aligned" if hostif.get('tx_align_status', []) == ["aligned"] else "not aligned"
            outStr += txalign.ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("Rate",)
        for hostif in hostifs:
            outStr += hostif.get('rate', 'unknown').ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("Enabled",)
        for hostif in hostifs:
            outStr += str(hostif.get('enabled', False)).ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("FEC",)
        for hostif in hostifs:
            fecrx = hostif.get('fec_decoding', False)
            fectx = hostif.get('fec_encoding', False)
            if fecrx and fectx:
                outStr += "TX & RX".ljust(colWidth)
            elif fecrx and not fectx:
                outStr += "RX".ljust(colWidth)
            elif not fecrx and fectx:
                outStr += "TX".ljust(colWidth)
            else:
                outStr += "None".ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("Reset",)
        for hostif in hostifs:
            rxrst = hostif.get('rx_reset', False)
            txrst = hostif.get('tx_reset', False)
            if rxrst and txrst:
                outStr += "TX & RX".ljust(colWidth)
            elif rxrst and not txrst:
                outStr += "RX".ljust(colWidth)
            elif not rxrst and txrst:
                outStr += "TX".ljust(colWidth)
            else:
                outStr += "None".ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("Loopback",)
        for hostif in hostifs:
            outStr += str(hostif.get('loopback', False)).ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("Indep Tributary",)
        for hostif in hostifs:
            outStr += str(hostif.get('indep_tributary', 15)).ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("Coupled Tributary Map",)
        for hostif in hostifs:
            outStr += str(hostif.get('coupled_tributary', 65535)).ljust(colWidth)
        outStr += "\n"
    return outStr

def HandleNetworkInterfaceStatus(module):
    '''
    This function creates a string with the status output for all of the
    network interfaces on the supplied module.
    '''
    outStr = ""
    numNetIfs = module.get('num_net_ifs', 0)
    if numNetIfs:
        outStr += "\n"
        colWidth = 58 / numNetIfs

        outStr += " "*23 + "Network Interfaces".center(58) + "\n"
        outStr += " "*23
        netifs = module.get('network_interfaces', [])
        for netif in netifs:
            outStr += GetNetIfName(module, netif).center(colWidth)
        outStr += "\n"
        outStr += " "*23
        for netif in netifs:
            outStr += "-"*(colWidth - 2) + "  "
        outStr += "\n"
        outStr += "%22s " % ("Modulation",)
        for netif in netifs:
            outStr += netif.get('modulation', "unknown").ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("Frequency",)
        for netif in netifs:
            freq = netif.get('laser_frequency', 0) / 1000000000000.0
            freqStr = "%3.2f THz, Channel %d" % (freq, netif.get('channel', 0))
            outStr += freqStr.ljust(colWidth)
        outStr += "\n"
        if Parser.args.verbose:
            outStr += "%22s " % ("Fine Tune Frequency",)
            for netif in netifs:
                freq = netif.get('fine_tune_laser_frequency', 0) / 1000000000.0
                freqStr = "%3.2f GHz" % (freq,)
                outStr += freqStr.ljust(colWidth)
            outStr += "\n"
        outStr += "%22s " % ("Current BER",)
        for netif in netifs:
            ber = netif.get('current_ber', 0)
            berStr = "%1.3e" % (ber,)
            outStr += berStr.ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("Current OSNR",)
        for netif in netifs:
            osnr = netif.get('current_osnr', 0)
            osnrStr = "%1.2fdBm" % (osnr,)
            outStr += osnrStr.ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("Current Chromatic Disp",)
        for netif in netifs:
            cd = netif.get('current_cd', 0)
            cdStr = "%dps/nm" % (cd,)
            outStr += cdStr.ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("TX/RX Power",)
        for netif in netifs:
            measoutpwr = netif.get('current_output_power', 0)
            measinpwr = netif.get('current_input_power', 0)
            pwrStr = "%1.2fdBm/%1.2fdBm" % (measoutpwr, measinpwr)
            outStr += pwrStr.ljust(colWidth)
        outStr += "\n"
        if Parser.args.verbose:
            outStr += "%22s " % ("Cfg TX Power",)
            for netif in netifs:
                cfgoutpwr = netif.get('output_power', 0)
                pwrStr = "%1.2fdBm" % (cfgoutpwr)
                outStr += pwrStr.ljust(colWidth)
            outStr += "\n"
        outStr += "%22s " % ("Encoding",)
        for netif in netifs:
            encode = netif.get('differential_encoding', False)
            encodeStr = "differential" if encode else "non-differential"
            outStr += encodeStr.ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("Alignment",)
        for netif in netifs:
            txaln = netif.get('tx_align_status', []) == ['aligned']
            rxaln = netif.get('rx_align_status', []) == ['aligned']
            if txaln and rxaln:
                outStr += "TX & RX".ljust(colWidth)
            elif txaln and not rxaln:
                outStr += "TX".ljust(colWidth)
            elif not txaln and rxaln:
                outStr += "RX".ljust(colWidth)
            else:
                outStr += "None".ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("Grid Spacing",)
        for netif in netifs:
            outStr += netif.get('grid_spacing', "unknown").ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("FEC Mode",)
        for netif in netifs:
            outStr += netif.get('fec_mode', "unknown").ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("Uncorrectable FEC Errs",)
        for netif in netifs:
            outStr += str(netif.get('uncorrectable_fec', 0)).ljust(colWidth)
        outStr += "\n"
        outStr += "%22s " % ("TX/RX Turn-up",)
        for netif in netifs:
            try:
                txturn = netif.get('tx_turn_up', [])[-1]
            except IndexError:
                txturn = "unknown"
            try:
                rxturn = netif.get('rx_turn_up', [])[-1]
            except IndexError:
                rxturn = "unknown"
            turnStr = "%s/%s" % (txturn, rxturn)
            outStr += turnStr.ljust(colWidth)
        outStr += "\n"
        if Parser.args.verbose:
            outStr += "%22s " % ("Master Enable",)
            for netif in netifs:
                outStr += str(netif.get('master_enable', False)).ljust(colWidth)
            outStr += "\n"
            outStr += "%22s " % ("TX Enable",)
            for netif in netifs:
                outStr += str(netif.get('tx_enabled', False)).ljust(colWidth)
            outStr += "\n"
            outStr += "%22s " % ("Loopback",)
            for netif in netifs:
                outStr += str(netif.get('loopback', False)).ljust(colWidth)
            outStr += "\n"
            outStr += "%22s " % ("Indep Tributary Map",)
            for netif in netifs:
                outStr += str(netif.get('indep_tributary', [])).ljust(colWidth)
            outStr += "\n"
            outStr += "%22s " % ("Coupled Tributary Map",)
            for netif in netifs:
                outStr += str(netif.get('coupled_tributary', [])).ljust(colWidth)
            outStr += "\n"
    return outStr

def HandleStatus():
    '''
    This command displays the status of the optical modules in the system.
    '''
    jsonStr = TaiHostCmd.run("GetSummary")
    try:
        cmdResult = json.loads(jsonStr)
    except ValueError as e:
        print "Invalid output from taihost: %s (%s)\n" % (jsonStr,e)
        return -1
    retVal = -1 if 'errorMsg' in cmdResult else 0
    if Parser.args.json:
        outStr = jsonStr
    else:
        outStr = ""
        for module in cmdResult.get('modules', []):
            if Parser.args.args and Parser.args.args[0] != module.get('location'):
                continue
            if outStr != "":
                outStr += "\n"
            outStr += "Module: %s %s %s %s S/N:%s %2.2fC %2.2fV\n" % \
                (module.get('location', '?'), 
                 module.get('oper_status', 'unknown'),
                 module.get('vendor_name', 'unknown'),
                 module.get('part_num', 'unknown'),
                 module.get('serial_num', 'unknown'),
                 module.get('internal_temp', -99.99),
                 module.get('supply_voltage', -99.99))
            outStr += "    Laser: %3.2f THz - %3.2f THz, %1.2f GHz fine tune, %s lanes\n" % \
                (module.get('min_laser_freq', 0.0) / 1000000000000.0,
                 module.get('max_laser_freq', 0.0) / 1000000000000.0,
                 module.get('fine_tune_freq', 0.0) / 1000000000.0,
                 module.get('net_mode', 'unknown'))
            if Parser.args.verbose:
                outStr += "           grid support %s\n" % \
                    (' '.join(module.get('grid_support',[])))
                outStr += "    Firmware version A: %g, B: %g\n" % \
                    (module.get('fw_version_a',0.0),
                     module.get('fw_version_b',0.0))
            outStr += HandleHostInterfaceStatus(module)
            outStr += HandleNetworkInterfaceStatus(module)

    print outStr
    return retVal

#-------------------------------------------------------------------------------
#
#   Command Dictionary - This dictionary lists all of the command strings as
#   the dictionary keys, and the function which handles the execution of the
#   command as the value.
#   Commands that are setup as "Internal" are not displayed in the help menu.
#   Internal commands can be deprecated and output/display format can be
#   changed without notice.
#-------------------------------------------------------------------------------

taiHostCtlCmds = {
    #Command            : ( Function, Help, jsonSupport, Internal)
    "echo"              : ( HandleEcho, "Echo back the supplied string", False),
    "reload"            : ( HandleReload, "Reapply config to hardware", False),
    "status"            : ( HandleStatus, "Display the status of the transponders", False),
    "logmsg"            : ( HandleLogMessage, "Outputs a message to the log file", False)
}


#-------------------------------------------------------------------------------
#
#   Command line parsing code
#
#-------------------------------------------------------------------------------

class TaiHostCtlParser:

    def __init__(self):
        '''
        Create the parser
        '''
        epilogStr = "The commands are:\n"
        for cmd in sorted(taiHostCtlCmds.iterkeys()):
            if not taiHostCtlCmds[cmd][2]:
                epilogStr += "%-20s  %s\n" % (cmd, taiHostCtlCmds[cmd][1])
        epilogStr += "\nSee the taihostctl man page for more information"
        self.parser = argparse.ArgumentParser(description="taihost daemon control interface, version %s" % (_TaiHostCtlVersion,),
                                              usage='%(prog)s [-h] [-j] [-v] [command [args]]', epilog=epilogStr,
                                              formatter_class=argparse.RawDescriptionHelpFormatter)
        self.parser.add_argument("command", choices=taiHostCtlCmds.keys(), default="status", nargs='?',
                                 metavar="command", help="Command to execute, default is 'status'")
        self.parser.add_argument("-v", "--verbose", action="store_true", default=False,
                                 help="Increase the amount of output.")
        self.parser.add_argument("-j", "--json", action="store_true", default=False,
                                 help="json output.")
        self.parser.add_argument("args", nargs=argparse.REMAINDER, help="Additional command parameters")

    def ParseCmdLine(self):
        '''
        Parse the command line parameters
        '''
        try:
            self.args = self.parser.parse_args()
        except:
            sys.exit(-1)


#-------------------------------------------------------------------------------
#
#   Main program entry point
#
#-------------------------------------------------------------------------------

def main():

    # Parse the command line parameters
    global Parser
    Parser = TaiHostCtlParser()
    Parser.ParseCmdLine()

    # Handle broken pipe problems
    signal.signal(signal.SIGPIPE, signal.SIG_DFL)

    # Create a connection to taihost
    global TaiHostCmd
    try:
        TaiHostCmd = TaiHostCmdClass()
    except IOError as e:
        errMsg = "Unable to communicate with taihost. Is it running?"
        if Parser.args.json:
            print json.dumps({"errorMsg" : errMsg})
        else:
            print errMsg
        return -1
    except ValueError as e:
        errMsg = "Unable to communicate with taihost. Is it running?"
        if Parser.args.json:
            print json.dumps({"errorMsg" : errMsg})
        else:
            print errMsg
        return -1

    # Execute the command
    try:
        return taiHostCtlCmds[Parser.args.command][0]()
    except IOError as e:
        errMsg = "*** Command interrupted (%s). Output incomplete." % (e,)
        if Parser.args.json:
            print json.dumps({"errorMsg" : errMsg})
        else:
            print errMsg
        TaiHostCmd.close()
    except KeyboardInterrupt:
        errMsg = "*** Command interrupted. Output incomplete."
        if Parser.args.json:
            print json.dumps({"errorMsg" : errMsg})
        else:
            print errMsg
        TaiHostCmd.close()

    return -1



#-------------------------------------------------------------------------------
#
#   Are we being executed or imported?
#
#-------------------------------------------------------------------------------

if __name__ == '__main__':
    rc = main()
    sys.exit(rc)
