feat: pre-computed state store and async ACME issuance (fixes timeout mismatch)
- Add lib/state.py: in-memory state store with subsystem collectors (firewall, dnsmasq, nginx, acme, wireguard) - Refactor all handlers: read from state on GET, call refresh_state() after mutations instead of invoking subprocesses per request - daemon/server.py: add refresh_state(), /status/all, /status/refresh; populate state at startup - webui/api/certs.py: async step-by-step ACME issuance (validate, issue with request_id, poll status) replacing blocking endpoint - webui/server.py: render pages from state instead of direct lib calls - Update templates, JS for async cert issuance with polling UI - Update tests for state-based mocking; add test_state.py - Fix SIM105 lint issue (contextlib.suppress) - Add TODO.md with certificate issuance issue tracking Resolves: WebUI 30s timeout freeze during cert issuance (Problem 1)
This commit is contained in:
+106
-217
@@ -1,28 +1,21 @@
|
||||
"""Firewall daemon handler.
|
||||
|
||||
Executes firewall-cmd and ip commands with sudo, returns structured results.
|
||||
Parsing helpers are imported from lib.firewall.
|
||||
Reads from the pre-computed state for status endpoints. Executes
|
||||
firewall-cmd with sudo for mutations. Refers state after each mutation.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from daemon.server import NotFoundError, registry
|
||||
from daemon.server import NotFoundError, refresh_state, registry
|
||||
from lib.common import load_json, run, save_json
|
||||
from lib.firewall import (
|
||||
_normalize_target,
|
||||
_parse_active_zones,
|
||||
_parse_zone_output,
|
||||
)
|
||||
from lib.firewall import (
|
||||
config_pending as _config_pending,
|
||||
)
|
||||
from lib.firewall import (
|
||||
get_config as _get_lib_config,
|
||||
)
|
||||
from lib.firewall import (
|
||||
save_backup as _save_backup,
|
||||
)
|
||||
@@ -30,16 +23,16 @@ from lib.firewall import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROJECT_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
DATA_DIR = PROJECT_DIR / "data" / "firewall"
|
||||
RULES_FILE = DATA_DIR / "rules.json"
|
||||
CONFIG_DIR = PROJECT_DIR / "config" / "firewall"
|
||||
CONFIG_FILE = CONFIG_DIR / "config.json"
|
||||
DEFAULT_CONFIG = {"zones": {}}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
def _get_state() -> dict[str, Any] | None:
|
||||
"""Return the current firewall state from the state store."""
|
||||
from lib.state import state as state_store
|
||||
|
||||
return state_store.get("firewall")
|
||||
|
||||
|
||||
def _ensure_config_file() -> None:
|
||||
@@ -62,10 +55,6 @@ def _reload() -> None:
|
||||
run(["firewall-cmd", "--reload"], sudo=True)
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(UTC).isoformat()
|
||||
|
||||
|
||||
def _fp_to_str(fp: dict[str, Any]) -> str:
|
||||
parts = [f"port={fp['port']}", f"proto={fp['proto']}"]
|
||||
if "toaddr" in fp:
|
||||
@@ -85,92 +74,22 @@ def _get_forward_ports(zone_name: str) -> list[str]:
|
||||
return []
|
||||
|
||||
|
||||
def _get_state() -> dict[str, Any]:
|
||||
"""Return the complete current state of firewalld."""
|
||||
zone_names = run(["firewall-cmd", "--get-zones"], sudo=True).split()
|
||||
active_raw = run(["firewall-cmd", "--get-active-zones"], sudo=True)
|
||||
active = _parse_active_zones(active_raw)
|
||||
services = run(["firewall-cmd", "--get-services"], sudo=True).split() or []
|
||||
link_out = run(["ip", "-o", "link", "show"], sudo=True)
|
||||
addr_out = run(["ip", "-o", "addr", "show"], sudo=True)
|
||||
|
||||
iface_map: dict[str, dict[str, Any]] = {}
|
||||
for line in link_out.splitlines():
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
raw_name = parts[1].rstrip(":")
|
||||
state = "UNKNOWN"
|
||||
mtu = None
|
||||
mac = None
|
||||
for i, p in enumerate(parts):
|
||||
if p == "state" and i + 1 < len(parts):
|
||||
state = parts[i + 1]
|
||||
if p == "mtu" and i + 1 < len(parts):
|
||||
mtu = int(parts[i + 1])
|
||||
if p.startswith("link/ether") and i + 1 < len(parts):
|
||||
mac = parts[i + 1]
|
||||
iface_map[raw_name] = {
|
||||
"name": raw_name,
|
||||
"display_name": raw_name.partition("@")[0],
|
||||
"mac": mac,
|
||||
"state": state,
|
||||
"mtu": mtu,
|
||||
"ips": [],
|
||||
"ipv6": [],
|
||||
"zone": None,
|
||||
}
|
||||
|
||||
for line in addr_out.splitlines():
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
addr_name = parts[1]
|
||||
addr_key = "ipv6" if parts[2] == "inet6" else "ips"
|
||||
for entry in iface_map.values():
|
||||
if entry["display_name"] == addr_name:
|
||||
entry[addr_key].append(parts[3])
|
||||
break
|
||||
|
||||
for zone_name, ifaces in active.items():
|
||||
for raw_if in ifaces:
|
||||
clean = raw_if.partition("@")[0]
|
||||
for entry in iface_map.values():
|
||||
if entry["display_name"] == clean or entry["name"] == raw_if:
|
||||
entry["zone"] = zone_name
|
||||
break
|
||||
|
||||
ifaces = list(iface_map.values())
|
||||
|
||||
zones: dict[str, dict[str, Any]] = {}
|
||||
for zn in zone_names:
|
||||
try:
|
||||
zones[zn] = _parse_zone_output(
|
||||
zn, run(["firewall-cmd", f"--zone={zn}", "--list-all"], sudo=True)
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return {
|
||||
"active_zones": active,
|
||||
"interfaces": ifaces,
|
||||
"available_services": services,
|
||||
"zones": zones,
|
||||
"rich_rules": {n: z.get("rich-rules", []) for n, z in zones.items()},
|
||||
"timestamp": _now_iso(),
|
||||
}
|
||||
|
||||
|
||||
def _config_apply() -> dict[str, Any]:
|
||||
"""Apply the declarative config to live firewalld."""
|
||||
from lib.firewall import get_config as _get_lib_config
|
||||
|
||||
cfg = _get_lib_config()
|
||||
cfg_zones = cfg.get("zones", {})
|
||||
|
||||
_save_backup(_get_state())
|
||||
full_state: dict[str, Any] = {
|
||||
"active_zones": {},
|
||||
"interfaces": [],
|
||||
"available_services": [],
|
||||
"zones": {},
|
||||
"rich_rules": {},
|
||||
"timestamp": "",
|
||||
}
|
||||
_save_backup(full_state)
|
||||
|
||||
available = run(["firewall-cmd", "--get-zones"], sudo=True).split()
|
||||
applied: list[str] = []
|
||||
@@ -314,7 +233,15 @@ def _config_apply() -> dict[str, Any]:
|
||||
applied.append(zone_name)
|
||||
|
||||
_reload()
|
||||
backup_path = _save_backup(_get_state())
|
||||
full_state = {
|
||||
"active_zones": {},
|
||||
"interfaces": [],
|
||||
"available_services": [],
|
||||
"zones": {},
|
||||
"rich_rules": {},
|
||||
"timestamp": "",
|
||||
}
|
||||
backup_path = _save_backup(full_state)
|
||||
logger.info("Firewall config applied to %d zones", len(applied))
|
||||
return {
|
||||
"applied_zones": applied,
|
||||
@@ -323,114 +250,67 @@ def _config_apply() -> dict[str, Any]:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routes
|
||||
# Routes — GET endpoints read from state, mutations call refresh_state
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_READ_TAGS = {"firewall", "interfaces", "zones"}
|
||||
|
||||
def _get_fw_state() -> dict[str, Any]:
|
||||
fw = _get_state()
|
||||
if fw is None:
|
||||
return {}
|
||||
return fw
|
||||
|
||||
|
||||
@registry.register("GET", "/firewall/interfaces", cache_tags=_READ_TAGS)
|
||||
@registry.register("GET", "/firewall/interfaces")
|
||||
def get_interfaces(_request: Any, _body: Any) -> list[dict[str, Any]]:
|
||||
link_out = run(["ip", "-o", "link", "show"], sudo=True)
|
||||
addr_out = run(["ip", "-o", "addr", "show"], sudo=True)
|
||||
zones_out = run(["firewall-cmd", "--get-active-zones"], sudo=True)
|
||||
|
||||
iface_map: dict[str, dict[str, Any]] = {}
|
||||
for line in link_out.splitlines():
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
raw_name = parts[1].rstrip(":")
|
||||
state = "UNKNOWN"
|
||||
mtu = None
|
||||
mac = None
|
||||
for i, p in enumerate(parts):
|
||||
if p == "state" and i + 1 < len(parts):
|
||||
state = parts[i + 1]
|
||||
if p == "mtu" and i + 1 < len(parts):
|
||||
mtu = int(parts[i + 1])
|
||||
if p.startswith("link/ether") and i + 1 < len(parts):
|
||||
mac = parts[i + 1]
|
||||
iface_map[raw_name] = {
|
||||
"name": raw_name,
|
||||
"display_name": raw_name.partition("@")[0],
|
||||
"mac": mac,
|
||||
"state": state,
|
||||
"mtu": mtu,
|
||||
"ips": [],
|
||||
"ipv6": [],
|
||||
"zone": None,
|
||||
}
|
||||
|
||||
for line in addr_out.splitlines():
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
addr_name = parts[1]
|
||||
addr_key = "ipv6" if parts[2] == "inet6" else "ips"
|
||||
for entry in iface_map.values():
|
||||
if entry["display_name"] == addr_name:
|
||||
entry[addr_key].append(parts[3])
|
||||
break
|
||||
|
||||
active = _parse_active_zones(zones_out)
|
||||
for zone_name, ifaces in active.items():
|
||||
for raw_if in ifaces:
|
||||
clean = raw_if.partition("@")[0]
|
||||
for entry in iface_map.values():
|
||||
if entry["display_name"] == clean or entry["name"] == raw_if:
|
||||
entry["zone"] = zone_name
|
||||
break
|
||||
|
||||
return list(iface_map.values())
|
||||
fw = _get_fw_state()
|
||||
return fw.get("interfaces", [])
|
||||
|
||||
|
||||
@registry.register("GET", "/firewall/zones", cache_tags=_READ_TAGS)
|
||||
@registry.register("GET", "/firewall/zones")
|
||||
def get_zones(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
available = run(["firewall-cmd", "--get-zones"], sudo=True).split()
|
||||
active = _parse_active_zones(run(["firewall-cmd", "--get-active-zones"], sudo=True))
|
||||
return {"active": active, "available": available}
|
||||
fw = _get_fw_state()
|
||||
active = fw.get("active_zones", {})
|
||||
zones = fw.get("zones", {})
|
||||
return {"active": active, "available": list(zones.keys())}
|
||||
|
||||
|
||||
@registry.register("GET", "/firewall/zones/info", cache_tags=_READ_TAGS)
|
||||
@registry.register("GET", "/firewall/zones/info")
|
||||
def get_zone_info(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body or "zone" not in body:
|
||||
raise ValueError("'zone' is required")
|
||||
zone = body["zone"]
|
||||
if zone not in run(["firewall-cmd", "--get-zones"], sudo=True).split():
|
||||
fw = _get_fw_state()
|
||||
zones = fw.get("zones", {})
|
||||
if zone not in zones:
|
||||
raise NotFoundError(f"Zone '{zone}' does not exist")
|
||||
raw = run(["firewall-cmd", f"--zone={zone}", "--list-all"], sudo=True)
|
||||
return _parse_zone_output(zone, raw)
|
||||
return zones[zone]
|
||||
|
||||
|
||||
@registry.register("GET", "/firewall/zones/all", cache_tags=_READ_TAGS)
|
||||
@registry.register("GET", "/firewall/zones/all")
|
||||
def get_all_zones_info(_request: Any, _body: Any) -> list[dict[str, Any]]:
|
||||
active = _parse_active_zones(run(["firewall-cmd", "--get-active-zones"], sudo=True))
|
||||
fw = _get_fw_state()
|
||||
active = fw.get("active_zones", {})
|
||||
zones = fw.get("zones", {})
|
||||
result: list[dict[str, Any]] = []
|
||||
for zone_name in active:
|
||||
try:
|
||||
raw = run(["firewall-cmd", f"--zone={zone_name}", "--list-all"], sudo=True)
|
||||
result.append(_parse_zone_output(zone_name, raw))
|
||||
except Exception:
|
||||
continue
|
||||
if zone_name in zones:
|
||||
result.append(zones[zone_name])
|
||||
return result
|
||||
|
||||
|
||||
@registry.register("GET", "/firewall/services", cache_tags=_READ_TAGS)
|
||||
@registry.register("GET", "/firewall/services")
|
||||
def get_services(_request: Any, _body: Any) -> list[str]:
|
||||
return run(["firewall-cmd", "--get-services"], sudo=True).split()
|
||||
fw = _get_fw_state()
|
||||
return fw.get("available_services", [])
|
||||
|
||||
|
||||
@registry.register("GET", "/firewall/config", cache_tags=_READ_TAGS)
|
||||
@registry.register("GET", "/firewall/config")
|
||||
def get_config(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
return _get_config()
|
||||
|
||||
|
||||
@registry.register("POST", "/firewall/config", invalidate=_READ_TAGS)
|
||||
@registry.register("POST", "/firewall/config")
|
||||
def save_config_handler(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body or "zones" not in body:
|
||||
raise ValueError("'zones' key is required")
|
||||
@@ -438,10 +318,11 @@ def save_config_handler(_request: Any, body: dict[str, Any] | None) -> dict[str,
|
||||
raise ValueError("'zones' must be a dict")
|
||||
_save_config(body)
|
||||
logger.info("Firewall config saved (%d zones)", len(body["zones"]))
|
||||
refresh_state(["firewall"])
|
||||
return {"config_saved": True}
|
||||
|
||||
|
||||
@registry.register("PATCH", "/firewall/config", invalidate=_READ_TAGS)
|
||||
@registry.register("PATCH", "/firewall/config")
|
||||
def patch_config(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body must be a JSON object")
|
||||
@@ -451,22 +332,25 @@ def patch_config(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
merged = deep_merge(current, body)
|
||||
_save_config(merged)
|
||||
logger.info("Firewall config patched: %s", sorted(body.keys()))
|
||||
refresh_state(["firewall"])
|
||||
return {"config_saved": True}
|
||||
|
||||
|
||||
@registry.register("GET", "/firewall/config/pending", cache_tags=_READ_TAGS)
|
||||
def config_pending(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
return _config_pending(_get_state())
|
||||
@registry.register("GET", "/firewall/config/pending")
|
||||
def config_pending_handler(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
fw = _get_fw_state()
|
||||
return fw.get("pending", {})
|
||||
|
||||
|
||||
@registry.register("POST", "/firewall/config/apply", invalidate=_READ_TAGS)
|
||||
@registry.register("POST", "/firewall/config/apply")
|
||||
def config_apply(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
result = _config_apply()
|
||||
logger.info("Firewall config applied: %s", result.get("applied_zones", []))
|
||||
refresh_state(["firewall"])
|
||||
return result
|
||||
|
||||
|
||||
@registry.register("POST", "/firewall/zones/create", invalidate=_READ_TAGS)
|
||||
@registry.register("POST", "/firewall/zones/create")
|
||||
def create_zone(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
@@ -488,10 +372,11 @@ def create_zone(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
)
|
||||
_reload()
|
||||
logger.info("Zone '%s' created (target=%s)", zone_name, target)
|
||||
refresh_state(["firewall"])
|
||||
return {"zone": zone_name}
|
||||
|
||||
|
||||
@registry.register("DELETE", "/firewall/zones/delete", invalidate=_READ_TAGS)
|
||||
@registry.register("DELETE", "/firewall/zones/delete")
|
||||
def delete_zone(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body or "zone" not in body:
|
||||
raise ValueError("'zone' is required")
|
||||
@@ -502,10 +387,11 @@ def delete_zone(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
run(["firewall-cmd", f"--zone={zone}", "--delete", "--permanent"], sudo=True)
|
||||
_reload()
|
||||
logger.info("Zone '%s' deleted", zone)
|
||||
refresh_state(["firewall"])
|
||||
return {"zone": zone}
|
||||
|
||||
|
||||
@registry.register("POST", "/firewall/zones/interfaces", invalidate=_READ_TAGS)
|
||||
@registry.register("POST", "/firewall/zones/interfaces")
|
||||
def set_zone_interfaces(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
@@ -544,10 +430,11 @@ def set_zone_interfaces(_request: Any, body: dict[str, Any] | None) -> dict[str,
|
||||
)
|
||||
_reload()
|
||||
logger.info("Zone '%s' interfaces set to %s", zone, interfaces)
|
||||
refresh_state(["firewall"])
|
||||
return {"zone": zone, "interfaces": interfaces}
|
||||
|
||||
|
||||
@registry.register("POST", "/firewall/zones/services", invalidate=_READ_TAGS)
|
||||
@registry.register("POST", "/firewall/zones/services")
|
||||
def set_zone_services(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
@@ -582,10 +469,11 @@ def set_zone_services(_request: Any, body: dict[str, Any] | None) -> dict[str, A
|
||||
sudo=True,
|
||||
)
|
||||
_reload()
|
||||
refresh_state(["firewall"])
|
||||
return {"zone": zone, "services": services}
|
||||
|
||||
|
||||
@registry.register("POST", "/firewall/rich-rules/add", invalidate=_READ_TAGS)
|
||||
@registry.register("POST", "/firewall/rich-rules/add")
|
||||
def add_rich_rule(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
@@ -595,7 +483,6 @@ def add_rich_rule(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
raise ValueError("'zone' and 'rule' are required")
|
||||
if zone not in run(["firewall-cmd", "--get-zones"], sudo=True).split():
|
||||
raise NotFoundError(f"Zone '{zone}' does not exist")
|
||||
from uuid import uuid4
|
||||
|
||||
run(
|
||||
[
|
||||
@@ -613,10 +500,11 @@ def add_rich_rule(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
entry = {"id": rule_id, "rule": rule}
|
||||
cfg["zones"][zone]["rich_rules"].append(entry)
|
||||
_save_config(cfg)
|
||||
refresh_state(["firewall"])
|
||||
return {"zone": zone, "id": rule_id, "rule": rule}
|
||||
|
||||
|
||||
@registry.register("DELETE", "/firewall/rich-rules/remove", invalidate=_READ_TAGS)
|
||||
@registry.register("DELETE", "/firewall/rich-rules/remove")
|
||||
def remove_rich_rule(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
@@ -650,34 +538,22 @@ def remove_rich_rule(_request: Any, body: dict[str, Any] | None) -> dict[str, An
|
||||
r for r in zone_cfg.get("rich_rules", []) if r.get("id") != rule_id
|
||||
]
|
||||
_save_config(cfg)
|
||||
refresh_state(["firewall"])
|
||||
return {"zone": zone, "id": rule_id}
|
||||
|
||||
|
||||
@registry.register("GET", "/firewall/rich-rules", cache_tags=_READ_TAGS)
|
||||
@registry.register("GET", "/firewall/rich-rules")
|
||||
def list_rich_rules(_request: Any, body: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||
if not body or "zone" not in body:
|
||||
raise ValueError("'zone' is required")
|
||||
zone = body["zone"]
|
||||
raw = run(["firewall-cmd", f"--zone={zone}", "--list-rich-rules"], sudo=True)
|
||||
raw = raw.strip()
|
||||
if not raw:
|
||||
return []
|
||||
rules: list[str] = []
|
||||
current: list[str] = []
|
||||
for line in raw.splitlines():
|
||||
r = line.rstrip()
|
||||
if not r.endswith(";"):
|
||||
current.append(r)
|
||||
else:
|
||||
current.append(r)
|
||||
rules.append(" ".join(current))
|
||||
current = []
|
||||
if current:
|
||||
rules.append(" ".join(current))
|
||||
fw = _get_fw_state()
|
||||
rich_rules = fw.get("rich_rules", {})
|
||||
cfg = _get_config()
|
||||
cfg_entries = cfg.get("zones", {}).get(zone, {}).get("rich_rules", [])
|
||||
result: list[dict[str, Any]] = []
|
||||
for rule_str in rules:
|
||||
zone_rules = rich_rules.get(zone, [])
|
||||
for rule_str in zone_rules:
|
||||
matched = next((e for e in cfg_entries if e.get("rule") == rule_str), None)
|
||||
if matched:
|
||||
result.append({"id": matched["id"], "rule": rule_str})
|
||||
@@ -686,7 +562,7 @@ def list_rich_rules(_request: Any, body: dict[str, Any] | None) -> list[dict[str
|
||||
return result
|
||||
|
||||
|
||||
@registry.register("POST", "/firewall/masquerade", invalidate=_READ_TAGS)
|
||||
@registry.register("POST", "/firewall/masquerade")
|
||||
def set_masquerade(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
@@ -697,10 +573,11 @@ def set_masquerade(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]
|
||||
action = "--add-masquerade" if enable else "--remove-masquerade"
|
||||
run(["firewall-cmd", f"--zone={zone}", action, "--permanent"], sudo=True)
|
||||
_reload()
|
||||
refresh_state(["firewall"])
|
||||
return {"zone": zone, "masquerade": bool(enable)}
|
||||
|
||||
|
||||
@registry.register("POST", "/firewall/forward-port/add", invalidate=_READ_TAGS)
|
||||
@registry.register("POST", "/firewall/forward-port/add")
|
||||
def add_forward_port(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
@@ -711,7 +588,6 @@ def add_forward_port(_request: Any, body: dict[str, Any] | None) -> dict[str, An
|
||||
toport = body.get("toport")
|
||||
if not zone or port is None or not proto:
|
||||
raise ValueError("'zone', 'port', and 'proto' are required")
|
||||
from uuid import uuid4
|
||||
|
||||
fwd = f"port={port}/proto={proto}"
|
||||
if toaddr and toport:
|
||||
@@ -740,10 +616,11 @@ def add_forward_port(_request: Any, body: dict[str, Any] | None) -> dict[str, An
|
||||
cfg.setdefault("zones", {}).setdefault(zone, {}).setdefault("forward_ports", [])
|
||||
cfg["zones"][zone]["forward_ports"].append(entry)
|
||||
_save_config(cfg)
|
||||
refresh_state(["firewall"])
|
||||
return {"zone": zone, "id": fp_id, "port": int(port), "proto": proto}
|
||||
|
||||
|
||||
@registry.register("DELETE", "/firewall/forward-port/remove", invalidate=_READ_TAGS)
|
||||
@registry.register("DELETE", "/firewall/forward-port/remove")
|
||||
def remove_forward_port(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not body:
|
||||
raise ValueError("Request body required")
|
||||
@@ -752,7 +629,8 @@ def remove_forward_port(_request: Any, body: dict[str, Any] | None) -> dict[str,
|
||||
proto = body.get("proto", "").strip()
|
||||
if not zone or port is None or not proto:
|
||||
raise ValueError("'zone', 'port', and 'proto' are required")
|
||||
if zone not in run(["firewall-cmd", "--get-zones"], sudo=True).split():
|
||||
available = run(["firewall-cmd", "--get-zones"], sudo=True).split()
|
||||
if zone not in available:
|
||||
raise NotFoundError(f"Zone '{zone}' does not exist")
|
||||
fwd = f"port={port}/proto={proto}"
|
||||
cfg = _get_config()
|
||||
@@ -785,9 +663,20 @@ def remove_forward_port(_request: Any, body: dict[str, Any] | None) -> dict[str,
|
||||
fp for fp in fps if not (fp.get("port") == port and fp.get("proto") == proto)
|
||||
]
|
||||
_save_config(cfg)
|
||||
refresh_state(["firewall"])
|
||||
return {"zone": zone, "port": int(port), "proto": proto}
|
||||
|
||||
|
||||
@registry.register("GET", "/firewall/state", cache_tags=_READ_TAGS)
|
||||
@registry.register("GET", "/firewall/state")
|
||||
def get_state(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
return _get_state()
|
||||
fw = _get_state()
|
||||
if fw is None:
|
||||
return {}
|
||||
return {
|
||||
"active_zones": fw.get("active_zones", {}),
|
||||
"interfaces": fw.get("interfaces", []),
|
||||
"available_services": fw.get("available_services", []),
|
||||
"zones": fw.get("zones", {}),
|
||||
"rich_rules": fw.get("rich_rules", {}),
|
||||
"timestamp": fw.get("timestamp", ""),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user