-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
cded5a1
commit 792267c
Showing
1 changed file
with
122 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}") |