"""Mutable vCenter appliance networking / timesync state (lab persistence).""" from __future__ import annotations from typing import Any from app.db.pool import Database from app.vsphere.domain import api_state from app.vsphere.errors import invalid_argument _NETWORKING_KEY = "/api/appliance/networking" _TIMESYNC_KEY = "/api/appliance/timesync" _DEFAULT_NETWORKING: dict[str, Any] = { "hostname": "vcenter.lab.local", "node_name": "vcenter.lab.local", "default_gateway": "192.168.1.1", "dns": { "mode": "DHCP", "servers": ["8.8.8.8", "1.1.1.1"], "domains": ["lab.local"], }, "interfaces": [ { "name": "nic0", "status": "up", "mac": "00:50:56:aa:bb:cc", "ipv4": {"address": "192.168.1.50", "prefix": 24, "configurable": True}, } ], "proxy": {"enabled": False, "server": "", "port": 0, "username": ""}, "no_proxy": ["localhost", "127.0.0.1", ".lab.local"], } _DEFAULT_TIMESYNC: dict[str, Any] = { "mode": "NTP", "servers": ["time.lab.local"], "current_time": "2026-01-01T00:00:00.000Z", } def _merge_networking(payload: Any) -> dict[str, Any]: """Overlay stored fields onto the full lab default shape.""" base = dict(_DEFAULT_NETWORKING) if not isinstance(payload, dict): return base merged = {**base, **payload} dns_base = dict(_DEFAULT_NETWORKING["dns"]) dns_stored = payload.get("dns") if isinstance(payload.get("dns"), dict) else {} merged["dns"] = {**dns_base, **dns_stored} if not merged.get("interfaces"): merged["interfaces"] = list(_DEFAULT_NETWORKING["interfaces"]) return merged async def get_networking(database: Database) -> dict[str, Any]: payload = await api_state.get_payload_or_seed(database, "GET", _NETWORKING_KEY) if not isinstance(payload, dict): return {} dns = payload.get("dns") if isinstance(payload.get("dns"), dict) else {} if not list(dns.get("domains") or []) or not list(dns.get("servers") or []): restored = await api_state.restore_seed_payload(database, "GET", _NETWORKING_KEY) if isinstance(restored, dict): await save_networking(database, restored) return restored return payload async def save_networking(database: Database, networking: dict[str, Any]) -> None: await api_state.put_payload(database, "GET", _NETWORKING_KEY, networking) # Keep Automation API GET mirrors in sync for stub/matrix probes. hostname = str(networking.get("hostname") or "vcenter.lab.local") dns = networking.get("dns") or {} await api_state.put_payload( database, "GET", "/api/appliance/networking/dns/hostname", {"name": hostname} ) await api_state.put_payload( database, "GET", "/api/appliance/networking/dns/servers", {"mode": dns.get("mode") or "DHCP", "servers": list(dns.get("servers") or [])}, ) await api_state.put_payload( database, "GET", "/api/appliance/networking/dns/domains", list(dns.get("domains") or []), ) async def set_hostname(database: Database, name: str) -> str: hostname = name.strip() if not hostname: raise invalid_argument("name is required") networking = await get_networking(database) networking["hostname"] = hostname networking["node_name"] = hostname await save_networking(database, networking) return hostname async def get_hostname(database: Database) -> str: networking = await get_networking(database) return str(networking.get("hostname") or "vcenter.lab.local") async def set_dns_servers( database: Database, *, mode: str | None = None, servers: list[str] | None = None, ) -> dict[str, Any]: networking = await get_networking(database) dns = dict(networking.get("dns") or {}) if mode is not None: dns["mode"] = mode if servers is not None: dns["servers"] = [str(s) for s in servers] networking["dns"] = dns await save_networking(database, networking) return {"mode": dns.get("mode") or "DHCP", "servers": list(dns.get("servers") or [])} async def set_dns_domains(database: Database, domains: list[str]) -> list[str]: networking = await get_networking(database) dns = dict(networking.get("dns") or {}) dns["domains"] = [str(d) for d in domains] networking["dns"] = dns await save_networking(database, networking) return list(dns["domains"]) async def get_timesync(database: Database) -> dict[str, Any]: payload = await api_state.get_payload_or_seed(database, "GET", _TIMESYNC_KEY) return payload if isinstance(payload, dict) else {} async def seed_appliance_state(database: Database) -> None: """Write appliance networking/timesync seed baselines into vsphere_api_state.""" networking = dict(_DEFAULT_NETWORKING) await api_state.put_seed_payload(database, "GET", _NETWORKING_KEY, networking) await api_state.put_seed_payload(database, "GET", _TIMESYNC_KEY, dict(_DEFAULT_TIMESYNC)) await save_networking(database, networking) dns = networking.get("dns") or {} await api_state.put_seed_payload( database, "GET", "/api/appliance/networking/dns/hostname", {"name": networking["hostname"]}, ) await api_state.put_seed_payload( database, "GET", "/api/appliance/networking/dns/servers", {"mode": dns.get("mode") or "DHCP", "servers": list(dns.get("servers") or [])}, ) await api_state.put_seed_payload( database, "GET", "/api/appliance/networking/dns/domains", list(dns.get("domains") or []), )