From 59fb1273e8ba01c04c7bc3515a97615fc9991e45 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Sat, 17 Oct 2026 13:00:43 +0200 Subject: [PATCH] feat: the firmware a vendor offers, as a fact (4.16.0) FirmwareOfferMixin.get_firmware_offer() -> FirmwareOfferDict {current_version, latest_version, newer, download_url?}: what runs and the newest firmware the vendor offers, from its download catalogue or the device's own update check. Comparing vendor version strings stays with the driver, so it says whether the offer is newer; whether that is a warning is the application's call (NetOrk/netork#1111). Declared only. --- napalm_device_types/__init__.py | 4 ++++ napalm_device_types/firmware.py | 32 ++++++++++++++++++++++++++++++++ napalm_device_types/models.py | 10 ++++++++++ pyproject.toml | 2 +- tests/test_fact_getters.py | 15 +++++++++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 napalm_device_types/firmware.py diff --git a/napalm_device_types/__init__.py b/napalm_device_types/__init__.py index 8a9a5d6..596cc8c 100644 --- a/napalm_device_types/__init__.py +++ b/napalm_device_types/__init__.py @@ -90,6 +90,7 @@ from napalm_device_types.fixes import ( run_fix_steps, ) from napalm_device_types.health_metrics import HealthMetricsMixin +from napalm_device_types.firmware import FirmwareOfferMixin from napalm_device_types.licenses import LicenseMixin from napalm_device_types.host_reboot import HostRebootMixin from napalm_device_types.interface_filter import InterfaceFilterMixin @@ -116,6 +117,7 @@ from napalm_device_types.models import ( PowerAction, PowerStateDict, HypervisorHostDict, + FirmwareOfferDict, LicenseDict, FixResultDict, FixStepDict, @@ -171,6 +173,8 @@ from napalm_device_types.switch import SwitchDriver __all__ = [ "HypervisorHostDict", + "FirmwareOfferDict", + "FirmwareOfferMixin", "LicenseDict", "LicenseMixin", "EnsurePackageMixin", diff --git a/napalm_device_types/firmware.py b/napalm_device_types/firmware.py new file mode 100644 index 0000000..aed95e9 --- /dev/null +++ b/napalm_device_types/firmware.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +"""The firmware a vendor offers for a device. + +A fact for the caller (docs/STRUCTURE.md): a driver may know the newer firmware +from the vendor's download catalogue or the device's own update check; whether +running an older one is a problem is decided above the driver. Comparing the +vendor's version strings is vendor knowledge, so the driver says itself whether +the offer is newer. Declared only; a driver that can tell implements it. +""" + +from __future__ import annotations + +from typing import Optional, TYPE_CHECKING + +from napalm_device_types.models import FirmwareOfferDict + + +class FirmwareOfferMixin: + if TYPE_CHECKING: # pragma: no cover - declared for type checkers only + + def get_firmware_offer(self) -> Optional[FirmwareOfferDict]: + """The firmware running and the newest the vendor offers. + + None when the driver cannot tell for this device (an unknown model, + a catalogue that did not answer). + + Example:: + + {"current_version": "1.0.2.10", "latest_version": "1.0.2.12", + "newer": True, "download_url": "https://.../GS308E_1.0.2.12.zip"} + """ + ... diff --git a/napalm_device_types/models.py b/napalm_device_types/models.py index 0a4e9d9..cb9e8d9 100644 --- a/napalm_device_types/models.py +++ b/napalm_device_types/models.py @@ -1015,6 +1015,16 @@ class LicenseDict(TypedDict): withholds: NotRequired[List[str]] +class FirmwareOfferDict(TypedDict): + """The firmware a device runs and the newest its vendor offers + (``FirmwareOfferMixin.get_firmware_offer``). A fact, not a judgement.""" + + current_version: str + latest_version: str + newer: bool # the offer is newer than what runs, by the vendor's versioning + download_url: NotRequired[str] + + class HypervisorHostDict(TypedDict): """One host of a hypervisor or cluster (``HypervisorDriver.get_hypervisor_hosts``).""" diff --git a/pyproject.toml b/pyproject.toml index 9eab2a9..25af5dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "napalm-device-types" -version = "4.15.0" +version = "4.16.0" description = "Abstract device-type base classes for NAPALM drivers" readme = "README.md" requires-python = ">=3.10" diff --git a/tests/test_fact_getters.py b/tests/test_fact_getters.py index b6fa4e7..f715bfc 100644 --- a/tests/test_fact_getters.py +++ b/tests/test_fact_getters.py @@ -47,3 +47,18 @@ def test_the_getters_are_declared_only(): assert not hasattr(PackageProxyMixin, "get_package_proxy") assert not hasattr(LicenseMixin, "get_licenses") assert not hasattr(HypervisorDriver, "get_hypervisor_hosts") + + +def test_a_firmware_offer_says_what_runs_and_what_is_offered(): + """The vendor's newer firmware is a fact the driver can know (a download + catalogue, the device's own update check); whether it is a warning is the + application's call. Comparing vendor version strings stays with the driver, + so it says itself whether the offer is newer.""" + from napalm_device_types import FirmwareOfferDict, FirmwareOfferMixin + + hints = get_type_hints(FirmwareOfferDict) + assert set(hints) == {"current_version", "latest_version", "newer", "download_url"} + assert FirmwareOfferDict.__required_keys__ == frozenset( + {"current_version", "latest_version", "newer"} + ) + assert not hasattr(FirmwareOfferMixin, "get_firmware_offer")