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
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:
1 parent
64cc20ba00
commit
95222377f0
8 files changed
+389
-2
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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``.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user