fix(reboot): add windows99 vmx backup search package
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m14s
CD Pipeline / build-and-deploy (push) Successful in 4m45s
CD Pipeline / post-deploy-checks (push) Successful in 1m57s

This commit is contained in:
Your Name
2026-07-03 18:06:34 +08:00
parent b0dc42aa6f
commit 796b66c967
14 changed files with 1260 additions and 56 deletions

View File

@@ -2987,6 +2987,9 @@ def _enrich_from_current_readbacks(
from src.services.windows99_vmx_source_locator_readback import (
build_windows99_vmx_source_locator_readback,
)
from src.services.windows99_vmx_source_backup_search_package import (
build_windows99_vmx_source_backup_search_package,
)
from src.services.windows99_vmware_autostart_check_mode_package import (
build_windows99_vmware_autostart_check_mode_package,
)
@@ -3044,6 +3047,12 @@ def _enrich_from_current_readbacks(
repair_package=windows99_vmx_source_repair_package,
)
)
windows99_vmx_source_backup_search_package = (
build_windows99_vmx_source_backup_search_package(
reboot_slo,
locator_readback=windows99_vmx_source_locator_readback,
)
)
windows99_vmx_source_repair_action_chain = _dict(
windows99_vmx_source_repair_package.get("action_chain")
)
@@ -3818,6 +3827,74 @@ def _enrich_from_current_readbacks(
state["windows99_vmx_source_locator_vm_power_change_performed"] = bool(
windows99_vmx_source_locator_readback.get("vm_power_change_performed") is True
)
state["windows99_vmx_source_backup_search_package_status"] = str(
windows99_vmx_source_backup_search_package.get("status") or "unknown"
)
state["windows99_vmx_source_backup_search_package_ready"] = bool(
windows99_vmx_source_backup_search_package.get("package_ready") is True
)
state["windows99_vmx_source_backup_search_target_aliases"] = _strings(
windows99_vmx_source_backup_search_package.get("target_vm_aliases")
)
state["windows99_vmx_source_backup_search_next_executable_step"] = str(
windows99_vmx_source_backup_search_package.get("next_executable_step") or ""
)
state["windows99_vmx_source_backup_search_safe_next_step"] = str(
windows99_vmx_source_backup_search_package.get("safe_next_step") or ""
)
state["windows99_vmx_source_backup_search_receipt_readback_present"] = bool(
windows99_vmx_source_backup_search_package.get("receipt_readback_present")
is True
)
state["windows99_vmx_source_backup_search_host_count"] = _int(
windows99_vmx_source_backup_search_package.get("search_host_count")
)
state["windows99_vmx_source_backup_search_searched_host_count"] = _int(
windows99_vmx_source_backup_search_package.get("searched_host_count")
)
state["windows99_vmx_source_backup_search_blocked_host_count"] = _int(
windows99_vmx_source_backup_search_package.get("blocked_host_count")
)
state["windows99_vmx_source_backup_search_pending_host_count"] = _int(
windows99_vmx_source_backup_search_package.get("pending_host_count")
)
state["windows99_vmx_source_backup_search_blocked_host_aliases"] = _strings(
windows99_vmx_source_backup_search_package.get("blocked_host_aliases")
)
state["windows99_vmx_source_backup_search_candidate_source_count"] = _int(
windows99_vmx_source_backup_search_package.get("candidate_source_count")
)
state["windows99_vmx_source_backup_search_candidate_fingerprint_count"] = _int(
windows99_vmx_source_backup_search_package.get(
"candidate_source_fingerprint_count"
)
)
state["windows99_vmx_source_backup_search_candidate_source_found"] = bool(
windows99_vmx_source_backup_search_package.get("candidate_source_found")
is True
)
state["windows99_vmx_source_backup_search_apply_allowed_by_this_package"] = bool(
windows99_vmx_source_backup_search_package.get("apply_allowed_by_this_package")
is True
)
state["windows99_vmx_source_backup_search_raw_path_output"] = bool(
windows99_vmx_source_backup_search_package.get("raw_path_output") is True
)
state["windows99_vmx_source_backup_search_path_fingerprint_only"] = bool(
windows99_vmx_source_backup_search_package.get("path_fingerprint_only")
is True
)
state["windows99_vmx_source_backup_search_file_content_read"] = bool(
windows99_vmx_source_backup_search_package.get("file_content_read") is True
)
state["windows99_vmx_source_backup_search_remote_write_performed"] = bool(
windows99_vmx_source_backup_search_package.get("remote_write_performed")
is True
)
state["windows99_vmx_source_backup_search_vm_power_change_performed"] = bool(
windows99_vmx_source_backup_search_package.get("vm_power_change_performed")
is True
)
state["stale_snapshot_or_old_cd_runs_must_not_reopen_closed_work"] = True
state["p0_004_template_copy_apply_gate_production_http_status"] = 200
state["p0_004_template_copy_apply_gate_runtime_readback_state"] = (
@@ -4404,6 +4481,66 @@ def _enrich_from_current_readbacks(
evidence["windows99_vmx_source_locator_vm_power_change_performed"] = state[
"windows99_vmx_source_locator_vm_power_change_performed"
]
evidence["windows99_vmx_source_backup_search_package_status"] = state[
"windows99_vmx_source_backup_search_package_status"
]
evidence["windows99_vmx_source_backup_search_package_ready"] = state[
"windows99_vmx_source_backup_search_package_ready"
]
evidence["windows99_vmx_source_backup_search_target_aliases"] = state[
"windows99_vmx_source_backup_search_target_aliases"
]
evidence["windows99_vmx_source_backup_search_next_executable_step"] = state[
"windows99_vmx_source_backup_search_next_executable_step"
]
evidence["windows99_vmx_source_backup_search_safe_next_step"] = state[
"windows99_vmx_source_backup_search_safe_next_step"
]
evidence["windows99_vmx_source_backup_search_receipt_readback_present"] = (
state["windows99_vmx_source_backup_search_receipt_readback_present"]
)
evidence["windows99_vmx_source_backup_search_host_count"] = state[
"windows99_vmx_source_backup_search_host_count"
]
evidence["windows99_vmx_source_backup_search_searched_host_count"] = state[
"windows99_vmx_source_backup_search_searched_host_count"
]
evidence["windows99_vmx_source_backup_search_blocked_host_count"] = state[
"windows99_vmx_source_backup_search_blocked_host_count"
]
evidence["windows99_vmx_source_backup_search_pending_host_count"] = state[
"windows99_vmx_source_backup_search_pending_host_count"
]
evidence["windows99_vmx_source_backup_search_blocked_host_aliases"] = state[
"windows99_vmx_source_backup_search_blocked_host_aliases"
]
evidence["windows99_vmx_source_backup_search_candidate_source_count"] = state[
"windows99_vmx_source_backup_search_candidate_source_count"
]
evidence[
"windows99_vmx_source_backup_search_candidate_fingerprint_count"
] = state["windows99_vmx_source_backup_search_candidate_fingerprint_count"]
evidence["windows99_vmx_source_backup_search_candidate_source_found"] = state[
"windows99_vmx_source_backup_search_candidate_source_found"
]
evidence[
"windows99_vmx_source_backup_search_apply_allowed_by_this_package"
] = state["windows99_vmx_source_backup_search_apply_allowed_by_this_package"]
evidence["windows99_vmx_source_backup_search_raw_path_output"] = state[
"windows99_vmx_source_backup_search_raw_path_output"
]
evidence["windows99_vmx_source_backup_search_path_fingerprint_only"] = state[
"windows99_vmx_source_backup_search_path_fingerprint_only"
]
evidence["windows99_vmx_source_backup_search_file_content_read"] = state[
"windows99_vmx_source_backup_search_file_content_read"
]
evidence["windows99_vmx_source_backup_search_remote_write_performed"] = state[
"windows99_vmx_source_backup_search_remote_write_performed"
]
evidence[
"windows99_vmx_source_backup_search_vm_power_change_performed"
] = state["windows99_vmx_source_backup_search_vm_power_change_performed"]
evidence["drill_preflight_status"] = str(reboot_preflight.get("status") or "")
evidence["drill_preflight_ready"] = (
reboot_preflight_rollups.get("preflight_ready") is True
@@ -4684,11 +4821,29 @@ def _refresh_rollups_after_stockplatform_overlay(
rollups.get("active_p0_event_gated_by_fresh_reboot_window_only") is True
and not stock_blocked
)
rollups["windows99_vmx_source_backup_search_status"] = str(
state.get("windows99_vmx_source_backup_search_package_status") or "unknown"
)
rollups["windows99_vmx_source_backup_search_candidate_source_count"] = _int(
state.get("windows99_vmx_source_backup_search_candidate_source_count")
)
rollups["windows99_vmx_source_backup_search_blocked_host_count"] = _int(
state.get("windows99_vmx_source_backup_search_blocked_host_count")
)
summary = _dict(payload.setdefault("summary", {}))
summary["status"] = str(payload.get("status") or "")
summary["active_p0_state"] = str(state.get("active_p0_state") or "")
summary["active_p0_live_active_blockers"] = active_blockers
summary["windows99_vmx_source_backup_search_status"] = str(
state.get("windows99_vmx_source_backup_search_package_status") or "unknown"
)
summary["windows99_vmx_source_backup_search_candidate_source_count"] = _int(
state.get("windows99_vmx_source_backup_search_candidate_source_count")
)
summary["windows99_vmx_source_backup_search_safe_next_step"] = str(
state.get("windows99_vmx_source_backup_search_safe_next_step") or ""
)
summary["stockplatform_public_api_runtime_status"] = str(
state.get("stockplatform_public_api_runtime_status") or "unknown"
)
@@ -5309,6 +5464,33 @@ def _set_rollups_and_summary(
state.get("windows99_vmx_source_locator_vm_power_change_performed")
is True
),
"windows99_vmx_source_backup_search_status": str(
state.get("windows99_vmx_source_backup_search_package_status")
or "unknown"
),
"windows99_vmx_source_backup_search_package_ready": (
state.get("windows99_vmx_source_backup_search_package_ready") is True
),
"windows99_vmx_source_backup_search_candidate_source_count": _int(
state.get("windows99_vmx_source_backup_search_candidate_source_count")
),
"windows99_vmx_source_backup_search_blocked_host_count": _int(
state.get("windows99_vmx_source_backup_search_blocked_host_count")
),
"windows99_vmx_source_backup_search_apply_allowed_by_this_package": (
state.get(
"windows99_vmx_source_backup_search_apply_allowed_by_this_package"
)
is True
),
"windows99_vmx_source_backup_search_remote_write_performed": (
state.get("windows99_vmx_source_backup_search_remote_write_performed")
is True
),
"windows99_vmx_source_backup_search_vm_power_change_performed": (
state.get("windows99_vmx_source_backup_search_vm_power_change_performed")
is True
),
"p0_004_runtime_readback_ready": p0_004_ready,
"reboot_drill_preflight_runtime_readback_ready": (
state.get("reboot_drill_preflight_runtime_readback_state") == "ready"
@@ -5744,6 +5926,36 @@ def _set_rollups_and_summary(
state.get("windows99_vmx_source_locator_vm_power_change_performed")
is True
),
"windows99_vmx_source_backup_search_status": str(
state.get("windows99_vmx_source_backup_search_package_status")
or "unknown"
),
"windows99_vmx_source_backup_search_package_ready": (
state.get("windows99_vmx_source_backup_search_package_ready") is True
),
"windows99_vmx_source_backup_search_candidate_source_count": _int(
state.get("windows99_vmx_source_backup_search_candidate_source_count")
),
"windows99_vmx_source_backup_search_blocked_host_count": _int(
state.get("windows99_vmx_source_backup_search_blocked_host_count")
),
"windows99_vmx_source_backup_search_safe_next_step": str(
state.get("windows99_vmx_source_backup_search_safe_next_step") or ""
),
"windows99_vmx_source_backup_search_apply_allowed_by_this_package": (
state.get(
"windows99_vmx_source_backup_search_apply_allowed_by_this_package"
)
is True
),
"windows99_vmx_source_backup_search_remote_write_performed": (
state.get("windows99_vmx_source_backup_search_remote_write_performed")
is True
),
"windows99_vmx_source_backup_search_vm_power_change_performed": (
state.get("windows99_vmx_source_backup_search_vm_power_change_performed")
is True
),
"windows99_winrm_http_open": (
state.get("windows99_winrm_http_open") is True
),

View File

@@ -0,0 +1,323 @@
"""Windows99 VMX source backup-search check-mode package.
This readback turns "111 VMX source not found" into a fixed no-secret search
contract across backup/source hosts. It does not SSH from the API, read VMX
contents, expose raw paths, write Windows state, start VMs, stop VMs, restart
services, or reboot hosts.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from src.services.reboot_auto_recovery_slo_scorecard import (
_taipei_now_iso,
load_latest_reboot_auto_recovery_slo_scorecard,
)
from src.services.snapshot_paths import default_operations_dir
from src.services.windows99_vmx_source_locator_readback import (
build_windows99_vmx_source_locator_readback,
)
from src.services.windows99_vmx_source_repair_package import (
build_windows99_vmx_source_repair_package,
)
_DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__))
_SCHEMA_VERSION = "windows99_vmx_source_backup_search_package_v1"
_RECEIPT_SCHEMA_VERSION = "windows99_vmx_source_backup_search_receipt_v1"
_RECEIPT_FILE = "windows99-vmx-source-backup-search-receipt.snapshot.json"
_COLLECTOR_SCRIPT_PATH = (
"scripts/reboot-recovery/collect-windows99-vmx-source-backup-search.sh"
)
_DEFAULT_SEARCH_HOSTS = ["99", "110", "120", "121", "188", "112", "local"]
_FORBIDDEN_ACTIONS = [
"read_windows_password_or_secret",
"read_vmx_file_contents",
"emit_raw_internal_paths_on_public_endpoint",
"vm_power_change",
"vmrun_start_or_stop",
"host_reboot",
"windows_service_restart",
"windows_registry_apply",
"scheduled_task_register_or_modify",
"docker_nginx_k3s_db_firewall_restart",
"destructive_restore",
]
def _dict(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _list(value: Any) -> list[Any]:
return value if isinstance(value, list) else []
def _strings(value: Any) -> list[str]:
if isinstance(value, list):
return [str(item).strip() for item in value if str(item).strip()]
if isinstance(value, str):
return [part.strip() for part in value.split(",") if part.strip()]
return []
def _int(value: Any, default: int = 0) -> int:
if isinstance(value, bool):
return default
if isinstance(value, int):
return value
if isinstance(value, str):
try:
return int(value.strip())
except ValueError:
return default
return default
def _load_backup_search_receipt(operations_dir: Path | None = None) -> dict[str, Any]:
path = (operations_dir or _DEFAULT_OPERATIONS_DIR) / _RECEIPT_FILE
if not path.exists():
return {}
payload = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(payload, dict):
raise ValueError(f"{path}: expected JSON object")
if payload.get("schema_version") != _RECEIPT_SCHEMA_VERSION:
raise ValueError(f"{path}: unexpected schema_version")
serialized = json.dumps(payload, ensure_ascii=False)
forbidden = ["192.168.0.", "D:\\", "C:\\", "E:\\", "/Users/", "/Volumes/"]
leaked = [fragment for fragment in forbidden if fragment in serialized]
if leaked:
raise ValueError(f"{path}: unsafe backup-search receipt fragments: {leaked}")
return payload
def _receipt_applies(receipt: dict[str, Any], target_aliases: list[str]) -> bool:
receipt_aliases = _strings(receipt.get("target_vm_aliases"))
return bool(
receipt.get("collector_readback_present") is True
and receipt.get("target_host_alias") == "99"
and receipt_aliases
and set(receipt_aliases).issubset(set(target_aliases or receipt_aliases))
)
def _default_host_rows(locator_readback: dict[str, Any]) -> list[dict[str, Any]]:
locator_candidate_count = _int(locator_readback.get("candidate_source_count"), 0)
locator_known = locator_readback.get("candidate_source_count_known") is True
rows = [
{
"host_alias": "99",
"search_role": "windows_vmware_roots_via_locator",
"status": str(locator_readback.get("locate_status") or "not_collected"),
"candidate_source_count": locator_candidate_count if locator_known else 0,
"candidate_source_count_known": locator_known,
"collector": "windows99-vmx-source-locator-readback",
}
]
for alias in ["110", "120", "121", "188", "112", "local"]:
rows.append(
{
"host_alias": alias,
"search_role": "linux_or_local_backup_roots",
"status": "check_mode_pending",
"candidate_source_count": 0,
"candidate_source_count_known": False,
"collector": _COLLECTOR_SCRIPT_PATH,
}
)
return rows
def _host_rows_from_receipt(
receipt: dict[str, Any],
locator_readback: dict[str, Any],
) -> list[dict[str, Any]]:
rows = _list(receipt.get("host_rows"))
if rows:
return [dict(row) for row in rows if isinstance(row, dict)]
return _default_host_rows(locator_readback)
def build_windows99_vmx_source_backup_search_package(
scorecard: dict[str, Any],
*,
locator_readback: dict[str, Any] | None = None,
collector_receipt: dict[str, Any] | None = None,
operations_dir: Path | None = None,
generated_at: str | None = None,
) -> dict[str, Any]:
repair_package = build_windows99_vmx_source_repair_package(scorecard)
locator = locator_readback or build_windows99_vmx_source_locator_readback(
scorecard,
repair_package=repair_package,
operations_dir=operations_dir,
)
missing_aliases = _strings(repair_package.get("missing_vmx_aliases"))
target_aliases = missing_aliases or _strings(locator.get("target_vm_aliases"))
receipt = (
_dict(collector_receipt)
if collector_receipt is not None
else _load_backup_search_receipt(operations_dir)
)
receipt_applies = _receipt_applies(receipt, target_aliases)
host_rows = (
_host_rows_from_receipt(receipt, locator)
if receipt_applies
else _default_host_rows(locator)
)
searched_rows = [
row for row in host_rows if row.get("status") in {"searched", "not_matched"}
]
blocked_rows = [
row
for row in host_rows
if str(row.get("status") or "").startswith("blocked_")
or row.get("status") == "transport_intermittent"
]
pending_rows = [
row
for row in host_rows
if row.get("candidate_source_count_known") is not True
and row not in blocked_rows
]
candidate_count = sum(_int(row.get("candidate_source_count"), 0) for row in host_rows)
candidate_fingerprint_count = sum(
_int(row.get("candidate_source_fingerprint_count"), 0) for row in host_rows
)
locator_candidate_count = _int(locator.get("candidate_source_count"), 0)
locator_no_candidate = bool(
locator.get("candidate_source_count_known") is True
and locator_candidate_count == 0
)
search_required = bool(missing_aliases)
if not search_required:
status = "ready_no_windows99_vmx_backup_search_required"
next_step = "rerun_no_secret_vmware_verify_collector"
safe_next_step = "rerun_no_secret_collector_and_reboot_slo_scorecard_verify_only"
elif candidate_count > 0:
status = "candidate_fingerprint_found_requires_internal_path_resolution"
next_step = "resolve_candidate_fingerprint_to_authorized_internal_path"
safe_next_step = (
"resolve_candidate_fingerprint_to_authorized_internal_path_then_"
"build_scoped_relink_dry_run_no_write"
)
elif receipt_applies and blocked_rows:
status = "blocked_windows99_vmx_backup_source_not_found_some_hosts_unsearched"
next_step = "rerun_backup_search_for_blocked_hosts_or_offline_inventory"
safe_next_step = (
"rerun_windows99_vmx_source_backup_search_check_mode_for_blocked_hosts_"
"then_update_receipt_no_write"
)
elif receipt_applies:
status = "blocked_windows99_vmx_backup_source_not_found"
next_step = "continue_verified_backup_inventory_search_or_console_source_path"
safe_next_step = (
"recover_verified_external_backup_artifact_or_authorized_console_path_"
"then_build_scoped_relink_dry_run_no_write"
)
elif locator_no_candidate:
status = "check_mode_package_ready_windows99_vmx_backup_search_required"
next_step = "run_windows99_vmx_source_backup_search_check_mode"
safe_next_step = (
"bash scripts/reboot-recovery/collect-windows99-vmx-source-backup-"
"search.sh --timeout 35"
)
else:
status = "check_mode_package_ready_windows99_vmx_identity_or_backup_search_required"
next_step = "run_locator_confirmation_or_backup_search_check_mode"
safe_next_step = (
"confirm_locator_candidate_identity_or_run_backup_search_check_mode_"
"before_scoped_relink_dry_run"
)
return {
"schema_version": _SCHEMA_VERSION,
"generated_at": generated_at
or str(scorecard.get("generated_at") or _taipei_now_iso()),
"status": status,
"package_ready": search_required,
"target_host_alias": "99",
"target_vm_aliases": target_aliases,
"missing_vmx_aliases": missing_aliases,
"next_executable_step": next_step,
"safe_next_step": safe_next_step,
"collector_script_path": _COLLECTOR_SCRIPT_PATH,
"collector_schema_version": "windows99_vmx_source_backup_search_collector_v1",
"collector_command": (
"bash scripts/reboot-recovery/collect-windows99-vmx-source-backup-"
"search.sh --timeout 35"
),
"receipt_readback_present": receipt_applies,
"receipt_ref": str(receipt.get("receipt_ref") or "") if receipt_applies else "",
"receipt_generated_at": str(receipt.get("generated_at") or "")
if receipt_applies
else "",
"source_locator_status": str(locator.get("status") or "unknown"),
"source_locator_candidate_source_count": locator_candidate_count,
"source_locator_candidate_source_count_known": bool(
locator.get("candidate_source_count_known") is True
),
"search_host_aliases": _DEFAULT_SEARCH_HOSTS,
"host_rows": host_rows,
"search_host_count": len(host_rows),
"searched_host_count": len(searched_rows),
"blocked_host_count": len(blocked_rows),
"pending_host_count": len(pending_rows),
"blocked_host_aliases": _strings(
[row.get("host_alias") for row in blocked_rows]
),
"candidate_source_count": candidate_count,
"candidate_source_fingerprint_count": candidate_fingerprint_count,
"candidate_source_found": candidate_count > 0,
"internal_path_resolution_required": candidate_count > 0,
"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,
"vm_power_change_allowed": False,
"destructive_restore_allowed": False,
"apply_allowed_by_this_package": False,
"forbidden_actions": _FORBIDDEN_ACTIONS,
"operation_boundaries": {
"secret_value_read": False,
"password_prompt_allowed": False,
"file_content_read": False,
"raw_path_output": False,
"path_fingerprint_only": True,
"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,
},
"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",
],
"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"
),
}
def load_latest_windows99_vmx_source_backup_search_package() -> dict[str, Any]:
return build_windows99_vmx_source_backup_search_package(
load_latest_reboot_auto_recovery_slo_scorecard()
)