feat!: fix templates with the caller's values replace run_device_action

The driver filled run_device_action with an application's remediations
and their values: community "public", "Managed by netOrk", the
caller's address guessed from ss and a /24 ufw rule, the proxy from an
optional_args key.

It implements napalm-device-types' fix templates instead (4.12), and
the caller supplies every value:

- configure_snmp_agent(settings): net-snmp installed, snmpd.conf with
  one rocommunity line per allowed source, udp/161 opened in ufw or
  iptables for those sources only, snmpd enabled and restarted. Every
  step reads its exit status. A community, source or text that would
  break the file is refused.
- configure_package_proxy(url): apt's 00proxy, or removed for "".
- grant_container_engine_access(user): usermod -aG docker for the user
  named.
- ensure_package(name): from the base package, over get_packages and
  install_package.

run_device_action, its four _action_* methods and the unused
apt_update_upgrade are gone. The sudo hint no longer names an
application's settings.

BREAKING CHANGE: run_device_action is removed; callers use the
templates. Requires napalm-device-types 4.12.

Closes #27
This commit is contained in:
Christian Manivong committed 2026-10-16 12:08:37 +02:00
1 parent c98dd928e6
commit 272bf21f8e
4 files changed
+322 -358

No files matched your search

+2 -143
View File
@@ -807,151 +807,10 @@ def test_get_facts_fallback_vendor_when_dmi_empty(driver):
assert facts["vendor"] == "Linux" # fallback to VENDOR class attribute
# ---------------------------------------------------------------------------
# _action_fix_snmp / _action_apt_update_upgrade
# ---------------------------------------------------------------------------
def _fix_snmp_send_side_effect(command: str, read_timeout: float = 100) -> str:
"""Canned responses covering every _send() call _action_fix_snmp makes."""
if command.startswith("sudo -n true"):
return "" # passwordless sudo works
if command.startswith("command -v apt"):
return "/usr/bin/apt"
if command.startswith("command -v"):
return "" # ufw/iptables not found, other pkg managers not found
if command.startswith("ss -tnp"):
return "" # no netork_ip detected — skips firewall step
if command.startswith("cat /etc/snmp/snmpd.conf"):
return "agentAddress udp:161\nrocommunity public\n"
if command.startswith("snmpget"):
return "STRING: Linux test"
return ""
class TestActionFixSnmp:
def test_runs_apt_get_update_before_install(self, driver):
driver._pkg_manager = "apt"
driver._sudo_password = None
sudo_calls: list[str] = []
def _sudo_side_effect(command: str, read_timeout: float = 100) -> str:
sudo_calls.append(command)
return ""
with (
patch.object(driver, "_send", side_effect=_fix_snmp_send_side_effect),
patch.object(driver, "_sudo", side_effect=_sudo_side_effect),
):
driver._action_fix_snmp()
update_idx = next(i for i, c in enumerate(sudo_calls) if "apt-get update" in c)
install_idx = next(i for i, c in enumerate(sudo_calls) if "apt-get install" in c)
assert update_idx < install_idx
def test_install_exception_returns_failure_instead_of_raising(self, driver):
driver._pkg_manager = "apt"
driver._sudo_password = None
def _sudo_side_effect(command: str, read_timeout: float = 100) -> str:
if "apt-get install" in command:
raise TimeoutError("connection timed out")
return ""
with (
patch.object(driver, "_send", side_effect=_fix_snmp_send_side_effect),
patch.object(driver, "_sudo", side_effect=_sudo_side_effect),
):
result = driver._action_fix_snmp()
assert result["success"] is False
assert "install failed" in result["output"]
def test_apt_get_update_failure_is_non_fatal(self, driver):
"""apt-get update failing (e.g. transient network issue) must not
abort the whole action — install is still attempted."""
driver._pkg_manager = "apt"
driver._sudo_password = None
def _sudo_side_effect(command: str, read_timeout: float = 100) -> str:
if "apt-get update" in command:
raise TimeoutError("network unreachable")
return ""
with (
patch.object(driver, "_send", side_effect=_fix_snmp_send_side_effect),
patch.object(driver, "_sudo", side_effect=_sudo_side_effect),
):
result = driver._action_fix_snmp()
assert "apt-get update failed" in result["output"]
class TestActionAptUpdateUpgrade:
def test_skips_when_not_apt(self, driver):
driver._pkg_manager = "dnf"
result = driver._action_apt_update_upgrade()
assert result["success"] is True
assert "Skipped" in result["output"]
def test_fails_when_sudo_needs_password_and_none_configured(self, driver):
driver._pkg_manager = "apt"
driver._sudo_password = None
with patch.object(driver, "_send", return_value="__SUDO_NEEDS_PW__"):
result = driver._action_apt_update_upgrade()
assert result["success"] is False
assert "sudo requires a password" in result["output"]
def test_runs_update_then_upgrade_successfully(self, driver):
driver._pkg_manager = "apt"
driver._sudo_password = "secret" # noqa: S105
with (
patch.object(driver, "_send", return_value=""),
patch.object(
driver,
"_sudo",
side_effect=["Reading package lists... Done", "0 upgraded, 0 newly installed"],
) as mock_sudo,
):
result = driver._action_apt_update_upgrade()
assert result["success"] is True
assert "[update]" in result["output"]
assert "[upgrade]" in result["output"]
update_call, upgrade_call = mock_sudo.call_args_list
assert "apt-get update" in update_call.args[0]
# full-upgrade (not plain upgrade) — plain upgrade silently holds back
# packages whose newer version needs to install/remove dependencies.
assert "apt-get full-upgrade" in upgrade_call.args[0]
def test_exception_during_upgrade_returns_failure(self, driver):
driver._pkg_manager = "apt"
driver._sudo_password = "secret" # noqa: S105
with (
patch.object(driver, "_send", return_value=""),
patch.object(
driver, "_sudo", side_effect=["update ok", TimeoutError("connection lost")]
),
):
result = driver._action_apt_update_upgrade()
assert result["success"] is False
assert "[error]" in result["output"]
class TestRunDeviceActionDispatch:
def test_apt_update_upgrade_action_dispatches(self, driver):
with patch.object(
driver, "_action_apt_update_upgrade", return_value={"success": True, "output": ""}
) as mock_action:
driver.run_device_action("apt_update_upgrade")
mock_action.assert_called_once()
class TestNoDockerOfItsOwn:
"""Containers are read and handled by netOrk over the Engine API, through
``open_container_engine`` (NetOrk/netork#765). The old CLI methods are gone;
only the OS action that lets the login user use Docker stays."""
only the fix that lets the login user use Docker stays."""
def test_the_cli_methods_are_gone(self):
for name in ("get_docker_info", "get_docker_outdated", "reconstruct_docker_run", "_docker_bin"):
@@ -959,7 +818,7 @@ class TestNoDockerOfItsOwn:
def test_the_engine_access_and_the_permission_fix_stay(self):
assert hasattr(LinuxDriver, "open_container_engine")
assert hasattr(LinuxDriver, "_action_fix_docker_permissions")
assert callable(LinuxDriver.grant_container_engine_access)
class TestUninstallPackage: