Files
awoooi/scripts/reboot-recovery/tests/test_install_host112_guest_recovery_contract.py

297 lines
9.9 KiB
Python

from __future__ import annotations
import os
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
INSTALLER = ROOT / "scripts/reboot-recovery/install-host112-guest-recovery.sh"
def source() -> str:
return INSTALLER.read_text(encoding="utf-8")
def test_installer_shell_syntax_is_valid() -> None:
result = subprocess.run(
["bash", "-n", str(INSTALLER)],
text=True,
capture_output=True,
check=False,
timeout=10,
)
assert result.returncode == 0, result.stdout + result.stderr
def test_apply_and_rollback_require_safe_control_identity() -> None:
text = source()
assert '--trace-id ID --run-id ID --work-item-id ID' in text
assert 'BLOCKER incomplete_control_identity' in text
assert 'BLOCKER unsafe_control_identity' in text
assert 'BLOCKER installer_control_identity_required' in text
assert '^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$' in text
def test_sudo_policy_has_exact_correlated_apply_and_dry_run_only() -> None:
text = source()
assert "SUDOERS_ARGUMENT_REGEX='^--apply --trace-id " in text
assert "SUDOERS_DRY_RUN_ARGUMENT_REGEX='^--dry-run --trace-id " in text
assert '"$TARGET_USER" "$READINESS_TARGET" "$SUDOERS_ARGUMENT_REGEX"' in text
assert (
'"$TARGET_USER" "$READINESS_TARGET" '
'"$SUDOERS_DRY_RUN_ARGUMENT_REGEX"' in text
)
assert 'SUDO_CORRELATED_APPLY_NOPASSWD' in text
assert 'SUDO_CORRELATED_DRY_RUN_NOPASSWD' in text
assert 'SUDO_LEGACY_UNCORRELATED_APPLY_NOPASSWD' in text
assert 'SUDO_UNSAFE_IDENTITY_NOPASSWD' in text
assert 'SUDO_POLICY_READY' in text
assert 'sudo_policy_is_nopasswd' in text
assert 'Options:' in text
assert '!authenticate' in text
def test_sudo_policy_parser_requires_nopasswd_and_match_in_same_entry() -> None:
text = source()
parser = text[
text.index("sudo_policy_output_has_nopasswd_match() {") : text.index(
"agent99_direct_authorization_ready() {"
)
]
broad_password_rule = """Sudoers entry: /etc/sudoers
RunAsUsers: ALL
Commands:
ALL
Matched: /usr/local/sbin/readiness --apply
"""
exact_nopasswd_rule = broad_password_rule + """Sudoers entry: /etc/sudoers.d/agent99
RunAsUsers: root
Options: !authenticate
Commands:
/usr/local/sbin/readiness ^--apply .*$
Matched: /usr/local/sbin/readiness --apply --trace-id safe
"""
split_entry_false_positive = """Sudoers entry: /etc/sudoers.d/one
Options: !authenticate
Commands:
/usr/bin/true
Sudoers entry: /etc/sudoers
Commands:
ALL
Matched: /usr/local/sbin/readiness --apply
"""
def parse(sample: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["bash", "-c", parser + "\nsudo_policy_output_has_nopasswd_match"],
input=sample,
text=True,
capture_output=True,
check=False,
timeout=10,
)
assert parse(broad_password_rule).returncode != 0
assert parse(exact_nopasswd_rule).returncode == 0
assert parse(split_entry_false_positive).returncode != 0
def test_windows99_direct_route_is_fingerprint_bound_and_not_forced() -> None:
text = source()
route = text[
text.index("agent99_direct_authorization_ready() {") : text.index(
"readiness_check_no_write_ready() {"
)
]
assert 'HOST112_AGENT99_SOURCE_ADDRESS:-192.168.0.99' in text
assert 'HOST112_AGENT99_EXPECTED_KEY_FINGERPRINT' in text
assert 'AGENT99_99_DIRECT_ROUTE_READY' in text
assert 'index($1, "command=") == 0' in route
assert 'has_restrict($1)' in route
assert 'ssh-keygen -lf -' in route
assert 'cat "$AUTH_FILE"' not in text
assert 'from="%s",restrict,command="%s --check"' in text
assert 'CONTROL_SOURCE_ADDRESS:-192.168.0.110' in text
assert 'BLOCKER control_public_key_not_staged=$CONTROL_PUBLIC_KEY_PATH' in text
assert "never discovers, reads, copies, or derives a private key" in text
def test_apply_is_transactional_and_has_a_verified_rollback() -> None:
text = source()
apply_path = text[text.index("static_source_preflight\n") :]
handler = text[
text.index("handle_apply_failure() {") : text.index(
"static_source_preflight() {"
)
]
assert 'set -Eeuo pipefail' in text
assert 'TRANSACTION_ACTIVE=1' in apply_path
assert "trap 'handle_apply_failure $?' ERR" in apply_path
assert "trap 'handle_apply_failure 130' INT TERM" in apply_path
assert 'restore_from_backup "$BACKUP_DIR"' in handler
assert 'verify_rollback "$BACKUP_DIR"' in handler
assert 'ROLLBACK_VERIFIED=1' in handler
assert 'install_failed_rolled_back_verified' in handler
assert 'cmp -s "$backup_dir/rootfs/${path#/}" "$path"' in text
assert 'timer_was_enabled' in text
assert 'timer_was_active' in text
def test_static_gate_precedes_timer_activation_and_no_oneshot_is_started() -> None:
text = source()
apply_path = text[text.index("static_source_preflight\n") :]
static_gate = apply_path.index("post_install_static_gate\n")
timer_start = apply_path.index(
"systemctl start awoooi-host112-guest-recovery.timer"
)
final_gate = apply_path.index("check\n", timer_start)
receipt = apply_path.index(
'write_install_receipt "installed_timer_activated_recovery_separate"'
)
assert static_gate < timer_start < final_gate < receipt
assert 'systemctl enable awoooi-host112-guest-recovery.timer' in apply_path
assert 'systemctl enable --now awoooi-host112-guest-recovery.timer' not in text
assert 'systemctl start awoooi-host112-guest-recovery.service' not in text
assert "grep -Fq 'TimeoutStartSec=480'" in text
assert "for tool in bash cksum cmp flock" in text
assert "TimeoutStartSec=600" not in text
assert 'INSTALLER_SYNCHRONOUS_RECOVERY_START=0' in text
assert 'RECOVERY_EXECUTION_PERFORMED=0' in text
def test_install_receipt_does_not_claim_a_recovery_run() -> None:
text = source()
receipt = text[
text.index("write_install_receipt() {") : text.index(
"handle_apply_failure() {"
)
]
assert 'schema_version=host112_guest_recovery_install_receipt_v1' in receipt
assert 'install_verified=$install_verified' in receipt
assert 'manager_runtime_write_performed=0' in receipt
assert 'recovery_execution_performed=0' in receipt
assert 'recovery_run_id=none' in receipt
assert 'recovery_receipt_ref=none' in receipt
assert 'agent99_99_direct_runtime_probe=not_started' in receipt
assert 'rollback_attempted=$rollback_attempted' in receipt
assert 'rollback_verified=$rollback_verified' in receipt
assert 'static_post_gate_blockers=${STATIC_POST_GATE_BLOCKERS:-not_evaluated}' in receipt
def test_static_post_gate_reports_named_blockers_before_rollback() -> None:
text = source()
gate = text[
text.index("post_install_static_gate() {") : text.index(
"wait_for_recovery_service_idle() {"
)
]
for blocker in (
"target_hashes_mismatch",
"unit_static_verify_failed",
"target_files_not_ready",
"unit_contract_not_ready",
"sudo_policy_not_ready",
"agent99_direct_route_not_ready",
"readiness_no_write_not_ready",
"timer_not_enabled",
):
assert blocker in gate
assert 'STATIC_POST_GATE_BLOCKERS=' in gate
assert 'READINESS_CHECK_EXIT=' in gate
def test_check_gate_requires_policy_route_timer_and_no_write_readback() -> None:
text = source()
check = text[text.index("check() {") : text.index("require_root() {")]
for required in (
'SUDO_POLICY_READY',
'AGENT99_99_DIRECT_ROUTE_READY',
'READINESS_NO_WRITE_READY',
'TIMER_ENABLED',
'TIMER_ACTIVE',
'CHECK_GATE_READY',
):
assert required in check
assert 'runtime_write_performed=0' in text
assert 'manager_runtime_write_performed=0' in text
assert 'receipt_write_performed=0' in text
def test_clean_install_timer_stop_is_state_aware_and_idempotent(
tmp_path: Path,
) -> None:
text = source()
function = text[
text.index("stop_recovery_timer_if_running() {") : text.index(
"\n\nresolve_target_user",
text.index("stop_recovery_timer_if_running() {"),
)
]
runner = tmp_path / "run-stop-contract.sh"
runner.write_text(
"#!/usr/bin/env bash\nset -u\n" + function + "\nstop_recovery_timer_if_running\n",
encoding="utf-8",
)
runner.chmod(0o755)
fake_bin = tmp_path / "bin"
fake_bin.mkdir()
log = tmp_path / "systemctl.log"
systemctl = fake_bin / "systemctl"
systemctl.write_text(
"""#!/usr/bin/env bash
printf '%s\n' "$*" >>"${SYSTEMCTL_LOG:?}"
if [ "${1:-}" = "is-active" ]; then
printf '%s\n' "${FAKE_TIMER_STATE:-not-found}"
[ "${FAKE_TIMER_STATE:-not-found}" = "active" ]
fi
""",
encoding="utf-8",
)
systemctl.chmod(0o755)
timeout = fake_bin / "timeout"
timeout.write_text(
"#!/usr/bin/env bash\nshift\n\"$@\"\n",
encoding="utf-8",
)
timeout.chmod(0o755)
env = os.environ | {
"PATH": f"{fake_bin}:{os.environ['PATH']}",
"SYSTEMCTL_LOG": str(log),
}
clean = subprocess.run(
["bash", str(runner)],
text=True,
capture_output=True,
env=env | {"FAKE_TIMER_STATE": "not-found"},
check=False,
timeout=10,
)
assert clean.returncode == 0, clean.stdout + clean.stderr
assert "stop awoooi-host112-guest-recovery.timer" not in log.read_text(
encoding="utf-8"
)
log.write_text("", encoding="utf-8")
active = subprocess.run(
["bash", str(runner)],
text=True,
capture_output=True,
env=env | {"FAKE_TIMER_STATE": "active"},
check=False,
timeout=10,
)
assert active.returncode == 0, active.stdout + active.stderr
assert "stop awoooi-host112-guest-recovery.timer" in log.read_text(
encoding="utf-8"
)