local_config.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. """
  2. Small readers for appliance-set state files.
  3. Two distinct surfaces, same shape (defensive, silent on missing files,
  4. side-effect-free):
  5. - ``read_local_toml`` reads ``/etc/bambuddy/local.toml`` (the file the
  6. appliance setup wizard writes during firstboot with the user's hostname,
  7. timezone, and locale).
  8. - ``read_ntp_gate`` reads ``/run/bambuddy/time-synced`` (the appliance's
  9. ntp-gate.sh signals time-sync state here once chrony reports sync, or
  10. when the 3-minute timeout elapses with a "warning" marker).
  11. Universal across install shapes:
  12. - On the Bambuddy Appliance: both files exist by the time bambuddy.service
  13. starts; we surface their values to the frontend.
  14. - On Docker / manual installs: both files are absent; we degrade silently.
  15. These readers are read-only and side-effect-free. They do NOT call
  16. hostnamectl / timedatectl / chronyc — system-state changes are the
  17. appliance's firstboot.sh responsibility (root, runs before this process
  18. exists). Here we just expose state so the frontend can render accordingly.
  19. """
  20. from __future__ import annotations
  21. import logging
  22. from pathlib import Path
  23. from typing import Literal, TypedDict
  24. import tomllib
  25. log = logging.getLogger(__name__)
  26. DEFAULT_PATH = Path("/etc/bambuddy/local.toml")
  27. DEFAULT_NTP_GATE_PATH = Path("/run/bambuddy/time-synced")
  28. # Three states: synced ("ok"), gated-and-timed-out ("warning"), or unknown (None).
  29. TimeSyncState = Literal["ok", "warning"] | None
  30. class LocalConfig(TypedDict, total=False):
  31. hostname: str
  32. timezone: str
  33. locale: str
  34. def read_local_toml(path: Path = DEFAULT_PATH) -> LocalConfig:
  35. """Read the appliance local.toml. Missing / invalid file returns empty dict.
  36. Only the keys actually present in the file are returned — the caller checks
  37. `if "locale" in config:` rather than relying on defaults. Non-string values
  38. are dropped with a warning to keep this defensive on a hand-edited file.
  39. """
  40. if not path.is_file():
  41. return {}
  42. try:
  43. with path.open("rb") as f:
  44. data = tomllib.load(f)
  45. except (OSError, tomllib.TOMLDecodeError) as exc:
  46. log.warning("local.toml at %s could not be parsed: %s", path, exc)
  47. return {}
  48. result: LocalConfig = {}
  49. for key in ("hostname", "timezone", "locale"):
  50. value = data.get(key)
  51. if value is None:
  52. continue
  53. if not isinstance(value, str):
  54. log.warning("local.toml: %r is %s, expected str — ignoring", key, type(value).__name__)
  55. continue
  56. result[key] = value # type: ignore[literal-required]
  57. return result
  58. def read_ntp_gate(path: Path = DEFAULT_NTP_GATE_PATH) -> TimeSyncState:
  59. """Read the appliance NTP gate file. Returns "ok", "warning", or None.
  60. Wire contract with bambuddy-appliance/firstboot/ntp-gate.sh:
  61. - File absent: gate hasn't been evaluated yet, or this isn't an appliance
  62. install. Caller should treat as "unknown / don't gate."
  63. - File content starts with "ok": chrony reported sync within 3 minutes.
  64. - File content starts with "warning": 3-minute timeout elapsed without
  65. sync. The user has already waited and the wizard proceeded with a
  66. degraded clock — auth tokens may have incorrect expiry, TLS certs may
  67. fail validation. UI should surface this.
  68. - Anything else: defensive fall-through to None.
  69. """
  70. try:
  71. body = path.read_text(errors="replace").strip()
  72. except FileNotFoundError:
  73. return None
  74. except OSError as exc:
  75. log.warning("ntp-gate file at %s could not be read: %s", path, exc)
  76. return None
  77. if body.startswith("ok"):
  78. return "ok"
  79. if body.startswith("warning"):
  80. return "warning"
  81. return None