refactor: daemon collectors, thin webui proxies, pure config reads
- move state collectors from lib/state.py to daemon/collectors/ (7 modules, registration side-effect; daemon/server.py imports the package before the first populate()) - webui/api: new daemon_route() decorator factory in common.py collapses the try/except daemon-proxy boilerplate in all 8 blueprints (rules/params/body/transform keep responses identical) - firewall: interface-coverage invariant — config is the source of truth for zone interfaces (absent key = empty, no hands-off zones); pure validate_coverage() enforced at save (400) and apply (409, force: true overrides), top-level `unmanaged` exemption - lib: get_config() reads are now pure (no dir creation or writes); new lib/bootstrap.py creates runtime dirs and persists the one-shot nginx legacy migration at daemon start, after system_import (lib.nginx.migrate_config_file) - lib/common: compute_pending() apply-bookkeeping helper - daemon: emit_and_refresh() handler helper; refresh_state(bump=) so /status/refresh no longer bumps versions (poll/mutation only) - acme: move --log last so acme.sh never treats a real arg as the log-file argument - docs: AGENTS.md, config.md, state-model.md, api.md updated; HARDEN.md dropped (plan implemented); apply-confirm force wording Tests: 917 passed; ruff check + format clean.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""DNSMasq state collector."""
|
||||
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from lib import schema
|
||||
from lib.common import compute_pending, run_proc, strip_apply_meta
|
||||
from lib.dnsmasq import DEFAULT_CFG, get_config
|
||||
from lib.state import _now_iso, register_collector
|
||||
|
||||
|
||||
def _collect_dnsmasq() -> schema.DnsmasqState:
|
||||
"""Collect dnsmasq status, config, and leases.
|
||||
|
||||
Returns:
|
||||
Dict containing config, service status, leases, and timestamp.
|
||||
"""
|
||||
DNSMASQ_CONF = "/etc/dnsmasq.d/vacuum-wall.conf"
|
||||
LEASE_FILE = "/var/lib/misc/dnsmasq.leases"
|
||||
|
||||
# Load config (lib defaults; fall back to them when the file is broken)
|
||||
try:
|
||||
cfg = get_config()
|
||||
except Exception:
|
||||
cfg = deepcopy(DEFAULT_CFG)
|
||||
|
||||
# Service status
|
||||
service_active = False
|
||||
try:
|
||||
proc = run_proc(["systemctl", "is-active", "dnsmasq"], sudo=True)
|
||||
service_active = proc.stdout.strip() == "active"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Leases
|
||||
leases: list[dict[str, Any]] = []
|
||||
try:
|
||||
result = run_proc(["cat", LEASE_FILE], sudo=True, check=True)
|
||||
for line in result.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
try:
|
||||
ts = datetime.fromtimestamp(int(parts[0]), tz=UTC)
|
||||
except (ValueError, OSError):
|
||||
ts = None
|
||||
leases.append(
|
||||
{
|
||||
"expires": ts.isoformat() if ts else "",
|
||||
"mac": parts[1],
|
||||
"ip": parts[2],
|
||||
"hostname": parts[3] if len(parts) > 3 else "",
|
||||
"interface": parts[4] if len(parts) > 4 else "",
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Check config file on disk
|
||||
conf_exists = Path(DNSMASQ_CONF).is_file()
|
||||
|
||||
pending_changes, pending_diff = compute_pending(cfg)
|
||||
safe_cfg = strip_apply_meta(cfg)
|
||||
|
||||
return {
|
||||
"config": safe_cfg,
|
||||
"status": {
|
||||
"service_active": service_active,
|
||||
"config_file_exists": conf_exists,
|
||||
"active_leases": len(leases),
|
||||
"pending_changes": pending_changes,
|
||||
"pending_diff": pending_diff,
|
||||
},
|
||||
"leases": leases,
|
||||
"timestamp": _now_iso(),
|
||||
}
|
||||
|
||||
|
||||
register_collector("dnsmasq", _collect_dnsmasq)
|
||||
# dnsmasq has no volatile fields — leases change slowly enough to treat as structural
|
||||
Reference in New Issue
Block a user