From 792267cd0713a6580c0e4b827b93575a6415208f Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Wed, 16 Aug 2023 15:41:45 +0200 Subject: [PATCH] Add initial LinkStateTask implementation This is an initial re-implementation of the link state monitor parts of Zino 1. Some bits and pieces are still missing, and some TODO points a strewn throughout. --- src/zino/tasks/linkstatetask.py | 122 ++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/zino/tasks/linkstatetask.py diff --git a/src/zino/tasks/linkstatetask.py b/src/zino/tasks/linkstatetask.py new file mode 100644 index 000000000..749b3e6b2 --- /dev/null +++ b/src/zino/tasks/linkstatetask.py @@ -0,0 +1,122 @@ +import logging +import re +from dataclasses import dataclass +from typing import Any + +from zino.snmp import SNMP, SparseWalkResponse +from zino.statemodels import InterfaceState, Port +from zino.tasks.task import Task + +_logger = logging.getLogger(__name__) + +BASE_POLL_LIST = ("ifIndex", "ifDescr", "ifAlias", "ifAdminStatus", "ifOperStatus", "ifLastChange") + + +@dataclass +class BaseInterfaceRow: + index: int + descr: str + alias: str + admin_status: str + oper_status: str + last_change: int + + +class LinkStateTask(Task): + """Fetches and stores state information about router ports/links. + + Things Zino 1 does at this point that this implementation ignores: + + 1. Zino 1 would fetch and record OLD-CISCO-INTERFACES-MIB::locIfReason, but this isn't very useful for anything + other than very old equipment. + + 2. Zino 1 collects and records interface stacking/layering information from IF-MIB::ifStackTable. It was used to + deem an interface as either significant or insignificant for making events about. It was used because Uninett's + old convention was to set interface descriptions on only the sub-unit of Juniper ports, but this is no longer the + case: Descriptions are mandated for both physical ports and their sub-units. + """ + + async def run(self): + snmp = SNMP(self.device) + poll_list = [("IF-MIB", column) for column in BASE_POLL_LIST] + attrs = await snmp.sparsewalk(*poll_list) + _logger.debug("%s ifattrs: %r", self.device.name, attrs) + + self._update_attrs(attrs) + + def _update_attrs(self, new_attrs: SparseWalkResponse): + for index, row in new_attrs.items(): + self._update_single_interface(row) + + def _update_single_interface(self, row: dict[str, Any]): + data = BaseInterfaceRow(*(row.get(attr) for attr in BASE_POLL_LIST)) + + # First a few sanity checks + if not data.descr: + return + if not data.index: + return + + # If watch pattern exists, only watch matching interfaces + if self.device.watchpat: + if not re.match(self.device.watchpat, data.descr): + _logger.debug("%s intf %s not watched", self.device.name, data.descr) + return + + # If ignore pattern exists, ignore matching interfaces + if self.device.ignorepat: + if re.match(self.device.ignorepat, data.descr): + _logger.debug("%s intf %s ignored", self.device.name, data.descr) + return + + # Now ensure we have a state object to record information in + ports = self.state.devices.get(self.device.name).ports + if data.index not in ports: + ports[data.index] = Port(ifindex=data.index) + port = ports[data.index] + + port.ifdescr = data.descr + + for attr in ("ifAdminStatus", "ifOperStatus"): + if not row.get(attr): + raise MissingInterfaceTableData(self.device.name, data.index, attr) + + state = f"admin{data.admin_status.capitalize()}" + # A special tweak so that we report ports in oper-down (but admin-up) state first time we see them + if not port.state and data.oper_status != "up" and state != "adminDown": + port.state = InterfaceState.UNKNOWN + if state == "adminUp": + state = data.oper_status + + state = InterfaceState(state) + if port.state and port.state != state: + # TODO make or update event + # TODO Re-verify state change after 2 minutes + _logger.info( + "%s port %s ix %s port changed state from %s to %s", + self.device.name, + data.descr, + data.index, + port.state, + state, + ) + + port.state = state + + async def _get_if_attrs(self, snmp: SNMP, attr: str) -> dict[int, str]: + response = await snmp.bulkwalk("IF-MIB", attr, max_repetitions=10) + if_attrs = {row.oid[-1]: row.value for row in response if row.value} + _logger.debug("%s %s: %r", self.device.name, attr, if_attrs) + return if_attrs + + def _record_if_attrs(self, attr: str, data: dict[int, str]): + ports = self.state.devices.get(self.device.name).ports + for ifindex, value in data.items(): + if ifindex not in ports: + ports[ifindex] = Port(ifindex=ifindex) + setattr(ports[ifindex], attr, value) + + +class MissingInterfaceTableData(Exception): + def __init__(self, router, port, variable): + super().__init__(f"No {variable} from {router} for port {port}")