Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3ed0f1bdd | ||
|
|
055ec3da71 | ||
|
|
e334dc0058 | ||
|
|
5a1aa66db4 | ||
|
|
5b54174cc4 | ||
|
|
2af99995e9 | ||
|
|
c868d41322 | ||
|
|
00172f7a69 | ||
|
|
e318c97c20 | ||
|
|
852c1d59df | ||
|
|
fa88850535 | ||
|
|
2fb1226734 | ||
|
|
8ce7ea52e7 |
No files matched your search
@@ -71,9 +71,9 @@ arguments.
|
||||
|
||||
| Getter | Status | OPNsense API Endpoint |
|
||||
|---|---|---|
|
||||
| `get_facts` | ✅ | `GET /api/core/system/status` |
|
||||
| `get_facts` | ✅ | `GET /api/diagnostics/system/system_information` (name, version) + `GET /api/core/system/status` (fallback on older versions) |
|
||||
| `get_interfaces` | ✅ | `GET /api/interfaces/overview/export` |
|
||||
| `get_interfaces_ip` | ✅ | `GET /api/interfaces/addresses/export` |
|
||||
| `get_interfaces_ip` | ✅ | `GET /api/interfaces/overview/export`; a CARP address only on the node that is MASTER for it |
|
||||
| `get_interfaces_counters` | ✅ | `GET /api/diagnostics/interface/get_interface_statistics` |
|
||||
| `get_arp_table` | ✅ | `GET /api/diagnostics/interface/get_arp` |
|
||||
| `get_ipv6_neighbors_table` | ✅ | `GET /api/diagnostics/interface/get_ndp` |
|
||||
@@ -88,6 +88,8 @@ arguments.
|
||||
| `get_vlans` | ✅ | `GET /api/interfaces/vlan_settings/search_item` |
|
||||
| `get_mac_address_table` | ❌ | Not applicable (firewall, no L2 switching) |
|
||||
| `ping` | ✅ | `POST /api/diagnostics/ping/set` + `start` + `search_jobs` + `stop`/`remove` |
|
||||
| `get_listening_sockets` | ✅ | `GET /api/diagnostics/interface/get_socket_statistics` + `/api/core/service/search` + `/api/wireguard/service/show` (WireGuard's kernel sockets by listen port) |
|
||||
| `get_virtual_ips` | ✅ | `GET /api/interfaces/overview/export`: every address bound to a CARP VHID (CARP address and IP aliases on it), with this node's CARP state. `interfaces/vip_settings` is not read: it carries the CARP passwords |
|
||||
| `ping_sweep` | ✅ ³ | same endpoints, one batch of parallel jobs at a time |
|
||||
|
||||
> ¹ Requires the `os-lldpd` plugin. Returns empty dict if the plugin is not installed.
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
"""What listens on the firewall, and which OPNsense service it is. Pure: no I/O.
|
||||
|
||||
``/api/diagnostics/interface/get_socket_statistics`` is netstat's socket list
|
||||
with sockstat's user, command and PID merged in, read by configd as root. The
|
||||
contract this serves -- ``napalm_device_types.models.ListeningSocketsDict`` --
|
||||
wants the sockets that listen: a TCP socket in LISTEN and a bound UDP socket,
|
||||
both without a peer (``*:*``).
|
||||
|
||||
**Which service.** netOrk keeps a firewall's services under the names of its
|
||||
service list (``/api/core/service/search``), so a socket's ``unit`` is one of
|
||||
those names, or None. The statistics name the process, cut to ten characters
|
||||
by FreeBSD (``mdns-repea``): a name the service list has wins, then one the
|
||||
command starts with or -- cut short -- is the start of, then the few daemons
|
||||
whose service is called otherwise (``sshd`` is ``openssh``, ``ospfd`` is
|
||||
``frr``). The admin UI and the captive portal are both lighttpd; the portal's
|
||||
runs as ``www``.
|
||||
|
||||
**WireGuard has no process.** Its sockets belong to the kernel (``??``) and are
|
||||
told by the listen ports of the WireGuard interfaces.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set
|
||||
|
||||
from napalm_device_types.models import ListeningSocketDict
|
||||
|
||||
#: FRR's daemons, all under the one service ``frr``.
|
||||
_FRR_DAEMONS = "zebra mgmtd staticd ospfd ospf6d bgpd bfdd ripd ripngd isisd pimd watchfrr".split()
|
||||
#: Daemons whose OPNsense service has another name, by the command as reported.
|
||||
_SERVICE_OF: Dict[str, str] = {
|
||||
"sshd": "openssh",
|
||||
"kea-ctrl-a": "kea-dhcp",
|
||||
"kea-ctrl-agent": "kea-dhcp",
|
||||
"charon": "strongswan",
|
||||
**{daemon: "frr" for daemon in _FRR_DAEMONS},
|
||||
}
|
||||
#: FreeBSD cuts a command at this many characters in the socket list.
|
||||
_COMMAND_LENGTH = 10
|
||||
#: Below this length a service name is too short to match a command's start (``pf``).
|
||||
_PREFIX_MIN = 4
|
||||
_NOBODY = "??"
|
||||
_PEERLESS = "*"
|
||||
|
||||
|
||||
def _service_of(command: str, user: str, services: Set[str]) -> Optional[str]:
|
||||
if command == "lighttpd":
|
||||
name = "captiveportal" if user == "www" else "webgui"
|
||||
return name if name in services else None
|
||||
if command in services:
|
||||
return command
|
||||
alias = _SERVICE_OF.get(command)
|
||||
if alias in services:
|
||||
return alias
|
||||
matches = [
|
||||
name
|
||||
for name in services
|
||||
if (len(name) >= _PREFIX_MIN and command.startswith(name))
|
||||
or (len(command) == _COMMAND_LENGTH and name.startswith(command))
|
||||
]
|
||||
return matches[0] if len(matches) == 1 else None
|
||||
|
||||
|
||||
def _address(protocol: str, address: str) -> tuple:
|
||||
"""``(address, interface)``: ``*`` is every address of the socket's family."""
|
||||
if address == "*":
|
||||
return ("::" if protocol.endswith("6") else "0.0.0.0"), None
|
||||
host, _, zone = address.partition("%")
|
||||
return host, zone or None
|
||||
|
||||
|
||||
def _socket(
|
||||
entry: Dict[str, Any], services: Set[str], wireguard_ports: Set[int]
|
||||
) -> Optional[ListeningSocketDict]:
|
||||
protocol = str(entry.get("protocol") or "")
|
||||
proto = protocol[:3]
|
||||
local, remote = entry.get("local") or {}, entry.get("remote") or {}
|
||||
port = str(local.get("port") or "")
|
||||
if proto not in ("tcp", "udp") or not port.isdigit():
|
||||
return None
|
||||
if remote.get("address") != _PEERLESS or remote.get("port") != _PEERLESS:
|
||||
return None
|
||||
command, pid = str(entry.get("command") or _NOBODY), str(entry.get("pid") or _NOBODY)
|
||||
process = None if command == _NOBODY else command
|
||||
if process is not None:
|
||||
unit = _service_of(process, str(entry.get("user") or ""), services)
|
||||
elif proto == "udp" and int(port) in wireguard_ports and "wireguard" in services:
|
||||
unit = "wireguard"
|
||||
else:
|
||||
unit = None
|
||||
address, interface = _address(protocol, str(local.get("address") or ""))
|
||||
return {
|
||||
"proto": proto,
|
||||
"address": address,
|
||||
"port": int(port),
|
||||
"interface": interface,
|
||||
"process": process,
|
||||
"pid": int(pid) if pid.isdigit() else None,
|
||||
"unit": unit,
|
||||
"container_id": None,
|
||||
}
|
||||
|
||||
|
||||
def wireguard_listen_ports(show: Dict[str, Any]) -> Set[int]:
|
||||
"""The listen ports of the WireGuard interfaces in ``/api/wireguard/service/show``."""
|
||||
ports: Set[int] = set()
|
||||
for row in show.get("rows") or []:
|
||||
port = str(row.get("listen-port") or "")
|
||||
if row.get("type") == "interface" and port.isdigit():
|
||||
ports.add(int(port))
|
||||
return ports
|
||||
|
||||
|
||||
def parse_socket_statistics(
|
||||
statistics: Dict[str, Any], services: Iterable[str], wireguard_ports: Set[int]
|
||||
) -> List[ListeningSocketDict]:
|
||||
"""The listening sockets of ``get_socket_statistics``, sorted by protocol,
|
||||
port and address. Every section is read: the UNIX sockets have no ``local``."""
|
||||
names = set(services)
|
||||
sockets: List[ListeningSocketDict] = []
|
||||
for section in (statistics.get("statistics") or {}).values():
|
||||
for entry in (section or {}).values() if isinstance(section, dict) else ():
|
||||
socket = _socket(entry, names, wireguard_ports) if isinstance(entry, dict) else None
|
||||
if socket is not None:
|
||||
sockets.append(socket)
|
||||
return sorted(
|
||||
sockets, key=lambda s: (s["proto"], s["port"], s["address"], s["interface"] or "")
|
||||
)
|
||||
+151
-19
@@ -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
|
||||
@@ -52,14 +53,40 @@ _REFRESH_INTERVAL = 3
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from napalm_device_types import FingerprintRule, FirewallDriver
|
||||
from napalm_device_types import ConfigSyncMixin, FingerprintRule, FirewallDriver
|
||||
from napalm_device_types.models import (
|
||||
ConfigSyncDict,
|
||||
DhcpHaDict,
|
||||
ListeningSocketsDict,
|
||||
VirtualIpsDict,
|
||||
)
|
||||
from napalm.base.exceptions import ConnectionException, ConnectionClosedException, MergeConfigException
|
||||
|
||||
from napalm_opnsense.ha import config_sync, dhcp_ha
|
||||
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):
|
||||
_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)."""
|
||||
|
||||
VENDOR = "OPNsense"
|
||||
@@ -170,6 +197,22 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def _get_text(self, path: str) -> str:
|
||||
"""Perform a GET request and return the body as text.
|
||||
|
||||
For the few endpoints that answer with a file rather than JSON, such
|
||||
as the configuration backup.
|
||||
|
||||
:raises ConnectionClosedException: if called before :meth:`open`.
|
||||
:raises RequestException: on HTTP-level errors.
|
||||
"""
|
||||
if self.session is None:
|
||||
raise ConnectionClosedException("Not connected – call open() first.")
|
||||
url = self.base_url.rstrip("/") + path
|
||||
response = self.session.get(url, timeout=self.timeout)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
|
||||
def _post(self, path: str, data: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Perform a POST request against the OPNsense REST API.
|
||||
|
||||
@@ -192,16 +235,33 @@ class OPNsenseDriver(OPNsensePingMixin, 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())
|
||||
@@ -213,7 +273,7 @@ class OPNsenseDriver(OPNsensePingMixin, 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),
|
||||
@@ -263,6 +323,10 @@ class OPNsenseDriver(OPNsensePingMixin, 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::
|
||||
|
||||
{
|
||||
@@ -298,8 +362,14 @@ class OPNsenseDriver(OPNsensePingMixin, 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
|
||||
@@ -307,6 +377,8 @@ class OPNsenseDriver(OPNsensePingMixin, 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
|
||||
@@ -833,22 +905,19 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
|
||||
the JSON-serialised staged routes when a candidate has been loaded,
|
||||
or an empty string otherwise.
|
||||
|
||||
Calls ``GET /api/core/backup/download/this``.
|
||||
Calls ``GET /api/core/backup/download/this``, which answers with
|
||||
``config.xml`` itself rather than JSON. A failed read raises: an empty
|
||||
``running`` would claim the device has no configuration and hide why
|
||||
the read failed, e.g. an API key without the backup privilege.
|
||||
"""
|
||||
configs: dict[str, str] = {"running": "", "startup": "", "candidate": ""}
|
||||
|
||||
if retrieve in ("all", "running", "startup"):
|
||||
try:
|
||||
response = self._get("/api/core/backup/download/this")
|
||||
xml_text: str = (
|
||||
response if isinstance(response, str) else str(response)
|
||||
)
|
||||
if retrieve in ("all", "running"):
|
||||
configs["running"] = xml_text
|
||||
if retrieve in ("all", "startup"):
|
||||
configs["startup"] = xml_text
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to fetch running/startup config: %s", exc)
|
||||
xml_text = self._get_text("/api/core/backup/download/this")
|
||||
if retrieve in ("all", "running"):
|
||||
configs["running"] = xml_text
|
||||
if retrieve in ("all", "startup"):
|
||||
configs["startup"] = xml_text
|
||||
|
||||
if retrieve in ("all", "candidate") and self._candidate_config is not None:
|
||||
configs["candidate"] = json.dumps(self._candidate_config, indent=2)
|
||||
@@ -1789,6 +1858,60 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
|
||||
})
|
||||
return sorted(result, key=lambda x: x["name"].lower())
|
||||
|
||||
def get_listening_sockets(self) -> ListeningSocketsDict:
|
||||
"""Every listening TCP and bound UDP socket, with the OPNsense service behind it.
|
||||
|
||||
Read from ``/api/diagnostics/interface/get_socket_statistics``, which
|
||||
configd collects as root, so every socket names its process where it has
|
||||
one (``attributed``). Which service: :mod:`napalm_opnsense.listening`.
|
||||
"""
|
||||
statistics = self._get("/api/diagnostics/interface/get_socket_statistics")
|
||||
services = [service["name"] for service in self.get_services()]
|
||||
try:
|
||||
wireguard = wireguard_listen_ports(self._get("/api/wireguard/service/show"))
|
||||
except Exception as exc:
|
||||
# No WireGuard here: its kernel sockets stay nobody's.
|
||||
logger.debug("WireGuard listen ports not read: %s", exc)
|
||||
wireguard = set()
|
||||
return {
|
||||
"attributed": True,
|
||||
"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 get_dhcp_ha(self) -> DhcpHaDict:
|
||||
"""Kea's HA configuration: on or off, this firewall's peer name, every peer.
|
||||
|
||||
Configuration, not state (:mod:`napalm_opnsense.ha`).
|
||||
|
||||
:raises: whatever either request raises -- an unread answer is not "off".
|
||||
"""
|
||||
return dhcp_ha(
|
||||
self._get("/api/kea/dhcpv4/get"), self._get("/api/kea/dhcpv4/searchPeer")
|
||||
)
|
||||
|
||||
def get_config_sync(self) -> ConfigSyncDict:
|
||||
"""Where this firewall pushes its configuration (HA sync), and for which services.
|
||||
|
||||
:raises: whatever the request raises -- an unread answer is not "no sync".
|
||||
"""
|
||||
return config_sync(self._get("/api/core/hasync/get"))
|
||||
|
||||
def manage_service(self, name: str, action: str) -> dict[str, Any]:
|
||||
"""Execute a lifecycle action on an OPNsense service.
|
||||
|
||||
@@ -2881,8 +3004,17 @@ class OPNsenseDriver(OPNsensePingMixin, FirewallDriver):
|
||||
# Re-add all enabled A/AAAA records, one override per logical record.
|
||||
# Two rows for the same name is a state netOrk's own database can
|
||||
# legitimately be in; the device must still end up with one.
|
||||
# An enabled override made by hand already answers for its record: the
|
||||
# caller's records include what it read off this device, and adding
|
||||
# those again under the tag left each one twice (NetOrk/netork#1123).
|
||||
added = 0
|
||||
pushed: set[tuple[str, str, str]] = set()
|
||||
pushed: set[tuple[str, str, str]] = {
|
||||
(entry["hostname"].lower(), entry.get("record_type", "A"), entry["ip"])
|
||||
for entry in existing
|
||||
if entry["domain"].rstrip(".") == zone_clean
|
||||
and not entry["description"].startswith(self._NETORK_TAG)
|
||||
and entry.get("enabled", True)
|
||||
}
|
||||
for rec in records:
|
||||
if not rec.get("enabled", True):
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"""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 CARP protocol
|
||||
knowledge, so it lives here with the driver that speaks CARP
|
||||
(:func:`carp_virtual_ip`), not in napalm-device-types, which carries only the
|
||||
shape of the reading.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from typing import Any, Optional
|
||||
|
||||
from napalm_device_types.models import VirtualIpDict
|
||||
|
||||
#: CARP's state of a VHID on one member -> (running, holds).
|
||||
_CARP_STATES: dict[str, tuple[bool, bool]] = {
|
||||
"MASTER": (True, True),
|
||||
"BACKUP": (True, False),
|
||||
"INIT": (False, False),
|
||||
"DISABLED": (False, False),
|
||||
}
|
||||
|
||||
|
||||
def _address(text: str) -> Optional[tuple[str, Optional[int]]]:
|
||||
"""``"10.0.0.1/24"`` -> ``("10.0.0.1", 24)``; None for what is no address."""
|
||||
host, slash, prefix = text.partition("/")
|
||||
try:
|
||||
address = ipaddress.ip_address(host)
|
||||
except ValueError:
|
||||
return None
|
||||
if not slash:
|
||||
return str(address), None
|
||||
if not prefix.isdigit() or int(prefix) > address.max_prefixlen:
|
||||
return None
|
||||
return str(address), int(prefix)
|
||||
|
||||
|
||||
def carp_virtual_ip(
|
||||
*,
|
||||
address: str,
|
||||
prefix_length: Optional[int],
|
||||
vhid: object,
|
||||
state: Optional[str],
|
||||
instance: str,
|
||||
interface: Optional[str],
|
||||
) -> Optional[VirtualIpDict]:
|
||||
"""One address bound to a CARP VHID, as a virtual IP; None for what is no address.
|
||||
|
||||
Who holds it comes from CARP's *state* on this member, not from its
|
||||
interfaces: MASTER holds it, BACKUP stands by, INIT and DISABLED neither run
|
||||
nor hold. A state this does not know says nothing (``running`` and ``holds``
|
||||
None), and the caller falls back on the interfaces. *address* may carry its
|
||||
prefix (``"10.0.0.1/24"``), which then wins over *prefix_length*.
|
||||
"""
|
||||
parsed = _address(address.strip()) if address else None
|
||||
if parsed is None:
|
||||
return None
|
||||
normalised, written = parsed
|
||||
prefix = written if written is not None else prefix_length
|
||||
if prefix is not None and not 0 <= prefix <= ipaddress.ip_address(normalised).max_prefixlen:
|
||||
prefix = None
|
||||
vhid_text = str(vhid).strip() if vhid is not None else ""
|
||||
running, holds = _CARP_STATES.get((state or "").strip().upper(), (None, None))
|
||||
return {
|
||||
"mechanism": "carp",
|
||||
"instance": instance or f"vhid {vhid_text}",
|
||||
"address": normalised,
|
||||
"prefix_length": prefix,
|
||||
"interface": interface,
|
||||
"group_key": f"vhid:{vhid_text}" if vhid_text.isdigit() else None,
|
||||
"running": running,
|
||||
"holds": holds,
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
+1
-1
@@ -24,7 +24,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"napalm>=4.0.0",
|
||||
"napalm_device_types>=0.2.0",
|
||||
"napalm_device_types>=4.11.0",
|
||||
"requests>=2.28.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -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,62 @@
|
||||
[
|
||||
{
|
||||
"identifier": "lan",
|
||||
"device": "em0",
|
||||
"description": "MGMT",
|
||||
"addr4": "192.0.2.2/24",
|
||||
"ipv4": [
|
||||
{"ipaddr": "192.0.2.2/24"},
|
||||
{"ipaddr": "192.0.2.1/24", "vhid": "108", "status": "MASTER", "advbase": "1", "advskew": "0", "peer": "224.0.0.18", "peer6": "ff02::12"}
|
||||
],
|
||||
"ipv6": [
|
||||
{"ipaddr": "fe80::1:2/64"},
|
||||
{"ipaddr": "2001:db8:8::1/64", "vhid": "108", "status": "MASTER", "advbase": "1", "advskew": "0", "peer": "224.0.0.18", "peer6": "ff02::12"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "opt2",
|
||||
"device": "vlan0040",
|
||||
"description": "SERVER",
|
||||
"addr4": "198.51.100.2/24",
|
||||
"ipv4": [
|
||||
{"ipaddr": "198.51.100.2/24"},
|
||||
{"ipaddr": "198.51.100.140/32", "vhid": "140", "status": "MASTER", "advbase": "1", "advskew": "0", "peer": "224.0.0.18", "peer6": "ff02::12"},
|
||||
{"ipaddr": "198.51.100.1/24", "vhid": "140", "status": "MASTER", "advbase": "1", "advskew": "0", "peer": "224.0.0.18", "peer6": "ff02::12"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "opt9",
|
||||
"device": "vlan0011",
|
||||
"description": "",
|
||||
"addr4": "203.0.113.2/24",
|
||||
"ipv4": [
|
||||
{"ipaddr": "203.0.113.2/24"},
|
||||
{"ipaddr": "203.0.113.1/24", "vhid": "111", "status": "INIT", "advbase": "1", "advskew": "0", "peer": "224.0.0.18", "peer6": "ff02::12"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "opt3",
|
||||
"device": "vlan0020",
|
||||
"description": "IOT",
|
||||
"addr4": "192.0.2.130/25",
|
||||
"ipv4": [
|
||||
{"ipaddr": "192.0.2.130/25"},
|
||||
{"ipaddr": "192.0.2.140/32"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "lo0",
|
||||
"device": "lo0",
|
||||
"description": "Loopback",
|
||||
"addr4": "127.0.0.1/8",
|
||||
"ipv4": [{"ipaddr": "127.0.0.1/8"}],
|
||||
"ipv6": [{"ipaddr": "::1/128"}, {"ipaddr": "fe80::1/64"}]
|
||||
},
|
||||
{
|
||||
"identifier": "wg1",
|
||||
"device": "wg1",
|
||||
"description": "WireGuard",
|
||||
"addr4": "",
|
||||
"ipv4": null
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,935 @@
|
||||
{
|
||||
"statistics": {
|
||||
"Active Internet connections": {
|
||||
"tcp4/[192.0.2.1:443-192.0.2.50:38544]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "192.0.2.1",
|
||||
"port": "443",
|
||||
"0": "192.0.2.1:443"
|
||||
},
|
||||
"remote": {
|
||||
"address": "192.0.2.50",
|
||||
"port": "38544",
|
||||
"0": "192.0.2.50:38544"
|
||||
},
|
||||
"receive-high-water": 65700,
|
||||
"send-high-water": 65700,
|
||||
"receive-low-water": 1,
|
||||
"send-low-water": 2048,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 525600,
|
||||
"send-mbuf-bytes-max": 525600,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "7199.77",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "0.02",
|
||||
"flow-id": "f6ae054e",
|
||||
"flow-type": 130,
|
||||
"user": "root",
|
||||
"command": "lighttpd",
|
||||
"pid": "46564",
|
||||
"fd": "19",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"tcp4/[192.0.2.1:443-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "192.0.2.1",
|
||||
"port": "443",
|
||||
"0": "192.0.2.1:443"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "33552.00",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "root",
|
||||
"command": "lighttpd",
|
||||
"pid": "46564",
|
||||
"fd": "11",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"tcp4/[198.51.100.1:443-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "198.51.100.1",
|
||||
"port": "443",
|
||||
"0": "198.51.100.1:443"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "33552.00",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "root",
|
||||
"command": "lighttpd",
|
||||
"pid": "46564",
|
||||
"fd": "10",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"tcp4/[127.0.0.1:443-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "443",
|
||||
"0": "127.0.0.1:443"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "33552.00",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "root",
|
||||
"command": "lighttpd",
|
||||
"pid": "46564",
|
||||
"fd": "7",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"tcp4/[192.0.2.1:80-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "192.0.2.1",
|
||||
"port": "80",
|
||||
"0": "192.0.2.1:80"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "33552.00",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "root",
|
||||
"command": "lighttpd",
|
||||
"pid": "46564",
|
||||
"fd": "16",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"tcp4/[*:8000-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "*",
|
||||
"port": "8000",
|
||||
"0": "*:8000"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "41043.28",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "www",
|
||||
"command": "lighttpd",
|
||||
"pid": "7612",
|
||||
"fd": "7",
|
||||
"proto": "tcp4",
|
||||
"listen-queue-sizes": {
|
||||
"qlen": "0",
|
||||
"incqlen": "0",
|
||||
"maxqlen": "128"
|
||||
}
|
||||
},
|
||||
"tcp6/[*:8000-*:*]": {
|
||||
"protocol": "tcp6",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "*",
|
||||
"port": "8000",
|
||||
"0": "*:8000"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "41043.28",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "www",
|
||||
"command": "lighttpd",
|
||||
"pid": "7612",
|
||||
"fd": "4",
|
||||
"proto": "tcp6",
|
||||
"listen-queue-sizes": {
|
||||
"qlen": "0",
|
||||
"incqlen": "0",
|
||||
"maxqlen": "128"
|
||||
}
|
||||
},
|
||||
"tcp4/[192.0.2.1:8080-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "192.0.2.1",
|
||||
"port": "8080",
|
||||
"0": "192.0.2.1:8080"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "21096.55",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "root",
|
||||
"command": "crowdsec",
|
||||
"pid": "90423",
|
||||
"fd": "26",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"tcp4/[127.0.0.1:6060-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "6060",
|
||||
"0": "127.0.0.1:6060"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "41044.32",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "root",
|
||||
"command": "crowdsec",
|
||||
"pid": "90423",
|
||||
"fd": "20",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"tcp4/[192.0.2.1:10050-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "192.0.2.1",
|
||||
"port": "10050",
|
||||
"0": "192.0.2.1:10050"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "41044.31",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "zabbix",
|
||||
"command": "zabbix_age",
|
||||
"pid": "70051",
|
||||
"fd": "5",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"tcp4/[192.0.2.1:22-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "192.0.2.1",
|
||||
"port": "22",
|
||||
"0": "192.0.2.1:22"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "41054.14",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "root",
|
||||
"command": "sshd",
|
||||
"pid": "12554",
|
||||
"fd": "6",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"tcp4/[127.0.0.1:22-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "22",
|
||||
"0": "127.0.0.1:22"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "41054.14",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "root",
|
||||
"command": "sshd",
|
||||
"pid": "12554",
|
||||
"fd": "11",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"tcp4/[203.0.113.1:53-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "203.0.113.1",
|
||||
"port": "53",
|
||||
"0": "203.0.113.1:53"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "9830.17",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "unbound",
|
||||
"command": "unbound",
|
||||
"pid": "10992",
|
||||
"fd": "116",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"tcp4/[127.0.0.1:953-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "953",
|
||||
"0": "127.0.0.1:953"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "9830.17",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "unbound",
|
||||
"command": "unbound",
|
||||
"pid": "10992",
|
||||
"fd": "117",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"tcp4/[127.0.0.1:8000-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "8000",
|
||||
"0": "127.0.0.1:8000"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "10006.15",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "root",
|
||||
"command": "kea-ctrl-a",
|
||||
"pid": "14979",
|
||||
"fd": "7",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"tcp4/[127.0.0.1:2604-*:*]": {
|
||||
"protocol": "tcp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "2604",
|
||||
"0": "127.0.0.1:2604"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 0,
|
||||
"send-high-water": 0,
|
||||
"receive-low-water": 0,
|
||||
"send-low-water": 0,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 0,
|
||||
"send-mbuf-bytes-max": 0,
|
||||
"retransmit-timer": "0.00",
|
||||
"persist-timer": "0.00",
|
||||
"keepalive-timer": "0.00",
|
||||
"msl2-timer": "0.00",
|
||||
"delay-ack-timer": "0.00",
|
||||
"inactivity-timer": "41047.92",
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "frr",
|
||||
"command": "ospfd",
|
||||
"pid": "89416",
|
||||
"fd": "11",
|
||||
"proto": "tcp4"
|
||||
},
|
||||
"udp4/[203.0.113.1:53-*:*]": {
|
||||
"protocol": "udp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "203.0.113.1",
|
||||
"port": "53",
|
||||
"0": "203.0.113.1:53"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 42080,
|
||||
"send-high-water": 57344,
|
||||
"receive-low-water": 1,
|
||||
"send-low-water": 2048,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 336640,
|
||||
"send-mbuf-bytes-max": 458752,
|
||||
"flow-id": "b8a211",
|
||||
"flow-type": 63,
|
||||
"user": "unbound",
|
||||
"command": "unbound",
|
||||
"pid": "10992",
|
||||
"fd": "115",
|
||||
"proto": "udp4"
|
||||
},
|
||||
"udp4/[192.0.2.1:67-*:*]": {
|
||||
"protocol": "udp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "192.0.2.1",
|
||||
"port": "67",
|
||||
"0": "192.0.2.1:67"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 42080,
|
||||
"send-high-water": 57344,
|
||||
"receive-low-water": 1,
|
||||
"send-low-water": 2048,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 336640,
|
||||
"send-mbuf-bytes-max": 458752,
|
||||
"flow-id": "b899f5",
|
||||
"flow-type": 63,
|
||||
"user": "root",
|
||||
"command": "kea-dhcp4",
|
||||
"pid": "12917",
|
||||
"fd": "19",
|
||||
"proto": "udp4"
|
||||
},
|
||||
"udp4/[*:123-*:*]": {
|
||||
"protocol": "udp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "*",
|
||||
"port": "123",
|
||||
"0": "*:123"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 42080,
|
||||
"send-high-water": 57344,
|
||||
"receive-low-water": 1,
|
||||
"send-low-water": 2048,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 336640,
|
||||
"send-mbuf-bytes-max": 458752,
|
||||
"flow-id": "c23b49",
|
||||
"flow-type": 63,
|
||||
"user": "root",
|
||||
"command": "ntpd",
|
||||
"pid": "69031",
|
||||
"fd": "21",
|
||||
"proto": "udp4"
|
||||
},
|
||||
"udp6/[*:123-*:*]": {
|
||||
"protocol": "udp6",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "*",
|
||||
"port": "123",
|
||||
"0": "*:123"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 42080,
|
||||
"send-high-water": 57344,
|
||||
"receive-low-water": 1,
|
||||
"send-low-water": 2048,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 336640,
|
||||
"send-mbuf-bytes-max": 458752,
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "root",
|
||||
"command": "ntpd",
|
||||
"pid": "69031",
|
||||
"fd": "20",
|
||||
"proto": "udp6"
|
||||
},
|
||||
"udp4/[198.51.100.1:5353-*:*]": {
|
||||
"protocol": "udp4",
|
||||
"receive-bytes-waiting": 42052,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "198.51.100.1",
|
||||
"port": "5353",
|
||||
"0": "198.51.100.1:5353"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 42080,
|
||||
"send-high-water": 57344,
|
||||
"receive-low-water": 1,
|
||||
"send-low-water": 2048,
|
||||
"receive-mbuf-bytes": 321536,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 336640,
|
||||
"send-mbuf-bytes-max": 458752,
|
||||
"flow-id": "13f3",
|
||||
"flow-type": 63,
|
||||
"user": "root",
|
||||
"command": "mdns-repea",
|
||||
"pid": "12222",
|
||||
"fd": "10",
|
||||
"proto": "udp4"
|
||||
},
|
||||
"udp4/[*:1900-*:*]": {
|
||||
"protocol": "udp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "*",
|
||||
"port": "1900",
|
||||
"0": "*:1900"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 42080,
|
||||
"send-high-water": 57344,
|
||||
"receive-low-water": 1,
|
||||
"send-low-water": 2048,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 336640,
|
||||
"send-mbuf-bytes-max": 458752,
|
||||
"flow-id": "c27",
|
||||
"flow-type": 63,
|
||||
"user": "root",
|
||||
"command": "udpbroadca",
|
||||
"pid": "78873",
|
||||
"fd": "3",
|
||||
"proto": "udp4"
|
||||
},
|
||||
"udp4/[192.0.2.1:161-*:*]": {
|
||||
"protocol": "udp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "192.0.2.1",
|
||||
"port": "161",
|
||||
"0": "192.0.2.1:161"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 42080,
|
||||
"send-high-water": 57344,
|
||||
"receive-low-water": 1,
|
||||
"send-low-water": 2048,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 336640,
|
||||
"send-mbuf-bytes-max": 458752,
|
||||
"flow-id": "130d",
|
||||
"flow-type": 63,
|
||||
"user": "root",
|
||||
"command": "snmpd",
|
||||
"pid": "53743",
|
||||
"fd": "8",
|
||||
"proto": "udp4"
|
||||
},
|
||||
"udp4/[*:51821-*:*]": {
|
||||
"protocol": "udp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "*",
|
||||
"port": "51821",
|
||||
"0": "*:51821"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 42080,
|
||||
"send-high-water": 57344,
|
||||
"receive-low-water": 1,
|
||||
"send-low-water": 2048,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 336640,
|
||||
"send-mbuf-bytes-max": 458752,
|
||||
"flow-id": "945",
|
||||
"flow-type": 63,
|
||||
"user": "??",
|
||||
"command": "??",
|
||||
"pid": "??",
|
||||
"fd": "??",
|
||||
"proto": "udp4"
|
||||
},
|
||||
"udp6/[*:51821-*:*]": {
|
||||
"protocol": "udp6",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "*",
|
||||
"port": "51821",
|
||||
"0": "*:51821"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 42080,
|
||||
"send-high-water": 57344,
|
||||
"receive-low-water": 1,
|
||||
"send-low-water": 2048,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 336640,
|
||||
"send-mbuf-bytes-max": 458752,
|
||||
"flow-id": "0",
|
||||
"flow-type": 0,
|
||||
"user": "??",
|
||||
"command": "??",
|
||||
"pid": "??",
|
||||
"fd": "??",
|
||||
"proto": "udp6"
|
||||
},
|
||||
"udp4/[*:29281-*:*]": {
|
||||
"protocol": "udp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "*",
|
||||
"port": "29281",
|
||||
"0": "*:29281"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 42080,
|
||||
"send-high-water": 57344,
|
||||
"receive-low-water": 1,
|
||||
"send-low-water": 2048,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 336640,
|
||||
"send-mbuf-bytes-max": 458752,
|
||||
"flow-id": "c23c01",
|
||||
"flow-type": 63,
|
||||
"user": "??",
|
||||
"command": "??",
|
||||
"pid": "??",
|
||||
"fd": "??",
|
||||
"proto": "udp4"
|
||||
},
|
||||
"udp4/[*:*-*:*]": {
|
||||
"protocol": "udp4",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"local": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"remote": {
|
||||
"address": "*",
|
||||
"port": "*",
|
||||
"0": "*:*"
|
||||
},
|
||||
"receive-high-water": 42080,
|
||||
"send-high-water": 57344,
|
||||
"receive-low-water": 1,
|
||||
"send-low-water": 2048,
|
||||
"receive-mbuf-bytes": 0,
|
||||
"send-mbuf-bytes": 0,
|
||||
"receive-mbuf-bytes-max": 336640,
|
||||
"send-mbuf-bytes-max": 458752,
|
||||
"flow-id": "1441",
|
||||
"flow-type": 63,
|
||||
"user": "_lldpd",
|
||||
"command": "lldpd",
|
||||
"pid": "72768",
|
||||
"fd": "6",
|
||||
"proto": "udp4"
|
||||
}
|
||||
},
|
||||
"Active UNIX domain sockets": {
|
||||
"fffff80016c50c80 - /var/run/configd.socket": {
|
||||
"address": "fffff80016c50c80",
|
||||
"type": "stream",
|
||||
"receive-bytes-waiting": 0,
|
||||
"send-bytes-waiting": 0,
|
||||
"vnode": "0",
|
||||
"connection": "fffff8006e7b8640",
|
||||
"first-reference": "0",
|
||||
"next-reference": "0",
|
||||
"path": "/var/run/configd.socket"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
"""carp_virtual_ip: one CARP address as a virtual IP, with its holder in the reading.
|
||||
|
||||
CARP differs from vip-manager and keepalived in what the caller can see. The
|
||||
address is configured on every member, and the interface list of a firewall need
|
||||
not show it at all -- so the interfaces cannot say who holds it. CARP's own state
|
||||
can: the member in MASTER holds it, a BACKUP stands by. That is CARP protocol
|
||||
knowledge, so it lives with the driver that speaks CARP (netOrk #1121), not in
|
||||
napalm-device-types.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from napalm_opnsense.virtual_ips import carp_virtual_ip
|
||||
|
||||
|
||||
def _entry(**overrides):
|
||||
fields = {
|
||||
"address": "172.22.8.1",
|
||||
"prefix_length": 24,
|
||||
"vhid": "108",
|
||||
"state": "MASTER",
|
||||
"instance": "MGMT",
|
||||
"interface": "vtnet0",
|
||||
}
|
||||
fields.update(overrides)
|
||||
return carp_virtual_ip(**fields)
|
||||
|
||||
|
||||
def test_a_master_holds_the_address():
|
||||
assert _entry() == {
|
||||
"mechanism": "carp",
|
||||
"instance": "MGMT",
|
||||
"address": "172.22.8.1",
|
||||
"prefix_length": 24,
|
||||
"interface": "vtnet0",
|
||||
"group_key": "vhid:108",
|
||||
"running": True,
|
||||
"holds": True,
|
||||
}
|
||||
|
||||
|
||||
def test_a_backup_stands_by():
|
||||
entry = _entry(state="BACKUP")
|
||||
assert entry is not None
|
||||
assert (entry["running"], entry["holds"]) == (True, False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state", ["INIT", "DISABLED"])
|
||||
def test_a_member_whose_carp_is_not_up_neither_runs_nor_holds(state):
|
||||
entry = _entry(state=state)
|
||||
assert entry is not None
|
||||
assert (entry["running"], entry["holds"]) == (False, False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state", [None, "", "SOMETHING-NEW"])
|
||||
def test_an_unknown_state_says_nothing_about_the_holder(state):
|
||||
"""None leaves the holder to the interfaces, as for the other mechanisms."""
|
||||
entry = _entry(state=state)
|
||||
assert entry is not None
|
||||
assert (entry["running"], entry["holds"]) == (None, None)
|
||||
|
||||
|
||||
def test_the_state_is_read_case_blind():
|
||||
entry = _entry(state=" master ")
|
||||
assert entry is not None
|
||||
assert entry["holds"] is True
|
||||
|
||||
|
||||
def test_the_address_is_normalised():
|
||||
entry = _entry(address="FD00::0001", prefix_length=64)
|
||||
assert entry is not None
|
||||
assert entry["address"] == "fd00::1"
|
||||
|
||||
|
||||
def test_an_address_written_with_its_prefix_takes_that_prefix():
|
||||
entry = _entry(address="10.42.100.1/24", prefix_length=None)
|
||||
assert entry is not None
|
||||
assert (entry["address"], entry["prefix_length"]) == ("10.42.100.1", 24)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("address", ["", "not-an-address", "10.0.0.300"])
|
||||
def test_what_is_no_address_is_no_virtual_ip(address):
|
||||
assert _entry(address=address) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix", [33, -1])
|
||||
def test_an_impossible_prefix_is_dropped(prefix):
|
||||
entry = _entry(prefix_length=prefix)
|
||||
assert entry is not None
|
||||
assert entry["prefix_length"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("vhid", [None, "", "abc"])
|
||||
def test_without_a_numeric_vhid_there_is_no_group(vhid):
|
||||
entry = _entry(vhid=vhid)
|
||||
assert entry is not None
|
||||
assert entry["group_key"] is None
|
||||
|
||||
|
||||
def test_a_vhid_given_as_a_number_groups_the_same_way():
|
||||
entry = _entry(vhid=108)
|
||||
assert entry is not None
|
||||
assert entry["group_key"] == "vhid:108"
|
||||
|
||||
|
||||
def test_an_empty_instance_falls_back_to_the_vhid():
|
||||
entry = _entry(instance="")
|
||||
assert entry is not None
|
||||
assert entry["instance"] == "vhid 108"
|
||||
+118
-3
@@ -2,6 +2,7 @@
|
||||
|
||||
import json
|
||||
import pytest
|
||||
import requests
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from napalm_opnsense.opnsense import OPNsenseDriver
|
||||
@@ -1656,24 +1657,74 @@ class TestRollback:
|
||||
|
||||
class TestGetConfigCandidate:
|
||||
def test_candidate_empty_without_staged_config(self, driver):
|
||||
driver._get = lambda path: "<opnsense/>"
|
||||
driver._get_text = lambda path: "<opnsense/>"
|
||||
result = driver.get_config()
|
||||
assert result["candidate"] == ""
|
||||
|
||||
def test_candidate_contains_staged_routes(self, driver):
|
||||
driver._get = lambda path: "<opnsense/>"
|
||||
driver._get_text = lambda path: "<opnsense/>"
|
||||
driver.load_merge_candidate(config=VALID_ROUTES_CONFIG)
|
||||
result = driver.get_config()
|
||||
assert "WAN_GW" in result["candidate"]
|
||||
|
||||
def test_candidate_is_valid_json(self, driver):
|
||||
driver._get = lambda path: "<opnsense/>"
|
||||
driver._get_text = lambda path: "<opnsense/>"
|
||||
driver.load_merge_candidate(config=VALID_ROUTES_CONFIG)
|
||||
result = driver.get_config()
|
||||
parsed = json.loads(result["candidate"])
|
||||
assert isinstance(parsed, list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_config() — running/startup
|
||||
#
|
||||
# /api/core/backup/download/this answers with config.xml itself, not JSON.
|
||||
# These go through the real session path: a stubbed _get hid for months that
|
||||
# the getter called .json() on XML and always returned an empty "running".
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CONFIG_XML = '<?xml version="1.0"?>\n<opnsense><system><hostname>gw</hostname></system></opnsense>\n'
|
||||
|
||||
|
||||
def _make_xml_response(text):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.text = text
|
||||
mock_resp.json.side_effect = json.JSONDecodeError("Expecting value", text, 0)
|
||||
mock_resp.raise_for_status.return_value = None
|
||||
return mock_resp
|
||||
|
||||
|
||||
class TestGetConfigRunning:
|
||||
def test_running_is_the_xml_the_device_sent(self, driver):
|
||||
driver.session.get.return_value = _make_xml_response(CONFIG_XML)
|
||||
result = driver.get_config(retrieve="running")
|
||||
assert result["running"] == CONFIG_XML
|
||||
|
||||
def test_startup_mirrors_running(self, driver):
|
||||
driver.session.get.return_value = _make_xml_response(CONFIG_XML)
|
||||
result = driver.get_config()
|
||||
assert result["startup"] == result["running"] == CONFIG_XML
|
||||
|
||||
def test_reads_the_backup_download_endpoint(self, driver):
|
||||
driver.session.get.return_value = _make_xml_response(CONFIG_XML)
|
||||
driver.get_config(retrieve="running")
|
||||
url = driver.session.get.call_args.args[0]
|
||||
assert url.endswith("/api/core/backup/download/this")
|
||||
|
||||
def test_failed_read_raises_instead_of_returning_empty(self, driver):
|
||||
# An empty "running" says "the device has no configuration"; the
|
||||
# caller needs the reason, e.g. a key without backup privilege.
|
||||
resp = MagicMock()
|
||||
resp.raise_for_status.side_effect = requests.HTTPError("403 Forbidden")
|
||||
driver.session.get.return_value = resp
|
||||
with pytest.raises(requests.HTTPError, match="403"):
|
||||
driver.get_config(retrieve="running")
|
||||
|
||||
def test_candidate_only_does_not_touch_the_device(self, driver):
|
||||
driver.get_config(retrieve="candidate")
|
||||
driver.session.get.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# create_dhcp_reservation()
|
||||
#
|
||||
@@ -2545,6 +2596,70 @@ class TestSyncDnsZone:
|
||||
added = [d for p, d in calls if p.endswith("addhostoverride")]
|
||||
assert len(added) == 1
|
||||
|
||||
def test_does_not_copy_an_override_made_by_hand(self, driver):
|
||||
"""The records netOrk sends include what its poll read off this very
|
||||
device, hand-made overrides among them. Adding those again under the
|
||||
tag left every one of them twice on gw-01 and gw-02 (NetOrk/netork#1123)."""
|
||||
rows = [_override_row("by-hand", host="nas", ip="172.22.40.8", description="")]
|
||||
calls = self._record_calls(driver, rows)
|
||||
|
||||
driver.sync_dns_zone("office.local", [
|
||||
{"hostname": "nas", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
])
|
||||
|
||||
assert [p for p, _ in calls if p.endswith("addhostoverride")] == []
|
||||
assert [p for p, _ in calls if "delhostoverride" in p] == []
|
||||
|
||||
def test_clears_a_copy_it_once_made_of_a_hand_made_override(self, driver):
|
||||
"""Devices that already carry the copy lose it on the next sync."""
|
||||
rows = [
|
||||
_override_row("by-hand", host="NAS", ip="172.22.40.8", description="NAS"),
|
||||
_override_row("copy", host="nas", ip="172.22.40.8"),
|
||||
]
|
||||
calls = self._record_calls(driver, rows)
|
||||
|
||||
driver.sync_dns_zone("office.local", [
|
||||
{"hostname": "nas", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
{"hostname": "NAS", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
])
|
||||
|
||||
assert [p.rsplit("/", 1)[-1] for p, _ in calls if "delhostoverride" in p] == ["copy"]
|
||||
assert [p for p, _ in calls if p.endswith("addhostoverride")] == []
|
||||
|
||||
def test_still_adds_a_record_that_differs_from_the_hand_made_one(self, driver):
|
||||
rows = [_override_row("by-hand", host="nas", ip="172.22.40.8", description="")]
|
||||
calls = self._record_calls(driver, rows)
|
||||
|
||||
driver.sync_dns_zone("office.local", [
|
||||
{"hostname": "nas", "ip": "172.22.40.9", "record_type": "A", "enabled": True},
|
||||
])
|
||||
|
||||
added = [d for p, d in calls if p.endswith("addhostoverride")]
|
||||
assert [d["host"]["server"] for d in added] == ["172.22.40.9"]
|
||||
|
||||
def test_a_disabled_hand_made_override_does_not_stand_in(self, driver):
|
||||
"""It answers nothing, so the record still has to be added."""
|
||||
rows = [_override_row("by-hand", host="nas", ip="172.22.40.8", description="",
|
||||
enabled="0")]
|
||||
calls = self._record_calls(driver, rows)
|
||||
|
||||
driver.sync_dns_zone("office.local", [
|
||||
{"hostname": "nas", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
])
|
||||
|
||||
assert len([p for p, _ in calls if p.endswith("addhostoverride")]) == 1
|
||||
|
||||
def test_a_hand_made_override_in_another_zone_does_not_stand_in(self, driver):
|
||||
rows = [_override_row("by-hand", host="nas", domain="other.local", ip="172.22.40.8",
|
||||
description="")]
|
||||
calls = self._record_calls(driver, rows)
|
||||
|
||||
driver.sync_dns_zone("office.local", [
|
||||
{"hostname": "nas", "ip": "172.22.40.8", "record_type": "A", "enabled": True},
|
||||
])
|
||||
|
||||
assert len([p for p, _ in calls if p.endswith("addhostoverride")]) == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# uninstall_package – says what it cannot do
|
||||
|
||||
@@ -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": {}})
|
||||
@@ -0,0 +1,132 @@
|
||||
"""What the firewall is called, which version it runs, and which addresses it serves.
|
||||
|
||||
``/api/core/system/status`` on OPNsense 26.7 carries neither name nor version
|
||||
(``metadata`` and ``subsystems`` only), so every firewall read as nameless and
|
||||
"unknown". ``/api/diagnostics/system/system_information`` has both: ``name`` is
|
||||
the FQDN, ``versions[0]`` reads ``"OPNsense 26.7.7-amd64"``.
|
||||
|
||||
An address bound to a CARP VHID sits on the interface of every node, in state
|
||||
MASTER or BACKUP. Only the MASTER answers for it, so only there is it one of the
|
||||
node's addresses -- the same as keepalived, where only the master has the address
|
||||
at all (netOrk #1121: a virtual IP's badge sits on its holder's interface).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from napalm_opnsense.opnsense import OPNsenseDriver
|
||||
|
||||
OVERVIEW = json.loads(
|
||||
(Path(__file__).parent / "fixtures" / "interfaces_overview_carp.json").read_text()
|
||||
)
|
||||
SYSTEM_INFORMATION = {
|
||||
"name": "gw-02.home.manivong.de",
|
||||
"versions": ["OPNsense 26.7.7-amd64", "FreeBSD 15.1-RELEASE-p4", "OpenSSL 3.5.9"],
|
||||
}
|
||||
STATUS_26_7 = {"metadata": {"translations": {}}, "subsystems": {}}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def driver() -> OPNsenseDriver:
|
||||
with patch("napalm_opnsense.opnsense.requests.Session"):
|
||||
return OPNsenseDriver(hostname="fw", username="k", password="s")
|
||||
|
||||
|
||||
def _answers(driver, system_information=SYSTEM_INFORMATION, status=STATUS_26_7, overview=OVERVIEW):
|
||||
def get(path: str):
|
||||
if path == "/api/diagnostics/system/system_information":
|
||||
if isinstance(system_information, Exception):
|
||||
raise system_information
|
||||
return system_information
|
||||
if path == "/api/core/system/status":
|
||||
return status
|
||||
if path == "/api/interfaces/overview/export":
|
||||
return overview
|
||||
raise AssertionError(f"unexpected GET {path}")
|
||||
|
||||
driver._get = get
|
||||
return driver
|
||||
|
||||
|
||||
class TestIdentity:
|
||||
def test_the_name_is_the_short_host_and_the_fqdn_the_whole(self, driver):
|
||||
facts = _answers(driver).get_facts()
|
||||
assert (facts["hostname"], facts["fqdn"]) == ("gw-02", "gw-02.home.manivong.de")
|
||||
|
||||
def test_the_version_comes_without_the_architecture(self, driver):
|
||||
assert _answers(driver).get_facts()["os_version"] == "26.7.7"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line, version",
|
||||
[
|
||||
("OPNsense 26.7.7_1-aarch64", "26.7.7_1"),
|
||||
("OPNsense 25.1", "25.1"),
|
||||
("OPNsense 24.7.12-i386", "24.7.12"),
|
||||
],
|
||||
)
|
||||
def test_version_spellings(self, driver, line, version):
|
||||
info = {"name": "fw", "versions": [line]}
|
||||
assert _answers(driver, system_information=info).get_facts()["os_version"] == version
|
||||
|
||||
def test_a_name_without_a_domain_is_both(self, driver):
|
||||
facts = _answers(driver, system_information={"name": "fw", "versions": []}).get_facts()
|
||||
assert (facts["hostname"], facts["fqdn"]) == ("fw", "fw")
|
||||
|
||||
def test_without_system_information_the_status_answer_still_counts(self, driver):
|
||||
"""An older OPNsense, or an API key without the diagnostics privilege."""
|
||||
status = {"hostname": "opnsense01", "version": "24.7"}
|
||||
facts = _answers(driver, system_information=RuntimeError("403"), status=status).get_facts()
|
||||
assert (facts["hostname"], facts["os_version"]) == ("opnsense01", "24.7")
|
||||
|
||||
def test_nothing_known_is_empty_and_unknown(self, driver):
|
||||
facts = _answers(driver, system_information={}).get_facts()
|
||||
assert (facts["hostname"], facts["fqdn"], facts["os_version"]) == ("", "", "unknown")
|
||||
|
||||
|
||||
class TestHeldAddresses:
|
||||
def test_the_master_serves_its_carp_addresses(self, driver):
|
||||
result = _answers(driver).get_interfaces_ip()
|
||||
assert set(result["em0"]["ipv4"]) == {"192.0.2.2", "192.0.2.1"}
|
||||
assert result["em0"]["ipv4"]["192.0.2.1"] == {"prefix_length": 24}
|
||||
assert set(result["vlan0040"]["ipv4"]) == {
|
||||
"198.51.100.2",
|
||||
"198.51.100.1",
|
||||
"198.51.100.140",
|
||||
}
|
||||
|
||||
def test_a_backup_serves_its_own_address_only(self, driver):
|
||||
backup = json.loads(json.dumps(OVERVIEW).replace('"MASTER"', '"BACKUP"'))
|
||||
result = _answers(driver, overview=backup).get_interfaces_ip()
|
||||
assert set(result["em0"]["ipv4"]) == {"192.0.2.2"}
|
||||
assert set(result["vlan0040"]["ipv4"]) == {"198.51.100.2"}
|
||||
|
||||
def test_a_vhid_that_is_not_up_is_not_served(self, driver):
|
||||
result = _answers(driver).get_interfaces_ip()
|
||||
assert set(result["vlan0011"]["ipv4"]) == {"203.0.113.2"}
|
||||
|
||||
def test_a_held_ipv6_carp_address_is_served(self, driver):
|
||||
result = _answers(driver).get_interfaces_ip()
|
||||
assert "2001:db8:8::1" in result["em0"]["ipv6"]
|
||||
|
||||
def test_an_address_without_a_vhid_is_left_as_before(self, driver):
|
||||
"""Only CARP addresses are added; a plain alias is a separate question."""
|
||||
result = _answers(driver).get_interfaces_ip()
|
||||
assert set(result["vlan0020"]["ipv4"]) == {"192.0.2.130"}
|
||||
|
||||
def test_without_a_primary_address_the_list_still_leaves_out_a_backup(self, driver):
|
||||
overview = [
|
||||
{
|
||||
"device": "vlan0099",
|
||||
"ipv4": [
|
||||
{"ipaddr": "192.0.2.66/26"},
|
||||
{"ipaddr": "192.0.2.65/26", "vhid": "199", "status": "BACKUP"},
|
||||
],
|
||||
}
|
||||
]
|
||||
result = _answers(driver, overview=overview).get_interfaces_ip()
|
||||
assert set(result["vlan0099"]["ipv4"]) == {"192.0.2.66"}
|
||||
@@ -0,0 +1,147 @@
|
||||
"""What listens on an OPNsense firewall, and which OPNsense service it is (netOrk #673).
|
||||
|
||||
OPNsense answers through its API, ``diagnostics/interface/get_socket_statistics``:
|
||||
netstat's sockets with sockstat's user, command and PID merged in, read by configd
|
||||
as root. A socket that listens has no peer (``*:*``). Which service holds it comes
|
||||
from the command -- cut to ten characters by FreeBSD -- matched against the
|
||||
firewall's own service list; WireGuard's sockets belong to the kernel and are
|
||||
told by their port.
|
||||
|
||||
The fixture is a real OPNsense 26.7.5 firewall's answer, cut down, with
|
||||
documentation addresses.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from napalm_opnsense.opnsense import OPNsenseDriver
|
||||
|
||||
STATISTICS = json.loads((Path(__file__).parent / "fixtures" / "socket_statistics.json").read_text())
|
||||
SERVICES = [
|
||||
"acme", "captiveportal", "configd", "cron", "crowdsec", "frr", "kea-dhcp", "lldpd",
|
||||
"login", "mdns-repeater", "ntpd", "openssh", "pf", "snmpd", "udpbroadcastrelay",
|
||||
"unbound", "webgui", "wireguard", "zabbix_agentd",
|
||||
] # fmt: skip
|
||||
WIREGUARD = {
|
||||
"rows": [
|
||||
{"if": "wg1", "type": "interface", "listen-port": "51821", "status": "up"},
|
||||
{"if": "wg1", "type": "peer", "allowed-ips": "10.99.99.2/32"},
|
||||
{"if": "wg2", "type": "interface", "listen-port": "29281", "status": "up"},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def _driver(services=SERVICES, wireguard=WIREGUARD) -> OPNsenseDriver:
|
||||
with patch("napalm_opnsense.opnsense.requests.Session"):
|
||||
drv = OPNsenseDriver(hostname="fw", username="k", password="s")
|
||||
drv.session = MagicMock()
|
||||
|
||||
def get(path: str) -> dict:
|
||||
if path == "/api/diagnostics/interface/get_socket_statistics":
|
||||
return STATISTICS
|
||||
if path == "/api/core/service/search":
|
||||
return {"rows": [{"id": n, "name": n, "running": 1} for n in services]}
|
||||
if path == "/api/wireguard/service/show":
|
||||
if isinstance(wireguard, Exception):
|
||||
raise wireguard
|
||||
return wireguard
|
||||
raise AssertionError(f"unexpected GET {path}")
|
||||
|
||||
drv._get = get # type: ignore[method-assign]
|
||||
return drv
|
||||
|
||||
|
||||
def _sockets(drv: OPNsenseDriver | None = None) -> dict:
|
||||
reading = (drv or _driver()).get_listening_sockets()
|
||||
return {(s["proto"], s["port"], s["address"]): s for s in reading["sockets"]}
|
||||
|
||||
|
||||
class TestTheReading:
|
||||
def test_read_as_root(self):
|
||||
assert _driver().get_listening_sockets()["attributed"] is True
|
||||
|
||||
def test_a_socket_comes_with_its_process_and_service(self):
|
||||
assert _sockets()[("tcp", 8080, "192.0.2.1")] == {
|
||||
"proto": "tcp",
|
||||
"address": "192.0.2.1",
|
||||
"port": 8080,
|
||||
"interface": None,
|
||||
"process": "crowdsec",
|
||||
"pid": 90423,
|
||||
"unit": "crowdsec",
|
||||
"container_id": None,
|
||||
}
|
||||
|
||||
def test_only_what_listens(self):
|
||||
"""A connection has a peer; a socket without a port listens on nothing."""
|
||||
sockets = _driver().get_listening_sockets()["sockets"]
|
||||
|
||||
assert len(sockets) == 25
|
||||
assert all(s["port"] > 0 for s in sockets)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"proto, port, address",
|
||||
[
|
||||
("tcp", 8000, "0.0.0.0"),
|
||||
("tcp", 8000, "::"),
|
||||
("udp", 123, "0.0.0.0"),
|
||||
("udp", 123, "::"),
|
||||
],
|
||||
)
|
||||
def test_every_address_of_a_family_is_its_any_address(self, proto, port, address):
|
||||
assert (proto, port, address) in _sockets()
|
||||
|
||||
def test_sorted_by_protocol_port_and_address(self):
|
||||
sockets = _driver().get_listening_sockets()["sockets"]
|
||||
keys = [(s["proto"], s["port"], s["address"]) for s in sockets]
|
||||
|
||||
assert keys == sorted(keys)
|
||||
|
||||
|
||||
class TestWhichService:
|
||||
@pytest.mark.parametrize(
|
||||
"proto, port, address, service",
|
||||
[
|
||||
("tcp", 22, "192.0.2.1", "openssh"),
|
||||
("tcp", 443, "192.0.2.1", "webgui"),
|
||||
("tcp", 8000, "0.0.0.0", "captiveportal"), # lighttpd as www
|
||||
("udp", 67, "192.0.2.1", "kea-dhcp"), # kea-dhcp4
|
||||
("tcp", 8000, "127.0.0.1", "kea-dhcp"), # kea-ctrl-agent, cut short
|
||||
("tcp", 2604, "127.0.0.1", "frr"), # ospfd
|
||||
("udp", 5353, "198.51.100.1", "mdns-repeater"), # cut short
|
||||
("udp", 1900, "0.0.0.0", "udpbroadcastrelay"),
|
||||
("tcp", 10050, "192.0.2.1", "zabbix_agentd"),
|
||||
("udp", 53, "203.0.113.1", "unbound"),
|
||||
("udp", 123, "0.0.0.0", "ntpd"),
|
||||
],
|
||||
)
|
||||
def test_by_its_command(self, proto, port, address, service):
|
||||
assert _sockets()[(proto, port, address)]["unit"] == service
|
||||
|
||||
def test_the_command_stays_as_reported(self):
|
||||
assert _sockets()[("udp", 5353, "198.51.100.1")]["process"] == "mdns-repea"
|
||||
|
||||
def test_a_service_the_firewall_does_not_list_is_none(self):
|
||||
without_snmpd = [n for n in SERVICES if n != "snmpd"]
|
||||
|
||||
assert _sockets(_driver(services=without_snmpd))[("udp", 161, "192.0.2.1")]["unit"] is None
|
||||
|
||||
def test_wireguard_s_kernel_sockets_by_their_port(self):
|
||||
wg = _sockets()[("udp", 51821, "0.0.0.0")]
|
||||
|
||||
assert (wg["process"], wg["pid"], wg["unit"]) == (None, None, "wireguard")
|
||||
|
||||
def test_without_wireguard_a_kernel_socket_is_nobody_s(self):
|
||||
sockets = _sockets(_driver(wireguard=RuntimeError("404")))
|
||||
|
||||
assert sockets[("udp", 29281, "0.0.0.0")]["unit"] is None
|
||||
|
||||
|
||||
def test_netork_finds_it():
|
||||
"""netOrk asks ``hasattr(driver, "get_listening_sockets")``."""
|
||||
assert hasattr(OPNsenseDriver, "get_listening_sockets")
|
||||
@@ -0,0 +1,130 @@
|
||||
"""The firewall's CARP addresses, with who holds each (netOrk #1121).
|
||||
|
||||
``/api/interfaces/overview/export`` -- the source ``get_interfaces_ip`` reads --
|
||||
lists every address on an interface, and an address bound to a CARP VHID carries
|
||||
the VHID and this node's CARP state. That covers both the CARP address itself and
|
||||
an IP alias bound to its VHID: both float with the VHID's MASTER. An address
|
||||
without a VHID stays on this node and is no virtual IP.
|
||||
|
||||
The overview has no CARP password in it; ``interfaces/vip_settings`` would, which
|
||||
is why it is not read. The fixture is the shape of a real OPNsense 26.7 answer,
|
||||
cut down, with documentation addresses.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from napalm_opnsense.opnsense import OPNsenseDriver
|
||||
from napalm_opnsense.virtual_ips import carp_addresses
|
||||
|
||||
OVERVIEW = json.loads(
|
||||
(Path(__file__).parent / "fixtures" / "interfaces_overview_carp.json").read_text()
|
||||
)
|
||||
|
||||
|
||||
def _driver(overview=OVERVIEW) -> OPNsenseDriver:
|
||||
with patch("napalm_opnsense.opnsense.requests.Session"):
|
||||
drv = OPNsenseDriver(hostname="fw", username="k", password="s")
|
||||
drv.session = MagicMock()
|
||||
|
||||
def get(path: str):
|
||||
if path == "/api/interfaces/overview/export":
|
||||
if isinstance(overview, Exception):
|
||||
raise overview
|
||||
return overview
|
||||
raise AssertionError(f"unexpected GET {path}")
|
||||
|
||||
drv._get = get # type: ignore[method-assign]
|
||||
return drv
|
||||
|
||||
|
||||
def _by_address(entries) -> dict:
|
||||
return {e["address"]: e for e in entries}
|
||||
|
||||
|
||||
class TestCarpAddresses:
|
||||
def test_every_address_bound_to_a_vhid_is_a_virtual_ip(self):
|
||||
assert [e["address"] for e in carp_addresses(OVERVIEW)] == [
|
||||
"192.0.2.1",
|
||||
"198.51.100.1",
|
||||
"198.51.100.140",
|
||||
"203.0.113.1",
|
||||
"2001:db8:8::1",
|
||||
]
|
||||
|
||||
def test_an_address_without_a_vhid_stays_on_this_node(self):
|
||||
addresses = {e["address"] for e in carp_addresses(OVERVIEW)}
|
||||
assert not addresses & {"192.0.2.2", "192.0.2.140", "127.0.0.1", "fe80::1"}
|
||||
|
||||
def test_the_carp_address_carries_its_interface_and_state(self):
|
||||
assert _by_address(carp_addresses(OVERVIEW))["192.0.2.1"] == {
|
||||
"mechanism": "carp",
|
||||
"instance": "MGMT",
|
||||
"address": "192.0.2.1",
|
||||
"prefix_length": 24,
|
||||
"interface": "em0",
|
||||
"group_key": "vhid:108",
|
||||
"running": True,
|
||||
"holds": True,
|
||||
}
|
||||
|
||||
def test_an_alias_on_a_vhid_follows_that_vhid(self):
|
||||
alias = _by_address(carp_addresses(OVERVIEW))["198.51.100.140"]
|
||||
assert (alias["group_key"], alias["prefix_length"], alias["holds"]) == (
|
||||
"vhid:140",
|
||||
32,
|
||||
True,
|
||||
)
|
||||
|
||||
def test_a_vhid_not_up_neither_runs_nor_holds(self):
|
||||
entry = _by_address(carp_addresses(OVERVIEW))["203.0.113.1"]
|
||||
assert (entry["running"], entry["holds"]) == (False, False)
|
||||
|
||||
def test_an_interface_without_a_description_is_named_by_its_identifier(self):
|
||||
assert _by_address(carp_addresses(OVERVIEW))["203.0.113.1"]["instance"] == "opt9"
|
||||
|
||||
def test_on_a_backup_node_nothing_is_held(self):
|
||||
backup = json.loads(json.dumps(OVERVIEW).replace('"MASTER"', '"BACKUP"'))
|
||||
entries = carp_addresses(backup)
|
||||
assert entries and all(e["holds"] is False for e in entries)
|
||||
|
||||
def test_the_answer_wrapped_in_an_object_reads_the_same(self):
|
||||
assert carp_addresses({"interfaces": OVERVIEW}) == carp_addresses(OVERVIEW)
|
||||
|
||||
@pytest.mark.parametrize("answer", [[], {}, {"interfaces": None}])
|
||||
def test_a_firewall_without_interfaces_has_none(self, answer):
|
||||
assert carp_addresses(answer) == []
|
||||
|
||||
@pytest.mark.parametrize("answer", ["nonsense", None, 42])
|
||||
def test_an_answer_of_another_shape_is_refused(self, answer):
|
||||
with pytest.raises(ValueError):
|
||||
carp_addresses(answer)
|
||||
|
||||
def test_odd_entries_are_skipped(self):
|
||||
overview = [
|
||||
{"identifier": "opt1", "ipv4": ["192.0.2.9/24", {"vhid": "1"}, None]},
|
||||
"not-an-interface",
|
||||
{"identifier": "opt2", "ipv4": [{"ipaddr": "bogus", "vhid": "2"}]},
|
||||
]
|
||||
assert carp_addresses(overview) == []
|
||||
|
||||
|
||||
class TestGetVirtualIps:
|
||||
def test_a_firewall_reports_carp_and_does_not_look_for_the_linux_mechanisms(self):
|
||||
reading = _driver().get_virtual_ips()
|
||||
assert reading["vip_manager"] is None
|
||||
assert reading["keepalived"] is None
|
||||
assert len(reading["carp"]) == 5
|
||||
|
||||
def test_a_firewall_without_carp_reports_an_empty_list(self):
|
||||
reading = _driver(overview=[OVERVIEW[3], OVERVIEW[4]]).get_virtual_ips()
|
||||
assert reading["carp"] == []
|
||||
|
||||
def test_a_failed_request_is_raised_not_reported_as_none_declared(self):
|
||||
with pytest.raises(RuntimeError):
|
||||
_driver(overview=RuntimeError("API down")).get_virtual_ips()
|
||||
Reference in new issue
Block a user