feat(opnsense): add create_dhcp_reservation() for Kea DHCPv4 static mappings
Only Kea (os-kea plugin) is supported — no active OPNsense environment with legacy ISC DHCP was available to verify a second code path against. Payload/response shapes (searchSubnet, searchReservation, addReservation, setReservation, delReservation, service/reconfigure) were confirmed against a real OPNsense box via a live add + verify + delete cycle before writing this method and its tests.
This commit is contained in:
@@ -1243,3 +1243,100 @@ class TestGetConfigCandidate:
|
||||
result = driver.get_config()
|
||||
parsed = json.loads(result["candidate"])
|
||||
assert isinstance(parsed, list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# create_dhcp_reservation()
|
||||
#
|
||||
# Payload/response shapes below are taken verbatim from a live probe against
|
||||
# a real OPNsense box running the Kea DHCPv4 (os-kea) plugin — searchSubnet,
|
||||
# searchReservation, addReservation, delReservation, and service/reconfigure
|
||||
# were all exercised live (including a real add + verify + delete cycle) to
|
||||
# confirm the exact request/response schema before writing this driver
|
||||
# method and these tests against it.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
KEA_SUBNETS_RESPONSE = {
|
||||
"rows": [
|
||||
{"uuid": "82766878-c5ac-41f3-b7b0-e24d2419beb3", "subnet": "172.22.0.0/24"},
|
||||
{"uuid": "6854cab3-ebb5-4031-b987-0edcc6723546", "subnet": "172.22.8.0/24"},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class TestCreateDhcpReservation:
|
||||
def test_adds_new_reservation_when_none_exists(self, driver):
|
||||
driver.session.get.return_value = _make_json_response(KEA_SUBNETS_RESPONSE)
|
||||
driver.session.post.side_effect = [
|
||||
_make_json_response({"rows": []}), # searchReservation — no match
|
||||
_make_json_response({"result": "saved", "uuid": "new-uuid-123"}), # addReservation
|
||||
_make_json_response({"status": "ok"}), # service/reconfigure
|
||||
]
|
||||
|
||||
driver.create_dhcp_reservation(
|
||||
mac="02:aa:bb:cc:dd:ee", ip="172.22.8.253", hostname="new-vm"
|
||||
)
|
||||
|
||||
add_call = driver.session.post.call_args_list[1]
|
||||
assert add_call.args[0] == "https://opnsense.example.com/api/kea/dhcpv4/addReservation"
|
||||
payload = add_call.kwargs["json"]["reservation"]
|
||||
assert payload["subnet"] == "6854cab3-ebb5-4031-b987-0edcc6723546"
|
||||
assert payload["ip_address"] == "172.22.8.253"
|
||||
assert payload["hw_address"] == "02:aa:bb:cc:dd:ee"
|
||||
assert payload["hostname"] == "new-vm"
|
||||
assert payload["description"] == "[netork]"
|
||||
|
||||
reconfigure_call = driver.session.post.call_args_list[2]
|
||||
assert reconfigure_call.args[0] == "https://opnsense.example.com/api/kea/service/reconfigure"
|
||||
|
||||
def test_updates_existing_reservation_for_same_ip(self, driver):
|
||||
driver.session.get.return_value = _make_json_response(KEA_SUBNETS_RESPONSE)
|
||||
driver.session.post.side_effect = [
|
||||
_make_json_response(
|
||||
{"rows": [{"uuid": "existing-uuid-456", "ip_address": "172.22.8.253"}]}
|
||||
),
|
||||
_make_json_response({"result": "saved", "uuid": "existing-uuid-456"}),
|
||||
_make_json_response({"status": "ok"}),
|
||||
]
|
||||
|
||||
driver.create_dhcp_reservation(mac="02:aa:bb:cc:dd:ee", ip="172.22.8.253")
|
||||
|
||||
set_call = driver.session.post.call_args_list[1]
|
||||
assert (
|
||||
set_call.args[0]
|
||||
== "https://opnsense.example.com/api/kea/dhcpv4/setReservation/existing-uuid-456"
|
||||
)
|
||||
|
||||
def test_raises_when_ip_not_in_any_kea_subnet(self, driver):
|
||||
driver.session.get.return_value = _make_json_response(KEA_SUBNETS_RESPONSE)
|
||||
|
||||
with pytest.raises(ValueError, match="No Kea-managed subnet"):
|
||||
driver.create_dhcp_reservation(mac="02:aa:bb:cc:dd:ee", ip="10.99.99.99")
|
||||
|
||||
driver.session.post.assert_not_called()
|
||||
|
||||
def test_raises_when_kea_rejects_reservation(self, driver):
|
||||
driver.session.get.return_value = _make_json_response(KEA_SUBNETS_RESPONSE)
|
||||
driver.session.post.side_effect = [
|
||||
_make_json_response({"rows": []}),
|
||||
_make_json_response(
|
||||
{
|
||||
"result": "failed",
|
||||
"validations": {"reservation.ip_address": "Address not in specified subnet"},
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
with pytest.raises(RuntimeError, match="Kea rejected"):
|
||||
driver.create_dhcp_reservation(mac="02:aa:bb:cc:dd:ee", ip="172.22.8.253")
|
||||
|
||||
# reconfigure must NOT be called after a rejected reservation
|
||||
assert driver.session.post.call_count == 2
|
||||
|
||||
def test_raises_when_kea_plugin_unavailable(self, driver):
|
||||
driver.session.get.side_effect = Exception("404 Not Found")
|
||||
|
||||
with pytest.raises(RuntimeError, match="Kea DHCPv4 plugin unavailable"):
|
||||
driver.create_dhcp_reservation(mac="02:aa:bb:cc:dd:ee", ip="172.22.8.253")
|
||||
|
||||
driver.session.post.assert_not_called()
|
||||
|
||||
Reference in New Issue
Block a user