Support VPS local preflight collection
Some checks are pending
CI / lint-and-test (push) Waiting to run
Some checks are pending
CI / lint-and-test (push) Waiting to run
This commit is contained in:
parent
637d285ecb
commit
0d70c09f6f
5 changed files with 55 additions and 17 deletions
|
|
@ -27,7 +27,7 @@
|
||||||
"rio_strategy_nodes": 2,
|
"rio_strategy_nodes": 2,
|
||||||
"sources": 28
|
"sources": 28
|
||||||
},
|
},
|
||||||
"generated_at_utc": "2026-07-10T01:39:08.583122+00:00",
|
"generated_at_utc": "2026-07-10T01:41:16.353240+00:00",
|
||||||
"mode": "operator_runbook_generation_only",
|
"mode": "operator_runbook_generation_only",
|
||||||
"operator_environment": {
|
"operator_environment": {
|
||||||
"operator_commands_run_sql_if_executed": true,
|
"operator_commands_run_sql_if_executed": true,
|
||||||
|
|
@ -337,8 +337,8 @@
|
||||||
"pre_apply_validation_commands": [
|
"pre_apply_validation_commands": [
|
||||||
{
|
{
|
||||||
"applies_production_packet": false,
|
"applies_production_packet": false,
|
||||||
"command_template": ".venv/bin/python scripts/collect_working_leo_production_preflight.py --execute --out docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.json --markdown-out docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.md",
|
"command_template": ".venv/bin/python scripts/collect_working_leo_production_preflight.py --execute --transport ssh --out docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.json --markdown-out docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.md",
|
||||||
"description": "Collect a production baseline for every preflight SQL file through a read-only transaction wrapper. During an authorized apply window, still run each phase preflight again immediately before its phase.",
|
"description": "Collect a production baseline for every preflight SQL file through a read-only transaction wrapper. During an authorized apply window, still run each phase preflight again immediately before its phase. Use --transport local-docker when running this collector directly on the VPS.",
|
||||||
"expected_artifact": "docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.md",
|
"expected_artifact": "docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.md",
|
||||||
"id": "readonly_production_preflight_collector",
|
"id": "readonly_production_preflight_collector",
|
||||||
"mutates_production_db": false
|
"mutates_production_db": false
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Working Leo Authorized Apply Runbook
|
# Working Leo Authorized Apply Runbook
|
||||||
|
|
||||||
Generated UTC: `2026-07-10T01:39:08.583122+00:00`
|
Generated UTC: `2026-07-10T01:41:16.353240+00:00`
|
||||||
Mode: `operator_runbook_generation_only`
|
Mode: `operator_runbook_generation_only`
|
||||||
Status: `ready_not_executed`
|
Status: `ready_not_executed`
|
||||||
Ready for authorized operator: `True`
|
Ready for authorized operator: `True`
|
||||||
|
|
@ -94,8 +94,8 @@ Do not run apply commands unless the operator explicitly authorizes production D
|
||||||
|
|
||||||
## Pre-Apply Validation Commands
|
## Pre-Apply Validation Commands
|
||||||
|
|
||||||
- `readonly_production_preflight_collector`: Collect a production baseline for every preflight SQL file through a read-only transaction wrapper. During an authorized apply window, still run each phase preflight again immediately before its phase.
|
- `readonly_production_preflight_collector`: Collect a production baseline for every preflight SQL file through a read-only transaction wrapper. During an authorized apply window, still run each phase preflight again immediately before its phase. Use --transport local-docker when running this collector directly on the VPS.
|
||||||
- command: `.venv/bin/python scripts/collect_working_leo_production_preflight.py --execute --out docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.json --markdown-out docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.md`
|
- command: `.venv/bin/python scripts/collect_working_leo_production_preflight.py --execute --transport ssh --out docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.json --markdown-out docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.md`
|
||||||
- artifact: `docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.md`
|
- artifact: `docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.md`
|
||||||
- mutates production DB: `False`
|
- mutates production DB: `False`
|
||||||
- applies production packet: `False`
|
- applies production packet: `False`
|
||||||
|
|
|
||||||
|
|
@ -138,10 +138,11 @@ PRE_APPLY_VALIDATION_COMMANDS: list[dict[str, Any]] = [
|
||||||
"id": "readonly_production_preflight_collector",
|
"id": "readonly_production_preflight_collector",
|
||||||
"description": (
|
"description": (
|
||||||
"Collect a production baseline for every preflight SQL file through a read-only transaction wrapper. "
|
"Collect a production baseline for every preflight SQL file through a read-only transaction wrapper. "
|
||||||
"During an authorized apply window, still run each phase preflight again immediately before its phase."
|
"During an authorized apply window, still run each phase preflight again immediately before its phase. "
|
||||||
|
"Use --transport local-docker when running this collector directly on the VPS."
|
||||||
),
|
),
|
||||||
"command_template": (
|
"command_template": (
|
||||||
".venv/bin/python scripts/collect_working_leo_production_preflight.py --execute "
|
".venv/bin/python scripts/collect_working_leo_production_preflight.py --execute --transport ssh "
|
||||||
"--out docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.json "
|
"--out docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.json "
|
||||||
"--markdown-out docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.md"
|
"--markdown-out docs/reports/leo-working-state-20260709/working-leo-production-preflight-current.md"
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -86,8 +86,11 @@ def preflight_entries(report_dir: Path = REPO_REPORT_DIR) -> list[dict[str, Any]
|
||||||
return generated["phases"]["preflight_order"]
|
return generated["phases"]["preflight_order"]
|
||||||
|
|
||||||
|
|
||||||
def build_ssh_command(args: argparse.Namespace) -> list[str]:
|
def build_command(args: argparse.Namespace) -> list[str]:
|
||||||
command = [
|
docker_psql = f"docker exec -i {args.container} psql -U {args.db_user} -d {args.db} -v ON_ERROR_STOP=1 -P pager=off"
|
||||||
|
if args.transport == "local-docker":
|
||||||
|
return docker_psql.split()
|
||||||
|
return [
|
||||||
"ssh",
|
"ssh",
|
||||||
"-i",
|
"-i",
|
||||||
str(args.ssh_key),
|
str(args.ssh_key),
|
||||||
|
|
@ -98,15 +101,16 @@ def build_ssh_command(args: argparse.Namespace) -> list[str]:
|
||||||
"-o",
|
"-o",
|
||||||
f"ConnectTimeout={args.connect_timeout}",
|
f"ConnectTimeout={args.connect_timeout}",
|
||||||
args.ssh_target,
|
args.ssh_target,
|
||||||
(
|
docker_psql,
|
||||||
f"docker exec -i {args.container} "
|
|
||||||
f"psql -U {args.db_user} -d {args.db} -v ON_ERROR_STOP=1 -P pager=off"
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
return command
|
|
||||||
|
|
||||||
|
|
||||||
def command_for_report(args: argparse.Namespace) -> str:
|
def command_for_report(args: argparse.Namespace) -> str:
|
||||||
|
if args.transport == "local-docker":
|
||||||
|
return (
|
||||||
|
f"docker exec -i {args.container} psql -U {args.db_user} -d {args.db} "
|
||||||
|
"-v ON_ERROR_STOP=1 -P pager=off"
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
"ssh -i [SSH_KEY] -o BatchMode=yes -o StrictHostKeyChecking=accept-new "
|
"ssh -i [SSH_KEY] -o BatchMode=yes -o StrictHostKeyChecking=accept-new "
|
||||||
f"-o ConnectTimeout={args.connect_timeout} {args.ssh_target} "
|
f"-o ConnectTimeout={args.connect_timeout} {args.ssh_target} "
|
||||||
|
|
@ -119,7 +123,7 @@ def collect_preflight(args: argparse.Namespace) -> dict[str, Any]:
|
||||||
entries = preflight_entries(args.report_dir)
|
entries = preflight_entries(args.report_dir)
|
||||||
results: list[dict[str, Any]] = []
|
results: list[dict[str, Any]] = []
|
||||||
mode = "live_vps_readonly_preflight" if args.execute else "validated_not_executed"
|
mode = "live_vps_readonly_preflight" if args.execute else "validated_not_executed"
|
||||||
command = build_ssh_command(args)
|
command = build_command(args)
|
||||||
|
|
||||||
for index, entry in enumerate(entries, start=1):
|
for index, entry in enumerate(entries, start=1):
|
||||||
path = args.report_dir / entry["file"]
|
path = args.report_dir / entry["file"]
|
||||||
|
|
@ -176,6 +180,7 @@ def collect_preflight(args: argparse.Namespace) -> dict[str, Any]:
|
||||||
"production_apply_executed": False,
|
"production_apply_executed": False,
|
||||||
"readonly_transaction_wrapper": True,
|
"readonly_transaction_wrapper": True,
|
||||||
"ssh_target": args.ssh_target if args.execute else None,
|
"ssh_target": args.ssh_target if args.execute else None,
|
||||||
|
"transport": args.transport,
|
||||||
"command_redacted": command_for_report(args) if args.execute else None,
|
"command_redacted": command_for_report(args) if args.execute else None,
|
||||||
"preflight_count": len(results),
|
"preflight_count": len(results),
|
||||||
"passes": sum(1 for item in results if item["ok"]),
|
"passes": sum(1 for item in results if item["ok"]),
|
||||||
|
|
@ -234,6 +239,12 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
parser.add_argument("--out", type=Path, default=DEFAULT_OUT)
|
parser.add_argument("--out", type=Path, default=DEFAULT_OUT)
|
||||||
parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MARKDOWN_OUT)
|
parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MARKDOWN_OUT)
|
||||||
parser.add_argument("--execute", action="store_true", help="run the preflight SQL against the VPS")
|
parser.add_argument("--execute", action="store_true", help="run the preflight SQL against the VPS")
|
||||||
|
parser.add_argument(
|
||||||
|
"--transport",
|
||||||
|
choices=["ssh", "local-docker"],
|
||||||
|
default="ssh",
|
||||||
|
help="Use ssh from an operator machine, or local-docker when already running on the VPS.",
|
||||||
|
)
|
||||||
parser.add_argument("--ssh-target", default=DEFAULT_SSH_TARGET)
|
parser.add_argument("--ssh-target", default=DEFAULT_SSH_TARGET)
|
||||||
parser.add_argument("--ssh-key", type=Path, default=DEFAULT_SSH_KEY)
|
parser.add_argument("--ssh-key", type=Path, default=DEFAULT_SSH_KEY)
|
||||||
parser.add_argument("--connect-timeout", type=int, default=15)
|
parser.add_argument("--connect-timeout", type=int, default=15)
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,13 @@ sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||||
import collect_working_leo_production_preflight as preflight # noqa: E402
|
import collect_working_leo_production_preflight as preflight # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
def _args(tmp_path: Path, *, execute: bool = False) -> argparse.Namespace:
|
def _args(tmp_path: Path, *, execute: bool = False, transport: str = "ssh") -> argparse.Namespace:
|
||||||
return argparse.Namespace(
|
return argparse.Namespace(
|
||||||
report_dir=REPO_ROOT / preflight.REPO_REPORT_DIR,
|
report_dir=REPO_ROOT / preflight.REPO_REPORT_DIR,
|
||||||
out=tmp_path / "preflight.json",
|
out=tmp_path / "preflight.json",
|
||||||
markdown_out=tmp_path / "preflight.md",
|
markdown_out=tmp_path / "preflight.md",
|
||||||
execute=execute,
|
execute=execute,
|
||||||
|
transport=transport,
|
||||||
ssh_target="root@example.invalid",
|
ssh_target="root@example.invalid",
|
||||||
ssh_key=tmp_path / "ssh-key",
|
ssh_key=tmp_path / "ssh-key",
|
||||||
connect_timeout=3,
|
connect_timeout=3,
|
||||||
|
|
@ -89,6 +90,31 @@ def test_execute_mode_wraps_sql_readonly_and_uses_ssh(monkeypatch, tmp_path: Pat
|
||||||
assert call["input"].rstrip().endswith("commit;")
|
assert call["input"].rstrip().endswith("commit;")
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_mode_can_run_local_docker_on_vps(monkeypatch, tmp_path: Path):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class Completed:
|
||||||
|
returncode = 0
|
||||||
|
stdout = "ok\n"
|
||||||
|
stderr = ""
|
||||||
|
|
||||||
|
def fake_run(command, *, input, text, capture_output, timeout, check):
|
||||||
|
calls.append({"command": command, "input": input})
|
||||||
|
return Completed()
|
||||||
|
|
||||||
|
monkeypatch.setattr(preflight.subprocess, "run", fake_run)
|
||||||
|
|
||||||
|
report = preflight.collect_preflight(_args(tmp_path, execute=True, transport="local-docker"))
|
||||||
|
|
||||||
|
assert report["status"] == "pass"
|
||||||
|
assert report["transport"] == "local-docker"
|
||||||
|
assert len(calls) == 5
|
||||||
|
for call in calls:
|
||||||
|
assert call["command"][:3] == ["docker", "exec", "-i"]
|
||||||
|
assert "ssh" not in call["command"]
|
||||||
|
assert call["input"].startswith("\\set ON_ERROR_STOP on\nbegin transaction read only;\n")
|
||||||
|
|
||||||
|
|
||||||
def test_markdown_preserves_claim_ceiling(tmp_path: Path):
|
def test_markdown_preserves_claim_ceiling(tmp_path: Path):
|
||||||
report = preflight.collect_preflight(_args(tmp_path))
|
report = preflight.collect_preflight(_args(tmp_path))
|
||||||
out = tmp_path / "preflight.md"
|
out = tmp_path / "preflight.md"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue