fix: name and version from system_information; a CARP address on its MASTER's interface
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 30s
CI / test (3.11) (pull_request) Successful in 28s
CI / test (3.12) (pull_request) Successful in 29s
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 30s
CI / test (3.11) (pull_request) Successful in 28s
CI / test (3.12) (pull_request) Successful in 29s
get_facts read hostname and version from /api/core/system/status, which on OPNsense 26.7 carries neither: every firewall read as nameless and "unknown", and netOrk kept naming one by its address. /api/diagnostics/system/system_information has both (name is the FQDN, versions[0] "OPNsense 26.7.7-amd64"). The status answer still counts where the new one gives nothing. get_interfaces_ip listed each interface's primary address only, so a CARP address never showed as the holder's. It now lists an address bound to a VHID on the node that is MASTER for it, and leaves it off a BACKUP -- as keepalived puts the address on the master alone. netOrk's VIP badge sits on that interface (netOrk #1121).
This commit is contained in:
1 parent
055ec3da71
commit
e3ed0f1bdd
3 files changed
+185
-6
No files matched your search
@@ -38,6 +38,7 @@ from __future__ import annotations
|
||||
import difflib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
from ipaddress import ip_address, ip_network
|
||||
@@ -68,6 +69,23 @@ from napalm_opnsense.port_forwards import alias_index, port_forwards, wan_interf
|
||||
from napalm_opnsense.virtual_ips import carp_addresses
|
||||
|
||||
|
||||
_ARCHITECTURE_SUFFIX = re.compile(r"-(amd64|aarch64|arm64|i386|armv7)$")
|
||||
|
||||
|
||||
def _opnsense_version(versions: Any) -> str:
|
||||
"""``["OPNsense 26.7.7-amd64", "FreeBSD ..."]`` -> ``"26.7.7"``; "" when absent."""
|
||||
for line in versions if isinstance(versions, list) else []:
|
||||
product, _, version = str(line).partition(" ")
|
||||
if product == "OPNsense" and version:
|
||||
return _ARCHITECTURE_SUFFIX.sub("", version.strip())
|
||||
return ""
|
||||
|
||||
|
||||
def _carp_master(entry: dict[str, Any]) -> bool:
|
||||
"""Whether this node is MASTER for an interface address bound to a CARP VHID."""
|
||||
return str(entry.get("status") or "").strip().upper() == "MASTER"
|
||||
|
||||
|
||||
class OPNsenseDriver(OPNsensePingMixin, ConfigSyncMixin, FirewallDriver):
|
||||
"""NAPALM driver for OPNsense (read-only, REST API)."""
|
||||
|
||||
@@ -217,16 +235,33 @@ class OPNsenseDriver(OPNsensePingMixin, ConfigSyncMixin, FirewallDriver):
|
||||
def get_facts(self) -> dict[str, Any]:
|
||||
"""Return a dictionary of general device facts.
|
||||
|
||||
Calls ``GET /api/core/system/status``.
|
||||
Name and version come from ``GET /api/diagnostics/system/system_information``
|
||||
(``name`` is the FQDN, ``versions[0]`` reads ``"OPNsense 26.7.7-amd64"``):
|
||||
``/api/core/system/status`` carries neither on OPNsense 26.7. The status
|
||||
answer is still read, and still counts where the other one gives nothing --
|
||||
an older OPNsense, or an API key without the diagnostics privilege.
|
||||
|
||||
Returned keys (NAPALM standard):
|
||||
``vendor``, ``model``, ``hostname``, ``fqdn``, ``os_version``,
|
||||
``serial_number``, ``uptime``, ``interface_list``.
|
||||
"""
|
||||
status = self._get("/api/core/system/status")
|
||||
try:
|
||||
information = self._get("/api/diagnostics/system/system_information")
|
||||
except Exception as exc:
|
||||
logger.debug("system_information not read: %s", exc)
|
||||
information = {}
|
||||
if not isinstance(information, dict):
|
||||
information = {}
|
||||
|
||||
hostname = status.get("hostname") or status.get("name") or ""
|
||||
version = status.get("version") or status.get("product_version") or "unknown"
|
||||
fqdn = str(information.get("name") or status.get("hostname") or status.get("name") or "")
|
||||
hostname = fqdn.split(".", 1)[0]
|
||||
version = (
|
||||
_opnsense_version(information.get("versions"))
|
||||
or status.get("version")
|
||||
or status.get("product_version")
|
||||
or "unknown"
|
||||
)
|
||||
|
||||
try:
|
||||
interface_list = list(self.get_interfaces().keys())
|
||||
@@ -238,7 +273,7 @@ class OPNsenseDriver(OPNsensePingMixin, ConfigSyncMixin, FirewallDriver):
|
||||
"vendor": self.VENDOR,
|
||||
"model": status.get("model") or self.VENDOR,
|
||||
"hostname": hostname,
|
||||
"fqdn": hostname,
|
||||
"fqdn": fqdn,
|
||||
"os_version": version,
|
||||
"serial_number": status.get("serial") or "",
|
||||
"uptime": status.get("uptime", -1),
|
||||
@@ -288,6 +323,10 @@ class OPNsenseDriver(OPNsensePingMixin, ConfigSyncMixin, FirewallDriver):
|
||||
|
||||
Uses ``GET /api/interfaces/overview/export`` (same source as get_interfaces).
|
||||
|
||||
An address bound to a CARP VHID is listed on every node; it counts as this
|
||||
node's only while the node is its MASTER, as keepalived puts the address on
|
||||
the master alone (netOrk #1121).
|
||||
|
||||
Structure::
|
||||
|
||||
{
|
||||
@@ -323,8 +362,14 @@ class OPNsenseDriver(OPNsensePingMixin, ConfigSyncMixin, FirewallDriver):
|
||||
_add(name, addr4, "ipv4")
|
||||
if addr6:
|
||||
_add(name, addr6, "ipv6")
|
||||
for family in ("ipv4", "ipv6"):
|
||||
for entry in iface.get(family) or []:
|
||||
if isinstance(entry, dict) and entry.get("vhid") and _carp_master(entry):
|
||||
_add(name, entry.get("ipaddr") or "", family)
|
||||
if not addr4:
|
||||
for entry in iface.get("ipv4") or []:
|
||||
if entry.get("vhid") and not _carp_master(entry):
|
||||
continue
|
||||
ip_field = entry.get("ipaddr") or entry.get("ip", "")
|
||||
subnet = entry.get("subnetbits") or entry.get("prefix_length")
|
||||
cidr = f"{ip_field}/{subnet}" if subnet and "/" not in ip_field else ip_field
|
||||
@@ -332,6 +377,8 @@ class OPNsenseDriver(OPNsensePingMixin, ConfigSyncMixin, FirewallDriver):
|
||||
_add(name, cidr, "ipv4")
|
||||
if not addr6:
|
||||
for entry in iface.get("ipv6") or []:
|
||||
if entry.get("vhid") and not _carp_master(entry):
|
||||
continue
|
||||
ip_field = entry.get("ipaddr") or entry.get("ip", "")
|
||||
prefix = entry.get("prefixlen") or entry.get("prefix_length")
|
||||
cidr = f"{ip_field}/{prefix}" if prefix and "/" not in ip_field else ip_field
|
||||
|
||||
Reference in new issue
Block a user