#!/usr/bin/env python

"""
    lldpmgrd

    LLDP manager daemon for SONiC

    Daemon which listens for changes in the PORT table of the State DB
    and updates LLDP configuration accordingly for that port by calling
    lldpcli.

    TODO: Also listen for changes in DEVICE_NEIGHBOR and PORT tables in
          Config DB and update LLDP config upon changes.
"""


try:
    import os
    import signal
    import subprocess
    import sys
    import syslog
    import os.path
    from swsscommon import swsscommon
except ImportError as err:
    raise ImportError("%s - required module not found" % str(err))

VERSION = "1.0"

SYSLOG_IDENTIFIER = "lldpmgrd"


# ========================== Syslog wrappers ==========================

def log_debug(msg):
    syslog.openlog(SYSLOG_IDENTIFIER)
    syslog.syslog(syslog.LOG_DEBUG, msg)
    syslog.closelog()


def log_info(msg):
    syslog.openlog(SYSLOG_IDENTIFIER)
    syslog.syslog(syslog.LOG_INFO, msg)
    syslog.closelog()


def log_warning(msg):
    syslog.openlog(SYSLOG_IDENTIFIER)
    syslog.syslog(syslog.LOG_WARNING, msg)
    syslog.closelog()


def log_error(msg):
    syslog.openlog(SYSLOG_IDENTIFIER)
    syslog.syslog(syslog.LOG_ERR, msg)
    syslog.closelog()


# ========================== Signal Handling ==========================

def signal_handler(sig, frame):
    if sig == signal.SIGHUP:
        log_info("Caught SIGHUP - ignoring...")
        return
    elif sig == signal.SIGINT:
        log_info("Caught SIGINT - exiting...")
        sys.exit(128 + sig)
    elif sig == signal.SIGTERM:
        log_info("Caught SIGTERM - exiting...")
        sys.exit(128 + sig)
    else:
        log_warning("Caught unhandled signal '" + sig + "'")

# ========================== Helpers ==================================

def is_port_exist(port_name):
    filename = "/sys/class/net/%s/operstate" % port_name
    if os.path.exists(filename):
        with open(filename) as fp:
            state = fp.read()
            return "up" in state
    else:
        filename = "/sys/class/net/%s/ifindex" % port_name
        return os.path.exists(filename)

# ============================== Classes ==============================

class LldpManager(object):
    """
    Class which subscribes to notifications of changes in the PORT table of
    the Redis State database and updates LLDP configuration accordingly for
    that port by calling lldpcli.
    Attributes:
        state_db: Handle to Redis State database via swsscommon lib
        config_db: Handle to Redis Config database via swsscommon lib
        pending_cmds: Dictionary where key is port name, value is pending
                      LLDP configuration command to run
    """
    REDIS_HOSTNAME = "localhost"
    REDIS_PORT = 6379
    REDIS_TIMEOUT_MS = 0

    def __init__(self):
        # Open a handle to the State database
        self.state_db = swsscommon.DBConnector(swsscommon.STATE_DB,
                                               self.REDIS_HOSTNAME,
                                               self.REDIS_PORT,
                                               self.REDIS_TIMEOUT_MS)

        # Open a handle to the Config database
        self.config_db = swsscommon.DBConnector(swsscommon.CONFIG_DB,
                                                self.REDIS_HOSTNAME,
                                                self.REDIS_PORT,
                                                self.REDIS_TIMEOUT_MS)

        self.pending_cmds = {}

    def generate_pending_lldp_config_cmd_for_port(self, port_name):
        """
        For port `port_name`, look up the description and alias in the Config database,
        then form the appropriate lldpcli configuration command and run it.
        """
        # Retrieve all entires for this port from the Port table
        port_table = swsscommon.Table(self.config_db, swsscommon.CFG_PORT_TABLE_NAME)
        (status, fvp) = port_table.get(port_name)
        if status:
            # Convert list of tuples to a dictionary
            port_table_dict = dict(fvp)

            # Get the port alias. If None or empty string, use port name instead
            port_alias = port_table_dict.get("alias")
            if not port_alias:
                log_info("Unable to retrieve port alias for port '{}'. Using port name instead.".format(port_name))
                port_alias = port_name
            
            # Get the port description. If None or empty string, we'll skip this configuration 
            port_desc = port_table_dict.get("description")

        else:
            log_error("Port '{}' not found in {} table in Config DB. Using port name instead of port alias.".format(port_name, swsscommon.CFG_PORT_TABLE_NAME))
            port_alias = port_name

        lldpcli_cmd = "lldpcli configure ports {0} lldp portidsubtype local {1}".format(port_name, port_alias)

        # if there is a description available, also configure that
        if port_desc:
            lldpcli_cmd += " description {}".format(port_desc)
        else:
            log_info("Unable to retrieve description for port '{}'. Not adding port description".format(port_name))

        # Add the command to our dictionary of pending commands, overwriting any
        # previous pending command for this port
        self.pending_cmds[port_name] = lldpcli_cmd

    def process_pending_cmds(self):
        # List of port names (keys of elements) to delete from self.pending_cmds
        to_delete = []

        for (port_name, cmd) in self.pending_cmds.iteritems():
            if not is_port_exist(port_name):
                # it doesn't make any sense to configure lldpd if the target port does not exist
                # let's postpone the command for the next iteration
                continue

            log_debug("Running command: '{}'".format(cmd))

            proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

            (stdout, stderr) = proc.communicate()

            # If the command succeeds, add the port name to our to_delete list.
            # We will delete this command from self.pending_cmds below.
            # If the command fails, log a message, but don't delete the command
            # from self.pending_cmds, so that the command will be retried the
            # next time this method is called.
            if proc.returncode == 0:
                to_delete.append(port_name)
            else:
                log_warning("Command failed '{}': {}".format(cmd, stderr))

        # Delete all successful commands from self.pending_cmds
        for port_name in to_delete:
            self.pending_cmds.pop(port_name, None)

    def run(self):
        """
        Subscribes to notifications of changes in the PORT table
        of the Redis State database. 
        Subscribe to STATE_DB - get notified of the creation of an interface
        Subscribe to CONFIG_DB - get notified of port config changes
        Update LLDP configuration accordingly.
        """
        # Set select timeout to 10 seconds
        SELECT_TIMEOUT_MS = 1000 * 10

        # Subscribe to PORT table notifications in the State DB
        sel = swsscommon.Select()
        sst = swsscommon.SubscriberStateTable(self.state_db, swsscommon.STATE_PORT_TABLE_NAME)
        sel.addSelectable(sst)
        sst = swsscommon.SubscriberStateTable(self.config_db, swsscommon.CFG_PORT_TABLE_NAME)
        sel.addSelectable(sst)

        # Listen for changes to the PORT table in the STATE_DB and CONFIG_DB
        while True:
            (state, c) = sel.select(SELECT_TIMEOUT_MS)

            if state == swsscommon.Select.OBJECT:
                (key, op, fvp) = sst.pop()

                fvp_dict = dict(fvp)

                # handle creation
                if op == "SET" and fvp_dict.get("state") == "ok":
                    self.generate_pending_lldp_config_cmd_for_port(key)

                # handle config change
                if op in ["SET", "DEL"] and (fvp_dict.get("alias") or fvp_dict.get("description")) :
                    self.generate_pending_lldp_config_cmd_for_port(key)

            # Process all pending commands
            self.process_pending_cmds()


# ============================= Functions =============================

def main():
    log_info("Starting up...")

    if not os.geteuid() == 0:
        log_error("Must be root to run this daemon")
        print "Error: Must be root to run this daemon"
        sys.exit(1)

    # Register our signal handlers
    signal.signal(signal.SIGHUP, signal_handler)
    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)

    # Instantiate a LldpManager object
    lldpmgr = LldpManager()
    lldpmgr.run()

if __name__ == "__main__":
    main()