"""Aggregate status handler. Exposes pending changes across all subsystems and a single apply-all endpoint that invokes each subsystem's apply in the correct order. """ from __future__ import annotations import logging from typing import Any from daemon.handlers.dnsmasq import apply_config as dnsmasq_apply_config from daemon.handlers.firewall import config_apply as firewall_config_apply from daemon.handlers.network import apply_all as network_apply_all from daemon.handlers.nginx import apply as nginx_apply from daemon.handlers.wireguard import apply as wireguard_apply from daemon.iface import GET_STATUS_PENDING, POST_STATUS_APPLY_ALL from daemon.server import refresh_state, registry from lib.firewall import fw_change_summary from lib.state import state as state_store logger = logging.getLogger(__name__) SYS_ORDER = ["networkd", "firewall", "wireguard", "dnsmasq", "nginx"] SYS_LABELS = { "networkd": "Network", "firewall": "Firewall", "wireguard": "WireGuard", "dnsmasq": "DHCP/DNS", "nginx": "Nginx", } SYS_APPLY = { "networkd": network_apply_all, "firewall": firewall_config_apply, "wireguard": wireguard_apply, "dnsmasq": dnsmasq_apply_config, "nginx": nginx_apply, } @registry.register(GET_STATUS_PENDING) def status_pending(_request: Any, _body: Any) -> dict[str, Any]: """Aggregate pending changes across all subsystems. Returns: Dict with per-subsystem pending status and total change count. """ fw = state_store.get("firewall") or {} pending_fw = fw.get("pending", {}) fw_needs_apply = pending_fw.get("needs_apply", False) fw_pending_list = pending_fw.get("pending", []) fw_changes = [] for c in fw_pending_list: zone = c.get("zone", "unknown") ctype = c.get("type", "unknown") summary = fw_change_summary(zone, ctype, c) fw_changes.append({"summary": summary, "detail": ""}) fw_result = { "needs_apply": fw_needs_apply, "change_count": len(fw_changes), "changes": fw_changes, } hash_subsystems = { "dnsmasq": _hash_subsystem("dnsmasq", state_store.get("dnsmasq")), "nginx": _hash_subsystem("nginx", state_store.get("nginx")), "wireguard": _hash_subsystem("wireguard", state_store.get("wireguard")), "networkd": _hash_subsystem("networkd", state_store.get("networkd")), } total = len(fw_changes) for _name, result in hash_subsystems.items(): total += len(result["changes"]) return { "firewall": fw_result, "dnsmasq": hash_subsystems["dnsmasq"], "nginx": hash_subsystems["nginx"], "wireguard": hash_subsystems["wireguard"], "networkd": hash_subsystems["networkd"], "total_changes": total, } @registry.register(POST_STATUS_APPLY_ALL) def status_apply_all(_request: Any, _body: Any) -> dict[str, Any]: """Apply pending changes for all subsystems in dependency order. Order: network -> firewall -> wireguard -> dnsmasq -> nginx. Returns: Dict with applied subsystems and any errors encountered. """ applied = [] errors = {} pending_data = status_pending(None, None) fw_pending = pending_data["firewall"]["needs_apply"] hash_pending = { "dnsmasq": pending_data["dnsmasq"]["pending_changes"], "nginx": pending_data["nginx"]["pending_changes"], "wireguard": pending_data["wireguard"]["pending_changes"], "networkd": pending_data["networkd"]["pending_changes"], } for name in SYS_ORDER: if name == "firewall": if not fw_pending: continue else: if not hash_pending.get(name, False): continue handler = SYS_APPLY[name] try: handler(None, None) applied.append(name) except Exception as exc: label = SYS_LABELS.get(name, name) errors[label] = str(exc) logger.error("Apply-all failed for %s: %s", name, exc) refresh_state(SYS_ORDER) return {"applied": applied, "errors": errors} def _hash_subsystem(name: str, state: dict[str, Any] | None) -> dict[str, Any]: """Build pending result for a hash-based subsystem.""" if state is None: return {"pending_changes": False, "summary": "Up to date", "changes": []} status = state.get("status", {}) pending = status.get("pending_changes", False) label = SYS_LABELS.get(name, name) if pending: summary = f"{label} configuration has unapplied changes" return { "pending_changes": True, "summary": summary, "changes": [{"summary": summary, "detail": ""}], } return {"pending_changes": False, "summary": "Up to date", "changes": []}