"""Dnsmasq config persistence for Vacuum Wall. Provides load/save for the declarative JSON config and upstream-management helpers used by the sync bus and network handler. All mutation and apply logic lives in daemon/handlers/dnsmasq.py. """ import logging from copy import deepcopy from pathlib import Path from typing import Any from lib.common import deep_merge, ensure_dirs, load_json, save_json logger = logging.getLogger(__name__) PROJECT_DIR = Path(__file__).resolve().parent.parent CONFIG_DIR = PROJECT_DIR / "config" / "dnsmasq" DATA_DIR = PROJECT_DIR / "data" / "dnsmasq" CONFIG_PATH = CONFIG_DIR / "config.json" FRAGMENTS_DIR = DATA_DIR / "fragments" DEFAULT_CFG: dict[str, Any] = { "dhcp": { "ranges": [], "static_leases": [], }, "dns": { "upstreams": ["8.8.8.8", "1.1.1.1"], "domain": None, "custom_records": [], }, } # ───────── config lifecycle ────────────────────────────────────────── def get_config() -> dict[str, Any]: """Load current dnsmasq config from JSON state file.""" ensure_dirs(CONFIG_DIR, DATA_DIR, FRAGMENTS_DIR) raw = load_json(CONFIG_PATH) if not raw: return deepcopy(DEFAULT_CFG) return deep_merge(deepcopy(DEFAULT_CFG), raw) def save_config(cfg: dict[str, Any]) -> None: """Persist config to JSON (does NOT touch on-disk dnsmasq config).""" ensure_dirs(CONFIG_DIR, DATA_DIR, FRAGMENTS_DIR) merged = deep_merge(deepcopy(DEFAULT_CFG), cfg) save_json(CONFIG_PATH, merged) logger.info("dnsmasq config saved") # ───────── upstream helpers ────────────────────────────────────────── def set_upstreams(servers: list[str]) -> None: """Set the list of upstream DNS forwarders.""" cfg = get_config() cfg["dns"]["upstreams"] = list(servers) save_config(cfg) logger.info("DNS upstreams set to %s", servers) def set_domain(domain: str | None) -> None: """Set (or clear) the local DNS domain.""" cfg = get_config() cfg["dns"]["domain"] = domain if domain else None save_config(cfg) logger.info("DNS domain set to '%s'", domain) __all__ = [ "get_config", "save_config", "set_domain", "set_upstreams", ]