local_config.py 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. """
  2. Read /etc/bambuddy/local.toml — the file the appliance setup wizard writes
  3. during firstboot to capture the user's hostname, timezone, and locale.
  4. Universal across install shapes:
  5. - On the Bambuddy Appliance: the wizard writes this file before bambuddy.service
  6. starts; we read it on every startup to surface defaults to the frontend.
  7. - On Docker / manual installs: the file is absent; we degrade silently. An
  8. operator who wants to seed defaults can drop their own local.toml into the
  9. expected path or override via DATA_DIR.
  10. The reader is read-only and side-effect-free. It does NOT call hostnamectl
  11. or timedatectl — that's the appliance's firstboot.sh responsibility (it has
  12. the root privileges to do so and runs before this process exists). What we
  13. do here is expose the values the wizard collected so the frontend i18n
  14. bootstrap can pick the right initial language.
  15. """
  16. from __future__ import annotations
  17. import logging
  18. from pathlib import Path
  19. from typing import TypedDict
  20. import tomllib
  21. log = logging.getLogger(__name__)
  22. DEFAULT_PATH = Path("/etc/bambuddy/local.toml")
  23. class LocalConfig(TypedDict, total=False):
  24. hostname: str
  25. timezone: str
  26. locale: str
  27. def read_local_toml(path: Path = DEFAULT_PATH) -> LocalConfig:
  28. """Read the appliance local.toml. Missing / invalid file returns empty dict.
  29. Only the keys actually present in the file are returned — the caller checks
  30. `if "locale" in config:` rather than relying on defaults. Non-string values
  31. are dropped with a warning to keep this defensive on a hand-edited file.
  32. """
  33. if not path.is_file():
  34. return {}
  35. try:
  36. with path.open("rb") as f:
  37. data = tomllib.load(f)
  38. except (OSError, tomllib.TOMLDecodeError) as exc:
  39. log.warning("local.toml at %s could not be parsed: %s", path, exc)
  40. return {}
  41. result: LocalConfig = {}
  42. for key in ("hostname", "timezone", "locale"):
  43. value = data.get(key)
  44. if value is None:
  45. continue
  46. if not isinstance(value, str):
  47. log.warning("local.toml: %r is %s, expected str — ignoring", key, type(value).__name__)
  48. continue
  49. result[key] = value # type: ignore[literal-required]
  50. return result