feat: report the firmware Netgear offers, not a warning (0.4.0) #12
No files matched your search
@@ -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",
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user