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
110 lines
4.2 KiB
Python
110 lines
4.2 KiB
Python
"""``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()
|