feat: fix routines as templates with the caller's values; deprecate run_device_action (4.12.0)
CI / test (3.10) (push) Successful in 29s
CI / test (3.11) (push) Successful in 26s
CI / test (3.12) (push) Successful in 30s
CI / test (3.13) (push) Successful in 30s
CI / test (3.10) (pull_request) Successful in 35s
CI / test (3.11) (pull_request) Successful in 33s
CI / test (3.12) (pull_request) Successful in 35s
CI / test (3.13) (pull_request) Successful in 34s

run_device_action was a string dispatch drivers filled with an
application's remediations, values hard-coded inside (community
"public", the caller's address, a /24 firewall rule).

napalm_device_types.fixes declares them as templates instead. A
template runs declared steps in order and stops at the first failure
(run_fix_steps); the driver implements the steps, or overrides the
whole method where its device takes the change in one call; every
value comes from the caller.

- SnmpAgentMixin.configure_snmp_agent(settings): install, configure,
  firewall, start. The caller verifies with its own SNMP client.
- EnsurePackageMixin.ensure_package(name): install only what is missing.
- PackageProxyMixin.configure_package_proxy(url): declared.
- ContainerEngineMixin.grant_container_engine_access(user): declared.

None is mixed into a role base, so a capability check stays truthful.

Closes #33
This commit is contained in:
christianmanivong committed 2026-10-16 12:06:19 +02:00
1 parent 64cc20ba00
commit 95222377f0
8 files changed
+389 -2

No files matched your search

+18
View File
@@ -81,6 +81,12 @@ from napalm_device_types.firewall import FirewallDriver
from napalm_device_types.hypervisor import HypervisorDriver
from napalm_device_types.os import OSDriver
from napalm_device_types.firewall_rules import FirewallRuleMixin
from napalm_device_types.fixes import (
EnsurePackageMixin,
PackageProxyMixin,
SnmpAgentMixin,
run_fix_steps,
)
from napalm_device_types.health_metrics import HealthMetricsMixin
from napalm_device_types.host_reboot import HostRebootMixin
from napalm_device_types.interface_filter import InterfaceFilterMixin
@@ -103,6 +109,10 @@ from napalm_device_types.models import (
ManagementProtocolsDict,
PowerAction,
PowerStateDict,
FixResultDict,
FixStepDict,
FixStepOutcomeDict,
SnmpAgentSettingsDict,
SnmpHealthProfile,
SnmpInterfacesSpec,
SnmpMemorySpec,
@@ -152,6 +162,14 @@ from napalm_device_types.storage import StorageDriver
from napalm_device_types.switch import SwitchDriver
__all__ = [
"EnsurePackageMixin",
"FixResultDict",
"FixStepDict",
"FixStepOutcomeDict",
"PackageProxyMixin",
"SnmpAgentMixin",
"SnmpAgentSettingsDict",
"run_fix_steps",
"SnmpHealthProfile",
"SnmpInterfacesSpec",
"SnmpMemorySpec",
+7 -1
View File
@@ -32,7 +32,7 @@ import shlex
from typing import Any, Dict, List, Optional, Sequence, TYPE_CHECKING, cast
from napalm_device_types.channel import ByteStream, CommandResult
from napalm_device_types.models import ContainerEngineDict
from napalm_device_types.models import ContainerEngineDict, FixResultDict
#: Engines this package knows how to reach, and the API each speaks.
ENGINES: Dict[str, str] = {"docker": "docker-engine"}
@@ -111,6 +111,12 @@ class ContainerEngineMixin:
def open_stream(self, command: str, *, privileged: bool = False) -> ByteStream: ...
def grant_container_engine_access(self, user: str) -> FixResultDict:
"""Let *user* reach the container engine without privilege (the
``docker`` group on most hosts). A fix template: see
:mod:`napalm_device_types.fixes`. Declared only; a driver implements it."""
...
def _container_engine_binary(self, engine: str) -> str:
"""The host command that is *engine*'s CLI. Override where it is not on PATH."""
return engine
+142
View File
@@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
"""Fix routines: changes an application wants made on a device, as templates.
A template fixes the order of the steps; the driver knows how each step is done on
its device; the caller supplies every value -- the community, the allowed sources,
the proxy URL, the user. A driver whose device takes the whole change in one call
(a REST API) overrides the template method instead of its steps.
None of these is mixed into a role base. A driver mixes in the fixes its device can
take, so ``callable(getattr(driver, "configure_snmp_agent", None))`` stays a truthful
answer; a driver that inherits one it cannot run sets it to ``None``.
They replace ``run_device_action`` (docs/STRUCTURE.md):
========================== ===================================================
``fix_snmp`` :meth:`SnmpAgentMixin.configure_snmp_agent`
``fix_apt_proxy`` :meth:`PackageProxyMixin.configure_package_proxy`
``fix_docker_permissions`` ``ContainerEngineMixin.grant_container_engine_access``
``install_lldpd`` and co. :meth:`EnsurePackageMixin.ensure_package`
========================== ===================================================
"""
from __future__ import annotations
from typing import Any, Callable, List, Sequence, Tuple, TYPE_CHECKING
from napalm_device_types.models import (
FixResultDict,
FixStepDict,
FixStepOutcomeDict,
PackageDict,
SnmpAgentSettingsDict,
)
#: One step of a fix: its name and the call that does it.
FixStep = Tuple[str, Callable[[], FixStepOutcomeDict]]
def run_fix_steps(steps: Sequence[FixStep]) -> FixResultDict:
"""Run *steps* in order and stop at the first that fails.
A step that raises has failed, with the exception's message as its output.
"""
done: List[FixStepDict] = []
for name, step in steps:
try:
outcome = step()
ok, output = bool(outcome["ok"]), str(outcome.get("output", ""))
except Exception as exc: # noqa: BLE001 - any failure of a step ends the fix
ok, output = False, str(exc)
done.append({"step": name, "ok": ok, "output": output})
if not ok:
break
return {
"success": bool(done) and all(s["ok"] for s in done) and len(done) == len(steps),
"steps": done,
"output": "\n".join(f"[{s['step']}] {s['output']}".rstrip() for s in done),
}
class SnmpAgentMixin:
"""Run an SNMP agent on the device that answers the caller.
The steps: install the agent, write its configuration, let the allowed sources
through the host's firewall, start it. Whether it answers is the caller's to ask,
with its own SNMP client.
"""
def configure_snmp_agent(self, settings: SnmpAgentSettingsDict) -> FixResultDict:
"""Install, configure, open and start the agent with *settings*."""
return run_fix_steps(
[
("install", self._install_snmp_agent),
("configure", lambda: self._write_snmp_agent_config(settings)),
("firewall", lambda: self._allow_snmp_sources(settings)),
("start", self._start_snmp_agent),
]
)
if TYPE_CHECKING: # pragma: no cover - the steps a driver implements
def _install_snmp_agent(self) -> FixStepOutcomeDict: ...
def _write_snmp_agent_config(self, settings: SnmpAgentSettingsDict) -> FixStepOutcomeDict: ...
def _allow_snmp_sources(self, settings: SnmpAgentSettingsDict) -> FixStepOutcomeDict:
"""Let ``settings["allowed_sources"]`` reach the agent; ``ok`` with nothing
to do where the host has no firewall of its own."""
...
def _start_snmp_agent(self) -> FixStepOutcomeDict:
"""(Re)start the agent so it reads the new configuration, and enable it at boot."""
...
class EnsurePackageMixin:
"""Make sure a package is installed, installing it only where it is missing.
Works over :class:`~napalm_device_types.packages.PackageManagementMixin`'s
``get_packages`` and ``install_package``, which the driver implements.
"""
def ensure_package(self, name: str) -> FixResultDict:
"""Install *name* unless it is installed already."""
def install() -> FixStepOutcomeDict:
if any(p.get("name") == name and p.get("installed") for p in self.get_packages()):
return {"ok": True, "output": f"{name} is already installed"}
result: Any = self.install_package(name) # type: ignore[func-returns-value]
# The contract says None (and raise on failure); some drivers still
# return {"success", "output"}.
if isinstance(result, dict):
return {"ok": bool(result.get("success")), "output": str(result.get("output", ""))}
return {"ok": True, "output": f"installed {name}"}
return run_fix_steps([("install", install)])
if TYPE_CHECKING: # pragma: no cover
def get_packages(self) -> List[PackageDict]: ...
def install_package(self, name: str, version: str = "") -> None: ...
class PackageProxyMixin:
"""Point the device's package manager at a caching proxy."""
if TYPE_CHECKING: # pragma: no cover
def configure_package_proxy(self, url: str) -> FixResultDict:
"""Have the package manager fetch through *url*; an empty *url* removes it."""
...
__all__ = [
"EnsurePackageMixin",
"FixStep",
"PackageProxyMixin",
"SnmpAgentMixin",
"run_fix_steps",
]
+33
View File
@@ -951,6 +951,39 @@ class HealthMetricsDict(TypedDict):
interfaces: NotRequired[Dict[str, HealthMetricsIfaceDict]]
class FixStepOutcomeDict(TypedDict):
"""What one step of a fix routine reports (see ``napalm_device_types.fixes``)."""
ok: bool
output: str
class FixStepDict(TypedDict):
"""One step of a fix routine, as ``FixResultDict`` lists it."""
step: str
ok: bool
output: str
class FixResultDict(TypedDict):
"""The result of a fix routine: every step that ran, up to the first failure."""
success: bool
steps: List[FixStepDict]
output: str # the steps' output, one "[step] output" line each
class SnmpAgentSettingsDict(TypedDict):
"""What the caller wants an SNMP agent to be (``SnmpAgentMixin``)."""
community: str # the read-only v2c community
allowed_sources: List[str] # addresses or CIDRs let through to the agent; [] changes no firewall
contact: NotRequired[str] # sysContact
location: NotRequired[str] # sysLocation
listen: NotRequired[str] # the address the agent binds to; absent: every address
class SnmpValueSpec(TypedDict, total=False):
"""Where an SNMP agent publishes one value, inside a ``SnmpHealthProfile``.
+5
View File
@@ -201,6 +201,11 @@ class OSDriver(UpdateMixin, ServiceControlMixin, PackageManagementMixin, HealthM
"""
Executes a named administrative action on the device.
**Deprecated** (docs/STRUCTURE.md): the actions become fix templates
in :mod:`napalm_device_types.fixes`, whose values the caller supplies --
``configure_snmp_agent``, ``configure_package_proxy``,
``grant_container_engine_access``, ``ensure_package``. Goes in 5.0.
This method is an extensibility point for driver-specific one-off
operations that do not fit any other NAPALM API method. Each driver
documents the action names it supports.