CI / test (3.10) (push) Failing after 19s
CI / test (3.11) (push) Failing after 17s
CI / test (3.12) (push) Failing after 20s
CI / test (3.10) (pull_request) Failing after 18s
CI / test (3.11) (pull_request) Failing after 20s
CI / test (3.12) (pull_request) Failing after 19s
The node is Debian: AptProxyMixin gives it get_package_proxy(), get_package_proxy_report(), check_package_proxy() and the fix configure_package_proxy(), as on any Debian host. Logged in as root, the transports are the exec path; the file is written without an exit inside _rc's braces, which would end the shell before the status (see #30). Requires napalm-device-types 4.22.0; version 0.7.0.
193 lines
6.9 KiB
Python
193 lines
6.9 KiB
Python
"""The fixes as napalm-device-types templates, with the caller's values (netOrk #1105).
|
|
|
|
``fix_snmp`` wrote an application's snmpd.conf (community "public", "Managed by
|
|
netOrk") and opened ufw for the caller's /24 guessed from ``ss``;
|
|
``install_lldpd`` was an action of its own. Now ``configure_snmp_agent(settings)``
|
|
takes every value from the caller, and ``ensure_package`` installs through the
|
|
contract's ``install_package``. Every step reads its exit status.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from napalm_proxmox import ProxmoxDriver
|
|
|
|
SETTINGS = {"community": "c0mm", "allowed_sources": ["10.7.224.20"], "location": "rack 1"}
|
|
|
|
|
|
class _Node:
|
|
"""The node's shell: answers by command fragment, with an exit status."""
|
|
|
|
def __init__(self, **status):
|
|
self.status = status # fragment -> exit status
|
|
self.ran: list[str] = []
|
|
|
|
def __call__(self, command: str) -> str:
|
|
self.ran.append(command)
|
|
if "SSH_CONNECTION" in command:
|
|
return "10.7.224.20 51234 10.0.0.9 22\n"
|
|
if command.startswith("dpkg-query"):
|
|
return "curl\tinstalled\n"
|
|
rc = next((code for frag, code in self.status.items() if frag in command), 0)
|
|
out = "ok" if rc == 0 else "E: failed"
|
|
return f"{out}\n__PVE_RC={rc}" if "__PVE_RC" in command else out
|
|
|
|
|
|
@pytest.fixture
|
|
def node():
|
|
return _Node()
|
|
|
|
|
|
@pytest.fixture
|
|
def driver(node):
|
|
d = ProxmoxDriver("pve", "root@pam", "pw")
|
|
d._exec_ssh_command = node
|
|
return d
|
|
|
|
|
|
class TestSnmpAgent:
|
|
def test_install_write_open_start_with_the_callers_values(self, driver, node):
|
|
result = driver.configure_snmp_agent(SETTINGS)
|
|
|
|
assert result["success"] is True
|
|
assert [s["step"] for s in result["steps"]] == ["install", "configure", "firewall", "start"]
|
|
assert any("apt-get install -y snmpd snmp" in c for c in node.ran)
|
|
write = next(c for c in node.ran if "/etc/snmp/snmpd.conf" in c)
|
|
assert "install -m 644" in write
|
|
b64 = re.search(r"echo ([A-Za-z0-9+/=]+) \| base64 -d", write).group(1)
|
|
import base64
|
|
|
|
conf = base64.b64decode(b64).decode()
|
|
assert "rocommunity c0mm 10.7.224.20/32" in conf
|
|
assert "sysLocation rack 1" in conf
|
|
assert "public" not in conf and "netork" not in conf.lower()
|
|
assert any("systemctl restart snmpd" in c for c in node.ran)
|
|
|
|
def test_no_temp_file_with_an_application_name(self, driver, node):
|
|
driver.configure_snmp_agent(SETTINGS)
|
|
|
|
assert not any("netork" in c for c in node.ran)
|
|
|
|
def test_ufw_opens_only_for_the_allowed_sources(self, driver, node):
|
|
node.status["command -v ufw"] = 0
|
|
|
|
driver.configure_snmp_agent(SETTINGS)
|
|
|
|
assert any("ufw allow from 10.7.224.20/32 to any port 161 proto udp" in c for c in node.ran)
|
|
assert not any(".0/24" in c for c in node.ran)
|
|
|
|
def test_a_failed_install_stops_the_fix(self, driver, node):
|
|
node.status["apt-get install -y snmpd"] = 100
|
|
|
|
result = driver.configure_snmp_agent(SETTINGS)
|
|
|
|
assert result["success"] is False
|
|
assert not any("/etc/snmp/snmpd.conf" in c for c in node.ran)
|
|
|
|
def test_the_management_peer(self, driver):
|
|
assert driver.get_management_peer() == "10.7.224.20"
|
|
|
|
|
|
class TestPackages:
|
|
def test_ensure_package_installs_what_is_missing(self, driver, node):
|
|
result = driver.ensure_package("lldpd")
|
|
|
|
assert result["success"] is True
|
|
assert any("apt-get install -y lldpd" in c for c in node.ran)
|
|
|
|
def test_ensure_package_leaves_alone_what_is_there(self, driver, node):
|
|
assert driver.ensure_package("curl")["success"] is True
|
|
assert not any("apt-get install" in c for c in node.ran)
|
|
|
|
def test_install_package_raises_when_apt_fails(self, driver, node):
|
|
node.status["apt-get install -y lldpd"] = 100
|
|
|
|
with pytest.raises(RuntimeError, match="lldpd"):
|
|
driver.install_package("lldpd")
|
|
|
|
def test_a_name_that_is_no_package_name_is_refused(self, driver):
|
|
with pytest.raises(ValueError):
|
|
driver.install_package("lldpd; reboot")
|
|
|
|
|
|
def test_the_old_actions_are_gone():
|
|
for name in ("run_device_action", "_action_fix_snmp", "_action_install_lldpd"):
|
|
assert not hasattr(ProxmoxDriver, name), name
|
|
|
|
|
|
_APT_REPORT = (
|
|
"APROXY_BEGIN\n[dump]\n{dump}[parts]\n{parts}[main]\n[sources]\n"
|
|
"http://download.proxmox.com/debian/pve/dists/trixie/\nAPROXY_END\n"
|
|
)
|
|
|
|
|
|
class _AptNode(_Node):
|
|
"""A node that answers apt's proxy report and a fetch."""
|
|
|
|
def __init__(self, proxy="", parts=""):
|
|
super().__init__()
|
|
self.proxy = proxy
|
|
self.parts = parts
|
|
|
|
def __call__(self, command: str) -> str:
|
|
if "apt-config dump" in command:
|
|
self.ran.append(command)
|
|
dump = f'Acquire::http::Proxy "{self.proxy}";\n' if self.proxy else ""
|
|
return _APT_REPORT.format(dump=dump, parts=self.parts)
|
|
if "apt-helper" in command:
|
|
self.ran.append(command)
|
|
return "APROXY_BEGIN\n[fetch]\nGet:1 x\n[rc]\n0\n[match]\n2\nAPROXY_END\n"
|
|
return super().__call__(command)
|
|
|
|
|
|
class TestPackageProxy:
|
|
"""Proxmox VE is Debian: apt's proxy is set and read as on any Debian host (netOrk #1397)."""
|
|
|
|
def test_it_takes_the_fix(self):
|
|
assert callable(getattr(ProxmoxDriver, "configure_package_proxy", None))
|
|
|
|
def test_the_fact(self):
|
|
d = ProxmoxDriver("pve", "root@pam", "pw")
|
|
d._exec_ssh_command = _AptNode(proxy="http://c:3142")
|
|
|
|
assert d.get_package_proxy() == "http://c:3142"
|
|
|
|
def test_written_as_root_read_back_and_fetched_through(self):
|
|
node = _AptNode(proxy="http://c:3142")
|
|
d = ProxmoxDriver("pve", "root@pam", "pw")
|
|
d._exec_ssh_command = node
|
|
|
|
result = d.configure_package_proxy("http://c:3142")
|
|
|
|
assert result["success"] is True, result["output"]
|
|
write = next(c for c in node.ran if "/etc/apt/apt.conf.d/00proxy" in c)
|
|
assert "install -m 644" in write
|
|
b64 = re.search(r"echo ([A-Za-z0-9+/=]+) \| base64 -d", write).group(1)
|
|
import base64
|
|
|
|
assert base64.b64decode(b64).decode() == 'Acquire::http::Proxy "http://c:3142";\n'
|
|
assert any("dists/trixie/InRelease" in c for c in node.ran)
|
|
|
|
def test_another_proxy_is_commented_out(self):
|
|
node = _AptNode(
|
|
proxy="http://c:3142",
|
|
parts='/etc/apt/apt.conf.d/90curtin-aptproxy:1:Acquire::http::Proxy "http://old:3128";\n',
|
|
)
|
|
d = ProxmoxDriver("pve", "root@pam", "pw")
|
|
d._exec_ssh_command = node
|
|
|
|
d.configure_package_proxy("http://c:3142")
|
|
|
|
assert any(c.startswith("{ sed -i -e '1{") for c in node.ran)
|
|
|
|
def test_the_proxy_answers(self):
|
|
d = ProxmoxDriver("pve", "root@pam", "pw")
|
|
d._exec_ssh_command = _AptNode()
|
|
|
|
result = d.check_package_proxy("http://c:3142/acng-report.html", expect="Cache")
|
|
|
|
assert result == {"reachable": True, "expected": True, "error": ""}
|