ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
fleet_reconcile.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Continuously converge ordinary CI runner hosts from one trusted snapshot."""
5
6from __future__ import annotations
7
8import argparse
9import json
10import os
11import re
12import signal
13import stat
14import sys
15import tempfile
16import time
17from collections.abc import Callable, Sequence
18from dataclasses import dataclass
19from pathlib import Path
20from threading import Thread
21from typing import Any, TextIO
22
23sys.path.insert(0, str(Path(__file__).resolve().parent))
24
25import fleet_model as fm
26import fleet_mutation_lock as fml
27import fleet_reconcile_arc_selftest as fras
28import fleet_reconcile_process as frp
29import fleet_reconcile_selftest as frs
30import fleet_wsl as fw
31
32SOURCE_DIGEST_FILE = ".ra8-source-sha256"
33STATE_FILE = "state.json"
34DEFAULT_FULL_INTERVAL = 7 * 24 * 60 * 60
35DEFAULT_PRODUCER_INTERVAL = 24 * 60 * 60
36PRIVATE_DIRECTORY_MODE = 0o700
37SELFTEST_CHANGED_TOTAL = 3
38SELFTEST_SIGNAL_BOUND = 8
39# ci_runner check mode empties and restages its build context: exactly these
40# two tasks report changed on an otherwise-converged producer.
41PRODUCER_CHECK_NOISE = 2
42# While ARC admission is deliberately held at zero, its post-renderer removes
43# the scale-set difference and only the context-restage check noise remains.
44PRODUCER_HELD_CHECK_NOISE = 1
45SHA256_RE = re.compile(r"[0-9a-f]{64}")
46ANSI_RE = re.compile(r"\x1b\‍[[0-9;]*m")
47
48
49RECAP_RE = re.compile(
50 r"^\s*([A-Za-z0-9_.-]+)\s+:\s+ok=(\d+)\s+changed=(\d+)\s+"
51 r"unreachable=(\d+)\s+failed=(\d+)\s+skipped=(\d+)\s+"
52 r"rescued=(\d+)\s+ignored=(\d+)\s*$"
53)
54
55
56@dataclass(frozen=True)
57class ReconcileOptions:
58 """Runtime policy for one fleet reconciliation."""
59
60 mode: str
61 force: bool
62 source_digest: str
63 state_dir: Path
64 full_interval: int
65 producer_interval: int
66 now: int
67
68
69CommandRunner = Callable[[Sequence[str]], frp.CommandResult]
70
71
72def recap_identity(data: dict[str, Any], host: str) -> str:
73 """Return Ansible's recap name without changing the fleet control identity."""
74 transport = fm.CLASSES[data["hosts"][host]["class"]].transport
75 return "localhost" if transport == "wsl" else host
76
77
78def runner_hosts(data: dict[str, Any]) -> list[str]:
79 """Return capacity-managed hosts in producer-before-consumer order."""
80 names = [name for name, host in data["hosts"].items() if host.get("runners")]
81 producer = str(data["runner_image"]["source_host"])
82 if producer not in names:
83 msg = "runner_image.source_host is not a capacity-managed host"
84 raise ValueError(msg)
85 return [producer, *[name for name in names if name != producer]]
86
87
88def parse_changed(output: str, host: str, expected_plays: int) -> int:
89 """Return changed tasks from exact successful Ansible recap rows."""
90 rows: list[tuple[int, int, int]] = []
91 for raw in output.splitlines():
92 match = RECAP_RE.fullmatch(ANSI_RE.sub("", raw))
93 if match is None or match.group(1) != host:
94 continue
95 changed = int(match.group(3))
96 unreachable = int(match.group(4))
97 failed = int(match.group(5))
98 rows.append((changed, unreachable, failed))
99 if len(rows) != expected_plays:
100 msg = f"{host}: expected {expected_plays} Ansible recap row(s), found {len(rows)}"
101 raise ValueError(msg)
102 if any(unreachable or failed for _, unreachable, failed in rows):
103 msg = f"{host}: Ansible recap reported a failed or unreachable play"
104 raise ValueError(msg)
105 return sum(changed for changed, _, _ in rows)
106
107
108def fleet_command(host: str, verb: str) -> list[str]:
109 """Build one command against the fleet entry point in this snapshot."""
110 if verb == "check":
111 arguments = ["check", host]
112 elif verb == "parked-check":
113 arguments = ["reconcile-parked-check", host]
114 elif verb == "activate":
115 arguments = ["reconcile-activate", host]
116 elif verb == "activation-check":
117 arguments = ["reconcile-activation-check", host]
118 elif verb == "parked-apply":
119 arguments = ["reconcile-parked-apply", host]
120 elif verb == "quarantine":
121 arguments = ["capacity-quarantine", host]
122 elif verb == "restore":
123 arguments = ["capacity-restore", host]
124 else:
125 msg = f"unsupported fleet reconcile verb: {verb}"
126 raise ValueError(msg)
127 return [sys.executable, str(fm.REPO_ROOT / "scripts/dev/fleet.py"), *arguments]
128
129
130def emit_result(result: frp.CommandResult, stream: TextIO = sys.stdout) -> None:
131 """Emit captured evidence without losing stderr attribution."""
132 stream.write(result.stdout)
133 sys.stderr.write(result.stderr)
134
135
136def load_state(path: Path) -> dict[str, Any]:
137 """Read prior receipts, refusing malformed state."""
138 if not path.exists():
139 return {"version": 1, "hosts": {}}
140 if path.is_symlink() or not path.is_file():
141 msg = f"state path is not a regular file: {path}"
142 raise ValueError(msg)
143 document = json.loads(path.read_text(encoding="ascii"))
144 if not isinstance(document, dict) or document.get("version") != 1:
145 msg = "fleet reconciliation state has an unsupported schema"
146 raise ValueError(msg)
147 hosts = document.get("hosts")
148 if not isinstance(hosts, dict):
149 msg = "fleet reconciliation state has no host receipt map"
150 raise TypeError(msg)
151 return document
152
153
154def save_state(path: Path, document: dict[str, Any]) -> None:
155 """Atomically publish reconciliation receipts."""
156 encoded = (json.dumps(document, indent=2, sort_keys=True) + "\n").encode("ascii")
157 fd, raw = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
158 temporary = Path(raw)
159 try:
160 os.fchmod(fd, 0o600)
161 with os.fdopen(fd, "wb") as stream:
162 stream.write(encoded)
163 stream.flush()
164 os.fsync(stream.fileno())
165 temporary.replace(path)
166 directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
167 try:
168 os.fsync(directory)
169 finally:
170 os.close(directory)
171 finally:
172 temporary.unlink(missing_ok=True)
173
174
175def full_apply_due(receipt: object, options: ReconcileOptions, interval: int) -> bool:
176 """Decide whether periodic full convergence is due for one host."""
177 if options.force or not isinstance(receipt, dict):
178 return True
179 if receipt.get("source_digest") != options.source_digest:
180 return True
181 applied = receipt.get("full_applied_at")
182 return not isinstance(applied, int) or options.now - applied >= interval
183
184
185def inspect_host(
186 data: dict[str, Any], host: str, run: CommandRunner, *, parked: bool = False
187) -> tuple[bool, int]:
188 """Run a read-only host check and return success plus drift count."""
189 verb = "parked-check" if parked else "check"
190 result = run(fleet_command(host, verb))
191 emit_result(result)
192 if result.status == fw.APPLY_REQUIRED_STATUS:
193 return True, 1
194 if result.status:
195 return False, 0
196 try:
197 changed = parse_changed(
198 result.stdout, recap_identity(data, host), len(data["hosts"][host]["provisions"])
199 )
200 except ValueError as error:
201 print(f"fleet-reconcile: {error}", file=sys.stderr)
202 return False, 0
203 return True, changed
204
205
206def inspect_activation_host(
207 data: dict[str, Any], host: str, run: CommandRunner
208) -> tuple[bool, int]:
209 """Check declared ARC authority while its rendered live ceiling stays zero."""
210 result = run(fleet_command(host, "activation-check"))
211 emit_result(result)
212 if result.status:
213 return False, 0
214 try:
215 changed = parse_changed(
216 result.stdout,
217 recap_identity(data, host),
218 len(data["hosts"][host]["provisions"]),
219 )
220 except ValueError as error:
221 print(f"fleet-reconcile: {error}", file=sys.stderr)
222 return False, 0
223 return True, changed
224
225
226def quarantine(host: str, run: CommandRunner) -> None:
227 """Drain a host after failed mutation so it cannot accept new work."""
228 print(f"fleet-reconcile: quarantining {host} at zero capacity", file=sys.stderr)
229 result = run(fleet_command(host, "quarantine"))
230 emit_result(result)
231 if result.status:
232 print(
233 f"fleet-reconcile: WARNING: could not quarantine {host} (rc={result.status})",
234 file=sys.stderr,
235 )
236
237
238def _activate_arc(
239 data: dict[str, Any], host: str, run: CommandRunner, expected_changes: int
240) -> tuple[bool, int]:
241 """Validate declared ARC authority at zero before the sole capacity opener."""
242 activation = run(fleet_command(host, "activate"))
243 emit_result(activation)
244 if activation.status or frp.interrupted_status():
245 quarantine(host, run)
246 return False, 0
247 clean, changed = inspect_activation_host(data, host, run)
248 if not clean or frp.interrupted_status() or changed != expected_changes:
249 quarantine(host, run)
250 return False, changed
251 restore = run(fleet_command(host, "restore"))
252 emit_result(restore)
253 if restore.status or frp.interrupted_status():
254 quarantine(host, run)
255 return False, 0
256 return True, 0
257
258
259def apply_host(
260 data: dict[str, Any], host: str, run: CommandRunner, *, expected_check_changes: int
261) -> tuple[bool, int]:
262 """Apply one host and prove the resulting declaration is idempotent."""
263 result = run(fleet_command(host, "parked-apply"))
264 emit_result(result)
265 if result.status or frp.interrupted_status():
266 quarantine(host, run)
267 return False, 0
268 clean, changed = inspect_host(data, host, run, parked=True)
269 if not clean or changed != expected_check_changes or frp.interrupted_status():
270 print(
271 f"fleet-reconcile: {host} did not reach an idempotent parked state "
272 f"(remaining changed={changed})",
273 file=sys.stderr,
274 )
275 quarantine(host, run)
276 return False, changed
277 host_class = fm.CLASSES[data["hosts"][host]["class"]]
278 if host_class.capacity_kind == "k8s":
279 return _activate_arc(data, host, run, expected_check_changes)
280 restore = run(fleet_command(host, "restore"))
281 emit_result(restore)
282 if restore.status or frp.interrupted_status():
283 quarantine(host, run)
284 return False, 0
285 clean, changed = inspect_host(data, host, run)
286 if not clean or changed != expected_check_changes or frp.interrupted_status():
287 quarantine(host, run)
288 return False, changed
289 return True, 0
290
291
292def reconcile_host(
293 data: dict[str, Any],
294 host: str,
295 receipt: object,
296 options: ReconcileOptions,
297 run: CommandRunner,
298) -> tuple[bool, dict[str, Any]]:
299 """Inspect and optionally converge one normal runner host."""
300 clean, changed = inspect_host(data, host, run)
301 if not clean or frp.interrupted_status():
302 return False, {}
303 producer = host == data["runner_image"]["source_host"]
304 interval = options.producer_interval if producer else options.full_interval
305 due = full_apply_due(receipt, options, interval)
306 expected_changes = PRODUCER_CHECK_NOISE if producer else 0
307 actionable_changes = changed != expected_changes
308 if options.mode == "check":
309 state = "CHECK-NOISE" if producer and not actionable_changes else "CURRENT"
310 if actionable_changes:
311 state = "DRIFT"
312 print(f"fleet-reconcile: {host}: {state} (changed={changed})")
313 return not actionable_changes, {}
314 if not actionable_changes and not due:
315 print(f"fleet-reconcile: {host}: current; no full converge due")
316 previous = receipt if isinstance(receipt, dict) else {}
317 return True, {**previous, "checked_at": options.now}
318 why = "drift" if actionable_changes else "periodic full verification"
319 print(f"fleet-reconcile: {host}: applying ({why}, changed={changed})")
320 held_arc = producer and fm.CLASSES[data["hosts"][host]["class"]].capacity_kind == "k8s"
321 held_changes = PRODUCER_HELD_CHECK_NOISE if held_arc else expected_changes
322 applied, _ = apply_host(data, host, run, expected_check_changes=held_changes)
323 if not applied:
324 return False, {}
325 return True, {
326 "checked_at": options.now,
327 "full_applied_at": options.now,
328 "source_digest": options.source_digest,
329 }
330
331
332def reconcile(
333 data: dict[str, Any], options: ReconcileOptions, run: CommandRunner = frp.command_runner
334) -> int:
335 """Reconcile producer then consumers, preserving dependency safety."""
336 state_path = options.state_dir / STATE_FILE
337 document = load_state(state_path)
338 receipts = document["hosts"]
339 failures = 0
340 producer_failed = False
341 for index, host in enumerate(runner_hosts(data)):
342 if frp.interrupted_status():
343 break
344 if index and producer_failed:
345 print(f"fleet-reconcile: {host}: BLOCKED by producer failure", file=sys.stderr)
346 failures += 1
347 continue
348
349 receipt_invalidated = False
350
351 def transaction_run(argv: Sequence[str], target: str = host) -> frp.CommandResult:
352 nonlocal receipt_invalidated
353 verb, _command_host = _command_identity(argv)
354 if verb == "parked-apply" and not receipt_invalidated:
355 receipts.pop(target, None)
356 save_state(state_path, document)
357 receipt_invalidated = True
358 return run(argv)
359
360 ok, receipt = reconcile_host(data, host, receipts.get(host), options, transaction_run)
361 if ok and options.mode == "apply":
362 receipts[host] = receipt
363 if not ok:
364 failures += 1
365 producer_failed = index == 0 and options.mode == "apply"
366 if options.mode == "apply":
367 receipts.pop(host, None)
368 save_state(state_path, document)
369 if options.mode == "apply":
370 save_state(state_path, document)
371 return 1 if failures else 0
372
373
374def validate_installed_authority(root: Path) -> str:
375 """Authenticate the root-owned snapshot selected by the systemd unit."""
376 lexical = root.absolute()
377 resolved = lexical.resolve(strict=True)
378 metadata = lexical.lstat()
379 if lexical != resolved or stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
380 msg = "installed reconciliation source is linked or not a real directory"
381 raise ValueError(msg)
382 if metadata.st_uid != 0 or metadata.st_mode & 0o022:
383 msg = "installed reconciliation source must be root-owned and not group/world writable"
384 raise ValueError(msg)
385 marker = root / SOURCE_DIGEST_FILE
386 marker_metadata = marker.lstat()
387 if (
388 stat.S_ISLNK(marker_metadata.st_mode)
389 or not stat.S_ISREG(marker_metadata.st_mode)
390 or marker_metadata.st_uid != 0
391 or marker_metadata.st_mode & 0o222
392 ):
393 msg = "installed reconciliation source digest has unsafe ownership or mode"
394 raise ValueError(msg)
395 digest = marker.read_text(encoding="ascii").strip()
396 if SHA256_RE.fullmatch(digest) is None:
397 msg = "installed reconciliation source digest is malformed"
398 raise ValueError(msg)
399 return digest
400
401
402def prepare_state_dir(path: Path) -> None:
403 """Create or validate the caller-private state directory."""
404 path.mkdir(mode=0o700, parents=True, exist_ok=True)
405 metadata = path.lstat()
406 if path.is_symlink() or not path.is_dir() or metadata.st_uid != os.getuid():
407 msg = f"state directory is not caller-owned: {path}"
408 raise ValueError(msg)
409 if stat.S_IMODE(metadata.st_mode) != PRIVATE_DIRECTORY_MODE:
410 msg = f"state directory is not mode 0700: {path}"
411 raise ValueError(msg)
412
413
414def _recap(host: str, changed: int = 0, failed: int = 0, unreachable: int = 0) -> str:
415 """Build one Ansible recap fixture line."""
416 return (
417 f"{host} : ok=9 changed={changed} unreachable={unreachable} failed={failed} "
418 "skipped=1 rescued=0 ignored=0\n"
419 )
420
421
422def _selftest_data() -> dict[str, Any]:
423 """Return one fleet containing a producer, consumer, and excluded bench."""
424 return {
425 "runner_image": {"source_host": "producer"},
426 "hosts": {
427 "consumer": {
428 "class": "docker_wsl",
429 "runners": {"instances": 1},
430 "provisions": ["one"],
431 },
432 "bench": {"class": "hil_bench", "provisions": ["bench"]},
433 "producer": {
434 "class": "docker_linux",
435 "runners": {"instances": 1},
436 "provisions": ["one", "two"],
437 },
438 },
439 }
440
441
442def _selftest_options(state_dir: Path, *, mode: str = "apply") -> ReconcileOptions:
443 """Return deterministic policy inputs for controller tests."""
444 return ReconcileOptions(
445 mode=mode,
446 force=False,
447 source_digest="a" * 64,
448 state_dir=state_dir,
449 full_interval=100,
450 producer_interval=50,
451 now=1000,
452 )
453
454
455def _command_identity(argv: Sequence[str]) -> tuple[str, str]:
456 """Return the fleet verb and host from a generated test command."""
457 identities = {
458 "capacity-quarantine": "quarantine",
459 "capacity-restore": "restore",
460 "reconcile-parked-apply": "parked-apply",
461 "reconcile-parked-check": "parked-check",
462 "reconcile-activate": "activate",
463 "reconcile-activation-check": "activation-check",
464 }
465 return identities.get(argv[2], argv[2]), argv[-1]
466
467
468def _check_result(data: dict[str, Any], host: str, changed: int = 0) -> frp.CommandResult:
469 """Return a successful check with the declared recap-row count."""
470 rows = [_recap(recap_identity(data, host), changed)]
471 rows.extend(_recap(recap_identity(data, host)) for _ in data["hosts"][host]["provisions"][1:])
472 return frp.CommandResult(0, "".join(rows), "")
473
474
475def _clean_check_result(data: dict[str, Any], host: str) -> frp.CommandResult:
476 """Return the exact accepted check result for one host class."""
477 producer = host == data["runner_image"]["source_host"]
478 return _check_result(data, host, PRODUCER_CHECK_NOISE if producer else 0)
479
480
481def _selftest_wsl_status_mapping(data: dict[str, Any], failures: list[str]) -> None:
482 """Prove safe WSL drift is actionable while probe failures stay fatal."""
483
484 def apply_required(_argv: Sequence[str]) -> frp.CommandResult:
485 return frp.CommandResult(fw.APPLY_REQUIRED_STATUS, "", "")
486
487 if inspect_host(data, "consumer", apply_required) != (True, 1):
488 failures.append("authenticated WSL stage drift did not request an apply")
489
490 def fatal_probe(_argv: Sequence[str]) -> frp.CommandResult:
491 return frp.CommandResult(5, "", "")
492
493 if inspect_host(data, "consumer", fatal_probe) != (False, 0):
494 failures.append("fatal WSL inspection error was treated as repairable drift")
495
496
497def _selftest_order_parsing_and_schedule(failures: list[str]) -> None:
498 """Prove runner selection, strict recap parsing, and interval decisions."""
499 data = _selftest_data()
500 if runner_hosts(data) != ["producer", "consumer"]:
501 failures.append("producer ordering or non-runner exclusion drifted")
502 if (
503 parse_changed(_recap("producer", 1) + _recap("producer", 2), "producer", 2)
504 != SELFTEST_CHANGED_TOTAL
505 ):
506 failures.append("changed recap sum drifted")
507 wsl_calls: list[tuple[str, str]] = []
508
509 def wsl_run(argv: Sequence[str]) -> frp.CommandResult:
510 wsl_calls.append(_command_identity(argv))
511 return _check_result(data, "consumer")
512
513 if inspect_host(data, "consumer", wsl_run) != (True, 0):
514 failures.append("WSL localhost recap was not mapped to its fleet identity")
515
516 _selftest_wsl_status_mapping(data, failures)
517 if wsl_calls != [("check", "consumer")]:
518 failures.append("WSL recap mapping changed its declared control identity")
519 try:
520 parse_changed(_recap("consumer"), recap_identity(data, "consumer"), 1)
521 failures.append("WSL declared name was accepted as its local recap identity")
522 except ValueError:
523 pass
524 try:
525 parse_changed(_recap("producer", failed=1), "producer", 1)
526 failures.append("failed recap was accepted")
527 except ValueError:
528 pass
529 try:
530 parse_changed(_recap("producer", unreachable=1), "producer", 1)
531 failures.append("unreachable recap was accepted")
532 except ValueError:
533 pass
534 options = _selftest_options(Path("/unused"))
535 fresh = {"source_digest": "a" * 64, "full_applied_at": 950}
536 if full_apply_due(fresh, options, options.full_interval):
537 failures.append("fresh matching receipt forced a full apply")
538 if not full_apply_due(fresh, options, options.producer_interval):
539 failures.append("expired producer receipt skipped its full apply")
540 stale_source = {**fresh, "source_digest": "b" * 64}
541 if not full_apply_due(stale_source, options, options.full_interval):
542 failures.append("source change did not force a full apply")
543
544
545def _selftest_check_mode(failures: list[str]) -> None:
546 """Prove producer staging noise is tolerated but consumer drift is not."""
547 data = _selftest_data()
548 with tempfile.TemporaryDirectory(prefix="ra8-fleet-reconcile-") as raw:
549 state_dir = Path(raw)
550 options = _selftest_options(state_dir, mode="check")
551 calls: list[tuple[str, str]] = []
552
553 def fake_run(argv: Sequence[str]) -> frp.CommandResult:
554 verb, host = _command_identity(argv)
555 calls.append((verb, host))
556 return _clean_check_result(data, host)
557
558 if reconcile(data, options, fake_run):
559 failures.append("producer check-mode staging noise failed the controller")
560 if calls != [("check", "producer"), ("check", "consumer")]:
561 failures.append("check mode invoked a mutation or reordered hosts")
562
563 def drift_run(argv: Sequence[str]) -> frp.CommandResult:
564 _, host = _command_identity(argv)
565 if host == "consumer":
566 return _check_result(data, host, 1)
567 return _clean_check_result(data, host)
568
569 if reconcile(data, options, drift_run) != 1:
570 failures.append("consumer drift passed check mode")
571
572 def producer_drift_run(argv: Sequence[str]) -> frp.CommandResult:
573 _, host = _command_identity(argv)
574 return _check_result(data, host, 3 if host == "producer" else 0)
575
576 if reconcile(data, options, producer_drift_run) != 1:
577 failures.append("producer drift was mistaken for staging noise")
578
579
580def _selftest_apply_and_receipt(failures: list[str]) -> None:
581 """Prove drift is applied, rechecked, and recorded atomically."""
582 data = _selftest_data()
583 with tempfile.TemporaryDirectory(prefix="ra8-fleet-reconcile-") as raw:
584 state_dir = Path(raw)
585 options = _selftest_options(state_dir)
586 receipt = {"source_digest": "a" * 64, "full_applied_at": 975}
587 save_state(
588 state_dir / STATE_FILE,
589 {"version": 1, "hosts": {"producer": receipt, "consumer": receipt}},
590 )
591 calls: list[tuple[str, str]] = []
592 consumer_checks = 0
593
594 def fake_run(argv: Sequence[str]) -> frp.CommandResult:
595 nonlocal consumer_checks
596 verb, host = _command_identity(argv)
597 calls.append((verb, host))
598 if verb == "check" and host == "consumer":
599 consumer_checks += 1
600 return _check_result(data, host, 2 if consumer_checks == 1 else 0)
601 if verb in {"check", "parked-check"}:
602 return _clean_check_result(data, host)
603 return frp.CommandResult(0, "", "")
604
605 if reconcile(data, options, fake_run):
606 failures.append("repairable consumer drift failed reconciliation")
607 expected = [
608 ("check", "producer"),
609 ("check", "consumer"),
610 ("parked-apply", "consumer"),
611 ("parked-check", "consumer"),
612 ("restore", "consumer"),
613 ("check", "consumer"),
614 ]
615 if calls != expected:
616 failures.append("consumer repair did not follow check/apply/recheck order")
617 stored = load_state(state_dir / STATE_FILE)["hosts"]["consumer"]
618 if stored.get("full_applied_at") != options.now:
619 failures.append("successful repair did not publish a receipt")
620
621
622def _selftest_failure_quarantine(failures: list[str]) -> None:
623 """Prove failed producer mutation drains it and blocks every consumer."""
624 data = _selftest_data()
625 with tempfile.TemporaryDirectory(prefix="ra8-fleet-reconcile-") as raw:
626 options = _selftest_options(Path(raw))
627 calls: list[tuple[str, str]] = []
628
629 def fake_run(argv: Sequence[str]) -> frp.CommandResult:
630 verb, host = _command_identity(argv)
631 calls.append((verb, host))
632 if verb in {"check", "parked-check"}:
633 return _clean_check_result(data, host)
634 return frp.CommandResult(1 if verb == "parked-apply" else 0, "", "")
635
636 if reconcile(data, options, fake_run) != 1:
637 failures.append("producer mutation failure did not fail reconciliation")
638 expected = [
639 ("check", "producer"),
640 ("parked-apply", "producer"),
641 ("quarantine", "producer"),
642 ]
643 if calls != expected:
644 failures.append("producer failure did not drain before blocking consumers")
645
646
647def _selftest_failed_repair_retries(failures: list[str]) -> None:
648 """Prove a failed repair invalidates success and forces the next full apply."""
649 data = _selftest_data()
650 with tempfile.TemporaryDirectory(prefix="ra8-fleet-reconcile-") as raw:
651 state_dir = Path(raw)
652 options = _selftest_options(state_dir)
653 receipt = {"source_digest": options.source_digest, "full_applied_at": 975}
654 save_state(
655 state_dir / STATE_FILE,
656 {"version": 1, "hosts": {"producer": receipt, "consumer": receipt}},
657 )
658 consumer_checks = 0
659
660 class SimulatedHardKill(BaseException):
661 """Model controller death at the parked-child launch boundary."""
662
663 def fail_repair(argv: Sequence[str]) -> frp.CommandResult:
664 nonlocal consumer_checks
665 verb, host = _command_identity(argv)
666 if verb == "check" and host == "consumer":
667 consumer_checks += 1
668 return _check_result(data, host, 1)
669 if verb in {"check", "parked-check"}:
670 return _clean_check_result(data, host)
671 if verb == "parked-apply":
672 persisted = load_state(state_dir / STATE_FILE)["hosts"]
673 if "consumer" in persisted:
674 failures.append("receipt was still successful when parked apply began")
675 raise SimulatedHardKill
676 return frp.CommandResult(0, "", "")
677
678 try:
679 reconcile(data, options, fail_repair)
680 failures.append("simulated hard kill returned through reconciliation")
681 except SimulatedHardKill:
682 pass
683 stored = load_state(state_dir / STATE_FILE)["hosts"]
684 if "consumer" in stored:
685 failures.append("failed repair retained its prior success receipt")
686 retry_calls: list[tuple[str, str]] = []
687
688 def retry(argv: Sequence[str]) -> frp.CommandResult:
689 verb, host = _command_identity(argv)
690 retry_calls.append((verb, host))
691 if verb in {"check", "parked-check"}:
692 return _clean_check_result(data, host)
693 return frp.CommandResult(0, "", "")
694
695 if reconcile(data, options, retry):
696 failures.append("immediate retry after failed repair did not converge")
697 if ("parked-apply", "consumer") not in retry_calls:
698 failures.append("missing receipt did not force the next consumer repair")
699
700
701def _selftest_postcheck_quarantine(failures: list[str]) -> None:
702 """Prove a consumer that remains changed is drained after its repair."""
703 data = _selftest_data()
704 with tempfile.TemporaryDirectory(prefix="ra8-fleet-reconcile-") as raw:
705 state_dir = Path(raw)
706 options = _selftest_options(state_dir)
707 receipt = {"source_digest": "a" * 64, "full_applied_at": 975}
708 save_state(
709 state_dir / STATE_FILE,
710 {"version": 1, "hosts": {"producer": receipt, "consumer": receipt}},
711 )
712 calls: list[tuple[str, str]] = []
713
714 def fake_run(argv: Sequence[str]) -> frp.CommandResult:
715 verb, host = _command_identity(argv)
716 calls.append((verb, host))
717 if verb in {"check", "parked-check"}:
718 return (
719 _check_result(data, host, 1)
720 if host == "consumer"
721 else _clean_check_result(data, host)
722 )
723 return frp.CommandResult(0, "", "")
724
725 if reconcile(data, options, fake_run) != 1:
726 failures.append("non-idempotent consumer repair passed")
727 expected_tail = [
728 ("parked-apply", "consumer"),
729 ("parked-check", "consumer"),
730 ("quarantine", "consumer"),
731 ]
732 if calls[-3:] != expected_tail:
733 failures.append("non-idempotent consumer was not quarantined")
734
735
736def _selftest_restore_quarantine(failures: list[str]) -> None:
737 """Prove a failed capacity restore is driven back to zero before return."""
738 data = _selftest_data()
739 with tempfile.TemporaryDirectory(prefix="ra8-fleet-reconcile-") as raw:
740 options = _selftest_options(Path(raw))
741 calls: list[tuple[str, str]] = []
742
743 def fake_run(argv: Sequence[str]) -> frp.CommandResult:
744 verb, host = _command_identity(argv)
745 calls.append((verb, host))
746 if verb in {"check", "parked-check"}:
747 return _clean_check_result(data, host)
748 return frp.CommandResult(1 if verb == "restore" else 0, "", "")
749
750 if reconcile(data, options, fake_run) != 1:
751 failures.append("failed capacity restore passed reconciliation")
752 expected_tail = [
753 ("parked-apply", "producer"),
754 ("parked-check", "producer"),
755 ("restore", "producer"),
756 ("quarantine", "producer"),
757 ]
758 if calls[-4:] != expected_tail:
759 failures.append("failed restore did not drive the host back to zero")
760
761
762def _selftest_state_safety(failures: list[str]) -> None:
763 """Prove private state, symlink refusal, locking, and atomic round trips."""
764 with tempfile.TemporaryDirectory(prefix="ra8-fleet-reconcile-") as raw:
765 state_dir = Path(raw)
766 prepare_state_dir(state_dir)
767 receipt = {"source_digest": "a" * 64, "full_applied_at": 950}
768 state_path = state_dir / STATE_FILE
769 save_state(state_path, {"version": 1, "hosts": {"producer": receipt}})
770 if load_state(state_path)["hosts"]["producer"] != receipt:
771 failures.append("atomic receipt round trip drifted")
772 state_path.unlink()
773 target = state_dir / "target.json"
774 target.write_text('{"version": 1, "hosts": {}}\n', encoding="ascii")
775 state_path.symlink_to(target)
776 try:
777 load_state(state_path)
778 failures.append("linked state file was accepted")
779 except ValueError:
780 pass
781
782
783def _selftest_timeout(failures: list[str]) -> None:
784 """Prove a timed-out mutation returns through the quarantine path."""
785 result = frp.command_runner(
786 [
787 sys.executable,
788 "-c",
789 "import sys,time; print('partial stdout', flush=True); "
790 "print('partial stderr', file=sys.stderr, flush=True); time.sleep(60)",
791 ],
792 timeout_seconds=0.05,
793 )
794 if result.status != frp.TIMEOUT_STATUS or "command timed out" not in result.stderr:
795 failures.append("a timed-out fleet command escaped fail-closed handling")
796 if result.stdout != "partial stdout\n":
797 failures.append("timed-out command evidence was discarded")
798
799
800def _selftest_signal_quarantine(failures: list[str]) -> None:
801 """Prove TERM stops the owned process group and completes quarantine."""
802 data = _selftest_data()
803 with tempfile.TemporaryDirectory(prefix="ra8-fleet-reconcile-signal-") as raw:
804 marker = Path(raw) / "quarantine"
805 ready = marker.with_suffix(".ready")
806
807 def request_stop() -> None:
808 deadline = time.monotonic() + 5
809 while not ready.exists() and time.monotonic() < deadline:
810 time.sleep(0.01)
811 if ready.exists():
812 os.kill(os.getpid(), signal.SIGTERM)
813
814 def fake_run(argv: Sequence[str]) -> frp.CommandResult:
815 verb, _host = _command_identity(argv)
816 if verb == "parked-apply":
817 code = (
818 "import pathlib,signal,subprocess,sys,time; "
819 "child=subprocess.Popen(['sleep','60']); "
820 "signal.signal(signal.SIGTERM, lambda *_: "
821 "(child.terminate(), child.wait(), sys.exit(143))); "
822 f"pathlib.Path({str(ready)!r}).write_text(str(child.pid)); "
823 "time.sleep(60)"
824 )
825 return frp.command_runner(
826 [sys.executable, "-c", code], timeout_seconds=SELFTEST_SIGNAL_BOUND
827 )
828 if verb == "quarantine":
829 marker.write_text("quarantined", encoding="ascii")
830 return frp.CommandResult(0, "", "")
831 return frp.CommandResult(2, "", "unexpected signal selftest command\n")
832
833 sender = Thread(target=request_stop, name="fleet-reconcile-signal-selftest")
834 sender.start()
835 started = time.monotonic()
836 with frp.stop_handlers():
837 applied, _changed = apply_host(data, "consumer", fake_run, expected_check_changes=0)
838 elapsed = time.monotonic() - started
839 sender.join(timeout=1)
840 if sender.is_alive():
841 failures.append("signal selftest sender did not finish")
842 if applied or frp.interrupted_status() != 128 + signal.SIGTERM:
843 failures.append("TERM did not preserve the interrupted service status")
844 if elapsed >= SELFTEST_SIGNAL_BOUND:
845 failures.append("interrupted controller did not finish its handler")
846 if not marker.exists():
847 failures.append("TERM during a mutation did not route through quarantine")
848 if not ready.exists():
849 failures.append("signal selftest mutation never started")
850 return
851 worker_pid = int(ready.read_text(encoding="ascii"))
852 deadline = time.monotonic() + 2
853 while time.monotonic() < deadline:
854 try:
855 os.kill(worker_pid, 0)
856 except ProcessLookupError:
857 break
858 time.sleep(0.01)
859 else:
860 failures.append("TERM left an owned mutation child running")
861
862
863def selftest() -> int:
864 """Exercise the unattended controller's safety-critical decisions."""
865 failures: list[str] = []
866 _selftest_order_parsing_and_schedule(failures)
867 _selftest_check_mode(failures)
868 _selftest_apply_and_receipt(failures)
869 _selftest_failure_quarantine(failures)
870 _selftest_failed_repair_retries(failures)
871 _selftest_postcheck_quarantine(failures)
872 _selftest_restore_quarantine(failures)
873 failures.extend(fras.run(apply_host))
874 _selftest_state_safety(failures)
875 failures.extend(fml.run_selftest())
876 _selftest_timeout(failures)
877 failures.extend(frp.run_selftest())
878 _selftest_signal_quarantine(failures)
879 failures.extend(frs.runtime_inventory_selftest(fm))
880 failures.extend(frs.run(fm.REPO_ROOT))
881 for failure in failures:
882 print(f"fleet_reconcile.py --selftest: FAIL: {failure}", file=sys.stderr)
883 if failures:
884 return 1
885 print("fleet_reconcile.py --selftest: PASS")
886 return 0
887
888
889def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
890 """Parse the offline selftest and live reconciliation boundaries."""
891 parser = argparse.ArgumentParser(description=__doc__)
892 parser.add_argument("--selftest", action="store_true")
893 parser.add_argument("--mode", choices=("apply", "check"), default="apply")
894 parser.add_argument("--force", action="store_true")
895 parser.add_argument("--require-installed-authority", action="store_true")
896 parser.add_argument("--state-dir", type=Path)
897 parser.add_argument("--full-interval", type=int, default=DEFAULT_FULL_INTERVAL)
898 parser.add_argument("--producer-interval", type=int, default=DEFAULT_PRODUCER_INTERVAL)
899 return parser.parse_args(argv)
900
901
902def _early_status(args: argparse.Namespace) -> int | None:
903 """Handle non-live modes and reject invalid option combinations."""
904 if args.selftest:
905 return selftest()
906 if args.force and args.mode != "apply":
907 print("fleet-reconcile: --force requires --mode apply", file=sys.stderr)
908 return 2
909 if args.full_interval <= 0 or args.producer_interval <= 0:
910 print("fleet-reconcile: convergence intervals must be positive", file=sys.stderr)
911 return 2
912 return None
913
914
915def main(argv: Sequence[str] | None = None) -> int:
916 """Enter selftest or one locked reconciliation transaction."""
917 args = parse_args(argv)
918 early_status = _early_status(args)
919 if early_status is not None:
920 return early_status
921 try:
922 source_digest = (
923 validate_installed_authority(fm.REPO_ROOT)
924 if args.require_installed_authority
925 else "manual-operator-checkout"
926 )
927 state_dir = args.state_dir or Path.home() / ".local/state/ra8-fleet-reconcile"
928 prepare_state_dir(state_dir)
929 if args.require_installed_authority:
930 fm.validate_runtime_inventory(state_dir)
931 data = fm.load()
932 options = ReconcileOptions(
933 args.mode,
934 args.force,
935 source_digest,
936 state_dir,
937 args.full_interval,
938 args.producer_interval,
939 int(time.time()),
940 )
941 if args.mode == "check":
942 with frp.stop_handlers():
943 status = reconcile(data, options)
944 return frp.interrupted_status() or status
945 with (
946 fml.mutation_lock(data, installed_local=args.require_installed_authority),
947 frp.stop_handlers(),
948 ):
949
950 def guarded_runner(argv: Sequence[str]) -> frp.CommandResult:
951 return frp.command_runner(argv, guardian=True)
952
953 status = reconcile(data, options, guarded_runner)
954 return frp.interrupted_status() or status
955 except fml.MutationLockBusyError as error:
956 print(f"fleet-reconcile: {error}", file=sys.stderr)
957 return fml.LOCK_BUSY_STATUS
958 except (
959 fml.MutationLockError,
960 OSError,
961 TypeError,
962 ValueError,
963 json.JSONDecodeError,
964 fm.FleetError,
965 ) as error:
966 print(f"fleet-reconcile: FATAL: {error}", file=sys.stderr)
967 return 2
968
969
970if __name__ == "__main__":
971 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298