style: ruff format and the lint rules of the driver standard

Mechanical: ruff format, and ruff check --fix with the rule set every NAPALM
driver uses (E, F, I, UP, B at line length 100, Python 3.10). What ruff
cannot fix comes in the next commit.
This commit is contained in:
Christian Manivong committed 2026-10-23 08:32:41 +02:00
1 parent 692a584fd2
commit 571d9abb5f
15 files changed
+690 -419

No files matched your search

+321 -245
View File
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
@@ -52,29 +51,31 @@ _REFRESH_POLLS = 20
_REFRESH_INTERVAL = 3
import requests
from requests.exceptions import RequestException
from napalm.base.exceptions import (
ConnectionClosedException,
ConnectionException,
MergeConfigException,
)
from napalm_device_types import ConfigSyncMixin, FingerprintRule, FirewallDriver, SnmpAgentMixin
from napalm_device_types.dhcp import normalize_mac
from napalm_device_types.models import (
FixStepOutcomeDict,
SnmpAgentSettingsDict,
ConfigSyncDict,
DhcpHaDict,
FixStepOutcomeDict,
ListeningSocketsDict,
SnmpAgentSettingsDict,
VirtualIpsDict,
)
from napalm.base.exceptions import ConnectionException, ConnectionClosedException, MergeConfigException
from requests.exceptions import RequestException
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.ospf_mixin import OPNsenseOspfMixin
from napalm_opnsense.resolver_mixin import OPNsenseResolverMixin
from napalm_opnsense.ping_mixin import OPNsensePingMixin
from napalm_opnsense.port_forwards import alias_index, port_forwards, wan_interfaces
from napalm_opnsense.resolver_mixin import OPNsenseResolverMixin
from napalm_opnsense.virtual_ips import carp_addresses
_ARCHITECTURE_SUFFIX = re.compile(r"-(amd64|aarch64|arm64|i386|armv7)$")
@@ -137,7 +138,10 @@ class OPNsenseDriver(
# OPNsense REST API settings
self.base_url = self.optional_args.get("base_url") or f"https://{hostname}"
# Accept "verify", "verify_ssl", or "ssl_verify" (NetOrk passes "verify_ssl")
_v = self.optional_args.get("verify", self.optional_args.get("verify_ssl", self.optional_args.get("ssl_verify", True)))
_v = self.optional_args.get(
"verify",
self.optional_args.get("verify_ssl", self.optional_args.get("ssl_verify", True)),
)
self.verify = bool(_v)
self.api_key = self.optional_args.get("api_key") or username
self.api_secret = self.optional_args.get("api_secret") or password
@@ -190,7 +194,7 @@ class OPNsenseDriver(
with socket.create_connection((host, port), timeout=5):
pass
return {"is_alive": True}
except (socket.error, OSError):
except OSError:
return {"is_alive": False}
# ------------------------------------------------------------------
@@ -319,7 +323,9 @@ class OPNsenseDriver(
if not name:
continue
interfaces[name] = {
"is_up": iface.get("status", "") == "up" if "status" in iface else bool(iface.get("up", False)),
"is_up": iface.get("status", "") == "up"
if "status" in iface
else bool(iface.get("up", False)),
"is_enabled": bool(iface.get("enabled", True)),
"description": iface.get("description") or iface.get("descr") or "",
"last_flapped": -1.0,
@@ -434,14 +440,16 @@ class OPNsenseDriver(
net = iface_obj.network
if net.is_loopback or net.is_link_local:
return
networks.append({
"network": str(net),
"interface": ifname,
"gateway": str(iface_obj.ip),
"family": family,
"prefix_length": net.prefixlen,
"vlan_id": vlan_id,
})
networks.append(
{
"network": str(net),
"interface": ifname,
"gateway": str(iface_obj.ip),
"family": family,
"prefix_length": net.prefixlen,
"vlan_id": vlan_id,
}
)
except ValueError:
pass
@@ -605,7 +613,9 @@ class OPNsenseDriver(
ospf_networks: set = set()
try:
ospf_data = self._get("/api/quagga/ospf/routes")
for prefix in (ospf_data if isinstance(ospf_data, list) else ospf_data.get("routes", {}).keys()):
for prefix in (
ospf_data if isinstance(ospf_data, list) else ospf_data.get("routes", {}).keys()
):
ospf_networks.add(str(prefix))
except Exception as exc:
logger.debug("Failed to fetch OSPF routes: %s", exc)
@@ -624,7 +634,11 @@ class OPNsenseDriver(
iface = route.get("netif") or route.get("interface", "")
# Clean up BSD link-layer gateway references
clean_gateway = "" if (not gateway or gateway.startswith("link#") or gateway == "0.0.0.0") else gateway
clean_gateway = (
""
if (not gateway or gateway.startswith("link#") or gateway == "0.0.0.0")
else gateway
)
# Determine address family from network address
family = "ipv6" if (":" in network or (gateway and ":" in gateway)) else "ipv4"
@@ -711,9 +725,7 @@ class OPNsenseDriver(
)
return neighbors
def get_lldp_neighbors_detail(
self, interface: str = ""
) -> dict[str, list[dict[str, Any]]]:
def get_lldp_neighbors_detail(self, interface: str = "") -> dict[str, list[dict[str, Any]]]:
"""Return detailed LLDP neighbor information.
Calls ``GET /api/lldpd/service/neighbor``.
@@ -978,7 +990,7 @@ class OPNsenseDriver(
if filename is not None:
try:
with open(filename, "r", encoding="utf-8") as fh:
with open(filename, encoding="utf-8") as fh:
config = fh.read()
except OSError as exc:
raise MergeConfigException(f"Cannot read file {filename!r}: {exc}") from exc
@@ -989,16 +1001,12 @@ class OPNsenseDriver(
raise MergeConfigException(f"Invalid JSON in candidate config: {exc}") from exc
if not isinstance(routes, list):
raise MergeConfigException(
"Candidate config must be a JSON array of route objects."
)
raise MergeConfigException("Candidate config must be a JSON array of route objects.")
for i, route in enumerate(routes):
if not isinstance(route, dict):
raise MergeConfigException(f"Route at index {i} must be a JSON object.")
if "network" not in route or "gateway" not in route:
raise MergeConfigException(
f"Route at index {i} is missing 'network' or 'gateway'."
)
raise MergeConfigException(f"Route at index {i} is missing 'network' or 'gateway'.")
self._candidate_config = routes
@@ -1041,7 +1049,9 @@ class OPNsenseDriver(
API returns an error for any route.
"""
if self._candidate_config is None:
raise MergeConfigException("No candidate config loaded. Call load_merge_candidate() first.")
raise MergeConfigException(
"No candidate config loaded. Call load_merge_candidate() first."
)
# Record the current backup ID so rollback() can restore exactly
# this state after OPNsense writes the new config to disk.
@@ -1147,14 +1157,16 @@ class OPNsenseDriver(
for p in info.get("plugin", []):
if p.get("installed") != "1":
continue
result.append({
"name": p.get("name", ""),
"version": p.get("version", ""),
"installed": True,
"description": p.get("comment", ""),
"size": 0,
"source": "opnsense-plugins",
})
result.append(
{
"name": p.get("name", ""),
"version": p.get("version", ""),
"installed": True,
"description": p.get("comment", ""),
"size": 0,
"source": "opnsense-plugins",
}
)
return sorted(result, key=lambda x: x["name"].lower())
def get_certificates(self) -> list[dict[str, Any]]:
@@ -1180,13 +1192,15 @@ class OPNsenseDriver(
result: list[dict[str, Any]] = []
for row in data.get("rows", []):
result.append({
"name": row.get("commonname") or row.get("descr") or row.get("refid", ""),
"issuer": row.get("%caref") or "",
"valid_from": _as_int(row.get("valid_from")),
"valid_to": _as_int(row.get("valid_to")),
"in_use_by": _as_int(row.get("in_use")),
})
result.append(
{
"name": row.get("commonname") or row.get("descr") or row.get("refid", ""),
"issuer": row.get("%caref") or "",
"valid_from": _as_int(row.get("valid_from")),
"valid_to": _as_int(row.get("valid_to")),
"in_use_by": _as_int(row.get("in_use")),
}
)
return result
def get_ddns_status(self) -> dict[str, Any] | None:
@@ -1336,13 +1350,16 @@ class OPNsenseDriver(
* ``lease_end`` — Unix timestamp when the lease expires (0 if unknown)
* ``state`` — raw state string
"""
def _parse_kea(rows: list[dict]) -> list[dict[str, Any]]:
result = []
for row in rows:
# OPNsense Kea uses "hwaddr"; standard Kea uses "hw-address"; ISC DHCP uses "mac"
mac = (
row.get("hwaddr") or row.get("hw-address") or row.get("mac") or ""
).lower().strip()
(row.get("hwaddr") or row.get("hw-address") or row.get("mac") or "")
.lower()
.strip()
)
if not mac:
continue
raw_end = row.get("expire") or row.get("ends") or 0
@@ -1350,13 +1367,17 @@ class OPNsenseDriver(
lease_end = int(raw_end)
except (ValueError, TypeError):
lease_end = 0
result.append({
"mac": mac,
"ip": (row.get("address") or row.get("ip-address") or row.get("ip") or "").strip(),
"hostname": (row.get("hostname") or "").strip(),
"lease_end": lease_end,
"state": str(row.get("state") or ""),
})
result.append(
{
"mac": mac,
"ip": (
row.get("address") or row.get("ip-address") or row.get("ip") or ""
).strip(),
"hostname": (row.get("hostname") or "").strip(),
"lease_end": lease_end,
"state": str(row.get("state") or ""),
}
)
return result
failures: list[str] = []
@@ -1365,7 +1386,9 @@ class OPNsenseDriver(
# 1. Kea DHCPv4 plugin
try:
data = self._get("/api/kea/leases4/search")
rows = data.get("rows") or data.get("leases") or (data if isinstance(data, list) else [])
rows = (
data.get("rows") or data.get("leases") or (data if isinstance(data, list) else [])
)
leases = _parse_kea(rows)
if leases:
return leases
@@ -1654,8 +1677,7 @@ class OPNsenseDriver(
return []
cidr_by_uuid = {
str(row.get("uuid") or ""): str(row.get("subnet") or "")
for row in self._kea_subnets()
str(row.get("uuid") or ""): str(row.get("subnet") or "") for row in self._kea_subnets()
}
known_cidrs = set(cidr_by_uuid.values())
@@ -1667,14 +1689,16 @@ class OPNsenseDriver(
else:
subnet = cidr_by_uuid.get(raw_subnet, "")
reservations.append({
"uuid": str(row.get("uuid") or ""),
"mac": str(row.get("hw_address") or ""),
"ip": str(row.get("ip_address") or ""),
"hostname": str(row.get("hostname") or ""),
"description": str(row.get("description") or ""),
"subnet": subnet,
})
reservations.append(
{
"uuid": str(row.get("uuid") or ""),
"mac": str(row.get("hw_address") or ""),
"ip": str(row.get("ip_address") or ""),
"hostname": str(row.get("hostname") or ""),
"description": str(row.get("description") or ""),
"subnet": subnet,
}
)
return reservations
def apply_dhcp_reservation(
@@ -1704,9 +1728,7 @@ class OPNsenseDriver(
}
}
path = (
f"/api/kea/dhcpv4/setReservation/{uuid}"
if uuid
else "/api/kea/dhcpv4/addReservation"
f"/api/kea/dhcpv4/setReservation/{uuid}" if uuid else "/api/kea/dhcpv4/addReservation"
)
result = self._post(path, payload)
if result.get("result") != "saved":
@@ -1824,19 +1846,19 @@ class OPNsenseDriver(
option_data[name] = value
pools = [
line.strip()
for line in str(record.get("pools") or "").splitlines()
if line.strip()
line.strip() for line in str(record.get("pools") or "").splitlines() if line.strip()
]
subnets.append({
"uuid": uuid,
"subnet": str(record.get("subnet") or row.get("subnet") or ""),
"description": str(record.get("description") or ""),
"pools": pools,
"option_data": option_data,
"match_client_id": str(record.get("match-client-id") or "0") == "1",
})
subnets.append(
{
"uuid": uuid,
"subnet": str(record.get("subnet") or row.get("subnet") or ""),
"description": str(record.get("description") or ""),
"pools": pools,
"option_data": option_data,
"match_client_id": str(record.get("match-client-id") or "0") == "1",
}
)
return subnets
def apply_dhcp_subnet(
@@ -1894,14 +1916,10 @@ class OPNsenseDriver(
record["option_data"] = merged_options
record["option_data_autocollect"] = "0"
path = (
f"/api/kea/dhcpv4/setSubnet/{uuid}" if uuid else "/api/kea/dhcpv4/addSubnet"
)
path = f"/api/kea/dhcpv4/setSubnet/{uuid}" if uuid else "/api/kea/dhcpv4/addSubnet"
result = self._post(path, {"subnet": record})
if result.get("result") != "saved":
raise RuntimeError(
f"Kea rejected DHCP subnet {subnet.get('subnet')}: {result}"
)
raise RuntimeError(f"Kea rejected DHCP subnet {subnet.get('subnet')}: {result}")
return {"success": True}
def commit_dhcp_subnets(self) -> dict[str, Any]:
@@ -1919,12 +1937,14 @@ class OPNsenseDriver(
data = self._get("/api/core/service/search")
result: list[dict[str, Any]] = []
for row in data.get("rows", []):
result.append({
"name": row.get("name") or row.get("id", ""),
"running": bool(row.get("running", 0)),
"enabled": True, # OPNsense has no separate enabled/disabled state
"pid": 0,
})
result.append(
{
"name": row.get("name") or row.get("id", ""),
"running": bool(row.get("running", 0)),
"enabled": True, # OPNsense has no separate enabled/disabled state
"pid": 0,
}
)
return sorted(result, key=lambda x: x["name"].lower())
def get_listening_sockets(self) -> ListeningSocketsDict:
@@ -1970,9 +1990,7 @@ class OPNsenseDriver(
: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")
)
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.
@@ -1993,10 +2011,14 @@ class OPNsenseDriver(
are enabled/disabled by installing or removing them).
"""
import re as _re
if not _re.match(r'^[a-zA-Z0-9_\-]+$', name):
if not _re.match(r"^[a-zA-Z0-9_\-]+$", name):
raise ValueError(f"Invalid service name: {name!r}")
if action not in ('start', 'stop', 'restart'):
return {"success": False, "output": f"Action '{action}' is not supported on OPNsense services"}
if action not in ("start", "stop", "restart"):
return {
"success": False,
"output": f"Action '{action}' is not supported on OPNsense services",
}
try:
result = self._post(f"/api/{name}/service/{action}")
return {"success": True, "output": str(result)}
@@ -2061,17 +2083,17 @@ class OPNsenseDriver(
# Byte counters may live in child-sa entries
bytes_in = 0
bytes_out = 0
for child in session.get("child-sas", {}).values() if isinstance(session.get("child-sas"), dict) else []:
for child in (
session.get("child-sas", {}).values()
if isinstance(session.get("child-sas"), dict)
else []
):
try:
bytes_in += int(child.get("bytes-in", 0) or 0)
bytes_out += int(child.get("bytes-out", 0) or 0)
except (ValueError, TypeError):
pass
description = (
session.get("local-id")
or session.get("description")
or ""
)
description = session.get("local-id") or session.get("description") or ""
key = f"ipsec-{name}"
tunnels[key] = {
"type": "IPsec",
@@ -2271,7 +2293,9 @@ class OPNsenseDriver(
"""
try:
if packages:
result = self._post("/api/core/firmware/upgrade", {"upgrade_packages": list(packages)})
result = self._post(
"/api/core/firmware/upgrade", {"upgrade_packages": list(packages)}
)
else:
result = self._post("/api/core/firmware/upgrade")
return {"success": True, "output": str(result)}
@@ -2302,14 +2326,16 @@ class OPNsenseDriver(
name = p.get("name", "")
comment = p.get("comment", "")
if q in name.lower() or q in comment.lower():
result.append({
"name": name,
"version": p.get("version", ""),
"installed": p.get("installed") == "1",
"description": comment,
"size": 0,
"source": "opnsense-plugins",
})
result.append(
{
"name": name,
"version": p.get("version", ""),
"installed": p.get("installed") == "1",
"description": comment,
"size": 0,
"source": "opnsense-plugins",
}
)
return sorted(result, key=lambda x: x["name"].lower())
def install_package(self, name: str) -> dict[str, Any]:
@@ -2318,7 +2344,8 @@ class OPNsenseDriver(
Calls ``POST /api/core/firmware/install/{name}``.
"""
import re as _re
if not _re.match(r'^[a-zA-Z0-9_\-\.]+$', name):
if not _re.match(r"^[a-zA-Z0-9_\-\.]+$", name):
raise ValueError(f"Invalid package name: {name!r}")
try:
result = self._post(f"/api/core/firmware/install/{name}")
@@ -2347,7 +2374,8 @@ class OPNsenseDriver(
a decision of its own rather than a detail of this one.
"""
import re as _re
if not _re.match(r'^[a-zA-Z0-9_\-\.]+$', name):
if not _re.match(r"^[a-zA-Z0-9_\-\.]+$", name):
raise ValueError(f"Invalid package name: {name!r}")
if not name.startswith("os-"):
raise NotImplementedError(
@@ -2464,57 +2492,72 @@ class OPNsenseDriver(
def _write_snmp_agent_config(self, settings: SnmpAgentSettingsDict) -> FixStepOutcomeDict:
community = settings["community"]
if not community or any(c.isspace() or c in "\"#" for c in community):
if not community or any(c.isspace() or c in '"#' for c in community):
raise ValueError("the community must be one word without quotes or #")
listen = settings.get("listen") or self.hostname
try:
listen_val = {str(ipaddress.ip_address(listen)): {"selected": 1}}
except ValueError:
listen_val = {} # a DNS name binds nothing; snmpd then listens everywhere
result = self._post("/api/netsnmp/general/set", {
"general": {
"enabled": "1",
"community": community,
"contact": settings.get("contact", ""),
"location": settings.get("location", ""),
"sysobjid": "",
"bindip": "",
"listen": listen_val,
}
})
result = self._post(
"/api/netsnmp/general/set",
{
"general": {
"enabled": "1",
"community": community,
"contact": settings.get("contact", ""),
"location": settings.get("location", ""),
"sysobjid": "",
"bindip": "",
"listen": listen_val,
}
},
)
if result.get("result") != "saved":
return {"ok": False, "output": f"configuration refused: {result}"}
return {"ok": True, "output": f"community set, listen {list(listen_val) or 'all addresses'}"}
return {
"ok": True,
"output": f"community set, listen {list(listen_val) or 'all addresses'}",
}
def _allow_snmp_sources(self, settings: SnmpAgentSettingsDict) -> FixStepOutcomeDict:
sources = [str(ipaddress.ip_network(s.strip(), strict=False)) for s in settings.get("allowed_sources", [])]
sources = [
str(ipaddress.ip_network(s.strip(), strict=False))
for s in settings.get("allowed_sources", [])
]
if not sources:
return {"ok": True, "output": "no sources given; firewall unchanged"}
found = self._post("/api/firewall/filter/searchRule", {"current": 1, "rowCount": -1, "searchPhrase": "SNMP"})
found = self._post(
"/api/firewall/filter/searchRule",
{"current": 1, "rowCount": -1, "searchPhrase": "SNMP"},
)
for row in found.get("rows") or []:
descr = str(row.get("description") or row.get("descr") or "")
if descr == self._SNMP_RULE_OLD or descr.startswith(self._SNMP_RULE_PREFIX):
self._post(f"/api/firewall/filter/delRule/{row['uuid']}")
for source in sources:
self._post("/api/firewall/filter/addRule", {
"rule": {
"enabled": "1",
"action": "pass",
"quick": "1",
"interface": "", # floating: wherever the source comes in
"direction": "in",
"ipprotocol": "inet6" if ":" in source else "inet",
"protocol": "udp",
"source_net": source,
"source_not": "0",
"destination_net": "(self)",
"destination_not": "0",
"destination_port": "161",
"log": "0",
"floating": "yes",
"descr": f"{self._SNMP_RULE_PREFIX}{source}",
}
})
self._post(
"/api/firewall/filter/addRule",
{
"rule": {
"enabled": "1",
"action": "pass",
"quick": "1",
"interface": "", # floating: wherever the source comes in
"direction": "in",
"ipprotocol": "inet6" if ":" in source else "inet",
"protocol": "udp",
"source_net": source,
"source_not": "0",
"destination_net": "(self)",
"destination_not": "0",
"destination_port": "161",
"log": "0",
"floating": "yes",
"descr": f"{self._SNMP_RULE_PREFIX}{source}",
}
},
)
self._post("/api/firewall/filter/apply", {})
return {"ok": True, "output": f"udp/161 open for {', '.join(sources)} only"}
@@ -2560,14 +2603,16 @@ class OPNsenseDriver(
content_raw = row.get("content", "") or ""
# OPNsense stores content as newline-separated values
content = [v.strip() for v in content_raw.splitlines() if v.strip()]
result.append({
"name": row.get("name", ""),
"type": row.get("type", ""),
"description": row.get("description", "") or "",
"content": content,
"enabled": str(row.get("enabled", "1")) == "1",
"proto": row.get("proto", "") or "",
})
result.append(
{
"name": row.get("name", ""),
"type": row.get("type", ""),
"description": row.get("description", "") or "",
"content": content,
"enabled": str(row.get("enabled", "1")) == "1",
"proto": row.get("proto", "") or "",
}
)
return sorted(result, key=lambda x: (x["type"], x["name"].lower()))
@@ -2596,7 +2641,7 @@ class OPNsenseDriver(
group_names: set = set()
try:
grp = self._get("/api/ifgroups/ifgroups/searchItem?current=1&rowCount=-1")
for g in (grp.get("rows") or []):
for g in grp.get("rows") or []:
n = g.get("ifname") or g.get("name") or ""
if n:
group_names.add(n)
@@ -2612,8 +2657,7 @@ class OPNsenseDriver(
iface_label = (row.get("%interface") or "").strip() or ", ".join(iface_list)
explicit_floating = (
str(row.get("floating", "0")) in ("1", "yes")
or row.get("floating") is True
str(row.get("floating", "0")) in ("1", "yes") or row.get("floating") is True
)
floating = explicit_floating or len(iface_list) > 1
@@ -2621,37 +2665,45 @@ class OPNsenseDriver(
category = (row.get("%categories") or row.get("category") or "").strip()
# Use %-prefixed display values for source/destination
source_net = (row.get("%source_net") or row.get("source_net") or "any").strip()
destination_net = (row.get("%destination_net") or row.get("destination_net") or "any").strip()
source_net = (row.get("%source_net") or row.get("source_net") or "any").strip()
destination_net = (
row.get("%destination_net") or row.get("destination_net") or "any"
).strip()
result.append({
"uuid": row.get("uuid", ""),
"sequence": int(row.get("sequence", 0) or 0),
"action": row.get("action", "pass"),
"quick": str(row.get("quick", "1")) == "1",
"interface": iface,
"interface_label": iface_label,
"floating": floating,
"is_group": len(iface_list) == 1 and iface_list[0] in group_names,
"direction": row.get("direction", "in"),
"ipprotocol": row.get("ipprotocol", "inet"),
"protocol": row.get("protocol", "any") or "any",
"source_net": source_net,
"source_port": row.get("source_port", "") or "",
"source_not": str(row.get("source_not", "0")) == "1",
"destination_net": destination_net,
"destination_port": row.get("destination_port", "") or "",
"destination_not": str(row.get("destination_not", "0")) == "1",
"description": row.get("description", "") or "",
"log": str(row.get("log", "0")) == "1",
"enabled": str(row.get("enabled", "1")) == "1",
"category": category,
"gateway": row.get("gateway", "") or "",
})
result.append(
{
"uuid": row.get("uuid", ""),
"sequence": int(row.get("sequence", 0) or 0),
"action": row.get("action", "pass"),
"quick": str(row.get("quick", "1")) == "1",
"interface": iface,
"interface_label": iface_label,
"floating": floating,
"is_group": len(iface_list) == 1 and iface_list[0] in group_names,
"direction": row.get("direction", "in"),
"ipprotocol": row.get("ipprotocol", "inet"),
"protocol": row.get("protocol", "any") or "any",
"source_net": source_net,
"source_port": row.get("source_port", "") or "",
"source_not": str(row.get("source_not", "0")) == "1",
"destination_net": destination_net,
"destination_port": row.get("destination_port", "") or "",
"destination_not": str(row.get("destination_not", "0")) == "1",
"description": row.get("description", "") or "",
"log": str(row.get("log", "0")) == "1",
"enabled": str(row.get("enabled", "1")) == "1",
"category": category,
"gateway": row.get("gateway", "") or "",
}
)
return sorted(result, key=lambda x: (x["floating"], x["is_group"], x["interface"], x["sequence"]))
return sorted(
result, key=lambda x: (x["floating"], x["is_group"], x["interface"], x["sequence"])
)
def apply_firewall_rule(self, rule: dict[str, Any], *, uuid: str | None = None) -> dict[str, Any]:
def apply_firewall_rule(
self, rule: dict[str, Any], *, uuid: str | None = None
) -> dict[str, Any]:
"""Create or update a single OPNsense firewall filter rule.
`rule` uses the vendor-neutral field names from
@@ -2757,7 +2809,7 @@ class OPNsenseDriver(
# Fetch the list of internal CAs to find the right one
ca_data = self._post("/api/trust/ca/search", {})
internal_ca_uuid = None
for row in (ca_data.get("rows") or []):
for row in ca_data.get("rows") or []:
if row.get("internal") == "1" or row.get("catype") == "internal":
internal_ca_uuid = row.get("uuid")
break
@@ -2865,18 +2917,20 @@ class OPNsenseDriver(
seen.add(key)
# The auto-PTR flag is "addptr" since 25.x, "ptrrecord" before.
ptr = row.get("addptr", row.get("ptrrecord", "1"))
result.append({
"uuid": row.get("uuid", ""),
"hostname": host,
"domain": domain,
"fqdn": fqdn,
"ip": ip,
"record_type": rr,
"description": row.get("description", "") or "",
"enabled": str(row.get("enabled", "1")) == "1",
"ptrrecord": str(ptr) == "1",
"service": "unbound",
})
result.append(
{
"uuid": row.get("uuid", ""),
"hostname": host,
"domain": domain,
"fqdn": fqdn,
"ip": ip,
"record_type": rr,
"description": row.get("description", "") or "",
"enabled": str(row.get("enabled", "1")) == "1",
"ptrrecord": str(ptr) == "1",
"service": "unbound",
}
)
except Exception as exc:
logger.warning("Failed to fetch Unbound host overrides: %s", exc)
return sorted(result, key=lambda e: e["fqdn"].lower())
@@ -2892,17 +2946,19 @@ class OPNsenseDriver(
host = row.get("host", "") or ""
domain = row.get("domain", "") or ""
fqdn = f"{host}.{domain}" if host and domain else host or domain
result.append({
"uuid": row.get("uuid", ""),
"hostname": host,
"domain": domain,
"fqdn": fqdn,
"ip": row.get("ip", "") or "",
"record_type": "A",
"description": row.get("description", "") or "",
"enabled": str(row.get("enabled", "1")) == "1",
"service": "dnsmasq",
})
result.append(
{
"uuid": row.get("uuid", ""),
"hostname": host,
"domain": domain,
"fqdn": fqdn,
"ip": row.get("ip", "") or "",
"record_type": "A",
"description": row.get("description", "") or "",
"enabled": str(row.get("enabled", "1")) == "1",
"service": "dnsmasq",
}
)
except Exception as exc:
logger.warning("Failed to fetch Dnsmasq host overrides: %s", exc)
return sorted(result, key=lambda e: e["fqdn"].lower())
@@ -2966,19 +3022,28 @@ class OPNsenseDriver(
zone_clean = zone_name.rstrip(".")
# Remove existing netork-managed overrides for this zone
existing = (self._get_unbound_host_overrides() if service == "unbound"
else self._get_dnsmasq_host_overrides())
existing = (
self._get_unbound_host_overrides()
if service == "unbound"
else self._get_dnsmasq_host_overrides()
)
for entry in existing:
if (entry["domain"].rstrip(".") == zone_clean
and entry["description"].startswith(self._NETORK_TAG)):
if entry["domain"].rstrip(".") == zone_clean and entry["description"].startswith(
self._NETORK_TAG
):
uid = entry["uuid"]
if uid:
if service == "unbound":
self._post(f"/api/unbound/settings/delhostoverride/{uid}")
else:
self._post(f"/api/dnsmasq/settings/delhostoverride/{uid}")
logger.info("Removed %s host override %s.%s (uuid %s)",
service, entry["hostname"], zone_clean, uid)
logger.info(
"Removed %s host override %s.%s (uuid %s)",
service,
entry["hostname"],
zone_clean,
uid,
)
# 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
@@ -3005,33 +3070,44 @@ class OPNsenseDriver(
continue
pushed.add(key)
if service == "unbound":
self._post("/api/unbound/settings/addhostoverride", {
"host": {
"enabled": "1",
"hostname": rec.get("hostname", ""),
"domain": zone_clean,
"rr": rtype,
"server": rec.get("ip", ""),
"description": self._NETORK_TAG,
"addptr": "1",
"ptrrecord": "1",
"mxprio": "",
"mx": "",
}
})
self._post(
"/api/unbound/settings/addhostoverride",
{
"host": {
"enabled": "1",
"hostname": rec.get("hostname", ""),
"domain": zone_clean,
"rr": rtype,
"server": rec.get("ip", ""),
"description": self._NETORK_TAG,
"addptr": "1",
"ptrrecord": "1",
"mxprio": "",
"mx": "",
}
},
)
else:
self._post("/api/dnsmasq/settings/addhostoverride", {
"hostoverride": {
"enabled": "1",
"host": rec.get("hostname", ""),
"domain": zone_clean,
"ip": rec.get("ip", ""),
"description": self._NETORK_TAG,
}
})
self._post(
"/api/dnsmasq/settings/addhostoverride",
{
"hostoverride": {
"enabled": "1",
"host": rec.get("hostname", ""),
"domain": zone_clean,
"ip": rec.get("ip", ""),
"description": self._NETORK_TAG,
}
},
)
added += 1
logger.info("Added %s host override %s.%s → %s",
service, rec.get("hostname"), zone_clean, rec.get("ip"))
logger.info(
"Added %s host override %s.%s → %s",
service,
rec.get("hostname"),
zone_clean,
rec.get("ip"),
)
if service == "unbound":
self._post("/api/unbound/service/reconfigure")