"""TypedDict schemas for every state collector's return value. Single source of truth for the state-store data shapes. The Markdown reference is ``docs/state-model.md``. """ from typing import Any, TypedDict __all__ = [ "AcmeAccount", "AcmeCert", "AcmeState", "CpuLoad", "DnsmasqDhcpLease", "DnsmasqState", "DnsmasqStatus", "FirewallInterface", "FirewallState", "FirewallZone", "MemoryStats", "NetworkdInterface", "NetworkdState", "NginxDomain", "NginxState", "SwapStats", "SystemState", "TrafficStats", "WgClassStatus", "WgPeer", "WgState", "WgStatus", "WgStatusPeer", ] # ── Shared notes ─────────────────────────────────────────────── # Every collector return carries a top-level `timestamp` (ISO-8601). # Subsystems with a declarative config expose pending state as a # status dict: `status: {"pending_changes": bool}`, except firewall, # which uses `pending: {config_pending() result}`. # ── Firewall ────────────────────────────────────────────────── class FirewallInterface(TypedDict): """A network interface as parsed from `ip link` / `ip addr`. Attributes: name: Interface name (e.g. "eth0"). mac: MAC address, or ``None`` if unavailable. state: Link state from `ip link` ("UP", "DOWN", "UNKNOWN", ...). mtu: MTU value, or ``None`` if unavailable. ips: IPv4 addresses as "ip/prefix" strings. ipv6: IPv6 addresses as "ip/prefix" strings. zone: Assigned firewalld zone name, or ``None``. """ name: str mac: str | None state: str mtu: int | None ips: list[str] ipv6: list[str] zone: str | None class FirewallZone(TypedDict, total=False): """A firewalld zone as parsed from `--list-all-zones`. The zone dict carries the HYPHENATED key "rich-rules" (see ``lib.firewall._parse_all_zones_output``), which TypedDict fields cannot express. Additional firewalld keys may also appear: "sources", "ports", "protocols", "forward-ports", "ics", "icmp-blocks", "module", "rich-rules". """ target: str interfaces: list[str] services: list[str] masquerade: bool # snake_case `rich_rules` exists only at the top-level FirewallState # (collector re-derivation, lib/state.py); the zone dict itself uses # the hyphenated "rich-rules" key. rich_rules: list[str] class FirewallState(TypedDict): """Complete firewalld state (collector: `_collect_firewall`). Attributes: config: Contents of config/firewall/config.json. active_zones: Zone name → assigned interfaces. default_zone: firewalld default zone name ("--get-default-zone"); catch-all zone for interfaces with no explicit assignment. interfaces: All system interfaces (see FirewallInterface). available_services: firewalld service catalog ("--get-services"). service_descriptions: Service name to one-line description, parsed from the firewalld service XML definitions (``lib.firewall.get_service_descriptions``). uncovered_interfaces: Network-config interfaces (excluding ``lo``/``wg*``) not in any live zone (advisory coverage warning, always present). zones: All zones as runtime dicts (see FirewallZone). rich_rules: Zone name → raw firewalld rich-rule strings. pending: config_pending() result: ``{pending: [...], needs_apply: bool, unmanaged_zones: {zone: {interfaces: [...]}}}``. timestamp: ISO-8601 collection time. """ config: dict[str, Any] active_zones: dict[str, list[str]] default_zone: str interfaces: list[FirewallInterface] available_services: list[str] service_descriptions: dict[str, str] uncovered_interfaces: list[str] zones: dict[str, FirewallZone] rich_rules: dict[str, list[str]] pending: dict[str, Any] timestamp: str # ── Dnsmasq ─────────────────────────────────────────────────── class DnsmasqDhcpLease(TypedDict): """A single dnsmasq DHCP lease from the lease file. Attributes: expires: ISO-8601 expiry timestamp, or "" if unparseable. mac: Client MAC address. ip: Leased IP address. hostname: Client hostname (may be ""). interface: Interface the lease was granted on (may be ""). """ expires: str mac: str ip: str hostname: str interface: str class PendingChange(TypedDict): """One field-level difference between the applied config and the current saved config (see `lib.common.deep_diff`). Attributes: path: Dotted (or indexed) path to the changed field. action: "added", "removed", or "changed". old: Value in the last applied config (None when added). new: Value in the current config (None when removed). """ path: str action: str old: Any new: Any class DnsmasqStatus(TypedDict): """Dnsmasq service status snapshot. Attributes: service_active: Whether the dnsmasq systemd unit is active. config_file_exists: Whether the rendered .conf is on disk. active_leases: Count of currently active leases. pending_changes: Whether the config is dirty vs the applied state. pending_diff: Field-level changes since the last apply (empty when up to date or when no applied snapshot is recorded). """ service_active: bool config_file_exists: bool active_leases: int pending_changes: bool pending_diff: list[PendingChange] class DnsmasqState(TypedDict): """Dnsmasq state (collector: `_collect_dnsmasq`). Attributes: config: config/dnsmasq/config.json, deep-merged with defaults. status: Service/config status (see DnsmasqStatus). leases: Active DHCP leases. timestamp: ISO-8601 collection time. """ config: dict[str, Any] status: DnsmasqStatus leases: list[DnsmasqDhcpLease] timestamp: str # ── Nginx ───────────────────────────────────────────────────── class NginxDomain(TypedDict, total=False): """One flattened domain+path entry (see `_resolve_paths`). Attributes: domain: Domain name (config key). path: Path prefix for this entry. backend: Resolved backend config dict. online: Whether a site .conf exists on disk. force_ssl: Redirect-to-HTTPS flag. backend_name: Backend config key this domain points at. cert: Certificate reference (e.g. "acme", "selfsigned", ...). is_management: Management UI path marker. is_websocket: WebSocket-capable path marker. """ domain: str path: str backend: dict[str, Any] online: bool force_ssl: bool backend_name: str cert: str | None is_management: bool is_websocket: bool class NginxState(TypedDict): """Nginx state (collector: `_collect_nginx`). Attributes: config: config/nginx/config.json. domains: Flattened domain entries (one per domain+path). status: ``{"pending_changes": bool}``. timestamp: ISO-8601 collection time. """ config: dict[str, Any] domains: list[NginxDomain] status: dict[str, Any] timestamp: str # ── ACME ────────────────────────────────────────────────────── class AcmeAccount(TypedDict): """ACME account status (see `_parse_account_conf`). Attributes: registered: Whether an account exists. email: Registered email ("" when not registered). ca: Human-readable CA name ("" when not registered). key_length: Account key size, or ``None``. """ registered: bool email: str ca: str key_length: int | None class AcmeCert(TypedDict, total=False): """One certificate entry from `list_certs()` output. Attributes: domain: Certificate domain name. expiry: Expiry date string. renewed: Last renewal date string. status: "valid" | "expired" | "active" | ... days_remaining: Days until expiry. Additional keys from `lib.acme.list_certs()` output may appear. """ domain: str expiry: str renewed: str status: str days_remaining: int class AcmeState(TypedDict): """ACME state (collector: `_collect_acme`). Attributes: certs: Certificate list (empty when collection failed). email: Registered ACME email. account: Account status (see AcmeAccount). status: Collection status; ``error`` is ``None`` on success or the failure message when cert collection was not possible. timestamp: ISO-8601 collection time. """ certs: list[AcmeCert] email: str account: AcmeAccount status: dict[str, str | None] timestamp: str # ── WireGuard ───────────────────────────────────────────────── # The collector tracks BOTH a legacy single interface (wg0) and one # wg- interface per access class. class WgStatusPeer(TypedDict): """A runtime peer parsed from `wg show` output. Attributes: public_key: Peer public key. endpoint: Last-seen endpoint, or ``None``. allowed_ips: Allowed IP/CIDR list. latest_handshake: Human-readable last handshake time, or ``None``. transfer_received: Received-bytes string from the transfer line. transfer_sent: Sent-bytes string from the transfer line. persistent_keepalive: Keepalive seconds, or ``None``. """ public_key: str endpoint: str | None allowed_ips: list[str] latest_handshake: str | None transfer_received: str transfer_sent: str persistent_keepalive: int | None class WgClassStatus(TypedDict): """Runtime status for one wg- interface. Attributes: up: Whether the class interface is up. interface: ``{public_key, listen_port}`` (empty when down). peers: Runtime peers of this class interface. """ up: bool interface: dict[str, Any] peers: list[WgStatusPeer] class WgStatus(TypedDict): """Aggregated WireGuard runtime status. Attributes: up: True when any managed interface is up. interface: Legacy single-interface info (public_key, listen_port). peers: Legacy single-interface runtime peers. classes: Per-access-class runtime status (keyed by class name). pending_changes: Whether the config is dirty vs the applied state. pending_diff: Field-level changes since the last apply (empty when up to date or when no applied snapshot is recorded). """ up: bool interface: dict[str, Any] peers: list[WgStatusPeer] classes: dict[str, WgClassStatus] pending_changes: bool pending_diff: list[PendingChange] class WgPeer(TypedDict, total=False): """A config-file peer (``private_key`` stripped). Attributes: name: Peer config key. public_key: Peer public key. endpoint: Configured endpoint, or ``None``. allowed_ips: Allowed IP/CIDR list. persistent_keepalive: Keepalive seconds, or ``None``. preshared_key: Preshared key, or ``None``. """ name: str public_key: str endpoint: str | None allowed_ips: list[str] persistent_keepalive: int | None preshared_key: str | None class WgState(TypedDict): """WireGuard state (collector: `_collect_wireguard`). Attributes: config: config/wireguard/config.json; ``private_key`` stripped from the interface AND from every access class. status: Runtime status (see WgStatus). peers: Config peers, private keys stripped. timestamp: ISO-8601 collection time. """ config: dict[str, Any] status: WgStatus peers: list[WgPeer] timestamp: str # ── Networkd ────────────────────────────────────────────────── # Matches parse_networkctl_status() output exactly (lib/network.py). # There is a single combined `addresses` list — no separate # `ipv6_addresses` or `routes` keys. class NetworkdInterface(TypedDict): """One networkd interface as parsed by `parse_networkctl_status`. Attributes: addresses: "ip/prefix" entries, IPv4+IPv6 combined. gateway: Default-route gateway, or ``None``. dns: Configured DNS server list. mac: MAC address, or ``None``. state: OperationalState (e.g. "routable", "degraded", "off"). link: Link type (e.g. "ether", "loopback", ...). """ addresses: list[str] gateway: str | None dns: list[str] mac: str | None state: str link: str class NetworkdState(TypedDict): """Networkd state (collector: `_collect_networkd`). Attributes: config: config/network/config.json. interfaces: Runtime state keyed by interface name. status: ``{"pending_changes": bool}``. timestamp: ISO-8601 collection time. """ config: dict[str, Any] interfaces: dict[str, NetworkdInterface] status: dict[str, Any] timestamp: str # ── System ──────────────────────────────────────────────────── class CpuLoad(TypedDict): """CPU load averages (see /proc/loadavg). Attributes: load1: 1-minute load average. load5: 5-minute load average. load15: 15-minute load average. """ load1: float load5: float load15: float class MemoryStats(TypedDict): """Memory usage (see /proc/meminfo). Attributes: total: Total memory in bytes. available: Available memory in bytes. used: Used memory in bytes. used_pct: Used percentage (0-100), rounded to 0.1. """ total: int available: int used: int used_pct: float class SwapStats(TypedDict): """Swap usage (see /proc/meminfo). Attributes: total: Total swap in bytes. used: Used swap in bytes. used_pct: Used percentage (0-100), rounded to 0.1. """ total: int used: int used_pct: float class TrafficStats(TypedDict): """Per-interface traffic counters (see /sys/class/net//statistics). Attributes: rx_bytes: Total bytes received. tx_bytes: Total bytes transmitted. rx_packets: Total packets received. tx_packets: Total packets transmitted. """ rx_bytes: int tx_bytes: int rx_packets: int tx_packets: int class SystemState(TypedDict): """System-wide metrics (collector: `_collect_system`). Attributes: load: CPU load averages (see CpuLoad). memory: Memory usage (see MemoryStats). swap: Swap usage (see SwapStats). traffic: Per-interface counters keyed by interface name. timestamp: ISO-8601 collection time. """ load: CpuLoad memory: MemoryStats swap: SwapStats traffic: dict[str, TrafficStats] timestamp: str