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,109 @@
|
||||
"""Kea's HA peers and the firewall's configuration sync, as facts (netOrk #1125).
|
||||
|
||||
Two answers of the OPNsense API, read as they are:
|
||||
|
||||
* ``/api/kea/dhcpv4/get`` (its ``ha`` section) and ``/api/kea/dhcpv4/searchPeer``:
|
||||
whether Kea's HA hook is on, which peer this firewall is, and each peer's name,
|
||||
role and HA listener URL. Configuration, not state -- Kea's control agent, which
|
||||
knows whether the pair is in hot standby right now, listens on localhost only.
|
||||
* ``/api/core/hasync/get``: the address this firewall pushes its configuration
|
||||
to (``synchronizetoip``) and the ticked sync items. An item is named by
|
||||
OPNsense's sync section, not by the service it covers; :data:`SYNC_ITEM_SERVICES`
|
||||
maps the ones that are a service to the name ``get_services`` reports.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from napalm_device_types.models import ConfigSyncDict, DhcpHaDict, DhcpHaPeerDict
|
||||
|
||||
#: Kea's DHCPv4 server as ``get_services`` names it.
|
||||
KEA_SERVICE = "kea-dhcp"
|
||||
|
||||
#: Sync item -> the service it carries the configuration of, by the name
|
||||
#: ``get_services`` reports. Items that are configuration only (aliases, rules,
|
||||
#: NAT, users ...) have no service and are reported as unmapped.
|
||||
SYNC_ITEM_SERVICES: dict[str, str] = {
|
||||
"captiveportal": "captiveportal",
|
||||
"cron": "cron",
|
||||
"ddclient": "ddclient",
|
||||
"dnsforwarder": "dnsmasq",
|
||||
"dnsresolver": "unbound",
|
||||
"hostwatch": "hostwatch",
|
||||
"kea": KEA_SERVICE,
|
||||
"mdns-repeater": "mdns-repeater",
|
||||
"netsnmp": "snmpd",
|
||||
"ntpd": "ntpd",
|
||||
"qemuguestagent": "qemu-ga",
|
||||
"quagga": "frr",
|
||||
"radvd": "radvd",
|
||||
"squid": "squid",
|
||||
"ssh": "openssh",
|
||||
"sysctl": "sysctl",
|
||||
"syslog-ng": "syslog-ng",
|
||||
"webgui": "webgui",
|
||||
"wireguard": "wireguard",
|
||||
"zabbixagent": "zabbix_agentd",
|
||||
}
|
||||
|
||||
|
||||
def _section(answer: Any, *path: str) -> dict:
|
||||
node = answer
|
||||
for key in path:
|
||||
if not isinstance(node, dict) or not isinstance(node.get(key), dict):
|
||||
raise ValueError(f"unexpected answer: no {'.'.join(path)}")
|
||||
node = node[key]
|
||||
return node
|
||||
|
||||
|
||||
def dhcp_ha(settings: Any, peers: Any) -> DhcpHaDict:
|
||||
"""Kea's HA configuration from the two answers.
|
||||
|
||||
``this_server_name`` is empty while left at its default, the hostname; the
|
||||
effective name is then in its ``%`` twin.
|
||||
|
||||
:raises ValueError: when *settings* has no ``dhcpv4.ha`` section.
|
||||
"""
|
||||
ha = _section(settings, "dhcpv4", "ha")
|
||||
result: DhcpHaDict = {
|
||||
"service": KEA_SERVICE,
|
||||
"enabled": str(ha.get("enabled", "0")) == "1",
|
||||
"peers": [],
|
||||
}
|
||||
this_server = str(ha.get("this_server_name") or ha.get("%this_server_name") or "").strip()
|
||||
if this_server:
|
||||
result["this_server"] = this_server
|
||||
rows = peers.get("rows") if isinstance(peers, dict) else None
|
||||
for row in rows or []:
|
||||
if not isinstance(row, dict) or not row.get("name"):
|
||||
continue
|
||||
peer: DhcpHaPeerDict = {"name": str(row["name"]), "role": str(row.get("role") or "")}
|
||||
if row.get("url"):
|
||||
peer["url"] = str(row["url"])
|
||||
result["peers"].append(peer)
|
||||
return result
|
||||
|
||||
|
||||
def config_sync(answer: Any) -> ConfigSyncDict:
|
||||
"""Where this firewall pushes its configuration, and which services' it pushes.
|
||||
|
||||
Ticked items without a target address push nothing, so they count for nothing.
|
||||
|
||||
:raises ValueError: when *answer* has no ``hasync`` section.
|
||||
"""
|
||||
section = _section(answer, "hasync")
|
||||
peer = str(section.get("synchronizetoip") or "").strip() or None
|
||||
if peer is None:
|
||||
return {"peer": None, "services": [], "unmapped": []}
|
||||
items = section.get("syncitems")
|
||||
ticked = sorted(
|
||||
name
|
||||
for name, item in (items.items() if isinstance(items, dict) else [])
|
||||
if isinstance(item, dict) and str(item.get("selected", "0")) == "1"
|
||||
)
|
||||
return {
|
||||
"peer": peer,
|
||||
"services": sorted({SYNC_ITEM_SERVICES[i] for i in ticked if i in SYNC_ITEM_SERVICES}),
|
||||
"unmapped": [i for i in ticked if i not in SYNC_ITEM_SERVICES],
|
||||
}
|
||||
Reference in new issue
Block a user