state: applied-config snapshots + per-field pending diffs
- lib/common: stamp_applied() now records a _last_applied_config snapshot alongside the hash; strip_apply_meta() centralizes bookkeeping-key stripping; deep_diff() reports field-level changes - state collectors (dnsmasq/nginx/wireguard/networkd) expose pending_diff so the dashboard can show exactly which fields changed since the last apply (wireguard diff excludes private_key paths) - dashboard pending-changes card renders per-change lines with a generic fallback when no snapshot is recorded - firewall: firewalld built-in zones no longer flagged as unmanaged; public-zone masquerade skipped in pending changes since apply drives it via nftables propagation - schema: PendingChange TypedDict; pending_diff on DnsmasqStatus / WgStatus; tests in test_common.py, test_firewall.py, test_state.py
This commit is contained in:
+51
-9
@@ -13,7 +13,16 @@ from pathlib import Path
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from lib import schema
|
||||
from lib.common import _APPLY_HASH_KEY, config_hash, load_json, run, run_proc
|
||||
from lib.common import (
|
||||
_APPLY_HASH_KEY,
|
||||
_LAST_APPLIED_CONFIG_KEY,
|
||||
config_hash,
|
||||
deep_diff,
|
||||
load_json,
|
||||
run,
|
||||
run_proc,
|
||||
strip_apply_meta,
|
||||
)
|
||||
from lib.firewall import (
|
||||
_parse_active_zones,
|
||||
_parse_all_zones_output,
|
||||
@@ -637,7 +646,13 @@ def _collect_dnsmasq() -> schema.DnsmasqState:
|
||||
cfg
|
||||
)
|
||||
|
||||
safe_cfg = {k: v for k, v in cfg.items() if k != _APPLY_HASH_KEY}
|
||||
safe_cfg = strip_apply_meta(cfg)
|
||||
pending_diff: list[dict[str, Any]] = []
|
||||
if pending_changes:
|
||||
snap = cfg.get(_LAST_APPLIED_CONFIG_KEY)
|
||||
if isinstance(snap, dict):
|
||||
pending_diff = deep_diff(snap, safe_cfg)
|
||||
|
||||
return {
|
||||
"config": safe_cfg,
|
||||
"status": {
|
||||
@@ -645,6 +660,7 @@ def _collect_dnsmasq() -> schema.DnsmasqState:
|
||||
"config_file_exists": conf_exists,
|
||||
"active_leases": len(leases),
|
||||
"pending_changes": pending_changes,
|
||||
"pending_diff": pending_diff,
|
||||
},
|
||||
"leases": leases,
|
||||
"timestamp": _now_iso(),
|
||||
@@ -732,11 +748,20 @@ def _collect_nginx() -> schema.NginxState:
|
||||
cfg
|
||||
)
|
||||
|
||||
safe_cfg = {k: v for k, v in cfg.items() if k != _APPLY_HASH_KEY}
|
||||
safe_cfg = strip_apply_meta(cfg)
|
||||
nginx_pending_diff: list[dict[str, Any]] = []
|
||||
if pending_changes:
|
||||
snap = cfg.get(_LAST_APPLIED_CONFIG_KEY)
|
||||
if isinstance(snap, dict):
|
||||
nginx_pending_diff = deep_diff(snap, safe_cfg)
|
||||
|
||||
return {
|
||||
"config": safe_cfg,
|
||||
"domains": domains,
|
||||
"status": {"pending_changes": pending_changes},
|
||||
"status": {
|
||||
"pending_changes": pending_changes,
|
||||
"pending_diff": nginx_pending_diff,
|
||||
},
|
||||
"timestamp": _now_iso(),
|
||||
}
|
||||
|
||||
@@ -939,7 +964,7 @@ def _collect_wireguard() -> schema.WgState:
|
||||
)
|
||||
|
||||
# Safe config (strip private keys from interface and access classes)
|
||||
safe = {k: v for k, v in cfg.items() if k != _APPLY_HASH_KEY}
|
||||
safe = strip_apply_meta(cfg)
|
||||
if "interface" in safe:
|
||||
safe["interface"] = dict(safe["interface"])
|
||||
safe["interface"].pop("private_key", None)
|
||||
@@ -1116,6 +1141,16 @@ def _collect_wireguard() -> schema.WgState:
|
||||
status["up"] = True
|
||||
|
||||
status["pending_changes"] = pending_changes
|
||||
pending_diff: list[dict[str, Any]] = []
|
||||
if pending_changes:
|
||||
snap = cfg.get(_LAST_APPLIED_CONFIG_KEY)
|
||||
if isinstance(snap, dict):
|
||||
# `safe` has private keys stripped; drop any private-key paths so
|
||||
# the pending summary never exposes key material.
|
||||
pending_diff = [
|
||||
d for d in deep_diff(snap, safe) if "private_key" not in d["path"]
|
||||
]
|
||||
status["pending_diff"] = pending_diff
|
||||
return {
|
||||
"config": safe,
|
||||
"status": status,
|
||||
@@ -1164,7 +1199,14 @@ def _collect_networkd() -> schema.NetworkdState:
|
||||
] != config_hash(net_cfg)
|
||||
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
safe_net_cfg = {k: v for k, v in net_cfg.items() if k != _APPLY_HASH_KEY}
|
||||
safe_net_cfg = strip_apply_meta(net_cfg)
|
||||
net_status: dict[str, Any] = {"pending_changes": pending_changes}
|
||||
net_pending_diff: list[dict[str, Any]] = []
|
||||
if pending_changes:
|
||||
snap = net_cfg.get(_LAST_APPLIED_CONFIG_KEY)
|
||||
if isinstance(snap, dict):
|
||||
net_pending_diff = deep_diff(snap, safe_net_cfg)
|
||||
net_status["pending_diff"] = net_pending_diff
|
||||
|
||||
try:
|
||||
raw = run(["networkctl", "status", "--json=short", "--all"], sudo=True)
|
||||
@@ -1173,21 +1215,21 @@ def _collect_networkd() -> schema.NetworkdState:
|
||||
return {
|
||||
"interfaces": {},
|
||||
"config": safe_net_cfg,
|
||||
"status": {"pending_changes": pending_changes},
|
||||
"status": net_status,
|
||||
"timestamp": _now_iso(),
|
||||
}
|
||||
except Exception:
|
||||
return {
|
||||
"interfaces": {},
|
||||
"config": safe_net_cfg,
|
||||
"status": {"pending_changes": pending_changes},
|
||||
"status": net_status,
|
||||
"timestamp": _now_iso(),
|
||||
}
|
||||
|
||||
return {
|
||||
"interfaces": result,
|
||||
"config": safe_net_cfg,
|
||||
"status": {"pending_changes": pending_changes},
|
||||
"status": net_status,
|
||||
"timestamp": _now_iso(),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user