ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
hil_secrets.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Resolve the HIL Tapo secrets from OpenBao, falling back to a local .env.
4
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.
9
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.
16
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
19from the environment.
20
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")
26
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.
30"""
31
32from __future__ import annotations
33
34import os
35import sys
36from pathlib import Path
37
38from dotenv import load_dotenv
39
40sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "secrets"))
41from openbao_client import OpenBaoClient, OpenBaoError, load_config
42
43# Map OpenBao KV keys -> the TAPO_* environment variables the consumer reads.
44#
45# TAPO_RELAY_* is intentionally NOT in this map: the relay plug's IP/MAC are
46# non-secret and resolved from .env only. Adding it here would enlarge the
47# all-keys-or-miss set checked in populate_env(), so an existing vault holding
48# only the board/pi keys would read as an incomplete miss and break board/pi
49# OpenBao resolution. Keep relay out of OpenBao resolution.
50_KEY_TO_ENV = {
51 "user": "TAPO_USER",
52 "pass": "TAPO_PASS",
53 "board_ip": "TAPO_BOARD_IP",
54 "board_mac": "TAPO_BOARD_MAC",
55 "pi_ip": "TAPO_PI_IP",
56 "pi_mac": "TAPO_PI_MAC",
57}
58
59
60def _fetch_from_openbao() -> dict | None:
61 """Read the Tapo secret from OpenBao, or None when it is not configured."""
62 cfg = load_config()
63 client = OpenBaoClient(cfg)
64 if not client.configured:
65 return None
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}
69
70
71def populate_env(env_file: Path, fallback_env: Path) -> str:
72 """Populate os.environ with the TAPO_* secrets and return the source used.
73
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.
77 """
78 source = "none"
79 if env_file.exists():
80 load_dotenv(env_file)
81 source = "dotenv"
82 elif fallback_env.exists():
83 load_dotenv(fallback_env)
84 source = "dotenv"
85
86 try:
87 secrets = _fetch_from_openbao()
88 except (OpenBaoError, OSError):
89 secrets = None
90
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
94 source = "openbao"
95 return source