Files
awoooi/scripts/reboot-recovery/build-windows99-vmx-source-backup-search-receipt.py
ogt a636fa5b11
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m12s
CD Pipeline / build-and-deploy (push) Successful in 7m4s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 1s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 19s
CD Pipeline / post-deploy-checks (push) Has been cancelled
fix(reboot): automate windows99 backup search receipt
2026-07-09 17:10:32 +08:00

245 lines
9.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Build a public-safe Windows99 VMX backup-search receipt.
The collector emits no-secret, path-fingerprint-only text. This builder turns
that stdout into the JSON receipt consumed by the AWOOOI runtime readback API.
It intentionally keeps raw LAN topology, filesystem paths, and fingerprint
values out of the public receipt.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Any
SCHEMA_VERSION = "windows99_vmx_source_backup_search_receipt_v1"
COLLECTOR_SCHEMA_VERSION = "windows99_vmx_source_backup_search_collector_v1"
DEFAULT_OUTPUT = Path("docs/operations/windows99-vmx-source-backup-search-receipt.snapshot.json")
FORBIDDEN_FRAGMENTS = ("192.168.0.", "D:\\", "C:\\", "E:\\", "/Users/", "/Volumes/")
def taipei_now_iso() -> str:
return datetime.now(timezone(timedelta(hours=8))).replace(microsecond=0).isoformat()
def safe_alias(value: str) -> str:
text = value.strip()
if re.fullmatch(r"[A-Za-z0-9_-]{1,32}", text):
return text
return "redacted"
def parse_kv_line(line: str) -> dict[str, str]:
values: dict[str, str] = {}
for token in line.strip().split():
if "=" not in token:
continue
key, value = token.split("=", 1)
values[key] = value
return values
def parse_collector_stdout(text: str) -> tuple[dict[str, str], list[dict[str, Any]]]:
globals_: dict[str, str] = {}
rows: list[dict[str, Any]] = []
for raw_line in text.splitlines():
line = raw_line.strip()
if not line:
continue
if line.startswith("HOST_SEARCH "):
kv = parse_kv_line(line[len("HOST_SEARCH ") :])
alias = safe_alias(kv.get("alias", "unknown"))
status = kv.get("status", "unknown").strip() or "unknown"
try:
count = int(kv.get("candidate_source_count", "0"))
except ValueError:
count = 0
fingerprints = [
part
for part in kv.get("path_fingerprints", "").split(",")
if re.fullmatch(r"[A-Fa-f0-9]{8,128}|fp[-A-Za-z0-9_]+", part)
]
known = status in {"searched", "not_matched"}
rows.append(
{
"host_alias": alias,
"search_role": "linux_backup_roots",
"status": status,
"candidate_source_count": max(count, 0),
"candidate_source_count_known": known,
"candidate_source_fingerprint_count": len(fingerprints),
"collector": "collect-windows99-vmx-source-backup-search.sh",
}
)
continue
if "=" in line:
key, value = line.split("=", 1)
globals_[key.strip()] = value.strip()
return globals_, rows
def locator_row(locator_receipt: dict[str, Any]) -> dict[str, Any]:
status = str(locator_receipt.get("locate_status") or "not_collected")
known = locator_receipt.get("candidate_source_count_known") is True
candidate_count = int(locator_receipt.get("candidate_source_count") or 0) if known else 0
fingerprint_count = int(locator_receipt.get("candidate_source_fingerprint_count") or 0)
return {
"host_alias": "99",
"search_role": "windows_vmware_roots_via_locator",
"status": status,
"candidate_source_count": candidate_count,
"candidate_source_count_known": known,
"candidate_source_fingerprint_count": fingerprint_count,
"collector": "windows99-vmx-source-locator-readback",
}
def load_locator_receipt(path: Path | None) -> dict[str, Any]:
if path is None or not path.exists():
return {}
payload = json.loads(path.read_text(encoding="utf-8"))
return payload if isinstance(payload, dict) else {}
def build_receipt(
collector_stdout: str,
*,
generated_at: str,
locator_receipt: dict[str, Any] | None = None,
) -> dict[str, Any]:
globals_, rows = parse_collector_stdout(collector_stdout)
if globals_.get("schema_version") != COLLECTOR_SCHEMA_VERSION:
raise ValueError("collector stdout missing expected schema_version")
host_rows: list[dict[str, Any]] = []
if locator_receipt:
host_rows.append(locator_row(locator_receipt))
host_rows.extend(rows)
searched_count = sum(
1 for row in host_rows if row.get("status") in {"searched", "not_matched"}
)
blocked_count = sum(
1
for row in host_rows
if str(row.get("status") or "").startswith("blocked_")
or row.get("status") == "transport_intermittent"
)
candidate_count = sum(int(row.get("candidate_source_count") or 0) for row in host_rows)
fingerprint_count = sum(
int(row.get("candidate_source_fingerprint_count") or 0) for row in host_rows
)
if candidate_count > 0:
search_status = "candidate_fingerprint_found_requires_internal_path_resolution"
next_step = "resolve_candidate_fingerprint_to_authorized_internal_path"
safe_next = (
"resolve_candidate_fingerprint_to_authorized_internal_path_then_"
"build_scoped_relink_dry_run_no_write"
)
elif blocked_count > 0:
search_status = "blocked_no_verified_vmx_source_found_blocked_hosts_unavailable"
next_step = "continue_verified_backup_inventory_search_or_authorized_console_source_path"
safe_next = (
"continue_verified_backup_inventory_search_or_authorized_console_source_path_"
"then_scoped_dry_run_no_write"
)
else:
search_status = "blocked_no_verified_vmx_source_found"
next_step = "continue_verified_backup_inventory_search_or_console_source_path"
safe_next = (
"recover_verified_external_backup_artifact_or_authorized_console_path_"
"then_build_scoped_relink_dry_run_no_write"
)
receipt = {
"schema_version": SCHEMA_VERSION,
"generated_at": generated_at,
"receipt_ref": "log://awoooi/p0-006/windows99-vmx-source-backup-search-111",
"source": "live_no_secret_filename_search",
"collector_readback_present": True,
"collector_schema_version": COLLECTOR_SCHEMA_VERSION,
"target_host_alias": "99",
"target_vm_aliases": ["111"],
"search_mode": "filename_path_fingerprint_only",
"host_rows": host_rows,
"searched_host_count": searched_count,
"blocked_host_count": blocked_count,
"candidate_source_count": candidate_count,
"candidate_source_fingerprint_count": fingerprint_count,
"search_status": search_status,
"next_executable_step": next_step,
"safe_next_step": safe_next,
"public_lan_topology_redacted": True,
"redacted_public_display_only": True,
"raw_path_output": False,
"path_fingerprint_only": True,
"file_content_read": False,
"secret_value_read": False,
"password_prompt_allowed": False,
"remote_write_performed": False,
"host_reboot_performed": False,
"vm_power_change_performed": False,
"windows_service_restart_performed": False,
"windows_registry_apply_performed": False,
"scheduled_task_write_performed": False,
"km_writeback_ref": "km://awoooi/reboot-slo/windows99-vmx-source-backup-search-111",
"playbook_trust_writeback_ref": (
"playbook://awoooi/windows99-vmx-source-backup-search/"
"windows99-vmx-source-backup-search-111"
),
"post_search_verifier": [
"GET /api/v1/agents/windows99-vmx-source-backup-search-package",
"GET /api/v1/agents/windows99-vmx-source-locator-readback",
"GET /api/v1/agents/reboot-auto-recovery-slo-scorecard",
"GET /api/v1/agents/awoooi-priority-work-order-readback",
],
}
serialized = json.dumps(receipt, ensure_ascii=False)
leaked = [fragment for fragment in FORBIDDEN_FRAGMENTS if fragment in serialized]
if leaked:
raise ValueError(f"unsafe receipt fragments: {leaked}")
return receipt
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Build the public-safe Windows99 VMX backup-search receipt."
)
parser.add_argument("--collector-output", type=Path)
parser.add_argument("--locator-receipt", type=Path)
parser.add_argument("--generated-at", default=taipei_now_iso())
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
parser.add_argument("--stdout", action="store_true")
return parser.parse_args(argv)
def main(argv: list[str]) -> int:
args = parse_args(argv)
text = (
args.collector_output.read_text(encoding="utf-8")
if args.collector_output
else sys.stdin.read()
)
receipt = build_receipt(
text,
generated_at=args.generated_at,
locator_receipt=load_locator_receipt(args.locator_receipt),
)
rendered = json.dumps(receipt, ensure_ascii=False, indent=2) + "\n"
if args.stdout:
sys.stdout.write(rendered)
else:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(rendered, encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))