3"""Native HIL listener mapping and validation for the fleet declaration.
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.
11from __future__
import annotations
16from pathlib
import Path
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",
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"
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",
50def board_policy(interface: dict[str, Any]) -> dict[str, Any]:
51 """Return the canonical installed helper policy declaration."""
53 "board_iface": interface[
"name"],
54 "mac": interface[
"mac"],
55 "phc_index": interface[
"phc_index"],
56 "sysfs_device": interface[
"sysfs_device"],
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()
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.
71 data: Complete fleet declaration, including the referenced bench host.
72 host: Candidate dev-box host.
75 Fleet-owned ``dev_box_hil_runner_*`` values, empty for a host without
76 a native HIL listener.
78 if host.get(
"class") ==
"hil_bench":
79 interface = host[
"board_interface"]
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"
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"],
92 declared = host.get(
"hil_runner")
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)
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"
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"
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),
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"]
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")
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
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")
154def _check_identity(name: str, declared: dict[str, Any]) -> list[str]:
155 """Validate a native listener's registration and workflow identity."""
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()
161 labels = declared.get(
"labels")
163 not isinstance(labels, list)
165 or any(
not isinstance(label, str)
or not label.strip()
for label
in labels)
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:
172 f
"{name}: hil_runner.labels declares the implicit 'self-hosted' label; "
173 "list only the listener's custom labels"
176 workflow = declared.get(
"workflow")
177 if isinstance(workflow, str)
and workflow.strip():
178 workflow_path = Path(workflow)
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"}
186 f
"{name}: hil_runner.workflow '{workflow}' is not a repository-relative "
187 ".github/workflows/*.yml path"
190 repository = declared.get(
"repository")
192 isinstance(repository, str)
193 and repository.strip()
195 not repository.startswith(
"https://github.com/")
196 or any(ch.isspace()
for ch
in repository)
199 bad.append(f
"{name}: hil_runner.repository must be an https://github.com owner/repo URL")
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"]
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":
216 f
"{name}: hil_runner.bench.host '{bench_name}' has class "
217 f
"{hosts[bench_name].get('class')}, not hil_bench"
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
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")
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.
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.
238 One message per invalid listener field or bench relationship.
240 if host[
"class"] ==
"hil_bench":
241 return _check_board_interface(name, host)
242 declared = host.get(
"hil_runner")
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)
252def check_uniqueness(hosts: dict[str, Any]) -> list[str]:
253 """Reject duplicate native listener registrations or workflow ownership.
256 hosts: Complete fleet host mapping.
259 One message per duplicated registration name or workflow path.
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):
268 value = declared[key]
271 f
"{host_name}: hil_runner.{key} '{value}' is already owned by {owners[value]}"
274 owners[value] = host_name