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:
1 parent
692a584fd2
commit
571d9abb5f
15 files changed
+690
-419
No files matched your search
@@ -21,14 +21,15 @@ told by the listen ports of the WireGuard interfaces.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
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] = {
|
||||
_SERVICE_OF: dict[str, str] = {
|
||||
"sshd": "openssh",
|
||||
"kea-ctrl-a": "kea-dhcp",
|
||||
"kea-ctrl-agent": "kea-dhcp",
|
||||
@@ -43,7 +44,7 @@ _NOBODY = "??"
|
||||
_PEERLESS = "*"
|
||||
|
||||
|
||||
def _service_of(command: str, user: str, services: Set[str]) -> Optional[str]:
|
||||
def _service_of(command: str, user: str, services: set[str]) -> str | None:
|
||||
if command == "lighttpd":
|
||||
name = "captiveportal" if user == "www" else "webgui"
|
||||
return name if name in services else None
|
||||
@@ -70,8 +71,8 @@ def _address(protocol: str, address: str) -> tuple:
|
||||
|
||||
|
||||
def _socket(
|
||||
entry: Dict[str, Any], services: Set[str], wireguard_ports: Set[int]
|
||||
) -> Optional[ListeningSocketDict]:
|
||||
entry: dict[str, Any], services: set[str], wireguard_ports: set[int]
|
||||
) -> ListeningSocketDict | None:
|
||||
protocol = str(entry.get("protocol") or "")
|
||||
proto = protocol[:3]
|
||||
local, remote = entry.get("local") or {}, entry.get("remote") or {}
|
||||
@@ -101,9 +102,9 @@ def _socket(
|
||||
}
|
||||
|
||||
|
||||
def wireguard_listen_ports(show: Dict[str, Any]) -> Set[int]:
|
||||
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()
|
||||
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():
|
||||
@@ -112,12 +113,12 @@ def wireguard_listen_ports(show: Dict[str, Any]) -> Set[int]:
|
||||
|
||||
|
||||
def parse_socket_statistics(
|
||||
statistics: Dict[str, Any], services: Iterable[str], wireguard_ports: Set[int]
|
||||
) -> List[ListeningSocketDict]:
|
||||
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] = []
|
||||
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
|
||||
|
||||
+321
-245
@@ -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")
|
||||
|
||||
@@ -33,7 +33,9 @@ class OPNsenseOspfMixin(OspfMixin):
|
||||
def get_ospf(self) -> dict[str, Any]:
|
||||
"""Router ID, areas and OSPF per interface; empty when OSPF does not run."""
|
||||
overview = self._get(f"{_DIAG}/ospfoverview").get("response") or {} # type: ignore[attr-defined]
|
||||
interfaces = (self._get(f"{_DIAG}/ospfinterface").get("response") or {}).get("interfaces") or {} # type: ignore[attr-defined]
|
||||
interfaces = (self._get(f"{_DIAG}/ospfinterface").get("response") or {}).get(
|
||||
"interfaces"
|
||||
) or {} # type: ignore[attr-defined]
|
||||
return {
|
||||
"router_id": overview.get("routerId") or None,
|
||||
"areas": list((overview.get("areas") or {}).keys()),
|
||||
|
||||
@@ -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
|
||||
@@ -45,8 +44,9 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Callable, Iterable
|
||||
from ipaddress import ip_address
|
||||
from typing import Any, Callable, Iterable, Optional
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -78,7 +78,7 @@ class OPNsensePingMixin:
|
||||
PING_SWEEP_MAX_TARGETS: int = 512
|
||||
|
||||
# Provided by the driver this mixin is mixed into.
|
||||
_ping_model_path_cache: Optional[tuple[str, ...]] = None
|
||||
_ping_model_path_cache: tuple[str, ...] | None = None
|
||||
|
||||
# ── NAPALM ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -119,9 +119,9 @@ class OPNsensePingMixin:
|
||||
*,
|
||||
count: int = 1,
|
||||
timeout: int = 1,
|
||||
max_targets: Optional[int] = None,
|
||||
on_progress: Optional[Callable[[int, int], None]] = None,
|
||||
should_stop: Optional[Callable[[], bool]] = None,
|
||||
max_targets: int | None = None,
|
||||
on_progress: Callable[[int, int], None] | None = None,
|
||||
should_stop: Callable[[], bool] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Ping many destinations, one batch of parallel firewall jobs at a time.
|
||||
|
||||
@@ -245,9 +245,7 @@ class OPNsensePingMixin:
|
||||
if attempt:
|
||||
time.sleep(_POLL_INTERVAL)
|
||||
rows = {
|
||||
job_id: row
|
||||
for job_id, row in self._read_ping_jobs().items()
|
||||
if job_id in wanted
|
||||
job_id: row for job_id, row in self._read_ping_jobs().items() if job_id in wanted
|
||||
}
|
||||
if wanted and all(
|
||||
_as_int(rows.get(job_id, {}).get("send")) >= count for job_id in wanted
|
||||
@@ -325,7 +323,7 @@ class OPNsensePingMixin:
|
||||
|
||||
@staticmethod
|
||||
def _row_to_napalm_reply(
|
||||
destination: str, row: Optional[dict[str, Any]], *, count: int
|
||||
destination: str, row: dict[str, Any] | None, *, count: int
|
||||
) -> dict[str, Any]:
|
||||
"""Turn one ``search_jobs`` row into NAPALM's ping reply shape.
|
||||
|
||||
|
||||
@@ -18,11 +18,12 @@ from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import socket
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
#: Port names OPNsense accepts in a rule. ``socket.getservbyname`` knows them
|
||||
#: too, but only where ``/etc/services`` exists -- a slim container has none.
|
||||
WELL_KNOWN_PORTS: Dict[str, int] = {
|
||||
WELL_KNOWN_PORTS: dict[str, int] = {
|
||||
"ftp": 21,
|
||||
"ssh": 22,
|
||||
"telnet": 23,
|
||||
@@ -49,7 +50,7 @@ WELL_KNOWN_PORTS: Dict[str, int] = {
|
||||
#: Alias types whose entries can be addresses.
|
||||
_ADDRESS_ALIASES = ("host", "network")
|
||||
|
||||
AliasIndex = Dict[str, Tuple[str, List[str]]]
|
||||
AliasIndex = dict[str, tuple[str, list[str]]]
|
||||
|
||||
|
||||
def wan_interfaces(overview: Any) -> set:
|
||||
@@ -61,20 +62,23 @@ def wan_interfaces(overview: Any) -> set:
|
||||
def alias_index(rows: Iterable[dict]) -> AliasIndex:
|
||||
"""``name -> (type, entries)`` from the alias list."""
|
||||
return {
|
||||
r["name"]: (r.get("type", ""), [e.strip() for e in str(r.get("content", "")).splitlines() if e.strip()])
|
||||
r["name"]: (
|
||||
r.get("type", ""),
|
||||
[e.strip() for e in str(r.get("content", "")).splitlines() if e.strip()],
|
||||
)
|
||||
for r in rows
|
||||
if r.get("name")
|
||||
}
|
||||
|
||||
|
||||
def _as_address(value: str) -> Optional[str]:
|
||||
def _as_address(value: str) -> str | None:
|
||||
try:
|
||||
return str(ipaddress.ip_address(value))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _addresses(target: str, aliases: AliasIndex) -> List[str]:
|
||||
def _addresses(target: str, aliases: AliasIndex) -> list[str]:
|
||||
"""The addresses a rule sends to: a literal, or a host/network alias's.
|
||||
|
||||
Anything else -- an interface address, a name resolved by DNS -- gives no
|
||||
@@ -89,7 +93,7 @@ def _addresses(target: str, aliases: AliasIndex) -> List[str]:
|
||||
return [a for a in (_as_address(e) for e in entries) if a]
|
||||
|
||||
|
||||
def _port(value: Any, protocol: str, aliases: AliasIndex) -> Optional[int]:
|
||||
def _port(value: Any, protocol: str, aliases: AliasIndex) -> int | None:
|
||||
"""A rule's port as a number: literal, start of a range, alias or name.
|
||||
|
||||
No port at all means every port, which the contract writes as 0.
|
||||
@@ -115,7 +119,7 @@ def _faces_the_wan(rule: dict, wan: set) -> bool:
|
||||
return bool(set(str(rule.get("interface", "")).split(",")) & wan)
|
||||
|
||||
|
||||
def _remote_host(rule: dict) -> Optional[str]:
|
||||
def _remote_host(rule: dict) -> str | None:
|
||||
"""A source restriction; an inverted one ("all but X") restricts nothing."""
|
||||
source = str(rule.get("source.network") or "").strip()
|
||||
if source in ("", "any") or rule.get("source.not") == "1":
|
||||
@@ -123,11 +127,11 @@ def _remote_host(rule: dict) -> Optional[str]:
|
||||
return source
|
||||
|
||||
|
||||
def _forwards_of(rule: dict, aliases: AliasIndex) -> List[dict]:
|
||||
def _forwards_of(rule: dict, aliases: AliasIndex) -> list[dict]:
|
||||
protocols = [p.upper() for p in str(rule.get("protocol") or "any").split("/") if p]
|
||||
target = str(rule.get("target", "")).strip()
|
||||
remote = _remote_host(rule)
|
||||
result: List[dict] = []
|
||||
result: list[dict] = []
|
||||
for protocol in protocols:
|
||||
external = _port(rule.get("destination.port", ""), protocol.lower(), aliases)
|
||||
if external is None:
|
||||
@@ -150,9 +154,9 @@ def _forwards_of(rule: dict, aliases: AliasIndex) -> List[dict]:
|
||||
return result
|
||||
|
||||
|
||||
def port_forwards(rules: Iterable[dict], wan: set, aliases: AliasIndex) -> List[dict]:
|
||||
def port_forwards(rules: Iterable[dict], wan: set, aliases: AliasIndex) -> list[dict]:
|
||||
"""The destination-NAT rules on a WAN interface, as ``PortForwardDict`` entries."""
|
||||
result: List[dict] = []
|
||||
result: list[dict] = []
|
||||
for rule in rules:
|
||||
if _faces_the_wan(rule, wan):
|
||||
result.extend(_forwards_of(rule, aliases))
|
||||
|
||||
@@ -45,7 +45,13 @@ class OPNsenseResolverMixin(DnsForwardMixin):
|
||||
def set_dns_forward(self, domain: str, server: str, port: int = 53) -> None:
|
||||
"""Forward *domain* to *server*; an existing forward of the domain is changed."""
|
||||
name = _domain(domain)
|
||||
row = {"enabled": "1", "type": "forward", "domain": name, "server": server, "port": str(port)}
|
||||
row = {
|
||||
"enabled": "1",
|
||||
"type": "forward",
|
||||
"domain": name,
|
||||
"server": server,
|
||||
"port": str(port),
|
||||
}
|
||||
known = [r for r in self._forward_rows() if _domain(r.get("domain", "")) == name]
|
||||
if known:
|
||||
result = self._post(f"{_SETTINGS}/setForward/{known[0]['uuid']}", {"dot": row}) # type: ignore[attr-defined]
|
||||
@@ -57,7 +63,11 @@ class OPNsenseResolverMixin(DnsForwardMixin):
|
||||
def remove_dns_forward(self, domain: str) -> None:
|
||||
"""Delete every forward of *domain*; nothing to do when there is none."""
|
||||
name = _domain(domain)
|
||||
uuids = [r["uuid"] for r in self._forward_rows() if _domain(r.get("domain", "")) == name and r.get("uuid")]
|
||||
uuids = [
|
||||
r["uuid"]
|
||||
for r in self._forward_rows()
|
||||
if _domain(r.get("domain", "")) == name and r.get("uuid")
|
||||
]
|
||||
if not uuids:
|
||||
return
|
||||
for uuid in uuids:
|
||||
|
||||
@@ -18,7 +18,7 @@ shape of the reading.
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from napalm_device_types.models import VirtualIpDict
|
||||
|
||||
@@ -31,7 +31,7 @@ _CARP_STATES: dict[str, tuple[bool, bool]] = {
|
||||
}
|
||||
|
||||
|
||||
def _address(text: str) -> Optional[tuple[str, Optional[int]]]:
|
||||
def _address(text: str) -> tuple[str, int | None] | None:
|
||||
"""``"10.0.0.1/24"`` -> ``("10.0.0.1", 24)``; None for what is no address."""
|
||||
host, slash, prefix = text.partition("/")
|
||||
try:
|
||||
@@ -48,12 +48,12 @@ def _address(text: str) -> Optional[tuple[str, Optional[int]]]:
|
||||
def carp_virtual_ip(
|
||||
*,
|
||||
address: str,
|
||||
prefix_length: Optional[int],
|
||||
prefix_length: int | None,
|
||||
vhid: object,
|
||||
state: Optional[str],
|
||||
state: str | None,
|
||||
instance: str,
|
||||
interface: Optional[str],
|
||||
) -> Optional[VirtualIpDict]:
|
||||
interface: str | None,
|
||||
) -> VirtualIpDict | None:
|
||||
"""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
|
||||
|
||||
Reference in new issue
Block a user