3"""Validate fresh-listener dry-run and post-apply service boundaries."""
5from __future__
import annotations
7LISTENER_ACTIVE_RETRIES = 15
8LISTENER_ACTIVE_DELAY_S = 2
9ASSERT_TASK_KEYS = frozenset({
"name",
"ansible.builtin.assert"})
10ASSERT_ARGUMENT_KEYS = frozenset({
"that",
"fail_msg"})
11STAT_TASK_KEYS = frozenset({
"name",
"become",
"ansible.builtin.stat",
"register",
"changed_when"})
14class CheckModeFixtureError(ValueError):
15 """A governed listener task is missing or duplicated."""
18def _named(tasks: list[dict[str, object]], name: str) -> tuple[int, dict[str, object]]:
19 """Return one uniquely named top-level task."""
20 matches = [(index, task)
for index, task
in enumerate(tasks)
if task.get(
"name") == name]
22 message = f
"task {name!r} is missing or duplicated"
23 raise CheckModeFixtureError(message)
27def _normalized(value: object) -> str:
28 """Collapse presentation whitespace without weakening expression bytes."""
29 return " ".join(str(value).split())
32def _conditions(task: dict[str, object]) -> tuple[str, ...]:
33 """Return normalized assert conditions, or an empty tuple."""
34 assertion = task.get(
"ansible.builtin.assert")
35 values = assertion.get(
"that")
if isinstance(assertion, dict)
else None
36 if not isinstance(values, list)
or any(
not isinstance(value, str)
for value
in values):
38 return tuple(_normalized(value)
for value
in values)
41def _exact_assert(task: dict[str, object], expected: tuple[str, ...]) -> bool:
42 """Return whether one fail-closed assertion has only its governed controls."""
43 assertion = task.get(
"ansible.builtin.assert")
45 set(task) == ASSERT_TASK_KEYS
46 and isinstance(assertion, dict)
47 and set(assertion) == ASSERT_ARGUMENT_KEYS
48 and isinstance(assertion.get(
"fail_msg"), str)
49 and _conditions(task) == expected
53def _exact_stat(task: dict[str, object], path: str, result: str) -> bool:
54 """Return whether one identity stat is exact and does not follow links."""
55 stat = task.get(
"ansible.builtin.stat")
57 set(task) == STAT_TASK_KEYS
58 and task.get(
"become")
is True
59 and isinstance(stat, dict)
60 and stat == {
"path": path,
"follow":
False}
61 and task.get(
"register") == result
62 and task.get(
"changed_when")
is False
66def _registration_errors(
67 registration: dict[str, object], identity: dict[str, object], token: dict[str, object]
69 """Require an exact no-follow registration marker and token decision."""
72 "not dev_box_hil_runner_registration.stat.exists or "
73 "dev_box_hil_runner_registration.stat.isreg"
76 "not dev_box_hil_runner_registration.stat.exists or "
77 "not dev_box_hil_runner_registration.stat.islnk"
80 token_expected = _normalized(
81 "dev_box_hil_runner_registration.stat.exists or "
82 "(dev_box_hil_runner_registration_token | default('') | length > 0)"
87 "{{ dev_box_hil_runner_root }}/.runner",
88 "dev_box_hil_runner_registration",
90 or not _exact_assert(identity, identity_expected)
91 or not _exact_assert(token, (token_expected,))
93 return [
"hil_runner.yml: first-registration identity/token preflight is not exact"]
97def _public_key_errors(key_stat: dict[str, object], identity: dict[str, object]) -> list[str]:
98 """Require an exact real, regular, no-follow public-key decision."""
101 "not dev_box_hil_runner_public_key_stat.stat.exists or "
102 "dev_box_hil_runner_public_key_stat.stat.isreg"
105 "not dev_box_hil_runner_public_key_stat.stat.exists or "
106 "not dev_box_hil_runner_public_key_stat.stat.islnk"
111 "{{ dev_box_hil_runner_home }}/.ssh/id_ed25519.pub",
112 "dev_box_hil_runner_public_key_stat",
113 )
or not _exact_assert(identity, identity_expected):
114 return [
"hil_runner.yml: fresh-account public-key identity preflight is not exact"]
118def _account_errors(user: dict[str, object], home: dict[str, object]) -> list[str]:
119 """Require ordinary check-mode planning for account and home creation."""
121 "name":
"{{ dev_box_hil_runner_user }}",
122 "group":
"{{ dev_box_hil_runner_group }}",
123 "home":
"{{ dev_box_hil_runner_home }}",
124 "shell":
"/usr/sbin/nologin",
127 "generate_ssh_key":
True,
128 "ssh_key_type":
"ed25519",
129 "ssh_key_file":
".ssh/id_ed25519",
130 "ssh_key_comment":
"ra8-hil@dev",
133 "path":
"{{ dev_box_hil_runner_home }}",
134 "state":
"directory",
135 "owner":
"{{ dev_box_hil_runner_user }}",
136 "group":
"{{ dev_box_hil_runner_group }}",
140 set(user) != {
"name",
"become",
"ansible.builtin.user"}
141 or user.get(
"become")
is not True
142 or user.get(
"ansible.builtin.user") != user_expected
143 or set(home) != {
"name",
"become",
"ansible.builtin.file"}
144 or home.get(
"become")
is not True
145 or home.get(
"ansible.builtin.file") != home_expected
147 return [
"hil_runner.yml: fresh-account user/home planning is not exact"]
151def _fresh_account_errors(tasks: list[dict[str, object]]) -> list[str]:
152 """Require token preflight, then stop before a planned key is consumed."""
154 "Require the fleet-derived native HIL declaration",
155 "Check whether this runner is already registered",
156 "Refuse a linked or non-regular runner registration identity",
157 "Require a short-lived token only for first registration",
158 "Install the official runner's Debian runtime dependencies",
159 "Inspect the dedicated HIL public key before account planning",
160 "Refuse a linked or non-regular dedicated HIL public key",
161 "Create the isolated HIL runner account and its dedicated SSH key",
162 "Keep the isolated runner home private",
163 "Explain the fresh-account check-mode boundary",
164 "End a fresh-account check before consuming the planned public key",
165 "Read the dedicated HIL public key",
167 found = [_named(tasks, name)
for name
in names]
168 indices = [index
for index, _
in found]
169 declaration, registration, registration_id, token, apt = found[:5]
170 key_stat, key_id, user, home, explain, end, slurp = found[5:]
171 boundary_when = [
"ansible_check_mode",
"not dev_box_hil_runner_public_key_stat.stat.exists"]
173 if registration[0] != declaration[0] + 1
or token[0] >= apt[0]:
174 errors.append(
"hil_runner.yml: registration/token preflight follows a mutator")
175 if indices != sorted(indices)
or indices[6:11] != list(range(indices[5] + 1, indices[5] + 6)):
176 errors.append(
"hil_runner.yml: fresh-account token/key boundary order is not exact")
177 errors.extend(_registration_errors(registration[1], registration_id[1], token[1]))
178 errors.extend(_public_key_errors(key_stat[1], key_id[1]))
179 errors.extend(_account_errors(user[1], home[1]))
180 if explain[1].get(
"when") != boundary_when
or not isinstance(
181 explain[1].get(
"ansible.builtin.debug"), dict
183 errors.append(
"hil_runner.yml: fresh-account boundary explanation is not exact")
184 if end[1].get(
"when") != boundary_when
or end[1].get(
"ansible.builtin.meta") !=
"end_host":
185 errors.append(
"hil_runner.yml: fresh-account check does not end before key consumption")
186 if end[0] >= slurp[0]:
187 errors.append(
"hil_runner.yml: fresh-account boundary follows its key consumer")
191def _fresh_package_errors(tasks: list[dict[str, object]]) -> list[str]:
192 """Require a fresh-package dry run to stop before package byte consumers."""
194 "Decide whether the pinned runner package must be installed",
195 "Explain the fresh-runner check-mode boundary",
196 "End a fresh-runner check before consuming planned package bytes",
197 "Install the official service launcher beside the runner",
198 "Read back the installed runner version",
200 found = [_named(tasks, name)
for name
in names]
201 decide, explain, end, launcher, version = found
202 boundary_when = [
"ansible_check_mode",
"dev_box_hil_runner_install_needed | bool"]
204 if explain[0] != decide[0] + 1
or end[0] != explain[0] + 1:
205 errors.append(
"hil_runner.yml: fresh-runner package boundary order is not exact")
206 if end[0] >= launcher[0]
or end[0] >= version[0]:
207 errors.append(
"hil_runner.yml: fresh-runner package boundary follows a byte consumer")
208 if explain[1].get(
"when") != boundary_when
or not isinstance(
209 explain[1].get(
"ansible.builtin.debug"), dict
211 errors.append(
"hil_runner.yml: fresh-runner boundary explanation is not exact")
212 if end[1].get(
"when") != boundary_when
or end[1].get(
"ansible.builtin.meta") !=
"end_host":
213 errors.append(
"hil_runner.yml: fresh-runner check does not end before byte consumers")
217def _listener_errors(tasks: list[dict[str, object]]) -> list[str]:
218 """Require planned start in check mode and live acceptance only after apply."""
219 start_at, start = _named(tasks,
"Enable and start the dedicated HIL listener")
220 verify_at, verify = _named(tasks,
"Verify the dedicated HIL listener is active")
221 service = start.get(
"ansible.builtin.systemd_service")
223 "name":
"{{ dev_box_hil_runner_service }}",
226 "daemon_reload":
True,
230 set(start) != {
"name",
"become",
"ansible.builtin.systemd_service"}
231 or start.get(
"become")
is not True
232 or service != start_expected
234 errors.append(
"hil_runner.yml: listener start check-mode planning is not exact")
235 command = verify.get(
"ansible.builtin.command")
236 argv = command.get(
"argv")
if isinstance(command, dict)
else None
241 "ansible.builtin.command",
250 verify_at != start_at + 1
251 or set(verify) != allowed
252 or verify.get(
"when") !=
"not ansible_check_mode"
253 or verify.get(
"become")
is not True
254 or verify.get(
"register") !=
"dev_box_hil_runner_active"
255 or verify.get(
"changed_when")
is not False
256 or verify.get(
"check_mode")
is not False
257 or verify.get(
"retries") != LISTENER_ACTIVE_RETRIES
258 or verify.get(
"delay") != LISTENER_ACTIVE_DELAY_S
259 or _normalized(verify.get(
"until"))
260 != _normalized(
"dev_box_hil_runner_active.stdout | trim == 'active'")
261 or argv != [
"systemctl",
"is-active",
"{{ dev_box_hil_runner_service }}"]
263 errors.append(
"hil_runner.yml: post-apply listener liveness proof is not exact")
267def errors(tasks: list[dict[str, object]]) -> list[str]:
268 """Return every fresh-listener check-mode boundary defect."""
270 return _fresh_account_errors(tasks) + _fresh_package_errors(tasks) + _listener_errors(tasks)
271 except CheckModeFixtureError
as error:
272 return [f
"hil_runner.yml: {error}"]