ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
fleet_bench.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Bind a mutating bench converge to the repository's bench lock."""
4
5from __future__ import annotations
6
7import argparse
8import contextlib
9import hashlib
10import io
11import json
12import os
13import pwd
14import re
15import subprocess
16from collections.abc import Callable, Mapping, Sequence
17from dataclasses import dataclass, replace
18from pathlib import Path
19
20import bench_lock_capability as blc
21
22LOCK_ID_RE = re.compile(r"[0-9a-f]{16}")
23MAINTENANCE_VAR = "hil_bench_maintenance_lock_id"
24HOLDER_PID_VAR = "hil_bench_maintenance_holder_pid"
25HOLDER_START_VAR = "hil_bench_maintenance_holder_start_ticks"
26HOLDER_TARGET_VAR = "hil_bench_maintenance_holder_target"
27INHERITED_LOCK_ERROR = "inherited bench lock is not the live holder"
28ARGPARSE_USAGE_ERROR = 2
29
30
31class BenchGuardError(ValueError):
32 """A mutating bench converge lacks a canonical live-hold identity."""
33
34
35@dataclass(frozen=True)
36class GuardRequest:
37 """Everything needed to wrap one converge without reaching a host."""
38
39 repo_root: Path
40 fleet_script: Path
41 original_argv: Sequence[str]
42 host_class: str
43 plays: Sequence[str]
44 mode: str
45 environment: Mapping[str, str]
46
47
48@dataclass(frozen=True)
49class FlowRequest:
50 """The user-controlled selectors at the bench apply boundary."""
51
52 host_class: str
53 plays: Sequence[str]
54 mode: str
55 tags: str
56 extra_vars: Sequence[str]
57 trusted_tags: bool = False
58
59
60LockAuthenticator = Callable[[Path, blc.Capability], bool]
61
62
63def _live_lock_matches(repo_root: Path, capability: blc.Capability) -> bool:
64 """Authenticate local wrapper ancestry and the remote kernel-held lock."""
65 client = repo_root / "scripts/hil/lib/bench_client.sh"
66 verifier = repo_root / "scripts/hil/lib/bench_lock_verify.py"
67 broker = repo_root / "scripts/hil/lib/bench_lock_broker.py"
68 host = repo_root / "scripts/hil/lib/bench_host.sh"
69 try:
70 resolved_client = client.resolve(strict=True)
71 resolved_verifier = verifier.resolve(strict=True)
72 resolved_broker = broker.resolve(strict=True)
73 resolved_host = host.resolve(strict=True)
74 blc.authenticate(capability, repo_root)
75 except OSError:
76 return False
77 except blc.CapabilityError:
78 return False
79 sources = (client, verifier, broker, host)
80 resolved = (resolved_client, resolved_verifier, resolved_broker, resolved_host)
81 if any(
82 path.absolute() != target or path.is_symlink() or not path.is_file()
83 for path, target in zip(sources, resolved, strict=False)
84 ):
85 return False
86 try:
87 digest = hashlib.sha256(host.read_bytes()).hexdigest()
88 broker_digest = hashlib.sha256(broker.read_bytes()).hexdigest()
89 except OSError:
90 return False
91 script = '. "$1"; bench_verify_live "$2" wrapped "$3" "$4"'
92 try:
93 home = Path(pwd.getpwuid(os.getuid()).pw_dir).resolve(strict=True)
94 result = subprocess.run( # noqa: S603 -- fixed bash and authenticated source path
95 [
96 "/bin/bash",
97 "--noprofile",
98 "--norc",
99 "-p",
100 "-c",
101 script,
102 "ra8-lock",
103 str(client),
104 capability.lock_id,
105 digest,
106 broker_digest,
107 ],
108 env={"HOME": str(home), "PATH": "/usr/bin:/bin"},
109 check=False,
110 timeout=30,
111 )
112 except (OSError, subprocess.TimeoutExpired):
113 return False
114 return result.returncode == 0
115
116
117def needs_guard(host_class: str, plays: Sequence[str], mode: str) -> bool:
118 """Return whether this converge can mutate the physical bench host."""
119 bench = host_class == "hil_bench" and "hil-bench" in plays
120 delegated = host_class == "dev_box" and "dev-box" in plays
121 return mode == "apply" and (bench or delegated)
122
123
124def _lock_id(environment: Mapping[str, str]) -> str:
125 """Return the inherited live-hold identity, rejecting ambiguous values."""
126 lock_id = environment.get("RA8_BENCH_LOCK_ID", "")
127 if lock_id and LOCK_ID_RE.fullmatch(lock_id) is None:
128 message = "RA8_BENCH_LOCK_ID is not the canonical 16-hex identity"
129 raise BenchGuardError(message)
130 return lock_id
131
132
133def _capability(environment: Mapping[str, str]) -> blc.Capability:
134 """Parse every inherited capability field as one indivisible identity."""
135 try:
136 return blc.from_environment(dict(environment))
137 except blc.CapabilityError as exc:
138 raise BenchGuardError(str(exc)) from exc
139
140
141def guarded_argv(
142 request: GuardRequest,
143 authenticate: LockAuthenticator = _live_lock_matches,
144) -> list[str]:
145 """Return a bench-lock wrapper argv, or empty once already guarded."""
146 if not needs_guard(request.host_class, request.plays, request.mode):
147 return []
148 lock_id = _lock_id(request.environment)
149 if lock_id:
150 capability = _capability(request.environment)
151 if not authenticate(request.repo_root, capability):
152 raise BenchGuardError(INHERITED_LOCK_ERROR)
153 return []
154 return [
155 "/bin/bash",
156 "-p",
157 str(request.repo_root / "scripts/hil/bench.sh"),
158 "run",
159 "--intent",
160 "Ansible bench-affecting converge",
161 "--for",
162 "2h",
163 "--wait",
164 "2h",
165 "--",
166 str(request.fleet_script),
167 *request.original_argv,
168 ]
169
170
171def ansible_extra(
172 host_class: str,
173 plays: Sequence[str],
174 mode: str,
175 environment: Mapping[str, str],
176) -> list[str]:
177 """Bind the validated outer hold to the remote role transaction."""
178 if not needs_guard(host_class, plays, mode):
179 return []
180 lock_id = _lock_id(environment)
181 if not lock_id:
182 message = "mutating bench converge is outside the bench lock"
183 raise BenchGuardError(message)
184 capability = _capability(environment)
185 values = {
186 HOLDER_PID_VAR: capability.holder_pid,
187 HOLDER_START_VAR: capability.holder_start_ticks,
188 HOLDER_TARGET_VAR: capability.target,
189 MAINTENANCE_VAR: lock_id,
190 }
191 return ["-e", json.dumps(values, sort_keys=True)]
192
193
194def control_flow_refusal(request: FlowRequest) -> str:
195 """Reject user-controlled Ansible flow/variables before the outer lock."""
196 if not needs_guard(request.host_class, request.plays, request.mode):
197 return ""
198 if request.tags and not request.trusted_tags:
199 return "bench-affecting applies do not accept --tags"
200 if request.extra_vars:
201 return "bench-affecting applies do not accept raw --extra-var"
202 return ""
203
204
205def _control_selftest() -> list[str]:
206 """Prove bench-affecting selectors are rejected before any wrapper."""
207 failures = []
208 for tags, extra_vars, trusted, label in (
209 ("hil-runner", (), False, "user tag selector"),
210 (
211 "",
212 ("dev_box_hil_runner_service=harmless.service",),
213 False,
214 "service override",
215 ),
216 ("", ("dev_box_hil_runner_bench_alias=other",), False, "bench override"),
217 ("", ("hil_bench_lock_dir=/tmp/fake",), False, "lock-path override"),
218 ):
219 request = FlowRequest("dev_box", ["dev-box"], "apply", tags, extra_vars, trusted)
220 if not control_flow_refusal(request):
221 failures.append(f"{label} was accepted before the bench wrapper")
222 trusted = FlowRequest("dev_box", ["dev-box"], "apply", "hil-runner", (), trusted_tags=True)
223 if control_flow_refusal(trusted):
224 failures.append("typed register-hil tag was refused")
225 return failures
226
227
228def parser_selftest(factory: Callable[[], argparse.ArgumentParser]) -> list[str]:
229 """Prove skip/start selectors are not public fleet arguments."""
230 failures: list[str] = []
231 for option in ("--skip-tags", "--start-at-task"):
232 with contextlib.redirect_stderr(io.StringIO()):
233 try:
234 factory().parse_args(["apply", "star", option, "anything"])
235 except SystemExit as exc:
236 if exc.code == ARGPARSE_USAGE_ERROR:
237 continue
238 failures.append(f"bench-affecting selector {option} was accepted")
239 return failures
240
241
242def _privileged_bash_selftest() -> list[str]:
243 """Execute the authentication shell prefix and require privileged mode."""
244 probe = 'case "$-" in *p*) printf "%s\\n" "$-" ;; *) exit 41 ;; esac'
245 try:
246 result = subprocess.run( # noqa: S603 -- exact Bash argv is the subject under test
247 [
248 "/bin/bash",
249 "--noprofile",
250 "--norc",
251 "-p",
252 "-c",
253 probe,
254 ],
255 env={"HOME": "/", "PATH": "/usr/bin:/bin"},
256 check=False,
257 capture_output=True,
258 text=True,
259 timeout=5,
260 )
261 except (OSError, subprocess.TimeoutExpired) as exc:
262 return [f"privileged Bash execution probe failed: {exc}"]
263 if result.returncode != 0 or "p" not in result.stdout.strip():
264 return ["live-lock Bash argv did not enter privileged mode"]
265 return []
266
267
268def run_selftest() -> list[str]:
269 """Exercise both sides of the lock decision without contacting a host."""
270 root = Path("/repo")
271 script = root / "scripts/dev/fleet.py"
272 bench = GuardRequest(root, script, ["apply", "star"], "hil_bench", ["hil-bench"], "apply", {})
273 wrapped = guarded_argv(bench)
274 failures = _privileged_bash_selftest()
275 expected_wrapper = ["/bin/bash", "-p", str(root / "scripts/hil/bench.sh"), "run"]
276 if wrapped[:4] != expected_wrapper:
277 failures.append("unguarded bench apply did not use the fixed privileged Bash wrapper")
278 if wrapped[-3:] != [str(script), "apply", "star"]:
279 failures.append("wrapper did not preserve the exact fleet argv")
280 held = {
281 "RA8_BENCH_HOLDER_PID": "41",
282 "RA8_BENCH_HOLDER_START_TICKS": "1000",
283 "RA8_BENCH_HOLDER_TARGET": "star.local",
284 "RA8_BENCH_LOCK_ID": "0123456789abcdef",
285 }
286 if guarded_argv(replace(bench, environment=held), lambda _root, _cap: True):
287 failures.append("already-held bench apply recursed into a second lock")
288 try:
289 guarded_argv(replace(bench, environment=held), lambda _root, _cap: False)
290 except BenchGuardError:
291 pass
292 else:
293 failures.append("well-formed forged or stale lock identity bypassed the wrapper")
294 expected = [
295 "-e",
296 '{"hil_bench_maintenance_holder_pid": 41, '
297 '"hil_bench_maintenance_holder_start_ticks": 1000, '
298 '"hil_bench_maintenance_holder_target": "star.local", '
299 '"hil_bench_maintenance_lock_id": "0123456789abcdef"}',
300 ]
301 if ansible_extra("hil_bench", ["hil-bench"], "apply", held) != expected:
302 failures.append("live hold identity did not bind to Ansible")
303 if guarded_argv(replace(bench, original_argv=["check", "star"], mode="check")):
304 failures.append("read-only check incorrectly required a live bench hold")
305 dev = GuardRequest(root, script, ["apply", "dev"], "dev_box", ["dev-box"], "apply", {})
306 if not guarded_argv(dev):
307 failures.append("delegated dev-box bench mutation was not wrapped")
308 try:
309 ansible_extra("hil_bench", ["hil-bench"], "apply", {"RA8_BENCH_LOCK_ID": "bad"})
310 except BenchGuardError:
311 pass
312 else:
313 failures.append("malformed inherited lock identity was accepted")
314 failures.extend(_control_selftest())
315 return failures