feat: report the firewall's CARP addresses as virtual IPs
CI / test (3.10) (push) Successful in 30s
CI / test (3.11) (push) Successful in 29s
CI / test (3.12) (push) Successful in 30s
CI / test (3.10) (pull_request) Successful in 29s
CI / test (3.11) (pull_request) Successful in 28s
CI / test (3.12) (pull_request) Successful in 30s

get_virtual_ips() reads every address bound to a CARP VHID -- the CARP
address and IP aliases on its VHID alike -- from
/api/interfaces/overview/export, with this node's CARP state, so the reading
says who holds each address. interfaces/vip_settings would list the same
addresses with each VHID's CARP password; it is not read.

vip_manager and keepalived are Linux mechanisms and are None here. Needs
napalm-device-types 4.10.0 (carp_virtual_ip).

netOrk #1121.
This commit is contained in:
Christian Manivong committed 2026-10-16 11:07:08 +02:00
1 parent e318c97c20
commit 00172f7a69
6 files changed
+275 -2

No files matched your search

+18 -1
View File
@@ -53,12 +53,13 @@ import requests
from requests.exceptions import RequestException
from napalm_device_types import FingerprintRule, FirewallDriver
from napalm_device_types.models import ListeningSocketsDict
from napalm_device_types.models import ListeningSocketsDict, VirtualIpsDict
from napalm.base.exceptions import ConnectionException, ConnectionClosedException, MergeConfigException
from napalm_opnsense.listening import parse_socket_statistics, wireguard_listen_ports
from napalm_opnsense.ping_mixin import OPNsensePingMixin
from napalm_opnsense.port_forwards import alias_index, port_forwards, wan_interfaces
from napalm_opnsense.virtual_ips import carp_addresses
class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
@@ -1824,6 +1825,22 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
"sockets": parse_socket_statistics(statistics, services, wireguard),
}
def get_virtual_ips(self) -> VirtualIpsDict:
"""The firewall's CARP addresses, and whether this node holds each.
From ``/api/interfaces/overview/export``: every address bound to a CARP
VHID, the CARP address and IP aliases on its VHID alike, with this node's
CARP state (:mod:`napalm_opnsense.virtual_ips`). vip-manager and keepalived
are Linux mechanisms and are not looked for (None).
:raises: whatever the request raises -- an unread overview is not "no CARP".
"""
return {
"vip_manager": None,
"keepalived": None,
"carp": carp_addresses(self._get("/api/interfaces/overview/export")),
}
def manage_service(self, name: str, action: str) -> dict[str, Any]:
"""Execute a lifecycle action on an OPNsense service.
+63
View File
@@ -0,0 +1,63 @@
"""The firewall's CARP addresses, read from its interface overview (netOrk #1121).
``/api/interfaces/overview/export`` lists every address of an interface; one bound
to a CARP VHID carries ``vhid`` and this node's CARP ``status``. That covers the
CARP address itself and an IP alias bound to its VHID alike -- both float with the
VHID's MASTER. An address without a VHID stays on this node and is no virtual IP.
``interfaces/vip_settings/search_item`` would list the same addresses, but with
each VHID's CARP password in it. The overview has none, so a secret never reaches
the caller.
What a state means (MASTER holds, BACKUP stands by) is the same on every CARP
firewall: :func:`napalm_device_types.virtual_ips.carp_virtual_ip`.
"""
from __future__ import annotations
import ipaddress
from typing import Any
from napalm_device_types.models import VirtualIpDict
from napalm_device_types.virtual_ips import carp_virtual_ip
def _interfaces(overview: Any) -> list[Any]:
"""The interface list, whether the answer is the list or wraps it."""
if isinstance(overview, dict):
return overview.get("interfaces") or []
if isinstance(overview, list):
return overview
raise ValueError(f"unexpected interface overview: {type(overview).__name__}")
def carp_addresses(overview: Any) -> list[VirtualIpDict]:
"""Every address bound to a CARP VHID in *overview*, sorted by address.
:raises ValueError: when the answer is neither the interface list nor wraps it.
"""
entries: list[VirtualIpDict] = []
for interface in _interfaces(overview):
if not isinstance(interface, dict):
continue
identifier = interface.get("identifier") or ""
for family in ("ipv4", "ipv6"):
for address in interface.get(family) or []:
if not isinstance(address, dict) or not address.get("vhid"):
continue
entry = carp_virtual_ip(
address=str(address.get("ipaddr") or ""),
prefix_length=None,
vhid=address.get("vhid"),
state=address.get("status"),
instance=interface.get("description") or identifier,
interface=interface.get("device") or identifier or None,
)
if entry is not None:
entries.append(entry)
return sorted(entries, key=_order)
def _order(entry: VirtualIpDict) -> tuple[int, int]:
address = ipaddress.ip_address(entry["address"])
return address.version, int(address)