fix: leave an unchanged Kea reservation and Kea alone
CI / test (3.10) (push) Successful in 35s
CI / test (3.11) (push) Successful in 32s
CI / test (3.12) (push) Successful in 34s
CI / test (3.10) (pull_request) Successful in 31s
CI / test (3.11) (pull_request) Successful in 30s
CI / test (3.12) (pull_request) Successful in 34s
CI / test (3.10) (push) Successful in 35s
CI / test (3.11) (push) Successful in 32s
CI / test (3.12) (push) Successful in 34s
CI / test (3.10) (pull_request) Successful in 31s
CI / test (3.11) (pull_request) Successful in 30s
CI / test (3.12) (pull_request) Successful in 34s
create_dhcp_reservation rewrote an existing reservation and called service/reconfigure even when it already held exactly what was asked, e.g. on a retried provisioning job. reconfigure stops and starts Kea (the Kea service controller keeps reconfigureForceRestart() = true and configd has no reload action), which once left a firewall without DHCP. A reservation whose subnet, MAC (normalised), IP, hostname and description already match is now left alone. Refs NetOrk/netork#1187
This commit is contained in:
1 parent
6a91f43fc3
commit
59796b3fa1
2 files changed
+115
-2
No files matched your search
@@ -55,6 +55,7 @@ import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from napalm_device_types import ConfigSyncMixin, FingerprintRule, FirewallDriver, SnmpAgentMixin
|
||||
from napalm_device_types.dhcp import normalize_mac
|
||||
from napalm_device_types.models import (
|
||||
FixStepOutcomeDict,
|
||||
SnmpAgentSettingsDict,
|
||||
@@ -1393,6 +1394,11 @@ class OPNsenseDriver(
|
||||
plugin as "reservations unsupported" (``RuntimeError``), not fall
|
||||
back to plain DHCP silently.
|
||||
|
||||
A reservation that already reads exactly as requested is left alone:
|
||||
no ``setReservation`` and no ``service/reconfigure``. The reconfigure
|
||||
stops and starts Kea rather than reloading it, so restarting the
|
||||
DHCP server for a write that changes nothing is pure risk.
|
||||
|
||||
:param mac: NIC MAC address (any common formatting; sent as-is to
|
||||
Kea's ``hw_address`` field).
|
||||
:param ip: IP address to reserve; must fall inside a subnet Kea
|
||||
@@ -1409,10 +1415,12 @@ class OPNsenseDriver(
|
||||
|
||||
ip_obj = ip_address(ip)
|
||||
subnet_uuid = None
|
||||
subnet_cidr = ""
|
||||
for row in subnets:
|
||||
try:
|
||||
if ip_obj in ip_network(row["subnet"], strict=False):
|
||||
subnet_uuid = row["uuid"]
|
||||
subnet_cidr = str(row["subnet"]).strip()
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
@@ -1422,7 +1430,7 @@ class OPNsenseDriver(
|
||||
# Idempotency: reuse an existing reservation for this IP if one
|
||||
# already exists (e.g. a retried provisioning job), rather than
|
||||
# creating a duplicate Kea rejects anyway.
|
||||
existing_uuid = None
|
||||
existing_row: dict[str, Any] | None = None
|
||||
try:
|
||||
existing = self._post(
|
||||
"/api/kea/dhcpv4/searchReservation",
|
||||
@@ -1430,7 +1438,7 @@ class OPNsenseDriver(
|
||||
)
|
||||
for row in existing.get("rows") or []:
|
||||
if row.get("ip_address") == ip:
|
||||
existing_uuid = row.get("uuid")
|
||||
existing_row = row
|
||||
break
|
||||
except Exception as exc:
|
||||
logger.debug("Kea reservation search failed, proceeding to add: %s", exc)
|
||||
@@ -1444,6 +1452,13 @@ class OPNsenseDriver(
|
||||
"description": self._NETORK_TAG,
|
||||
}
|
||||
}
|
||||
if existing_row is not None and self._kea_reservation_unchanged(
|
||||
existing_row, payload["reservation"], subnet_cidr
|
||||
):
|
||||
logger.debug("Kea reservation for %s is already as requested, not touched", ip)
|
||||
return
|
||||
|
||||
existing_uuid = existing_row.get("uuid") if existing_row is not None else None
|
||||
path = (
|
||||
f"/api/kea/dhcpv4/setReservation/{existing_uuid}"
|
||||
if existing_uuid
|
||||
@@ -1455,6 +1470,27 @@ class OPNsenseDriver(
|
||||
|
||||
self._post("/api/kea/service/reconfigure")
|
||||
|
||||
@staticmethod
|
||||
def _kea_reservation_unchanged(
|
||||
row: dict[str, Any], wanted: dict[str, Any], subnet_cidr: str
|
||||
) -> bool:
|
||||
"""Whether a ``searchReservation`` row already holds every field we would write.
|
||||
|
||||
The MAC is compared normalised: Kea keeps it as it was entered, in
|
||||
either case and with ``:`` or ``-``. The subnet is a model relation that
|
||||
comes back as the subnet's CIDR or its UUID depending on the OPNsense
|
||||
version, so either counts. A field the row does not carry counts as a
|
||||
difference -- writing once too often is the safe side.
|
||||
"""
|
||||
if str(row.get("subnet") or "").strip() not in {wanted["subnet"], subnet_cidr}:
|
||||
return False
|
||||
if normalize_mac(row.get("hw_address")) != normalize_mac(wanted["hw_address"]):
|
||||
return False
|
||||
return all(
|
||||
str(row.get(field) or "") == str(wanted[field] or "")
|
||||
for field in ("ip_address", "hostname", "description")
|
||||
)
|
||||
|
||||
def delete_dhcp_reservation_and_lease(self, mac: str, ip: str) -> dict[str, Any]:
|
||||
"""Remove a Kea DHCPv4 static reservation and any active lease for (mac, ip).
|
||||
|
||||
|
||||
Reference in new issue
Block a user