| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- """
- Small readers for appliance-set state files.
- Two distinct surfaces, same shape (defensive, silent on missing files,
- side-effect-free):
- - ``read_local_toml`` reads ``/etc/bambuddy/local.toml`` (the file the
- appliance setup wizard writes during firstboot with the user's hostname,
- timezone, and locale).
- - ``read_ntp_gate`` reads ``/run/bambuddy/time-synced`` (the appliance's
- ntp-gate.sh signals time-sync state here once chrony reports sync, or
- when the 3-minute timeout elapses with a "warning" marker).
- Universal across install shapes:
- - On the Bambuddy Appliance: both files exist by the time bambuddy.service
- starts; we surface their values to the frontend.
- - On Docker / manual installs: both files are absent; we degrade silently.
- These readers are read-only and side-effect-free. They do NOT call
- hostnamectl / timedatectl / chronyc — system-state changes are the
- appliance's firstboot.sh responsibility (root, runs before this process
- exists). Here we just expose state so the frontend can render accordingly.
- """
- from __future__ import annotations
- import logging
- from pathlib import Path
- from typing import Literal, TypedDict
- import tomllib
- log = logging.getLogger(__name__)
- DEFAULT_PATH = Path("/etc/bambuddy/local.toml")
- DEFAULT_NTP_GATE_PATH = Path("/run/bambuddy/time-synced")
- # Three states: synced ("ok"), gated-and-timed-out ("warning"), or unknown (None).
- TimeSyncState = Literal["ok", "warning"] | None
- class LocalConfig(TypedDict, total=False):
- hostname: str
- timezone: str
- locale: str
- def read_local_toml(path: Path = DEFAULT_PATH) -> LocalConfig:
- """Read the appliance local.toml. Missing / invalid file returns empty dict.
- Only the keys actually present in the file are returned — the caller checks
- `if "locale" in config:` rather than relying on defaults. Non-string values
- are dropped with a warning to keep this defensive on a hand-edited file.
- """
- if not path.is_file():
- return {}
- try:
- with path.open("rb") as f:
- data = tomllib.load(f)
- except (OSError, tomllib.TOMLDecodeError) as exc:
- log.warning("local.toml at %s could not be parsed: %s", path, exc)
- return {}
- result: LocalConfig = {}
- for key in ("hostname", "timezone", "locale"):
- value = data.get(key)
- if value is None:
- continue
- if not isinstance(value, str):
- log.warning("local.toml: %r is %s, expected str — ignoring", key, type(value).__name__)
- continue
- result[key] = value # type: ignore[literal-required]
- return result
- def read_ntp_gate(path: Path = DEFAULT_NTP_GATE_PATH) -> TimeSyncState:
- """Read the appliance NTP gate file. Returns "ok", "warning", or None.
- Wire contract with bambuddy-appliance/firstboot/ntp-gate.sh:
- - File absent: gate hasn't been evaluated yet, or this isn't an appliance
- install. Caller should treat as "unknown / don't gate."
- - File content starts with "ok": chrony reported sync within 3 minutes.
- - File content starts with "warning": 3-minute timeout elapsed without
- sync. The user has already waited and the wizard proceeded with a
- degraded clock — auth tokens may have incorrect expiry, TLS certs may
- fail validation. UI should surface this.
- - Anything else: defensive fall-through to None.
- """
- try:
- body = path.read_text(errors="replace").strip()
- except FileNotFoundError:
- return None
- except OSError as exc:
- log.warning("ntp-gate file at %s could not be read: %s", path, exc)
- return None
- if body.startswith("ok"):
- return "ok"
- if body.startswith("warning"):
- return "warning"
- return None
|