3"""Resolve the HIL Tapo secrets from OpenBao, falling back to a local .env.
5Prefers the self-hosted OpenBao vault (talked to over HTTP by
6openbao_client.py -- the existing k3s pod at BAO_ADDR, nothing spun up
7locally) and falls back to the local .env so the smart plugs stay
8controllable even when OpenBao or the k3s cluster is down.
10Resolution order (first that yields ALL required keys wins):
11 1. OpenBao -- AppRole login, then read a KV v2 secret. Attempted only when
12 the consumer credentials file exists and the server answers
13 within a short timeout.
14 2. .env -- python-dotenv into the process environment (the unchanged
15 legacy behaviour); also the fallback for ANY OpenBao failure.
17Note "ALL required keys": a partial OpenBao read is treated as a miss and
18falls through, rather than leaving some keys vault-sourced and others stale
21The OpenBao consumer credentials live OUTSIDE the repo in a 0600 file at
22~/.config/hil/openbao.env (override the path with HIL_OPENBAO_ENV). It holds
23how to reach the vault and the AppRole identity -- never the Tapo secrets
24themselves. See openbao_client.py for the shared keys; hil-specific:
25 BAO_SECRET_PATH secret path under the mount (default "ra8d2/tapo")
27populate_env() never raises on an OpenBao error: it always returns a source
28string ("openbao", "dotenv", or "none") and leaves the .env-derived
29environment intact for fallback.
32from __future__
import annotations
36from pathlib
import Path
38from dotenv
import load_dotenv
40sys.path.insert(0, str(Path(__file__).resolve().parents[1] /
"secrets"))
41from openbao_client
import OpenBaoClient, OpenBaoError, load_config
53 "board_ip":
"TAPO_BOARD_IP",
54 "board_mac":
"TAPO_BOARD_MAC",
55 "pi_ip":
"TAPO_PI_IP",
56 "pi_mac":
"TAPO_PI_MAC",
60def _fetch_from_openbao() -> dict | None:
61 """Read the Tapo secret from OpenBao, or None when it is not configured."""
63 client = OpenBaoClient(cfg)
64 if not client.configured:
66 secret_path = cfg.get(
"BAO_SECRET_PATH",
"ra8d2/tapo")
67 data = client.kv_get(secret_path)
68 return {env: data[key]
for key, env
in _KEY_TO_ENV.items()
if key
in data}
71def populate_env(env_file: Path, fallback_env: Path) -> str:
72 """Populate os.environ with the TAPO_* secrets and return the source used.
74 The .env baseline is loaded first so it is always present as a fallback,
75 then OpenBao values are overlaid on top when -- and only when -- the vault
76 yields a complete set. Any OpenBao error is swallowed.
82 elif fallback_env.exists():
83 load_dotenv(fallback_env)
87 secrets = _fetch_from_openbao()
88 except (OpenBaoError, OSError):
91 if secrets
and all(env
in secrets
for env
in _KEY_TO_ENV.values()):
92 for env, value
in secrets.items():
93 os.environ[env] = value