#!/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 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_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 + "'") # ============================== 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 """ REDIS_HOSTNAME = "localhost" REDIS_PORT = 6379 REDIS_TIMEOUT_USECS = 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_USECS) # Open a handle to the Config database self.config_db = swsscommon.DBConnector(swsscommon.CONFIG_DB, self.REDIS_HOSTNAME, self.REDIS_PORT, self.REDIS_TIMEOUT_USECS) def update_lldp_config_for_port(self, port_name): """ For port `port_name`, look up the neighboring device's hostname and corresponding port alias in the Config database, then form the appropriate lldpcli configuration command and run it. """ TABLE_SEPARATOR = "|" # Retrieve all entires for this port from the Port table port_table = swsscommon.Table(self.config_db, swsscommon.CFG_PORT_TABLE_NAME, TABLE_SEPARATOR) (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 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) # Retrieve all entires for this port from the Device Neighbor table device_neighbor_table = swsscommon.Table(self.config_db, swsscommon.CFG_DEVICE_NEIGHBOR_TABLE_NAME, TABLE_SEPARATOR) (status, fvp) = device_neighbor_table.get(port_name) if status: # Convert list of tuples to a dictionary device_neighbor_table_dict = dict(fvp) # Get neighbor host name and port name neighbor_hostname = device_neighbor_table_dict.get("name") neighbor_portname = device_neighbor_table_dict.get("port") # If we sucessfully obtained the neighbor's host name and port name, append a port description to the command if neighbor_hostname and neighbor_portname: lldpcli_cmd += " description {0}:{1}".format(neighbor_hostname, neighbor_portname) else: if not neighbor_hostname: log_info("Failed to retrieve neighbor host name for port '{}'. Not adding port description.".format(port_name)) if not neighbor_portname: log_info("Failed to retrieve neighbor port name for port '{}'. Not adding port description.".format(port_name)) else: log_info("Unable to retrieve neighbor information for port '{}'. Not adding port description.".format(port_name)) log_info("Running command: '{}'".format(lldpcli_cmd)) proc = subprocess.Popen(lldpcli_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (stdout, stderr) = proc.communicate() if proc.returncode != 0: log_error("Error running command '{}': {}".format(cmd, stderr)) def run(self): """ Infinite loop. Subscribes to notifications of changes in the PORT table of the Redis State database. When we are notified of the creation of an interface, update LLDP configuration accordingly. """ # 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) # Listen indefinitely for changes to the PORT table in the State DB while True: (state, c, fd) = sel.select() if state != swsscommon.Select.OBJECT: log_warning("sel.select() did not return swsscommon.Select.OBJECT") continue (key, op, fvp) = sst.pop() fvp_dict = dict(fvp) if op == "SET" and fvp_dict.get("state") == "ok": self.update_lldp_config_for_port(key) # ============================= 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()