From 6b305c3d4778d93fd9ac78a12370ca6762462144 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Fri, 16 Oct 2026 12:03:00 +0200 Subject: [PATCH] feat: declare the SNMP health OIDs instead of collecting them (0.2.0) netgear_smart no longer collects over SNMP. SNMP_HEALTH_PROFILE states where the switch publishes uptime (standard sysUpTime), memory (total and free in KiB) and CPU (a text value whose five-second figure counts); the application polls and computes (napalm-device-types docs/STRUCTURE.md). Interface counters now come from the 64-bit ifXTable where the switch has it, as the application reads them for every device. --- napalm_netgear/netgear_smart.py | 88 ++++++--------------------------- pyproject.toml | 2 +- tests/unit/test_snmp_profile.py | 33 +++++++++++++ 3 files changed, 48 insertions(+), 75 deletions(-) create mode 100644 tests/unit/test_snmp_profile.py diff --git a/napalm_netgear/netgear_smart.py b/napalm_netgear/netgear_smart.py index 38d8c30..43e24cc 100644 --- a/napalm_netgear/netgear_smart.py +++ b/napalm_netgear/netgear_smart.py @@ -30,7 +30,7 @@ CLI notes import re import socket import time -from typing import Any, Dict, List, Optional, Union +from typing import Any, ClassVar, Dict, List, Optional, Union import netaddr from netmiko import ConnectHandler @@ -69,6 +69,19 @@ class NetgearSmartDriver(ConfigLifecycleMixin, SwitchDriver): # Netmiko device type – handles prompt ``(hostname) >`` / ``(hostname) #`` NETMIKO_DEVICE_TYPE = "netgear_prosafe" + #: Where the switch publishes its health values over SNMP + #: (napalm-device-types ``SnmpHealthProfile``); the application polls. + #: CPU is text ("5 Secs ( 3.2%) 60 Secs ( …"); the five-second figure counts. + SNMP_HEALTH_PROFILE: ClassVar[Dict[str, Any]] = { + "uptime": {"oids": ["1.3.6.1.2.1.1.3.0"]}, + "cpu": {"oids": ["1.3.6.1.4.1.4526.100.1.1.4.9.0"], "pattern": r"5\s+Secs\s*\(\s*([\d.]+)\s*%\)"}, + "memory": { + "total": {"oids": ["1.3.6.1.4.1.4526.100.1.1.4.2.0"]}, + "free": {"oids": ["1.3.6.1.4.1.4526.100.1.1.4.1.0"]}, + "unit": "KiB", + }, + } + # PoE priority mapping between the CLI's "low/high/critical" and the # generic priority strings used across drivers. _POE_PRIORITY_MAP = {"low": "PPP_LOW", "high": "PPP_HIGH", "critical": "PPP_CRITICAL"} @@ -2040,76 +2053,3 @@ class NetgearSmartDriver(ConfigLifecycleMixin, SwitchDriver): finally: self._exit_config_mode() self._save_config() - - # ------------------------------------------------------------------ - # Health metrics (SNMP) - # ------------------------------------------------------------------ - - @classmethod - async def get_health_metrics(cls, snmp_get, snmp_walk) -> dict: - import asyncio - import re - from napalm_device_types._ucd_metrics import build_if_metrics, ticks_to_seconds - - _OID_SNMP_UPTIME = "1.3.6.1.2.1.1.3.0" - _NETGEAR_MEM_FREE = "1.3.6.1.4.1.4526.100.1.1.4.1.0" - _NETGEAR_MEM_TOTAL = "1.3.6.1.4.1.4526.100.1.1.4.2.0" - _NETGEAR_CPU_STR = "1.3.6.1.4.1.4526.100.1.1.4.9.0" - _OID_IF_DESCR = "1.3.6.1.2.1.2.2.1.2" - _OID_IF_SPEED = "1.3.6.1.2.1.2.2.1.5" - _OID_IF_IN_OCT = "1.3.6.1.2.1.2.2.1.10" - _OID_IF_OUT_OCT = "1.3.6.1.2.1.2.2.1.16" - _OID_IF_IN_ERR = "1.3.6.1.2.1.2.2.1.14" - _OID_IF_OUT_ERR = "1.3.6.1.2.1.2.2.1.20" - - ( - uptime_raw, - free_s, - total_s, - cpu_s, - descr, - speed, - in_oct, - out_oct, - in_err, - out_err, - ) = await asyncio.gather( - snmp_get(_OID_SNMP_UPTIME), - snmp_get(_NETGEAR_MEM_FREE), - snmp_get(_NETGEAR_MEM_TOTAL), - snmp_get(_NETGEAR_CPU_STR), - snmp_walk(_OID_IF_DESCR), - snmp_walk(_OID_IF_SPEED), - snmp_walk(_OID_IF_IN_OCT), - snmp_walk(_OID_IF_OUT_OCT), - snmp_walk(_OID_IF_IN_ERR), - snmp_walk(_OID_IF_OUT_ERR), - ) - - metrics: dict = {} - - secs = ticks_to_seconds(uptime_raw) - if secs is not None: - metrics["uptime_seconds"] = secs - - if free_s and total_s: - try: - free_kb = int(free_s) - total_kb = int(total_s) - used_kb = max(0, total_kb - free_kb) - metrics["memory_total_bytes"] = total_kb * 1024 - metrics["memory_used_bytes"] = used_kb * 1024 - metrics["memory_percent"] = round(used_kb / total_kb * 100, 1) if total_kb else 0.0 - except ValueError: - pass - - if cpu_s: - m = re.search(r"5\s+Secs\s*\(\s*([\d.]+)\s*%\)", cpu_s) - if m: - try: - metrics["cpu_percent"] = float(m.group(1)) - except ValueError: - pass - - build_if_metrics(metrics, descr, speed, in_oct, out_oct, in_err, out_err) - return metrics diff --git a/pyproject.toml b/pyproject.toml index c9204fe..bdaf042 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "napalm-netgear" -version = "0.1.0" +version = "0.2.0" description = "NAPALM driver for Netgear switches (Plus/Smart)" readme = "README.md" license = { text = "Apache-2.0" } diff --git a/tests/unit/test_snmp_profile.py b/tests/unit/test_snmp_profile.py new file mode 100644 index 0000000..abc24ac --- /dev/null +++ b/tests/unit/test_snmp_profile.py @@ -0,0 +1,33 @@ +"""The switch's SNMP health OIDs, declared as data (napalm-device-types docs/STRUCTURE.md). + +The driver does not collect over SNMP; the application polls with this profile. +""" + +import re + +from napalm_netgear.netgear_smart import NetgearSmartDriver + +PROFILE = NetgearSmartDriver.SNMP_HEALTH_PROFILE + + +def test_the_driver_does_not_collect_itself(): + assert "get_health_metrics" not in vars(NetgearSmartDriver) + + +def test_uptime_from_the_standard_sysuptime(): + assert PROFILE["uptime"] == {"oids": ["1.3.6.1.2.1.1.3.0"]} + + +def test_memory_from_total_and_free_in_kib(): + assert PROFILE["memory"] == { + "total": {"oids": ["1.3.6.1.4.1.4526.100.1.1.4.2.0"]}, + "free": {"oids": ["1.3.6.1.4.1.4526.100.1.1.4.1.0"]}, + "unit": "KiB", + } + + +def test_cpu_is_the_five_second_figure_in_a_text_value(): + cpu = PROFILE["cpu"] + assert cpu["oids"] == ["1.3.6.1.4.1.4526.100.1.1.4.9.0"] + text = " 5 Secs ( 3.2%) 60 Secs ( 4.1%) 300 Secs ( 2.0%)" + assert re.search(cpu["pattern"], text).group(1) == "3.2"