ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
fleet_hil.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Native HIL listener mapping and validation for the fleet declaration.
4
5The native listener belongs to a ``dev_box`` but is not scalable runner
6capacity. Keeping its registration identity and remote-bench relationship in
7this focused module prevents that distinction from bloating the fleet's
8capacity-arithmetic model while preserving one declarative front door.
9"""
10
11from __future__ import annotations
12
13import hashlib
14import json
15import re
16from pathlib import Path
17from typing import Any
18
19DEV_RUNNER_AUTHORITY = {
20 "dev_box_hil_runner_bench_home": "/var/lib/ra8-hil-client",
21 "dev_box_hil_runner_bench_user": "ra8-hil",
22 "dev_box_hil_runner_env_file": "/etc/ra8/hil-runner.env",
23 "dev_box_hil_runner_group": "ra8-hil",
24 "dev_box_hil_runner_home": "/var/lib/ra8-hil-runner",
25 "dev_box_hil_runner_root": "/opt/ra8-hil-runner",
26 "dev_box_hil_runner_service": "ra8-hil-runner.service",
27 "dev_box_hil_runner_sha256": "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d",
28 "dev_box_hil_runner_user": "ra8-hil",
29 "dev_box_hil_runner_version": "2.336.0",
30 "dev_box_hil_runner_work_dir": "_work",
31}
32BENCH_AUTHORITY = {
33 "hil_bench_arm_gcc_dumpversion": "13.3.1",
34 "hil_bench_arm_gcc_prefix": "/opt/arm-gnu-toolchain-13.3",
35 "hil_bench_arm_gcc_release": "13.3.rel1",
36 "hil_bench_arm_gcc_sha256_aarch64": (
37 "c8824bffd057afce2259f7618254e840715f33523a3d4e4294f471208f976764"
38 ),
39 "hil_bench_jlink_speed": 1000,
40 "hil_bench_lock_dir": "/var/lib/ra8-bench",
41 "hil_bench_python_context": "/opt/ra8-hil-python-context",
42 "hil_bench_python_marker": "/opt/ra8-hil-python/.ra8-lock-sha256",
43 "hil_bench_python_venv": "/opt/ra8-hil-python",
44 "hil_bench_ref": "dev",
45 "hil_bench_repo_url": "git@github.com:bsikar/ra8-firmware.git",
46 "hil_bench_uv_cache": "/opt/ra8-uv-cache",
47}
48
49
50def board_policy(interface: dict[str, Any]) -> dict[str, Any]:
51 """Return the canonical installed helper policy declaration."""
52 return {
53 "board_iface": interface["name"],
54 "mac": interface["mac"],
55 "phc_index": interface["phc_index"],
56 "sysfs_device": interface["sysfs_device"],
57 "version": 1,
58 }
59
60
61def policy_digest(interface: dict[str, Any]) -> str:
62 """Hash the canonical fleet-owned helper policy."""
63 payload = json.dumps(board_policy(interface), sort_keys=True, separators=(",", ":")) + "\n"
64 return hashlib.sha256(payload.encode()).hexdigest()
65
66
67def runner_vars(data: dict[str, Any], host: dict[str, Any]) -> dict[str, Any]:
68 """Map one declared native HIL listener onto the ``dev_box`` role.
69
70 Args:
71 data: Complete fleet declaration, including the referenced bench host.
72 host: Candidate dev-box host.
73
74 Returns:
75 Fleet-owned ``dev_box_hil_runner_*`` values, empty for a host without
76 a native HIL listener.
77 """
78 if host.get("class") == "hil_bench":
79 interface = host["board_interface"]
80 return {
81 **BENCH_AUTHORITY,
82 "hil_bench_arm_gcc_url": (
83 "https://developer.arm.com/-/media/Files/downloads/gnu/13.3.rel1/binrel/"
84 "arm-gnu-toolchain-13.3.rel1-aarch64-arm-none-eabi.tar.xz"
85 ),
86 "hil_bench_repo_dir": f"/home/{host['connect']['user']}/ra8-firmware",
87 "hil_bench_eth_iface": interface["name"],
88 "hil_bench_eth_mac": interface["mac"],
89 "hil_bench_eth_sysfs_device": interface["sysfs_device"],
90 "hil_bench_eth_phc_index": interface["phc_index"],
91 }
92 declared = host.get("hil_runner")
93 if not declared:
94 return {}
95 bench = declared["bench"]
96 bench_name = str(bench["host"])
97 bench_host = data["hosts"][bench_name]
98 bench_address = str(bench_host["connect"]["address"])
99 interface = bench_host["board_interface"]
100 bench_names = [bench_name, *[str(alias) for alias in bench.get("aliases", [])]]
101 if bench_address not in bench_names:
102 bench_names.append(bench_address)
103 return {
104 **DEV_RUNNER_AUTHORITY,
105 "dev_box_hil_runner_install_stamp": "2.336.0-isolated-v1",
106 "dev_box_hil_runner_url": (
107 "https://github.com/actions/runner/releases/download/v2.336.0/"
108 "actions-runner-linux-x64-2.336.0.tar.gz"
109 ),
110 "dev_box_hil_runner_name": declared["name"],
111 "dev_box_hil_runner_repo_url": declared["repository"],
112 "dev_box_hil_runner_labels": ",".join(declared["labels"]),
113 "dev_box_hil_runner_bench_alias": bench_name,
114 "dev_box_hil_runner_bench_address": bench_address,
115 "dev_box_hil_runner_bench_repo_dir": (
116 f"/home/{bench_host['connect']['user']}/ra8-firmware"
117 ),
118 "dev_box_hil_runner_bench_names": bench_names,
119 "dev_box_hil_runner_bench_iface": interface["name"],
120 "dev_box_hil_runner_bench_mac": interface["mac"],
121 "dev_box_hil_runner_bench_sysfs_device": interface["sysfs_device"],
122 "dev_box_hil_runner_bench_phc_index": interface["phc_index"],
123 "dev_box_hil_runner_bench_policy_sha256": policy_digest(interface),
124 }
125
126
127def _check_board_interface(name: str, host: dict[str, Any]) -> list[str]:
128 """Validate one permanent board-interface identity."""
129 interface = host.get("board_interface")
130 keys = {"name", "mac", "sysfs_device", "phc_index"}
131 if not isinstance(interface, dict) or set(interface) != keys:
132 return [f"{name}: hil_bench board_interface must contain exactly {sorted(keys)}"]
133 iface = interface["name"]
134 mac = interface["mac"]
135 device = interface["sysfs_device"]
136 phc = interface["phc_index"]
137 problems = []
138 if not isinstance(iface, str) or re.fullmatch(r"[A-Za-z][A-Za-z0-9_-]{0,14}", iface) is None:
139 problems.append(f"{name}: board_interface.name is not a physical interface spelling")
140 if not isinstance(mac, str) or re.fullmatch(r"[0-9a-f]{2}(?::[0-9a-f]{2}){5}", mac) is None:
141 problems.append(f"{name}: board_interface.mac is not canonical lowercase Ethernet")
142 if (
143 not isinstance(device, str)
144 or not device.startswith("/sys/devices/")
145 or ".." in Path(device).parts
146 or re.fullmatch(r"/[A-Za-z0-9_.:/-]+", device) is None
147 ):
148 problems.append(f"{name}: board_interface.sysfs_device is not canonical /sys/devices")
149 if type(phc) is not int or phc < 0:
150 problems.append(f"{name}: board_interface.phc_index must be a non-negative integer")
151 return problems
152
153
154def _check_identity(name: str, declared: dict[str, Any]) -> list[str]:
155 """Validate a native listener's registration and workflow identity."""
156 bad = [
157 f"{name}: hil_runner.{key} must be a non-empty string"
158 for key in ("name", "repository", "workflow")
159 if not isinstance(declared.get(key), str) or not declared[key].strip()
160 ]
161 labels = declared.get("labels")
162 if (
163 not isinstance(labels, list)
164 or not labels
165 or any(not isinstance(label, str) or not label.strip() for label in labels)
166 ):
167 bad.append(f"{name}: hil_runner.labels must be a non-empty list of strings")
168 elif len(labels) != len(set(labels)):
169 bad.append(f"{name}: hil_runner.labels contains a duplicate")
170 elif "self-hosted" in labels:
171 bad.append(
172 f"{name}: hil_runner.labels declares the implicit 'self-hosted' label; "
173 "list only the listener's custom labels"
174 )
175
176 workflow = declared.get("workflow")
177 if isinstance(workflow, str) and workflow.strip():
178 workflow_path = Path(workflow)
179 if (
180 workflow_path.is_absolute()
181 or ".." in workflow_path.parts
182 or workflow_path.parts[:2] != (".github", "workflows")
183 or workflow_path.suffix not in {".yml", ".yaml"}
184 ):
185 bad.append(
186 f"{name}: hil_runner.workflow '{workflow}' is not a repository-relative "
187 ".github/workflows/*.yml path"
188 )
189
190 repository = declared.get("repository")
191 if (
192 isinstance(repository, str)
193 and repository.strip()
194 and (
195 not repository.startswith("https://github.com/")
196 or any(ch.isspace() for ch in repository)
197 )
198 ):
199 bad.append(f"{name}: hil_runner.repository must be an https://github.com owner/repo URL")
200 return bad
201
202
203def _check_bench(name: str, bench: object, hosts: dict[str, Any]) -> list[str]:
204 """Validate the declared relationship to one instrument host."""
205 if not isinstance(bench, dict):
206 return [f"{name}: hil_runner.bench must be a mapping"]
207
208 bad = []
209 bench_name = bench.get("host")
210 if not isinstance(bench_name, str) or not bench_name:
211 bad.append(f"{name}: hil_runner.bench.host must name a declared hil_bench host")
212 elif bench_name not in hosts:
213 bad.append(f"{name}: hil_runner.bench.host '{bench_name}' is not declared")
214 elif hosts[bench_name].get("class") != "hil_bench":
215 bad.append(
216 f"{name}: hil_runner.bench.host '{bench_name}' has class "
217 f"{hosts[bench_name].get('class')}, not hil_bench"
218 )
219 aliases = bench.get("aliases", [])
220 if not isinstance(aliases, list) or any(
221 not isinstance(alias, str) or not alias.strip() for alias in aliases
222 ):
223 bad.append(f"{name}: hil_runner.bench.aliases must be a list of non-empty strings")
224 elif len(aliases) != len(set(aliases)):
225 bad.append(f"{name}: hil_runner.bench.aliases contains a duplicate")
226 return bad
227
228
229def check_runner(name: str, host: dict[str, Any], hosts: dict[str, Any]) -> list[str]:
230 """Validate one optional native HIL listener and its bench relation.
231
232 Args:
233 name: Fleet host name carrying the declaration.
234 host: That host's declaration.
235 hosts: Complete host mapping used to resolve the bench reference.
236
237 Returns:
238 One message per invalid listener field or bench relationship.
239 """
240 if host["class"] == "hil_bench":
241 return _check_board_interface(name, host)
242 declared = host.get("hil_runner")
243 if declared is None:
244 return []
245 if host["class"] != "dev_box":
246 return [f"{name}: hil_runner is supported only on class dev_box"]
247 if not isinstance(declared, dict):
248 return [f"{name}: hil_runner must be a mapping"]
249 return _check_identity(name, declared) + _check_bench(name, declared.get("bench"), hosts)
250
251
252def check_uniqueness(hosts: dict[str, Any]) -> list[str]:
253 """Reject duplicate native listener registrations or workflow ownership.
254
255 Args:
256 hosts: Complete fleet host mapping.
257
258 Returns:
259 One message per duplicated registration name or workflow path.
260 """
261 problems = []
262 for key in ("name", "workflow"):
263 owners: dict[str, str] = {}
264 for host_name, host in hosts.items():
265 declared = host.get("hil_runner")
266 if not isinstance(declared, dict) or not isinstance(declared.get(key), str):
267 continue
268 value = declared[key]
269 if value in owners:
270 problems.append(
271 f"{host_name}: hil_runner.{key} '{value}' is already owned by {owners[value]}"
272 )
273 else:
274 owners[value] = host_name
275 return problems