Apply the approved Implementation Baseline 1.1 architecture revisions onto the current mainline, verified faithful to the delivered change bundle: - Add aios_baseline: signed owner-delegation-grant.v1 / operational-epoch.v1 / task-principal.v1 auth chain, runtime contract + deployment-truth evaluator, declarative operation registry (effect-class + fail-closed dispatch), lineage ledger, release/recipe/saga ledger, bounded optimization controller, authenticated HTTP transport, resident worker loop and systemd host probe. - Extend existing Job authority (aios_gitops_job_controller/executor) with durable intent/receipt/reconcile and fail-closed admission; harden task_plane/store heartbeat CAS and terminal-state fencing; make reconciliation_controller fail-closed on incomplete/drift evidence. - Add isolated-validation profile templates (deploy/baseline-v1.1, systemd .example), source change/rollback tooling (tools/baseline_v1_1), the AT-01..AT-23 acceptance matrix, M0-M5 runbook, installation/rollback docs and the baseline_v1_1 test suite. - Keep legacy write authorities untouched; isolated-validation profile only. Staged blobs verified byte-identical to the delivered source-changes bundle. Cross-platform baseline tests pass; systemd/fcntl-dependent paths require the declared Linux x86_64 + CPython 3.12 + systemd runtime (site-bound).
104 lines
5.5 KiB
Python
104 lines
5.5 KiB
Python
"""Small registered host diagnostics; unit bindings come from trusted configuration."""
|
|
from __future__ import annotations
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import signal
|
|
import selectors
|
|
import stat
|
|
import subprocess
|
|
import time
|
|
|
|
|
|
class HostError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class SystemdHost:
|
|
PROPERTIES = ('Id', 'ActiveState', 'SubState', 'MainPID', 'InvocationID')
|
|
|
|
def __init__(self, bindings: dict[str, str], *, executable: str = '/usr/bin/systemctl',
|
|
timeout_seconds: float = 10):
|
|
if type(timeout_seconds) not in (int,float) or not 0 < timeout_seconds <= 60:
|
|
raise HostError('HOST_TIMEOUT_INVALID')
|
|
self.bindings = dict(bindings)
|
|
for binding,unit in self.bindings.items():
|
|
if not isinstance(binding,str) or len(binding.split(':'))!=3 or any(not re.fullmatch(r'[A-Za-z0-9_.-]+',part) for part in binding.split(':')):
|
|
raise HostError('HOST_TENANT_BINDING_INVALID')
|
|
if not isinstance(unit, str) or not re.fullmatch(r'[A-Za-z0-9_.@:-]+\.service', unit):
|
|
raise HostError('HOST_UNIT_INVALID')
|
|
if not Path(executable).is_absolute() or not Path(executable).is_file() or Path(executable).is_symlink():
|
|
raise HostError('HOST_EXECUTABLE_UNAVAILABLE')
|
|
info=Path(executable).stat()
|
|
if info.st_uid not in {0,os.geteuid()} or info.st_mode & 0o022:
|
|
raise HostError('HOST_EXECUTABLE_UNTRUSTED')
|
|
self._executable_identity=(info.st_dev,info.st_ino,info.st_size,info.st_mtime_ns)
|
|
self.executable = executable
|
|
self.timeout_seconds = timeout_seconds
|
|
|
|
def status(self, component_id: str, environment: str, *, tenant_id: str, deadline=None) -> dict:
|
|
if any(not isinstance(part,str) or not re.fullmatch(r'[A-Za-z0-9_.-]+',part) for part in (tenant_id,environment,component_id)):
|
|
raise HostError('HOST_SCOPE_INVALID')
|
|
unit = self.bindings.get(f'{tenant_id}:{environment}:{component_id}')
|
|
if not unit:
|
|
raise HostError('HOST_BINDING_NOT_FOUND')
|
|
remaining=self.timeout_seconds if deadline is None else min(self.timeout_seconds,deadline-time.time())
|
|
if remaining<=0: raise HostError('HOST_OPERATION_TIMEOUT')
|
|
stop_at=time.monotonic()+remaining
|
|
executable_fd=os.open(self.executable,os.O_RDONLY|os.O_NOFOLLOW|os.O_CLOEXEC)
|
|
try:
|
|
info=os.fstat(executable_fd)
|
|
if not stat.S_ISREG(info.st_mode) or (info.st_dev,info.st_ino,info.st_size,info.st_mtime_ns)!=self._executable_identity:
|
|
raise HostError('HOST_EXECUTABLE_IDENTITY_CHANGED')
|
|
executable_path=f'/proc/self/fd/{executable_fd}'
|
|
if not Path(executable_path).exists(): raise HostError('HOST_EXECUTOR_PLATFORM_UNSUPPORTED')
|
|
argv=[executable_path,'show','--no-pager','--property='+','.join(self.PROPERTIES),'--',unit]
|
|
process=subprocess.Popen(argv,stdin=subprocess.DEVNULL,stdout=subprocess.PIPE,stderr=subprocess.PIPE,
|
|
env={'PATH':'/usr/bin:/bin','LANG':'C'},start_new_session=True,pass_fds=(executable_fd,))
|
|
finally: os.close(executable_fd)
|
|
output=bytearray()
|
|
byte_count=0
|
|
try:
|
|
with selectors.DefaultSelector() as selector:
|
|
selector.register(process.stdout,selectors.EVENT_READ,'stdout')
|
|
selector.register(process.stderr,selectors.EVENT_READ,'stderr')
|
|
while selector.get_map():
|
|
remaining=stop_at-time.monotonic()
|
|
if remaining<=0: raise HostError('HOST_OPERATION_TIMEOUT')
|
|
for event,_mask in selector.select(remaining):
|
|
chunk=os.read(event.fileobj.fileno(),4096)
|
|
if not chunk:
|
|
selector.unregister(event.fileobj)
|
|
continue
|
|
byte_count+=len(chunk)
|
|
if byte_count>65536: raise HostError('HOST_RESPONSE_TOO_LARGE')
|
|
if event.data=='stdout': output.extend(chunk)
|
|
process.wait(timeout=max(0.001,stop_at-time.monotonic()))
|
|
except (HostError,subprocess.TimeoutExpired) as exc:
|
|
try: os.killpg(process.pid,signal.SIGKILL)
|
|
except ProcessLookupError: pass
|
|
process.wait()
|
|
if isinstance(exc,HostError): raise
|
|
raise HostError('HOST_OPERATION_TIMEOUT') from exc
|
|
finally:
|
|
process.stdout.close()
|
|
process.stderr.close()
|
|
if process.returncode != 0:
|
|
raise HostError('HOST_STATUS_UNAVAILABLE')
|
|
try: out=output.decode('utf-8')
|
|
except UnicodeDecodeError as exc: raise HostError('HOST_OBSERVATION_INVALID') from exc
|
|
properties = {}
|
|
for line in out.splitlines():
|
|
key, sep, value = line.partition('=')
|
|
if sep and key in self.PROPERTIES:
|
|
if key in properties:
|
|
raise HostError('HOST_DUPLICATE_PROPERTY')
|
|
properties[key] = value
|
|
if set(properties) != set(self.PROPERTIES) or properties['Id'] != unit:
|
|
raise HostError('HOST_OBSERVATION_INCOMPLETE')
|
|
if not properties['MainPID'].isdigit():
|
|
raise HostError('HOST_OBSERVATION_INVALID')
|
|
return {'operation':'service-status', 'tenant_id':tenant_id, 'component_id':component_id, 'environment':environment,
|
|
'unit':unit, 'healthy':properties['ActiveState'] == 'active',
|
|
'properties':properties, 'observed_at':time.time(), 'source':'systemd.show'}
|