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
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:
1 parent
ff86370f2f
commit
c8c9642af0
5 files changed
+81
-47
No files matched your search
@@ -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",
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
@@ -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