feat: report Kea's HA peers and the firewall's config sync
CI / test (3.10) (push) Successful in 32s
CI / test (3.11) (push) Successful in 33s
CI / test (3.12) (push) Successful in 34s
CI / test (3.10) (pull_request) Successful in 38s
CI / test (3.11) (pull_request) Successful in 37s
CI / test (3.12) (pull_request) Successful in 38s
CI / test (3.10) (push) Successful in 32s
CI / test (3.11) (push) Successful in 33s
CI / test (3.12) (push) Successful in 34s
CI / test (3.10) (pull_request) Successful in 38s
CI / test (3.11) (pull_request) Successful in 37s
CI / test (3.12) (pull_request) Successful in 38s
get_dhcp_ha reads kea/dhcpv4/get (the ha section) and kea/dhcpv4/searchPeer: whether Kea's HA hook is on, this firewall's peer name -- the % twin when left at its default -- and each peer's name, role and HA URL. get_config_sync reads core/hasync/get: the address this firewall pushes its configuration to and the ticked sync items, mapped to the service names get_services reports; items that are no service stay under `unmapped`. Both raise when the request fails. Configuration, not state: Kea's control agent, which knows whether the pair is in hot standby right now, listens on localhost only. Needs napalm-device-types 4.11.0 (ConfigSyncMixin). NetOrk/netork#1125.
This commit is contained in:
1 parent
5a1aa66db4
commit
e334dc0058
5 files changed
+694
-4
No files matched your search
@@ -0,0 +1,418 @@
|
||||
{
|
||||
"gw-01": {
|
||||
"kea_dhcpv4_get": {
|
||||
"dhcpv4": {
|
||||
"ha": {
|
||||
"enabled": "1",
|
||||
"this_server_name": "",
|
||||
"max_unacked_clients": "2",
|
||||
"%this_server_name": "gw"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kea_dhcpv4_searchPeer": {
|
||||
"rows": [
|
||||
{
|
||||
"uuid": "574e7b41-a5c7-4682-bc63-f4a99e07f262",
|
||||
"name": "gw",
|
||||
"role": "primary",
|
||||
"%role": "Primary",
|
||||
"url": "http://10.5.100.2:8001/"
|
||||
},
|
||||
{
|
||||
"uuid": "d6a4e812-4bc8-4713-bebb-b330ea65f587",
|
||||
"name": "gw-02",
|
||||
"role": "standby",
|
||||
"%role": "Standby",
|
||||
"url": "http://10.5.100.3:8001/"
|
||||
}
|
||||
],
|
||||
"rowCount": 2,
|
||||
"total": 2,
|
||||
"current": 1
|
||||
},
|
||||
"core_hasync_get": {
|
||||
"hasync": {
|
||||
"synchronizetoip": "10.5.100.3",
|
||||
"syncitems": {
|
||||
"aliases": {
|
||||
"value": "Aliases",
|
||||
"selected": 1
|
||||
},
|
||||
"authservers": {
|
||||
"value": "Authentication Servers",
|
||||
"selected": 1
|
||||
},
|
||||
"captiveportal": {
|
||||
"value": "Captive Portal",
|
||||
"selected": 1
|
||||
},
|
||||
"certs": {
|
||||
"value": "Certificates and Authorities",
|
||||
"selected": 1
|
||||
},
|
||||
"cron": {
|
||||
"value": "Cron",
|
||||
"selected": 0
|
||||
},
|
||||
"ddclient": {
|
||||
"value": "ddclient",
|
||||
"selected": 1
|
||||
},
|
||||
"dhcrelay": {
|
||||
"value": "DHCRelay",
|
||||
"selected": 0
|
||||
},
|
||||
"dnsforwarder": {
|
||||
"value": "Dnsmasq DNS/DHCP",
|
||||
"selected": 0
|
||||
},
|
||||
"categories": {
|
||||
"value": "Firewall Categories",
|
||||
"selected": 1
|
||||
},
|
||||
"ifgroups": {
|
||||
"value": "Firewall Groups",
|
||||
"selected": 1
|
||||
},
|
||||
"lvtemplate": {
|
||||
"value": "Firewall Log Templates",
|
||||
"selected": 1
|
||||
},
|
||||
"rules": {
|
||||
"value": "Firewall Rules",
|
||||
"selected": 1
|
||||
},
|
||||
"schedules": {
|
||||
"value": "Firewall Schedules",
|
||||
"selected": 1
|
||||
},
|
||||
"freeradius": {
|
||||
"value": "Freeradius",
|
||||
"selected": 0
|
||||
},
|
||||
"quagga": {
|
||||
"value": "FRR",
|
||||
"selected": 0
|
||||
},
|
||||
"hostwatch": {
|
||||
"value": "Host discovery service",
|
||||
"selected": 0
|
||||
},
|
||||
"suricata": {
|
||||
"value": "Intrusion Detection",
|
||||
"selected": 0
|
||||
},
|
||||
"ipsec": {
|
||||
"value": "IPsec",
|
||||
"selected": 0
|
||||
},
|
||||
"kea": {
|
||||
"value": "Kea DHCP",
|
||||
"selected": 1
|
||||
},
|
||||
"mdns-repeater": {
|
||||
"value": "mDNS Repeater",
|
||||
"selected": 0
|
||||
},
|
||||
"monit": {
|
||||
"value": "Monit System Monitoring",
|
||||
"selected": 0
|
||||
},
|
||||
"nat": {
|
||||
"value": "NAT",
|
||||
"selected": 1
|
||||
},
|
||||
"syslog": {
|
||||
"value": "Netflow / Insight",
|
||||
"selected": 0
|
||||
},
|
||||
"netsnmp": {
|
||||
"value": "Netsnmp",
|
||||
"selected": 0
|
||||
},
|
||||
"ntpd": {
|
||||
"value": "Network Time",
|
||||
"selected": 1
|
||||
},
|
||||
"opendns": {
|
||||
"value": "OpenDNS",
|
||||
"selected": 0
|
||||
},
|
||||
"ssh": {
|
||||
"value": "OpenSSH",
|
||||
"selected": 1
|
||||
},
|
||||
"openvpn": {
|
||||
"value": "OpenVPN",
|
||||
"selected": 0
|
||||
},
|
||||
"qemuguestagent": {
|
||||
"value": "QEMU Guest Agent",
|
||||
"selected": 1
|
||||
},
|
||||
"radvd": {
|
||||
"value": "Router Advertisements",
|
||||
"selected": 0
|
||||
},
|
||||
"shaper": {
|
||||
"value": "Shaper",
|
||||
"selected": 0
|
||||
},
|
||||
"squid": {
|
||||
"value": "Squid Web Proxy",
|
||||
"selected": 0
|
||||
},
|
||||
"staticroutes": {
|
||||
"value": "Static Routes",
|
||||
"selected": 1
|
||||
},
|
||||
"syslog-ng": {
|
||||
"value": "System logging",
|
||||
"selected": 1
|
||||
},
|
||||
"sysctl": {
|
||||
"value": "System Tunables",
|
||||
"selected": 1
|
||||
},
|
||||
"dnsresolver": {
|
||||
"value": "Unbound DNS",
|
||||
"selected": 1
|
||||
},
|
||||
"users": {
|
||||
"value": "Users and Groups",
|
||||
"selected": 1
|
||||
},
|
||||
"virtualip": {
|
||||
"value": "Virtual IPs",
|
||||
"selected": 1
|
||||
},
|
||||
"wol": {
|
||||
"value": "Wake on LAN",
|
||||
"selected": 0
|
||||
},
|
||||
"webgui": {
|
||||
"value": "Web GUI",
|
||||
"selected": 1
|
||||
},
|
||||
"wireguard": {
|
||||
"value": "WireGuard",
|
||||
"selected": 1
|
||||
},
|
||||
"zabbixagent": {
|
||||
"value": "Zabbix monitoring agent",
|
||||
"selected": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gw-02": {
|
||||
"kea_dhcpv4_get": {
|
||||
"dhcpv4": {
|
||||
"ha": {
|
||||
"enabled": "1",
|
||||
"this_server_name": "",
|
||||
"max_unacked_clients": "2",
|
||||
"%this_server_name": "gw-02"
|
||||
}
|
||||
}
|
||||
},
|
||||
"kea_dhcpv4_searchPeer": {
|
||||
"rows": [
|
||||
{
|
||||
"uuid": "574e7b41-a5c7-4682-bc63-f4a99e07f262",
|
||||
"name": "gw",
|
||||
"role": "primary",
|
||||
"%role": "Primary",
|
||||
"url": "http://10.5.100.2:8001/"
|
||||
},
|
||||
{
|
||||
"uuid": "d6a4e812-4bc8-4713-bebb-b330ea65f587",
|
||||
"name": "gw-02",
|
||||
"role": "standby",
|
||||
"%role": "Standby",
|
||||
"url": "http://10.5.100.3:8001/"
|
||||
}
|
||||
],
|
||||
"rowCount": 2,
|
||||
"total": 2,
|
||||
"current": 1
|
||||
},
|
||||
"core_hasync_get": {
|
||||
"hasync": {
|
||||
"synchronizetoip": "",
|
||||
"syncitems": {
|
||||
"aliases": {
|
||||
"value": "Aliases",
|
||||
"selected": 0
|
||||
},
|
||||
"authservers": {
|
||||
"value": "Authentication Servers",
|
||||
"selected": 0
|
||||
},
|
||||
"captiveportal": {
|
||||
"value": "Captive Portal",
|
||||
"selected": 0
|
||||
},
|
||||
"certs": {
|
||||
"value": "Certificates and Authorities",
|
||||
"selected": 0
|
||||
},
|
||||
"cron": {
|
||||
"value": "Cron",
|
||||
"selected": 0
|
||||
},
|
||||
"ddclient": {
|
||||
"value": "ddclient",
|
||||
"selected": 0
|
||||
},
|
||||
"dhcrelay": {
|
||||
"value": "DHCRelay",
|
||||
"selected": 0
|
||||
},
|
||||
"dnsforwarder": {
|
||||
"value": "Dnsmasq DNS/DHCP",
|
||||
"selected": 0
|
||||
},
|
||||
"categories": {
|
||||
"value": "Firewall Categories",
|
||||
"selected": 0
|
||||
},
|
||||
"ifgroups": {
|
||||
"value": "Firewall Groups",
|
||||
"selected": 0
|
||||
},
|
||||
"lvtemplate": {
|
||||
"value": "Firewall Log Templates",
|
||||
"selected": 0
|
||||
},
|
||||
"rules": {
|
||||
"value": "Firewall Rules",
|
||||
"selected": 0
|
||||
},
|
||||
"schedules": {
|
||||
"value": "Firewall Schedules",
|
||||
"selected": 0
|
||||
},
|
||||
"freeradius": {
|
||||
"value": "Freeradius",
|
||||
"selected": 0
|
||||
},
|
||||
"quagga": {
|
||||
"value": "FRR",
|
||||
"selected": 0
|
||||
},
|
||||
"hostwatch": {
|
||||
"value": "Host discovery service",
|
||||
"selected": 0
|
||||
},
|
||||
"suricata": {
|
||||
"value": "Intrusion Detection",
|
||||
"selected": 0
|
||||
},
|
||||
"ipsec": {
|
||||
"value": "IPsec",
|
||||
"selected": 0
|
||||
},
|
||||
"kea": {
|
||||
"value": "Kea DHCP",
|
||||
"selected": 0
|
||||
},
|
||||
"mdns-repeater": {
|
||||
"value": "mDNS Repeater",
|
||||
"selected": 0
|
||||
},
|
||||
"monit": {
|
||||
"value": "Monit System Monitoring",
|
||||
"selected": 0
|
||||
},
|
||||
"nat": {
|
||||
"value": "NAT",
|
||||
"selected": 0
|
||||
},
|
||||
"syslog": {
|
||||
"value": "Netflow / Insight",
|
||||
"selected": 0
|
||||
},
|
||||
"netsnmp": {
|
||||
"value": "Netsnmp",
|
||||
"selected": 0
|
||||
},
|
||||
"ntpd": {
|
||||
"value": "Network Time",
|
||||
"selected": 0
|
||||
},
|
||||
"opendns": {
|
||||
"value": "OpenDNS",
|
||||
"selected": 0
|
||||
},
|
||||
"ssh": {
|
||||
"value": "OpenSSH",
|
||||
"selected": 0
|
||||
},
|
||||
"openvpn": {
|
||||
"value": "OpenVPN",
|
||||
"selected": 0
|
||||
},
|
||||
"qemuguestagent": {
|
||||
"value": "QEMU Guest Agent",
|
||||
"selected": 0
|
||||
},
|
||||
"radvd": {
|
||||
"value": "Router Advertisements",
|
||||
"selected": 0
|
||||
},
|
||||
"shaper": {
|
||||
"value": "Shaper",
|
||||
"selected": 0
|
||||
},
|
||||
"squid": {
|
||||
"value": "Squid Web Proxy",
|
||||
"selected": 0
|
||||
},
|
||||
"staticroutes": {
|
||||
"value": "Static Routes",
|
||||
"selected": 0
|
||||
},
|
||||
"syslog-ng": {
|
||||
"value": "System logging",
|
||||
"selected": 0
|
||||
},
|
||||
"sysctl": {
|
||||
"value": "System Tunables",
|
||||
"selected": 0
|
||||
},
|
||||
"dnsresolver": {
|
||||
"value": "Unbound DNS",
|
||||
"selected": 0
|
||||
},
|
||||
"users": {
|
||||
"value": "Users and Groups",
|
||||
"selected": 0
|
||||
},
|
||||
"virtualip": {
|
||||
"value": "Virtual IPs",
|
||||
"selected": 0
|
||||
},
|
||||
"wol": {
|
||||
"value": "Wake on LAN",
|
||||
"selected": 0
|
||||
},
|
||||
"webgui": {
|
||||
"value": "Web GUI",
|
||||
"selected": 0
|
||||
},
|
||||
"wireguard": {
|
||||
"value": "WireGuard",
|
||||
"selected": 0
|
||||
},
|
||||
"zabbixagent": {
|
||||
"value": "Zabbix monitoring agent",
|
||||
"selected": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Kea's HA peers and the firewall's configuration sync, as facts (netOrk #1125).
|
||||
|
||||
The fixture is what gw-01 and gw-02 answered on 2026-10-16: Kea in hot standby
|
||||
(gw primary, gw-02 standby) and gw-01 pushing its configuration, Kea and
|
||||
Unbound included, to gw-02.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from napalm_opnsense.ha import config_sync, dhcp_ha
|
||||
from napalm_opnsense.opnsense import OPNsenseDriver
|
||||
|
||||
PAIR = json.loads((Path(__file__).parent / "fixtures" / "ha_pair.json").read_text())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def driver() -> OPNsenseDriver:
|
||||
d = OPNsenseDriver("gw-01", "user", "secret")
|
||||
d._session = MagicMock()
|
||||
return d
|
||||
|
||||
|
||||
def _answers(node: str):
|
||||
def _get(path: str):
|
||||
return {
|
||||
"/api/kea/dhcpv4/get": PAIR[node]["kea_dhcpv4_get"],
|
||||
"/api/kea/dhcpv4/searchPeer": PAIR[node]["kea_dhcpv4_searchPeer"],
|
||||
"/api/core/hasync/get": PAIR[node]["core_hasync_get"],
|
||||
}[path]
|
||||
|
||||
return _get
|
||||
|
||||
|
||||
class TestDhcpHa:
|
||||
def test_reads_the_peers_with_their_roles(self, driver):
|
||||
driver._get = _answers("gw-01")
|
||||
|
||||
ha = driver.get_dhcp_ha()
|
||||
|
||||
assert ha["service"] == "kea-dhcp"
|
||||
assert ha["enabled"] is True
|
||||
assert ha["peers"] == [
|
||||
{"name": "gw", "role": "primary", "url": "http://10.5.100.2:8001/"},
|
||||
{"name": "gw-02", "role": "standby", "url": "http://10.5.100.3:8001/"},
|
||||
]
|
||||
|
||||
def test_names_this_server_from_the_effective_value(self, driver):
|
||||
"""`this_server_name` is empty when left at its default -- the
|
||||
hostname; the effective name is in its `%` twin."""
|
||||
driver._get = _answers("gw-02")
|
||||
|
||||
assert driver.get_dhcp_ha()["this_server"] == "gw-02"
|
||||
|
||||
def test_an_explicit_server_name_wins(self):
|
||||
ha = dhcp_ha(
|
||||
{"dhcpv4": {"ha": {"enabled": "1", "this_server_name": "left", "%this_server_name": "x"}}},
|
||||
{"rows": []},
|
||||
)
|
||||
assert ha["this_server"] == "left"
|
||||
|
||||
def test_ha_switched_off(self):
|
||||
ha = dhcp_ha({"dhcpv4": {"ha": {"enabled": "0", "this_server_name": ""}}}, {"rows": []})
|
||||
|
||||
assert ha["enabled"] is False
|
||||
assert ha["peers"] == []
|
||||
assert "this_server" not in ha
|
||||
|
||||
def test_a_peer_without_url_has_no_url_key(self):
|
||||
ha = dhcp_ha(
|
||||
{"dhcpv4": {"ha": {"enabled": "1"}}},
|
||||
{"rows": [{"name": "a", "role": "primary", "url": ""}]},
|
||||
)
|
||||
assert ha["peers"] == [{"name": "a", "role": "primary"}]
|
||||
|
||||
def test_a_failed_read_raises_and_never_says_off(self, driver):
|
||||
driver._get = MagicMock(side_effect=RuntimeError("timeout"))
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
driver.get_dhcp_ha()
|
||||
|
||||
def test_an_answer_without_the_ha_section_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
dhcp_ha({"dhcpv4": {}}, {"rows": []})
|
||||
|
||||
|
||||
class TestConfigSync:
|
||||
def test_the_pushing_side_names_its_peer_and_services(self, driver):
|
||||
driver._get = _answers("gw-01")
|
||||
|
||||
sync = driver.get_config_sync()
|
||||
|
||||
assert sync["peer"] == "10.5.100.3"
|
||||
assert {"kea-dhcp", "unbound", "ntpd", "openssh", "wireguard"} <= set(sync["services"])
|
||||
assert sync["services"] == sorted(sync["services"])
|
||||
|
||||
def test_items_without_a_service_stay_visible(self, driver):
|
||||
driver._get = _answers("gw-01")
|
||||
|
||||
unmapped = driver.get_config_sync()["unmapped"]
|
||||
|
||||
assert {"aliases", "rules", "virtualip"} <= set(unmapped)
|
||||
assert "kea" not in unmapped
|
||||
|
||||
def test_only_selected_items_count(self, driver):
|
||||
driver._get = _answers("gw-01")
|
||||
|
||||
sync = driver.get_config_sync()
|
||||
|
||||
assert "dnsmasq" not in sync["services"] # dnsforwarder: not selected
|
||||
assert "cron" not in sync["unmapped"] and "cron" not in sync["services"]
|
||||
|
||||
def test_the_receiving_side_pushes_to_no_one(self, driver):
|
||||
driver._get = _answers("gw-02")
|
||||
|
||||
sync = driver.get_config_sync()
|
||||
|
||||
assert sync == {"peer": None, "services": [], "unmapped": []}
|
||||
|
||||
def test_without_a_peer_selected_items_are_not_pushed(self):
|
||||
"""Ticked items without a target address push nothing."""
|
||||
answer = {"hasync": {"synchronizetoip": "", "syncitems": {"kea": {"value": "Kea", "selected": 1}}}}
|
||||
|
||||
assert config_sync(answer) == {"peer": None, "services": [], "unmapped": []}
|
||||
|
||||
def test_a_failed_read_raises(self, driver):
|
||||
driver._get = MagicMock(side_effect=RuntimeError("timeout"))
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
driver.get_config_sync()
|
||||
|
||||
def test_an_answer_without_the_section_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
config_sync({"something": {}})
|
||||
Reference in new issue
Block a user