From 14fede3e564a38e9bbce96b7966ce2d7535aa51e Mon Sep 17 00:00:00 2001 From: Ahmed Karic Date: Wed, 12 Feb 2025 10:14:31 +0100 Subject: [PATCH] statd: Add LLDP operational data retrieval Enable statd to fetch LLDP neighbor data via lldpcli for verifying LLDP admin status. Currently, only chassis ID and chassis ID subtype are retrieved. --- src/confd/yang/infix-lldp.yang | 4 -- src/statd/python/yanger/__main__.py | 4 +- src/statd/python/yanger/infix_lldp.py | 96 +++++++++++++++++++++++++++ src/statd/statd.c | 3 + 4 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 src/statd/python/yanger/infix_lldp.py diff --git a/src/confd/yang/infix-lldp.yang b/src/confd/yang/infix-lldp.yang index 2de0a728..375c3077 100644 --- a/src/confd/yang/infix-lldp.yang +++ b/src/confd/yang/infix-lldp.yang @@ -76,10 +76,6 @@ module infix-lldp { deviation "/lldp:lldp/lldp:port/lldp:rx-statistics" { deviate not-supported; } - deviation "/lldp:lldp/lldp:port/lldp:remote-systems-data" { - deviate not-supported; - } - deviation "/lldp:lldp/lldp:port/lldp:message-fast-tx" { deviate not-supported; diff --git a/src/statd/python/yanger/__main__.py b/src/statd/python/yanger/__main__.py index 8c898c29..bf69178f 100644 --- a/src/statd/python/yanger/__main__.py +++ b/src/statd/python/yanger/__main__.py @@ -1,6 +1,5 @@ import logging import logging.handlers -import subprocess import json import sys # (built-in module) import os @@ -76,6 +75,9 @@ def main(): elif args.model == 'ietf-system': from . import ietf_system yang_data = ietf_system.operational() + elif args.model == 'ieee802-dot1ab-lldp': + from . import infix_lldp + yang_data = infix_lldp.operational() else: common.LOG.warning("Unsupported model %s", args.model) sys.exit(1) diff --git a/src/statd/python/yanger/infix_lldp.py b/src/statd/python/yanger/infix_lldp.py new file mode 100644 index 00000000..fcd5f5f5 --- /dev/null +++ b/src/statd/python/yanger/infix_lldp.py @@ -0,0 +1,96 @@ +from .host import HOST +from collections import defaultdict + +def operational(): + """Retrieve LLDP neighbor information and store in remote-systems-data under the correct port.""" + + # Reference: https://www.ieee802.org/1/files/public/YANGs/ieee802-types.yang + subtype_mapping = { + "component": "chassis-component", + "ifalias": "interface-alias", + "port": "port-component", + "mac": "mac-address", + "ip": "network-address", + "ifname": "interface-name", + "local": "local" + } + + DEFAULT_MAC = "00-00-00-00-00-00" + + port_data = defaultdict(lambda: {"remote-systems-data": [], "dest-mac-address": None}) + + data = HOST.run_json(["lldpcli", "show", "neighbors", "-f", "json"]) + + interfaces = data.get("lldp", {}).get("interface", []) + + if isinstance(interfaces, dict): + interfaces = [interfaces] + + for iface_entry in interfaces: + for iface_name, iface_data in iface_entry.items(): + remote_index = int(iface_data.get("rid", 0)) + time_mark = parse_time(iface_data.get("age")) + + chassis = iface_data.get("chassis", {}) + chassis_id_type, chassis_id_value = extract_chassis_id(chassis, subtype_mapping) + + port_info = iface_data.get("port", {}) + port_id_type = subtype_mapping.get(port_info.get("id", {}).get("type"), "unknown") + port_id_value = port_info.get("id", {}).get("value", "") + + dest_mac_address = ( + chassis_id_value.replace(":", "-") if chassis_id_type == "mac-address" else + port_id_value.replace(":", "-") if port_id_type == "mac-address" else + DEFAULT_MAC + ) + + remote_entry = { + "time-mark": time_mark, + "remote-index": remote_index, + "chassis-id-subtype": chassis_id_type, + "chassis-id": chassis_id_value, + "port-id-subtype": port_id_type, + "port-id": port_id_value + } + + port_data[iface_name]["remote-systems-data"].append(remote_entry) + + if port_data[iface_name]["dest-mac-address"] is None: + port_data[iface_name]["dest-mac-address"] = dest_mac_address + + formatted_output = { + "ieee802-dot1ab-lldp:lldp": { + "port": [ + { + "name": port_name, + "dest-mac-address": port_info["dest-mac-address"], + "remote-systems-data": port_info["remote-systems-data"] + } + for port_name, port_info in port_data.items() if port_info["remote-systems-data"] + ] + } + } + + return formatted_output + +def extract_chassis_id(chassis_block, subtype_mapping): + if "id" in chassis_block: + id_info = chassis_block["id"] + return subtype_mapping.get(id_info.get("type"), "unknown"), id_info.get("value", "") + + for _, value in chassis_block.items(): + if isinstance(value, dict) and "id" in value: + id_info = value["id"] + return subtype_mapping.get(id_info.get("type"), "unknown"), id_info.get("value", "") + + return "unknown", "" + +def parse_time(time_str): + """Convert LLDP time format to seconds""" + import re + if time_str: + match = re.search(r"(\d+)\s*day[s]*,\s*(\d+):(\d+):(\d+)", time_str) + if match: + days, hours, minutes, seconds = map(int, match.groups()) + return days * 86400 + hours * 3600 + minutes * 60 + seconds + return 0 diff --git a/src/statd/statd.c b/src/statd/statd.c index 062ffeaf..1fa66e41 100644 --- a/src/statd/statd.c +++ b/src/statd/statd.c @@ -41,6 +41,7 @@ #define XPATH_ROUTING_OSPF XPATH_ROUTING_BASE "/ospf" #define XPATH_CONTAIN_BASE "/infix-containers:containers" #define XPATH_DHCP_SERVER_BASE "/infix-dhcp-server:dhcp-server" +#define XPATH_LLDP_BASE "/ieee802-dot1ab-lldp:lldp" TAILQ_HEAD(sub_head, sub); @@ -344,6 +345,8 @@ static int subscribe_to_all(struct statd *statd) return SR_ERR_INTERNAL; if (subscribe(statd, "ietf-system", XPATH_SYSTEM_BASE, sr_generic_cb)) return SR_ERR_INTERNAL; + if (subscribe(statd, "ieee802-dot1ab-lldp", XPATH_LLDP_BASE, sr_generic_cb)) + return SR_ERR_INTERNAL; #ifdef CONTAINERS if (subscribe(statd, "infix-containers", XPATH_CONTAIN_BASE, sr_generic_cb)) return SR_ERR_INTERNAL;