feat: report the firmware Netgear offers, not a warning (0.4.0) #12

Merged
christianmanivong merged 1 commits from feat/firmware-offer into master 2026-10-17 11:10:40 +00:00
5 changed files with 81 additions and 47 deletions
Showing only changes of commit 4ccb2b5879 - Show all commits

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)