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
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
# ---------------------------------------------------------------------------
@@ -144,7 +144,7 @@ def _fw_get_latest(model: str) -> str | None:
return latest
class NetgearPlusDriver(SwitchDriver):
class NetgearPlusDriver(FirmwareOfferMixin, SwitchDriver):
"""HTTP-based NAPALM driver for Netgear Plus Smart Managed switches."""
VENDOR = "Netgear"
@@ -753,50 +753,29 @@ class NetgearPlusDriver(SwitchDriver):
Returns None to indicate SNMP health monitoring is not supported."""
return None
def get_device_warnings(self) -> list:
"""Check for available firmware updates and return a warning if the
device is not running the latest known firmware.
def get_firmware_offer(self) -> dict | None:
"""The firmware running and the newest Netgear offers for this model.
The latest version is looked up once per 24 hours (per model, per
worker process) via lightweight HEAD requests against Netgear's
download CDN. Returns an empty list when the model is unknown or
when the check fails.
The newest is looked up once per 24 hours (per model, per worker
process) with HEAD requests against Netgear's download CDN. None when
the model is unknown, a version cannot be read or compared, or the
check fails. Whether an older firmware is a warning is the caller's to
decide (napalm-device-types ``FirmwareOfferMixin``).
"""
warnings: list = []
try:
model_name: str = getattr(
self._connector.switch_model, "MODEL_NAME", ""
) or self._connector.switch_model.__class__.__name__
current_fw: str = self._get_switch_infos().get("switch_firmware", "")
if not model_name or not current_fw:
return warnings
latest_fw = _fw_get_latest(model_name)
latest_fw = _fw_get_latest(model_name) if model_name and current_fw else None
if latest_fw is None:
return warnings
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,
},
}
)
return None
newer = _fw_version_tuple(latest_fw) > _fw_version_tuple(current_fw)
except Exception:
pass
return warnings
return None
return {
"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 ──────────────────────────────────────────────────────────
def get_device_warnings(self) -> list:
"""Return device warnings. SNMP detection handled by poll task."""
return []
def get_snmp_config(self):
"""Return SNMP config if a community is configured on the switch."""
try:
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "napalm-netgear"
version = "0.3.0"
version = "0.4.0"
description = "NAPALM driver for Netgear switches (Plus/Smart)"
readme = "README.md"
license = { text = "Apache-2.0" }
@@ -26,7 +26,7 @@ classifiers = [
]
dependencies = [
"napalm>=4.0.0",
"napalm-device-types>=4.13.0",
"napalm-device-types>=4.16.0",
"netmiko>=4.0.0",
"paramiko>=5.0.0", # CVE-2026-44405
"netaddr",
+1 -1
View File
@@ -1,4 +1,4 @@
napalm>=4.0.0
napalm-device-types>=4.13.0
napalm-device-types>=4.16.0
netmiko>=4.0.0
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)