2018-03-02 18:46:22 -06:00
|
|
|
#!/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
|
2018-04-26 21:01:33 -05:00
|
|
|
import os.path
|
2018-03-02 18:46:22 -06:00
|
|
|
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 ==========================
|
|
|
|
|
2018-03-07 19:08:45 -06:00
|
|
|
def log_debug(msg):
|
|
|
|
syslog.openlog(SYSLOG_IDENTIFIER)
|
|
|
|
syslog.syslog(syslog.LOG_DEBUG, msg)
|
|
|
|
syslog.closelog()
|
|
|
|
|
|
|
|
|
2018-03-02 18:46:22 -06:00
|
|
|
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
|
2018-03-07 19:08:45 -06:00
|
|
|
pending_cmds: Dictionary where key is port name, value is pending
|
|
|
|
LLDP configuration command to run
|
2018-03-02 18:46:22 -06:00
|
|
|
"""
|
2018-03-07 19:08:45 -06:00
|
|
|
REDIS_TIMEOUT_MS = 0
|
2018-03-02 18:46:22 -06:00
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
# Open a handle to the Config database
|
2020-01-22 13:27:21 -06:00
|
|
|
self.config_db = swsscommon.DBConnector("CONFIG_DB",
|
|
|
|
self.REDIS_TIMEOUT_MS,
|
|
|
|
True)
|
2018-03-07 19:08:45 -06:00
|
|
|
|
2019-07-17 18:04:01 -05:00
|
|
|
# Open a handle to the Application database
|
2020-01-22 13:27:21 -06:00
|
|
|
self.appl_db = swsscommon.DBConnector("APPL_DB",
|
|
|
|
self.REDIS_TIMEOUT_MS,
|
|
|
|
True)
|
2019-07-17 18:04:01 -05:00
|
|
|
|
2018-03-07 19:08:45 -06:00
|
|
|
self.pending_cmds = {}
|
2018-03-02 18:46:22 -06:00
|
|
|
|
2019-07-17 18:04:01 -05:00
|
|
|
def is_port_up(self, port_name):
|
|
|
|
"""
|
|
|
|
Determine if a port is up or down by looking into the oper-status for the port in
|
|
|
|
PORT TABLE in the Application DB
|
|
|
|
"""
|
|
|
|
# Retrieve all entires for this port from the Port table
|
|
|
|
port_table = swsscommon.Table(self.appl_db, swsscommon.APP_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 oper-status for the port
|
|
|
|
if port_table_dict.has_key("oper_status"):
|
|
|
|
port_oper_status = port_table_dict.get("oper_status")
|
|
|
|
log_info("Port name {} oper status: {}".format(port_name, port_oper_status))
|
|
|
|
return port_oper_status == "up"
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
else:
|
2020-05-19 04:52:48 -05:00
|
|
|
# Retrieve PortInitDone entry from the Port table
|
|
|
|
(init_status, init_fvp) = port_table.get("PortInitDone")
|
|
|
|
#The initialization procedure is done, but don't have this port entry
|
|
|
|
if init_status:
|
|
|
|
log_error("Port '{}' not found in {} table in App DB".format(port_name, swsscommon.APP_PORT_TABLE_NAME))
|
2019-07-17 18:04:01 -05:00
|
|
|
return False
|
|
|
|
|
2018-03-07 19:08:45 -06:00
|
|
|
def generate_pending_lldp_config_cmd_for_port(self, port_name):
|
2018-03-02 18:46:22 -06:00
|
|
|
"""
|
2019-01-30 03:05:36 -06:00
|
|
|
For port `port_name`, look up the description and alias in the Config database,
|
|
|
|
then form the appropriate lldpcli configuration command and run it.
|
2018-03-02 18:46:22 -06:00
|
|
|
"""
|
2019-09-12 17:21:53 -05:00
|
|
|
port_desc = None
|
|
|
|
|
2018-03-02 18:46:22 -06:00
|
|
|
# Retrieve all entires for this port from the Port table
|
2018-04-11 03:50:17 -05:00
|
|
|
port_table = swsscommon.Table(self.config_db, swsscommon.CFG_PORT_TABLE_NAME)
|
2018-03-02 18:46:22 -06:00
|
|
|
(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
|
2019-01-30 03:05:36 -06:00
|
|
|
|
|
|
|
# Get the port description. If None or empty string, we'll skip this configuration
|
|
|
|
port_desc = port_table_dict.get("description")
|
|
|
|
|
2018-03-02 18:46:22 -06:00
|
|
|
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)
|
|
|
|
|
2019-01-30 03:05:36 -06:00
|
|
|
# if there is a description available, also configure that
|
|
|
|
if port_desc:
|
2020-02-09 22:34:55 -06:00
|
|
|
lldpcli_cmd += " description '{}'".format(port_desc)
|
2018-03-02 18:46:22 -06:00
|
|
|
else:
|
2019-01-30 03:05:36 -06:00
|
|
|
log_info("Unable to retrieve description for port '{}'. Not adding port description".format(port_name))
|
2018-03-02 18:46:22 -06:00
|
|
|
|
2018-03-07 19:08:45 -06:00
|
|
|
# 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 = []
|
2018-03-02 18:46:22 -06:00
|
|
|
|
2018-03-07 19:08:45 -06:00
|
|
|
for (port_name, cmd) in self.pending_cmds.iteritems():
|
|
|
|
log_debug("Running command: '{}'".format(cmd))
|
2018-03-02 18:46:22 -06:00
|
|
|
|
2018-03-07 19:08:45 -06:00
|
|
|
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
2018-03-02 18:46:22 -06:00
|
|
|
|
2018-03-07 19:08:45 -06:00
|
|
|
(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)
|
2018-03-02 18:46:22 -06:00
|
|
|
|
|
|
|
def run(self):
|
|
|
|
"""
|
2019-01-30 03:05:36 -06:00
|
|
|
Subscribes to notifications of changes in the PORT table
|
2019-07-17 18:04:01 -05:00
|
|
|
of the Redis Config/Application database.
|
|
|
|
Subscribe to APP_DB - get port oper status
|
2019-01-30 03:05:36 -06:00
|
|
|
Subscribe to CONFIG_DB - get notified of port config changes
|
|
|
|
Update LLDP configuration accordingly.
|
2018-03-02 18:46:22 -06:00
|
|
|
"""
|
2018-03-07 19:08:45 -06:00
|
|
|
# Set select timeout to 10 seconds
|
|
|
|
SELECT_TIMEOUT_MS = 1000 * 10
|
|
|
|
|
2018-03-02 18:46:22 -06:00
|
|
|
sel = swsscommon.Select()
|
|
|
|
|
2019-07-17 18:04:01 -05:00
|
|
|
# Subscribe to PORT table notifications in the Config DB
|
|
|
|
sst_confdb = swsscommon.SubscriberStateTable(self.config_db, swsscommon.CFG_PORT_TABLE_NAME)
|
|
|
|
sel.addSelectable(sst_confdb)
|
|
|
|
|
|
|
|
# Subscribe to PORT table notifications in the App DB
|
|
|
|
sst_appdb = swsscommon.SubscriberStateTable(self.appl_db, swsscommon.APP_PORT_TABLE_NAME)
|
|
|
|
sel.addSelectable(sst_appdb)
|
|
|
|
|
|
|
|
# Listen for changes to the PORT table in the CONFIG_DB and APP_DB
|
2018-03-02 18:46:22 -06:00
|
|
|
while True:
|
2018-04-16 20:38:48 -05:00
|
|
|
(state, c) = sel.select(SELECT_TIMEOUT_MS)
|
2018-03-07 19:08:45 -06:00
|
|
|
|
|
|
|
if state == swsscommon.Select.OBJECT:
|
2019-07-17 18:04:01 -05:00
|
|
|
(key, op, fvp) = sst_confdb.pop()
|
|
|
|
if fvp:
|
|
|
|
fvp_dict = dict(fvp)
|
|
|
|
|
|
|
|
# handle config change
|
|
|
|
if (fvp_dict.has_key("alias") or fvp_dict.has_key("description")) and (op in ["SET", "DEL"]):
|
|
|
|
if self.is_port_up(key):
|
|
|
|
self.generate_pending_lldp_config_cmd_for_port(key)
|
|
|
|
else:
|
|
|
|
self.pending_cmds.pop(key, None)
|
|
|
|
|
|
|
|
(key, op, fvp) = sst_appdb.pop()
|
|
|
|
if (key != "PortInitDone") and (key != "PortConfigDone"):
|
|
|
|
if fvp:
|
|
|
|
fvp_dict = dict(fvp)
|
|
|
|
|
|
|
|
# handle port status change
|
|
|
|
if fvp_dict.has_key("oper_status"):
|
|
|
|
if "up" in fvp_dict.get("oper_status"):
|
|
|
|
self.generate_pending_lldp_config_cmd_for_port(key)
|
|
|
|
else:
|
|
|
|
self.pending_cmds.pop(key, None)
|
2019-01-30 03:05:36 -06:00
|
|
|
|
2018-03-07 19:08:45 -06:00
|
|
|
# Process all pending commands
|
|
|
|
self.process_pending_cmds()
|
2018-03-02 18:46:22 -06:00
|
|
|
|
|
|
|
|
|
|
|
# ============================= 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()
|