CI / test (3.10) (push) Successful in 31s
CI / test (3.11) (push) Successful in 29s
CI / test (3.12) (push) Successful in 31s
CI / test (3.10) (pull_request) Successful in 31s
CI / test (3.11) (pull_request) Successful in 28s
CI / test (3.12) (pull_request) Successful in 30s
netOrk reached these through the private _get/_post (netOrk #1113); they are napalm-device-types 4.17's contracts now: - apply_updates passes the packages it is given (it dropped them) and get_update_progress reports the upgrade's state and log. - get_dns_forwards, set_dns_forward (an existing forward of the domain is changed, not doubled) and remove_dns_forward, each applied with a reconfigure; a refusal is raised. - get_ospf translates FRR's diagnostics into neutral terms. Requires napalm-device-types 4.17.0.
174 lines
6.5 KiB
Python
174 lines
6.5 KiB
Python
"""What netOrk reached through ``_get``/``_post``, as public methods (netOrk #1113).
|
|
|
|
netOrk followed the firmware upgrade, edited Unbound's forwards and read FRR's
|
|
OSPF state through this driver's private REST helpers. The contracts are
|
|
napalm-device-types 4.17's; the OPNsense API calls and FRR's field names stay
|
|
here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from napalm_opnsense.opnsense import OPNsenseDriver
|
|
|
|
|
|
@pytest.fixture
|
|
def driver():
|
|
with patch("napalm_opnsense.opnsense.requests.Session"):
|
|
drv = OPNsenseDriver(hostname="fw", username="k", password="s", optional_args={"verify": False})
|
|
drv.session = MagicMock()
|
|
yield drv
|
|
|
|
|
|
class TestUpdateProgress:
|
|
@pytest.mark.parametrize(
|
|
"status, state",
|
|
[("running", "running"), ("done", "done"), ("error", "failed"), ("", "idle"), (None, "idle")],
|
|
)
|
|
def test_the_upgrade_status_as_a_state(self, driver, status, state):
|
|
st = {"status": status, "log": "***GOT REQUEST TO UPGRADE***\nfetching\n"}
|
|
with patch.object(driver, "_get", return_value=st) as get:
|
|
progress = driver.get_update_progress()
|
|
|
|
get.assert_called_once_with("/api/core/firmware/upgradestatus")
|
|
assert progress == {"state": state, "log": "***GOT REQUEST TO UPGRADE***\nfetching\n"}
|
|
|
|
def test_the_packages_go_with_the_upgrade(self, driver):
|
|
"""netOrk named the packages it upgrades; ``apply_updates`` dropped them."""
|
|
with patch.object(driver, "_post", return_value={"status": "ok"}) as post:
|
|
result = driver.apply_updates(["opnsense", "openssl"])
|
|
|
|
post.assert_called_once_with(
|
|
"/api/core/firmware/upgrade", {"upgrade_packages": ["opnsense", "openssl"]}
|
|
)
|
|
assert result["success"] is True
|
|
|
|
def test_no_packages_is_the_whole_upgrade(self, driver):
|
|
with patch.object(driver, "_post", return_value={"status": "ok"}) as post:
|
|
driver.apply_updates([])
|
|
|
|
post.assert_called_once_with("/api/core/firmware/upgrade")
|
|
|
|
|
|
ROWS = [
|
|
{"uuid": "u1", "enabled": "1", "type": "forward", "domain": "lab.example", "server": "10.0.0.53", "port": "53"},
|
|
{"uuid": "u2", "enabled": "0", "type": "forward", "domain": "old.example", "server": "10.0.0.9", "port": ""},
|
|
{"uuid": "u3", "enabled": "1", "type": "dot", "domain": "", "server": "9.9.9.9", "port": "853"},
|
|
]
|
|
|
|
|
|
class TestDnsForwards:
|
|
def _post(self, calls):
|
|
def post(path, data=None):
|
|
calls.append((path, data))
|
|
if path.endswith("searchForward"):
|
|
return {"rows": ROWS}
|
|
return {"result": "saved", "status": "ok"}
|
|
|
|
return post
|
|
|
|
def test_the_forwards_without_dns_over_tls(self, driver):
|
|
calls = []
|
|
with patch.object(driver, "_post", side_effect=self._post(calls)):
|
|
forwards = driver.get_dns_forwards()
|
|
|
|
assert forwards == [
|
|
{"domain": "lab.example", "server": "10.0.0.53", "port": 53, "enabled": True},
|
|
{"domain": "old.example", "server": "10.0.0.9", "port": 53, "enabled": False},
|
|
]
|
|
|
|
def test_a_new_domain_is_added_and_applied(self, driver):
|
|
calls = []
|
|
with patch.object(driver, "_post", side_effect=self._post(calls)):
|
|
driver.set_dns_forward("new.example.", "10.0.0.53")
|
|
|
|
paths = [p for p, _ in calls]
|
|
assert paths[-2:] == ["/api/unbound/settings/addForward", "/api/unbound/service/reconfigure"]
|
|
added = calls[-2][1]["dot"]
|
|
assert added == {"enabled": "1", "type": "forward", "domain": "new.example", "server": "10.0.0.53", "port": "53"}
|
|
|
|
def test_a_known_domain_is_changed_not_doubled(self, driver):
|
|
calls = []
|
|
with patch.object(driver, "_post", side_effect=self._post(calls)):
|
|
driver.set_dns_forward("lab.example", "10.0.0.54", port=5353)
|
|
|
|
paths = [p for p, _ in calls]
|
|
assert "/api/unbound/settings/setForward/u1" in paths
|
|
assert "/api/unbound/settings/addForward" not in paths
|
|
|
|
def test_a_refusal_is_raised(self, driver):
|
|
def post(path, data=None):
|
|
if path.endswith("searchForward"):
|
|
return {"rows": []}
|
|
return {"result": "failed", "validations": {"dot.server": "invalid"}}
|
|
|
|
with patch.object(driver, "_post", side_effect=post), pytest.raises(RuntimeError, match="invalid"):
|
|
driver.set_dns_forward("x.example", "not-an-ip")
|
|
|
|
def test_removing_deletes_every_forward_of_the_domain(self, driver):
|
|
calls = []
|
|
with patch.object(driver, "_post", side_effect=self._post(calls)):
|
|
driver.remove_dns_forward("lab.example.")
|
|
|
|
paths = [p for p, _ in calls]
|
|
assert paths[-2:] == ["/api/unbound/settings/delForward/u1", "/api/unbound/service/reconfigure"]
|
|
|
|
def test_removing_an_unknown_domain_changes_nothing(self, driver):
|
|
calls = []
|
|
with patch.object(driver, "_post", side_effect=self._post(calls)):
|
|
driver.remove_dns_forward("nothing.example")
|
|
|
|
assert [p for p, _ in calls] == ["/api/unbound/settings/searchForward"]
|
|
|
|
|
|
OVERVIEW = {"response": {"routerId": "10.255.0.1", "areas": {"0.0.0.0": {}, "0.0.0.1": {}}}}
|
|
INTERFACES = {
|
|
"response": {
|
|
"interfaces": {
|
|
"wg0": {
|
|
"ipAddress": "10.255.0.1",
|
|
"ipAddressPrefixlen": 24,
|
|
"area": "0.0.0.0",
|
|
"networkType": "POINTOPOINT",
|
|
"authentication": "MessageDigest",
|
|
"nbrCount": 2,
|
|
"nbrAdjacentCount": 1,
|
|
"cost": 10,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
class TestOspf:
|
|
def test_frr_in_neutral_terms(self, driver):
|
|
def get(path):
|
|
return OVERVIEW if path.endswith("ospfoverview") else INTERFACES
|
|
|
|
with patch.object(driver, "_get", side_effect=get):
|
|
ospf = driver.get_ospf()
|
|
|
|
assert ospf == {
|
|
"router_id": "10.255.0.1",
|
|
"areas": ["0.0.0.0", "0.0.0.1"],
|
|
"interfaces": {
|
|
"wg0": {
|
|
"address": "10.255.0.1",
|
|
"prefix_length": 24,
|
|
"area": "0.0.0.0",
|
|
"network_type": "POINTOPOINT",
|
|
"authentication": "MessageDigest",
|
|
"neighbors": 2,
|
|
"neighbors_adjacent": 1,
|
|
"cost": 10,
|
|
}
|
|
},
|
|
}
|
|
|
|
def test_no_ospf_running(self, driver):
|
|
with patch.object(driver, "_get", return_value={"response": {}}):
|
|
assert driver.get_ospf() == {"router_id": None, "areas": [], "interfaces": {}}
|