diff --git a/napalm_device_types/__init__.py b/napalm_device_types/__init__.py index 7f942a6..8a9a5d6 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.licenses import LicenseMixin from napalm_device_types.host_reboot import HostRebootMixin from napalm_device_types.interface_filter import InterfaceFilterMixin from napalm_device_types.kernel import KERNEL_FACTS_COMMAND, KernelFactsMixin, parse_kernel_facts @@ -114,6 +115,8 @@ from napalm_device_types.models import ( ManagementProtocolsDict, PowerAction, PowerStateDict, + HypervisorHostDict, + LicenseDict, FixResultDict, FixStepDict, FixStepOutcomeDict, @@ -167,6 +170,9 @@ from napalm_device_types.storage import StorageDriver from napalm_device_types.switch import SwitchDriver __all__ = [ + "HypervisorHostDict", + "LicenseDict", + "LicenseMixin", "EnsurePackageMixin", "FixResultDict", "FixStepDict", diff --git a/napalm_device_types/fixes.py b/napalm_device_types/fixes.py index 33c2ea0..9656960 100644 --- a/napalm_device_types/fixes.py +++ b/napalm_device_types/fixes.py @@ -140,6 +140,12 @@ class PackageProxyMixin: """Have the package manager fetch through *url*; an empty *url* removes it.""" ... + def get_package_proxy(self) -> Optional[str]: + """The proxy the package manager fetches through now; None for none. + + A fact: whether it is the right one is the caller's to judge.""" + ... + __all__ = [ "EnsurePackageMixin", diff --git a/napalm_device_types/hypervisor.py b/napalm_device_types/hypervisor.py index 32173a4..eb42575 100644 --- a/napalm_device_types/hypervisor.py +++ b/napalm_device_types/hypervisor.py @@ -16,6 +16,7 @@ from napalm_device_types.packages import PackageManagementMixin from napalm_device_types.health_metrics import HealthMetricsMixin from napalm_device_types.provisioning import QEMU_GUEST_AGENTS, GuestAgents from napalm_device_types.models import ( + HypervisorHostDict, NICConfigDict, NetworkTargetDict, SnapshotDict, @@ -637,6 +638,12 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri """ ... + def get_hypervisor_hosts(self) -> List[HypervisorHostDict]: + """The hosts this hypervisor or its manager runs, and how each stands: + connected, in maintenance, with configuration issues, under a central + manager. Facts; what is a problem is the caller's to decide.""" + ... + def get_vm_cpu_types(self) -> List[VMCpuTypeDict]: """ List the virtual CPU models a new VM may be given on the node it will diff --git a/napalm_device_types/licenses.py b/napalm_device_types/licenses.py new file mode 100644 index 0000000..df0c672 --- /dev/null +++ b/napalm_device_types/licenses.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +"""Licenses and subscriptions a device reports. + +A fact for the caller (docs/STRUCTURE.md): whether a free edition, a missing +subscription or a license a feature needs is a problem is decided above the +driver. Declared only; a driver that can read licenses implements it. +""" + +from __future__ import annotations + +from typing import List, TYPE_CHECKING + +from napalm_device_types.models import LicenseDict + + +class LicenseMixin: + if TYPE_CHECKING: # pragma: no cover - declared for type checkers only + + def get_licenses(self) -> List[LicenseDict]: + """Every license or subscription the device knows of, with its status. + + Example:: + + [{"name": "pve-subscription", "status": "none"}, + {"name": "vSphere", "status": "active", "edition": "Free"}] + """ + ... diff --git a/napalm_device_types/models.py b/napalm_device_types/models.py index 756d806..91cbc63 100644 --- a/napalm_device_types/models.py +++ b/napalm_device_types/models.py @@ -993,6 +993,33 @@ class HealthMetricsDict(TypedDict): interfaces: NotRequired[Dict[str, HealthMetricsIfaceDict]] +LicenseStatus = Literal["active", "missing", "expired", "invalid", "none", "unknown"] + + +class LicenseDict(TypedDict): + """A license or subscription the device reports (``LicenseMixin.get_licenses``). + + A fact, not a judgement: whether a free edition or a missing subscription is + a problem is the caller's to decide. + """ + + name: str # what it licenses, as the device names it + status: LicenseStatus # "none": the device runs without one, as installed + edition: NotRequired[str] # the edition or level, e.g. "Free", "Enterprise Plus" + expires: NotRequired[Optional[str]] # ISO date, None for no expiry + detail: NotRequired[str] # the device's own words, e.g. why a feature is refused + + +class HypervisorHostDict(TypedDict): + """One host of a hypervisor or cluster (``HypervisorDriver.get_hypervisor_hosts``).""" + + name: str + connected: Optional[bool] # reachable by its manager; None where it cannot tell + maintenance: Optional[bool] # in maintenance mode + config_issues: List[str] # the manager's configuration warnings, verbatim + managed_by: NotRequired[Optional[str]] # the address of a central manager, if any + + class FixStepOutcomeDict(TypedDict): """What one step of a fix routine reports (see ``napalm_device_types.fixes``).""" diff --git a/pyproject.toml b/pyproject.toml index ada551f..2415910 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "napalm-device-types" -version = "4.13.0" +version = "4.14.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 new file mode 100644 index 0000000..11ddcca --- /dev/null +++ b/tests/test_fact_getters.py @@ -0,0 +1,40 @@ +"""Facts instead of warnings (docs/STRUCTURE.md, NetOrk/netork#1111). + +A driver reports what it reads; the application decides what is a warning. Three +facts that drivers so far only reported as warning codes get contracts of their +own: the package manager's proxy, licenses and subscriptions, and the state of a +hypervisor's hosts. All are declared only, so a capability check stays truthful. +""" + +from __future__ import annotations + +from typing import get_type_hints + +from napalm_device_types import ( + HypervisorDriver, + HypervisorHostDict, + LicenseDict, + LicenseMixin, + PackageProxyMixin, +) + + +def test_a_license_says_what_it_is_and_whether_it_holds(): + hints = get_type_hints(LicenseDict) + assert {"name", "status"} <= set(hints) + assert LicenseDict.__required_keys__ == frozenset({"name", "status"}) + assert set(hints) == {"name", "status", "edition", "expires", "detail"} + + +def test_a_hypervisor_host_says_how_it_stands(): + hints = get_type_hints(HypervisorHostDict) + assert set(hints) == {"name", "connected", "maintenance", "config_issues", "managed_by"} + assert HypervisorHostDict.__required_keys__ == frozenset( + {"name", "connected", "maintenance", "config_issues"} + ) + + +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")