feat: report the firmware Netgear offers, not a warning (0.4.0)
CI / test (3.10) (push) Failing after 16s
CI / test (3.11) (push) Successful in 23s
CI / test (3.12) (push) Successful in 26s
CI / test (3.10) (pull_request) Successful in 25s
CI / test (3.11) (pull_request) Successful in 23s
CI / test (3.12) (pull_request) Successful in 24s

The Plus driver looked the newest firmware up in Netgear's download
catalogue and named firmware_update_available itself. It now reports
get_firmware_offer() (napalm-device-types 4.16 FirmwareOfferMixin): what
runs, the newest offered, whether that is newer by Netgear's versioning,
and the download link; netOrk names the warning (netOrk #1111). The Smart
driver's empty get_device_warnings goes too. Requires
napalm-device-types 4.16.0.
This commit is contained in:
Christian Manivong committed 2026-10-17 13:02:36 +02:00
1 parent ff86370f2f
commit c8c9642af0
5 files changed
+81 -47

No files matched your search

+19 -40
View File
@@ -26,7 +26,7 @@ import threading
import time import time
from typing import Any from typing import Any
from napalm_device_types import FingerprintRule, SwitchDriver from napalm_device_types import FingerprintRule, FirmwareOfferMixin, SwitchDriver
from napalm.base.exceptions import ConnectionException from napalm.base.exceptions import ConnectionException
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -144,7 +144,7 @@ def _fw_get_latest(model: str) -> str | None:
return latest return latest
class NetgearPlusDriver(SwitchDriver): class NetgearPlusDriver(FirmwareOfferMixin, SwitchDriver):
"""HTTP-based NAPALM driver for Netgear Plus Smart Managed switches.""" """HTTP-based NAPALM driver for Netgear Plus Smart Managed switches."""
VENDOR = "Netgear" VENDOR = "Netgear"
@@ -753,50 +753,29 @@ class NetgearPlusDriver(SwitchDriver):
Returns None to indicate SNMP health monitoring is not supported.""" Returns None to indicate SNMP health monitoring is not supported."""
return None return None
def get_device_warnings(self) -> list: def get_firmware_offer(self) -> dict | None:
"""Check for available firmware updates and return a warning if the """The firmware running and the newest Netgear offers for this model.
device is not running the latest known firmware.
The latest version is looked up once per 24 hours (per model, per The newest is looked up once per 24 hours (per model, per worker
worker process) via lightweight HEAD requests against Netgear's process) with HEAD requests against Netgear's download CDN. None when
download CDN. Returns an empty list when the model is unknown or the model is unknown, a version cannot be read or compared, or the
when the check fails. check fails. Whether an older firmware is a warning is the caller's to
decide (napalm-device-types ``FirmwareOfferMixin``).
""" """
warnings: list = []
try: try:
model_name: str = getattr( model_name: str = getattr(
self._connector.switch_model, "MODEL_NAME", "" self._connector.switch_model, "MODEL_NAME", ""
) or self._connector.switch_model.__class__.__name__ ) or self._connector.switch_model.__class__.__name__
current_fw: str = self._get_switch_infos().get("switch_firmware", "") current_fw: str = self._get_switch_infos().get("switch_firmware", "")
latest_fw = _fw_get_latest(model_name) if model_name and current_fw else None
if not model_name or not current_fw:
return warnings
latest_fw = _fw_get_latest(model_name)
if latest_fw is None: if latest_fw is None:
return warnings return None
newer = _fw_version_tuple(latest_fw) > _fw_version_tuple(current_fw)
try:
current_t = _fw_version_tuple(current_fw)
latest_t = _fw_version_tuple(latest_fw)
except ValueError:
return warnings
if latest_t > current_t:
warnings.append(
{
"code": "firmware_update_available",
"meta": {
"download_url": (
f"{_FW_CDN_BASE}/{model_name}"
f"/{model_name}_{latest_fw}.zip"
),
"latest_version": latest_fw,
"current_version": current_fw,
},
}
)
except Exception: except Exception:
pass return None
return {
return warnings "current_version": current_fw,
"latest_version": latest_fw,
"newer": newer,
"download_url": f"{_FW_CDN_BASE}/{model_name}/{model_name}_{latest_fw}.zip",
}
-4
View File
@@ -1838,10 +1838,6 @@ class NetgearSmartDriver(ConfigLifecycleMixin, SnmpAgentMixin, SwitchDriver):
# ── SNMP / Health ────────────────────────────────────────────────────────── # ── SNMP / Health ──────────────────────────────────────────────────────────
def get_device_warnings(self) -> list:
"""Return device warnings. SNMP detection handled by poll task."""
return []
def get_snmp_config(self): def get_snmp_config(self):
"""Return SNMP config if a community is configured on the switch.""" """Return SNMP config if a community is configured on the switch."""
try: try:
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "napalm-netgear" name = "napalm-netgear"
version = "0.3.0" version = "0.4.0"
description = "NAPALM driver for Netgear switches (Plus/Smart)" description = "NAPALM driver for Netgear switches (Plus/Smart)"
readme = "README.md" readme = "README.md"
license = { text = "Apache-2.0" } license = { text = "Apache-2.0" }
@@ -26,7 +26,7 @@ classifiers = [
] ]
dependencies = [ dependencies = [
"napalm>=4.0.0", "napalm>=4.0.0",
"napalm-device-types>=4.13.0", "napalm-device-types>=4.16.0",
"netmiko>=4.0.0", "netmiko>=4.0.0",
"paramiko>=5.0.0", # CVE-2026-44405 "paramiko>=5.0.0", # CVE-2026-44405
"netaddr", "netaddr",
+1 -1
View File
@@ -1,4 +1,4 @@
napalm>=4.0.0 napalm>=4.0.0
napalm-device-types>=4.13.0 napalm-device-types>=4.16.0
netmiko>=4.0.0 netmiko>=4.0.0
netaddr netaddr
+59
View File
@@ -0,0 +1,59 @@
"""The newest firmware Netgear offers, as a fact (netOrk #1111).
The Plus driver looked the newest firmware up in Netgear's download catalogue
and named ``firmware_update_available`` itself. It reports the offer now --
what runs, what is offered, whether that is newer by Netgear's versioning --
and the application decides on the warning.
"""
from __future__ import annotations
from types import SimpleNamespace
import pytest
from napalm_netgear import netgear_plus
from napalm_netgear.netgear_plus import NetgearPlusDriver
from napalm_netgear.netgear_smart import NetgearSmartDriver
def _driver(monkeypatch, *, model="GS308E", running="1.00.10", latest="1.00.12"):
d = NetgearPlusDriver("sw", "admin", "pw")
d._connector = SimpleNamespace(switch_model=SimpleNamespace(MODEL_NAME=model))
monkeypatch.setattr(d, "_get_switch_infos", lambda: {"switch_firmware": running})
monkeypatch.setattr(netgear_plus, "_fw_get_latest", lambda m: latest)
return d
def test_a_newer_offer(monkeypatch):
assert _driver(monkeypatch).get_firmware_offer() == {
"current_version": "1.00.10",
"latest_version": "1.00.12",
"newer": True,
"download_url": f"{netgear_plus._FW_CDN_BASE}/GS308E/GS308E_1.00.12.zip",
}
def test_running_the_newest(monkeypatch):
offer = _driver(monkeypatch, running="1.00.12").get_firmware_offer()
assert offer["newer"] is False
@pytest.mark.parametrize(
"kwargs",
[{"latest": None}, {"running": ""}, {"running": "beta"}],
ids=["catalogue-unknown", "no-version", "unparsable"],
)
def test_cannot_tell(monkeypatch, kwargs):
assert _driver(monkeypatch, **kwargs).get_firmware_offer() is None
def test_not_connected_cannot_tell(monkeypatch):
d = _driver(monkeypatch)
d._connector = None
assert d.get_firmware_offer() is None
@pytest.mark.parametrize("cls", [NetgearPlusDriver, NetgearSmartDriver])
def test_no_warnings_of_their_own(cls):
assert "get_device_warnings" not in vars(cls)