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

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:
Christian Manivong committed 2026-10-21 01:01:52 +02:00
1 parent 6a91f43fc3
commit 5a6e573300
2 files changed
+135 -5

No files matched your search

+26 -5
View File
@@ -1316,7 +1316,16 @@ class OPNsenseDriver(
2. **ISC DHCP** (legacy) — ``POST /api/dhcpv4/leases/searchlease``
Fields: ``mac``, ``address``, ``hostname``, ``ends``
3. **ARP table** fallback — ``GET /api/diagnostics/interface/get_arp``
Provides IP only, hostname will be empty.
Provides IP only, hostname will be empty, ``state`` is ``"arp"``.
The first source with leases wins. Without any, the ARP table stands
in -- also when Kea or ISC answered empty, since a box can hand out
addresses through a service neither covers. An empty list is a real
answer only: Kea or ISC answered with no leases and the fallback added
none. When neither answered and the fallback gives no rows (it failed,
or the table is empty), there is no answer to give, and this raises
:class:`RuntimeError` naming each failure -- a caller would otherwise
take ``[]`` for a server without leases and drop the ones it knows.
Each returned entry contains:
@@ -1349,6 +1358,9 @@ class OPNsenseDriver(
})
return result
failures: list[str] = []
answered = False # a lease source answered, with or without leases
# 1. Kea DHCPv4 plugin
try:
data = self._get("/api/kea/leases4/search")
@@ -1356,8 +1368,10 @@ class OPNsenseDriver(
leases = _parse_kea(rows)
if leases:
return leases
answered = True
except Exception as exc:
logger.debug("Kea DHCP lease fetch failed: %s", exc)
failures.append(f"Kea: {exc}")
# 2. ISC DHCP (legacy)
try:
@@ -1368,20 +1382,27 @@ class OPNsenseDriver(
leases = _parse_kea(data.get("rows", []))
if leases:
return leases
answered = True
except Exception as exc:
logger.debug("ISC DHCP lease fetch failed: %s", exc)
failures.append(f"ISC DHCP: {exc}")
# 3. ARP table fallback (IP only, no hostname)
fallback: list[dict[str, Any]] = []
try:
arp = self.get_arp_table()
return [
fallback = [
{"mac": e["mac"], "ip": e["ip"], "hostname": "", "lease_end": 0, "state": "arp"}
for e in arp
for e in self.get_arp_table()
if e.get("mac") and e.get("ip")
]
if not fallback:
failures.append("ARP table: no entries")
except Exception as exc:
logger.warning("ARP table fallback failed: %s", exc)
return []
failures.append(f"ARP table: {exc}")
if fallback or answered:
return fallback
raise RuntimeError("No DHCP lease source answered -- " + "; ".join(failures))
def create_dhcp_reservation(self, mac: str, ip: str, hostname: str = "") -> None:
"""Create (or update) a Kea DHCPv4 static reservation (MAC → IP).