sonic-buildimage/dockers/docker-lldp-sv2/lldpmgrd

252 lines
9.0 KiB
Plaintext
Raw Normal View History

#!/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_up(port_name):
filename = "/sys/class/net/%s/operstate" % port_name
if not os.path.exists(filename):
return False
with open(filename) as fp:
state = fp.read()
if 'up' in state:
return True
else:
return False
# ============================== 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 neighboring device's hostname and
corresponding port 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
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)
(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))
# 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_up(port_name):
# it doesn't make any sense to configure lldpd if the target port is unavailable
# 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):
"""
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.
"""
# 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)
# Listen indefinitely for changes to the PORT table in the State DB
while True:
(state, c) = sel.select(SELECT_TIMEOUT_MS)
if state == swsscommon.Select.OBJECT:
(key, op, fvp) = sst.pop()
fvp_dict = dict(fvp)
if op == "SET" and fvp_dict.get("state") == "ok":
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()