fix: get_dhcp_leases raises when no lease source answered (#27)
CI / test (3.10) (push) Successful in 32s
CI / test (3.11) (push) Successful in 34s
CI / test (3.12) (push) Successful in 33s
CI / test (3.10) (pull_request) Successful in 32s
CI / test (3.11) (pull_request) Successful in 29s
CI / test (3.12) (pull_request) Successful in 34s
CI / test (3.10) (push) Successful in 32s
CI / test (3.11) (push) Successful in 34s
CI / test (3.12) (push) Successful in 33s
CI / test (3.10) (pull_request) Successful in 32s
CI / test (3.11) (pull_request) Successful in 29s
CI / test (3.12) (pull_request) Successful in 34s
When Kea, ISC DHCP and the ARP fallback all failed, get_dhcp_leases() returned [], which a caller cannot tell from a server with no leases. It now raises a RuntimeError naming each failure, and returns [] only when a source answered with nothing. An empty ARP table after both lease sources failed counts as unread too, since ARP rows are no lease data. Closes #27
This commit is contained in:
1 parent
6a91f43fc3
commit
5a6e573300
2 files changed
+135
-5
No files matched your search
@@ -0,0 +1,109 @@
|
||||
"""``get_dhcp_leases()``: an empty answer is only ever a real one (#27).
|
||||
|
||||
The getter tries Kea, then ISC DHCP, then falls back to the ARP table (rows
|
||||
with ``state: "arp"``). A caller stores the answer as the server's lease set,
|
||||
so ``[]`` must mean "a lease source answered and has no leases". When no lease
|
||||
source answered and the ARP fallback gives nothing either, the getter raises:
|
||||
an empty list there would wipe the leases the caller knows.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from napalm_opnsense.opnsense import OPNsenseDriver
|
||||
|
||||
KEA = "/api/kea/leases4/search"
|
||||
ISC = "/api/dhcpv4/leases/searchlease"
|
||||
ARP = "/api/diagnostics/interface/get_arp"
|
||||
|
||||
KEA_ROW = {"hwaddr": "AA:BB:CC:DD:EE:01", "address": "10.0.0.11", "hostname": "nas", "state": "0"}
|
||||
ISC_ROW = {"mac": "aa:bb:cc:dd:ee:02", "address": "10.0.0.12", "ends": "1700000000"}
|
||||
ARP_ROW = {"intf": "igb0", "mac": "AA:BB:CC:DD:EE:03", "ip": "10.0.0.13", "expires": "1200"}
|
||||
|
||||
|
||||
def _driver(kea=None, isc=None, arp=None) -> OPNsenseDriver:
|
||||
"""Each source answers its value, or raises it when it is an exception."""
|
||||
|
||||
def answer(value):
|
||||
if isinstance(value, Exception):
|
||||
raise value
|
||||
return value
|
||||
|
||||
driver = OPNsenseDriver("gw-01", "user", "secret")
|
||||
driver._get = MagicMock(side_effect=lambda path: answer({KEA: kea, ARP: arp}[path]))
|
||||
driver._post = MagicMock(side_effect=lambda path, data=None: answer({ISC: isc}[path]))
|
||||
return driver
|
||||
|
||||
|
||||
def _macs(leases: list[dict]) -> list[str]:
|
||||
return [lease["mac"] for lease in leases]
|
||||
|
||||
|
||||
class TestALeaseSourceAnswers:
|
||||
def test_kea_leases_come_first(self):
|
||||
driver = _driver(kea={"rows": [KEA_ROW]}, isc=RuntimeError("404"), arp=RuntimeError("x"))
|
||||
assert _macs(driver.get_dhcp_leases()) == ["aa:bb:cc:dd:ee:01"]
|
||||
|
||||
def test_isc_leases_when_kea_has_none(self):
|
||||
driver = _driver(kea={"rows": []}, isc={"rows": [ISC_ROW]}, arp=RuntimeError("x"))
|
||||
assert _macs(driver.get_dhcp_leases()) == ["aa:bb:cc:dd:ee:02"]
|
||||
|
||||
def test_isc_leases_when_kea_fails(self):
|
||||
driver = _driver(kea=RuntimeError("404"), isc={"rows": [ISC_ROW]}, arp=RuntimeError("x"))
|
||||
assert _macs(driver.get_dhcp_leases()) == ["aa:bb:cc:dd:ee:02"]
|
||||
|
||||
|
||||
class TestTheArpFallback:
|
||||
"""No source has leases: the ARP table stands in, marked ``state: "arp"`` (#1168)."""
|
||||
|
||||
def test_when_both_lease_sources_fail(self):
|
||||
driver = _driver(kea=RuntimeError("404"), isc=RuntimeError("404"), arp=[ARP_ROW])
|
||||
leases = driver.get_dhcp_leases()
|
||||
assert leases == [
|
||||
{
|
||||
"mac": "aa:bb:cc:dd:ee:03",
|
||||
"ip": "10.0.0.13",
|
||||
"hostname": "",
|
||||
"lease_end": 0,
|
||||
"state": "arp",
|
||||
}
|
||||
]
|
||||
|
||||
def test_when_a_lease_source_answers_empty(self):
|
||||
driver = _driver(kea={"rows": []}, isc=RuntimeError("404"), arp=[ARP_ROW])
|
||||
assert [lease["state"] for lease in driver.get_dhcp_leases()] == ["arp"]
|
||||
|
||||
|
||||
class TestAnEmptyAnswer:
|
||||
"""[] only when Kea or ISC answered and has no leases."""
|
||||
|
||||
def test_kea_empty_and_no_arp_fallback(self):
|
||||
driver = _driver(kea={"rows": []}, isc=RuntimeError("404"), arp=RuntimeError("timeout"))
|
||||
assert driver.get_dhcp_leases() == []
|
||||
|
||||
def test_isc_empty_and_an_empty_arp_table(self):
|
||||
driver = _driver(kea=RuntimeError("404"), isc={"rows": []}, arp=[])
|
||||
assert driver.get_dhcp_leases() == []
|
||||
|
||||
|
||||
class TestNothingAnswered:
|
||||
"""No lease source answered and the fallback gave no rows: that is no answer."""
|
||||
|
||||
def test_all_three_fail(self):
|
||||
driver = _driver(
|
||||
kea=RuntimeError("kea down"), isc=RuntimeError("isc down"), arp=RuntimeError("arp down")
|
||||
)
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
driver.get_dhcp_leases()
|
||||
message = str(excinfo.value)
|
||||
assert "kea down" in message
|
||||
assert "isc down" in message
|
||||
assert "arp down" in message
|
||||
|
||||
def test_lease_sources_fail_and_the_arp_table_is_empty(self):
|
||||
driver = _driver(kea=RuntimeError("kea down"), isc=RuntimeError("isc down"), arp=[])
|
||||
with pytest.raises(RuntimeError, match="kea down"):
|
||||
driver.get_dhcp_leases()
|
||||
Reference in new issue
Block a user