From c689253b3f0b75820188dc930d62c6b834a4fa4f Mon Sep 17 00:00:00 2001 From: Joe LeVeque Date: Fri, 2 Mar 2018 16:46:22 -0800 Subject: [PATCH] [LLDP] Add lldpmgrd Daemon to Manage LLDP Configuration (#1428) --- dockers/docker-lldp-sv2/Dockerfile.j2 | 56 ++++--- dockers/docker-lldp-sv2/lldpmgrd | 199 +++++++++++++++++++++++ dockers/docker-lldp-sv2/reconfigure.sh | 52 ------ dockers/docker-lldp-sv2/start.sh | 2 +- dockers/docker-lldp-sv2/supervisord.conf | 20 +-- rules/docker-lldp-sv2.mk | 2 +- 6 files changed, 245 insertions(+), 86 deletions(-) create mode 100755 dockers/docker-lldp-sv2/lldpmgrd delete mode 100755 dockers/docker-lldp-sv2/reconfigure.sh diff --git a/dockers/docker-lldp-sv2/Dockerfile.j2 b/dockers/docker-lldp-sv2/Dockerfile.j2 index bfb1f9ca67..f75e4ea314 100644 --- a/dockers/docker-lldp-sv2/Dockerfile.j2 +++ b/dockers/docker-lldp-sv2/Dockerfile.j2 @@ -1,37 +1,49 @@ FROM docker-config-engine -COPY \ -{% for deb in docker_lldp_sv2_debs.split(' ') -%} -debs/{{ deb }}{{' '}} -{%- endfor -%} -debs/ - -COPY python-wheels /python-wheels - -## Make apt-get non-interactive +# Make apt-get non-interactive ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y python-pip libbsd0 libevent-2.0-5 libjansson4 libwrap0 libxml2 libpci3 libperl5.20 +# Update apt's cache of available packages +RUN apt-get update -# Pre-install the fundamental packages -# Install Python SwSS SDK -# Install LLDP Sync Daemon +# Install dependencies +RUN apt-get install -y python-pip libbsd0 libevent-2.0-5 libjansson4 libwrap0 libxml2 libpci3 libperl5.20 libpython2.7 -RUN dpkg -i \ -{% for deb in docker_lldp_sv2_debs.split(' ') -%} -debs/{{ deb }}{{' '}} +{% if docker_lldp_sv2_debs.strip() -%} +# Copy locally-built Debian package dependencies +{%- for deb in docker_lldp_sv2_debs.split(' ') %} +COPY debs/{{ deb }} /debs/ {%- endfor %} -RUN pip install /python-wheels/swsssdk-2.0.1-py2-none-any.whl && \ - pip install /python-wheels/sonic_d-2.0.0-py2-none-any.whl && \ - apt-get remove -y python-pip && \ - apt-get purge -y && apt-get autoclean -y && apt-get autoremove -y && \ - rm -rf /debs /python-wheels ~/.cache +# Install locally-built Debian packages and implicitly install their dependencies +{%- for deb in docker_lldp_sv2_debs.split(' ') %} +RUN dpkg_apt() { [ -f $1 ] && { dpkg -i $1 || apt-get -y install -f; } || return 1; }; dpkg_apt /debs/{{ deb }} +{%- endfor %} +{%- endif %} + +{% if docker_lldp_sv2_whls.strip() -%} +# Copy locally-built Python wheel dependencies +{%- for whl in docker_lldp_sv2_whls.split(' ') %} +COPY python-wheels/{{ whl }} /python-wheels/ +{%- endfor %} + +# Install locally-built Python wheel dependencies +{%- for whl in docker_lldp_sv2_whls.split(' ') %} +RUN pip install /python-wheels/{{ whl }} +{%- endfor %} +{% endif %} + +# Clean up +RUN apt-get remove -y python-pip +RUN apt-get clean -y +RUN apt-get autoclean -y +RUN apt-get autoremove -y +RUN rm -rf /debs /python-wheels ~/.cache COPY ["start.sh", "/usr/bin/"] COPY ["supervisord.conf", "/etc/supervisor/conf.d/"] -COPY ["reconfigure.sh", "/usr/bin/"] COPY ["lldpd.conf.j2", "/usr/share/sonic/templates/"] COPY ["lldpd", "/etc/default/"] +COPY ["lldpmgrd", "/usr/bin/"] ENTRYPOINT ["/usr/bin/supervisord"] diff --git a/dockers/docker-lldp-sv2/lldpmgrd b/dockers/docker-lldp-sv2/lldpmgrd new file mode 100755 index 0000000000..dce64c10c2 --- /dev/null +++ b/dockers/docker-lldp-sv2/lldpmgrd @@ -0,0 +1,199 @@ +#!/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() diff --git a/dockers/docker-lldp-sv2/reconfigure.sh b/dockers/docker-lldp-sv2/reconfigure.sh deleted file mode 100755 index 515e771aa6..0000000000 --- a/dockers/docker-lldp-sv2/reconfigure.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash - -set -e - -# TODO: Listen to state database when it is ready -interfaces=$(sonic-cfggen -d -v "PORT.keys() | join(' ')") - -function wait_until_if_exists -{ - if=$1 - while [ ! -L /sys/class/net/"$if" ] ; - do - sleep 1 - done - echo interface "$if" is created -} - - -function wait_until_if_not_exists -{ - if=$1 - while [ -L /sys/class/net/"$if" ] ; - do - sleep 1 - done - echo interface "$if" is destroyed -} - - -while /bin/true ; -do - # wait until all interfaces are created - echo Wait until all interfaces are created - for i in $interfaces - do - wait_until_if_exists $i - done - - echo Wait 10 seconds while lldpd finds new interfaces - sleep 10 - - # apply lldpd configuration - echo Apply lldpd configuration - lldpcli -c /etc/lldpd.conf - - # wait until all interfaces are destroyed - echo Wait until all ifaces are destroyed - for i in $interfaces - do - wait_until_if_not_exists $i - done -done diff --git a/dockers/docker-lldp-sv2/start.sh b/dockers/docker-lldp-sv2/start.sh index 3a765c6f54..26337fb066 100755 --- a/dockers/docker-lldp-sv2/start.sh +++ b/dockers/docker-lldp-sv2/start.sh @@ -9,5 +9,5 @@ rm -f /var/run/rsyslogd.pid supervisorctl start rsyslogd supervisorctl start lldpd -supervisorctl start lldpd-conf-reload supervisorctl start lldp-syncd +supervisorctl start lldpmgrd diff --git a/dockers/docker-lldp-sv2/supervisord.conf b/dockers/docker-lldp-sv2/supervisord.conf index ab62d9ed2e..60334e1b8e 100644 --- a/dockers/docker-lldp-sv2/supervisord.conf +++ b/dockers/docker-lldp-sv2/supervisord.conf @@ -26,15 +26,7 @@ stderr_logfile=syslog # - `-ddd` means to stay in foreground, log warnings and info to console # - `-dddd` means to stay in foreground, log all to console command=/usr/sbin/lldpd -d -I Ethernet*,eth* -priority=100 -autostart=false -autorestart=false -stdout_logfile=syslog -stderr_logfile=syslog - -[program:lldpd-conf-reload] -command=/usr/bin/reconfigure.sh -priority=150 +priority=3 autostart=false autorestart=false stdout_logfile=syslog @@ -42,7 +34,15 @@ stderr_logfile=syslog [program:lldp-syncd] command=/usr/bin/env python2 -m lldp_syncd -priority=200 +priority=4 +autostart=false +autorestart=false +stdout_logfile=syslog +stderr_logfile=syslog + +[program:lldpmgrd] +command=/usr/bin/lldpmgrd +priority=5 autostart=false autorestart=false stdout_logfile=syslog diff --git a/rules/docker-lldp-sv2.mk b/rules/docker-lldp-sv2.mk index d9500d1d07..93f08a5412 100644 --- a/rules/docker-lldp-sv2.mk +++ b/rules/docker-lldp-sv2.mk @@ -2,7 +2,7 @@ DOCKER_LLDP_SV2 = docker-lldp-sv2.gz $(DOCKER_LLDP_SV2)_PATH = $(DOCKERS_PATH)/docker-lldp-sv2 -$(DOCKER_LLDP_SV2)_DEPENDS += $(LLDPD) +$(DOCKER_LLDP_SV2)_DEPENDS += $(LLDPD) $(LIBSWSSCOMMON) $(PYTHON_SWSSCOMMON) $(DOCKER_LLDP_SV2)_PYTHON_WHEELS += $(DBSYNCD_PY2) $(DOCKER_LLDP_SV2)_LOAD_DOCKERS += $(DOCKER_CONFIG_ENGINE) SONIC_DOCKER_IMAGES += $(DOCKER_LLDP_SV2)