fix(reboot): project vmx locator receipt readback
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 3m47s
CD Pipeline / build-and-deploy (push) Successful in 4m48s
CD Pipeline / post-deploy-checks (push) Successful in 1m52s

This commit is contained in:
Your Name
2026-07-03 12:53:33 +08:00
parent 7b02c452e3
commit ba0849f707
9 changed files with 438 additions and 36 deletions

View File

@@ -3457,6 +3457,24 @@ def _enrich_from_current_readbacks(
state["windows99_vmx_source_locator_expected_vmx_path_count"] = _int(
windows99_vmx_source_locator_readback.get("expected_vmx_path_count")
)
state["windows99_vmx_source_locator_candidate_source_count"] = _int(
windows99_vmx_source_locator_readback.get("candidate_source_count")
)
state["windows99_vmx_source_locator_alias_hint_candidate_count"] = _int(
windows99_vmx_source_locator_readback.get("alias_hint_candidate_count")
)
state["windows99_vmx_source_locator_unassigned_ubuntu_candidate_count"] = _int(
windows99_vmx_source_locator_readback.get("unassigned_ubuntu_candidate_count")
)
state["windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count"] = _int(
windows99_vmx_source_locator_readback.get(
"identity_unassigned_ubuntu_candidate_count"
)
)
state["windows99_vmx_source_locator_candidate_confirmation_required"] = bool(
windows99_vmx_source_locator_readback.get("candidate_confirmation_required")
is True
)
state["windows99_vmx_source_locator_candidate_source_count_known"] = bool(
windows99_vmx_source_locator_readback.get("candidate_source_count_known")
is True
@@ -3856,6 +3874,23 @@ def _enrich_from_current_readbacks(
evidence["windows99_vmx_source_locator_expected_vmx_path_count"] = state[
"windows99_vmx_source_locator_expected_vmx_path_count"
]
evidence["windows99_vmx_source_locator_candidate_source_count"] = state[
"windows99_vmx_source_locator_candidate_source_count"
]
evidence["windows99_vmx_source_locator_alias_hint_candidate_count"] = state[
"windows99_vmx_source_locator_alias_hint_candidate_count"
]
evidence["windows99_vmx_source_locator_unassigned_ubuntu_candidate_count"] = state[
"windows99_vmx_source_locator_unassigned_ubuntu_candidate_count"
]
evidence[
"windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count"
] = state[
"windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count"
]
evidence["windows99_vmx_source_locator_candidate_confirmation_required"] = state[
"windows99_vmx_source_locator_candidate_confirmation_required"
]
evidence["windows99_vmx_source_locator_candidate_source_count_known"] = state[
"windows99_vmx_source_locator_candidate_source_count_known"
]
@@ -4630,6 +4665,24 @@ def _set_rollups_and_summary(
"windows99_vmx_source_locator_expected_vmx_path_count": _int(
state.get("windows99_vmx_source_locator_expected_vmx_path_count")
),
"windows99_vmx_source_locator_candidate_source_count": _int(
state.get("windows99_vmx_source_locator_candidate_source_count")
),
"windows99_vmx_source_locator_alias_hint_candidate_count": _int(
state.get("windows99_vmx_source_locator_alias_hint_candidate_count")
),
"windows99_vmx_source_locator_unassigned_ubuntu_candidate_count": _int(
state.get("windows99_vmx_source_locator_unassigned_ubuntu_candidate_count")
),
"windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count": _int(
state.get(
"windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count"
)
),
"windows99_vmx_source_locator_candidate_confirmation_required": (
state.get("windows99_vmx_source_locator_candidate_confirmation_required")
is True
),
"windows99_vmx_source_locator_candidate_source_count_known": (
state.get("windows99_vmx_source_locator_candidate_source_count_known")
is True
@@ -4934,6 +4987,24 @@ def _set_rollups_and_summary(
"windows99_vmx_source_locator_expected_vmx_path_count": _int(
state.get("windows99_vmx_source_locator_expected_vmx_path_count")
),
"windows99_vmx_source_locator_candidate_source_count": _int(
state.get("windows99_vmx_source_locator_candidate_source_count")
),
"windows99_vmx_source_locator_alias_hint_candidate_count": _int(
state.get("windows99_vmx_source_locator_alias_hint_candidate_count")
),
"windows99_vmx_source_locator_unassigned_ubuntu_candidate_count": _int(
state.get("windows99_vmx_source_locator_unassigned_ubuntu_candidate_count")
),
"windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count": _int(
state.get(
"windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count"
)
),
"windows99_vmx_source_locator_candidate_confirmation_required": (
state.get("windows99_vmx_source_locator_candidate_confirmation_required")
is True
),
"windows99_vmx_source_locator_candidate_source_count_known": (
state.get("windows99_vmx_source_locator_candidate_source_count_known")
is True

View File

@@ -8,19 +8,25 @@ 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_repair_package import (
build_windows99_vmx_source_repair_package,
)
_DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__))
_SCHEMA_VERSION = "windows99_vmx_source_locator_readback_v1"
_COLLECTOR_SCHEMA_VERSION = "windows99_vmx_source_locator_collector_v1"
_LOCATOR_SCHEMA_VERSION = "windows99_vmx_source_locator_v1"
_RECEIPT_SCHEMA_VERSION = "windows99_vmx_source_locator_receipt_v1"
_RECEIPT_FILE = "windows99-vmx-source-locator-receipt.snapshot.json"
_COLLECTOR_SCRIPT_PATH = "scripts/reboot-recovery/locate-windows99-vmx-source.sh"
_LOCATOR_SCRIPT_PATH = "scripts/reboot-recovery/windows99-vmx-source-locator.ps1"
@@ -41,6 +47,33 @@ def _bool(value: Any) -> bool:
return value is True
def _int(value: Any, default: int | None = None) -> int | None:
if isinstance(value, bool):
return default
if isinstance(value, int):
return value
if isinstance(value, str) and value.strip().isdigit():
return int(value.strip())
return default
def _load_locator_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:\\", "uuid.", "generatedAddress"]
leaked = [fragment for fragment in forbidden if fragment in serialized]
if leaked:
raise ValueError(f"{path}: unsafe locator receipt fragments: {leaked}")
return payload
def _vm_rows_for_aliases(
repair_package: dict[str, Any],
target_aliases: list[str],
@@ -60,6 +93,8 @@ def build_windows99_vmx_source_locator_readback(
scorecard: dict[str, Any],
*,
repair_package: 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]:
package = repair_package or build_windows99_vmx_source_repair_package(scorecard)
@@ -111,6 +146,21 @@ def build_windows99_vmx_source_locator_readback(
"--collect"
)
receipt = collector_receipt if collector_receipt is not None else _load_locator_receipt(operations_dir)
receipt = _dict(receipt)
receipt_aliases = _strings(receipt.get("target_vm_aliases"))
receipt_applies = 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))
)
receipt_locate_status = str(receipt.get("locate_status") or "")
if receipt_applies:
status = "collector_readback_single_identity_candidate_confirmation_required"
next_step = "confirm_single_unassigned_ubuntu_candidate_before_scoped_relink_dry_run"
safe_next_step = str(receipt.get("safe_next_step") or safe_next_step)
return {
"schema_version": _SCHEMA_VERSION,
"generated_at": generated_at
@@ -127,8 +177,14 @@ def build_windows99_vmx_source_locator_readback(
"locator_script_path": _LOCATOR_SCRIPT_PATH,
"collector_schema_version": _COLLECTOR_SCHEMA_VERSION,
"locator_schema_version": _LOCATOR_SCHEMA_VERSION,
"collector_readback_present": False,
"collector_collection_status": "not_collected_by_api_readback",
"collector_readback_present": receipt_applies,
"collector_receipt_ref": str(receipt.get("receipt_ref") or "") if receipt_applies else "",
"collector_receipt_generated_at": str(receipt.get("generated_at") or "") if receipt_applies else "",
"collector_collection_status": (
str(receipt.get("collector_collection_status") or "")
if receipt_applies
else "not_collected_by_api_readback"
),
"management_channel_ready": management_channel_ready,
"can_collect_vmware_verify_without_secret": (
can_collect_vmware_verify_without_secret
@@ -143,15 +199,57 @@ def build_windows99_vmx_source_locator_readback(
),
"remote_locator_mode": "in_memory_stdin_scriptblock",
"expected_vmx_path_count": expected_vmx_path_count,
"expected_vmx_path_present_count": None,
"candidate_source_count": None,
"candidate_source_count_known": False,
"candidate_source_fingerprint_count": None,
"expected_vmx_path_present_count": (
_int(receipt.get("expected_vmx_path_present_count"))
if receipt_applies
else None
),
"candidate_source_count": (
_int(receipt.get("candidate_source_count")) if receipt_applies else None
),
"candidate_source_count_known": bool(receipt_applies),
"candidate_source_fingerprint_count": (
_int(receipt.get("candidate_source_fingerprint_count"), 0)
if receipt_applies
else None
),
"alias_hint_candidate_count": (
_int(receipt.get("alias_hint_candidate_count"), 0)
if receipt_applies
else None
),
"unassigned_ubuntu_candidate_count": (
_int(receipt.get("unassigned_ubuntu_candidate_count"), 0)
if receipt_applies
else None
),
"identity_metadata_read": bool(
receipt_applies and receipt.get("identity_metadata_read") is True
),
"identity_target_hint_count": (
_int(receipt.get("identity_target_hint_count"), 0)
if receipt_applies
else None
),
"identity_unassigned_ubuntu_candidate_count": (
_int(receipt.get("identity_unassigned_ubuntu_candidate_count"), 0)
if receipt_applies
else None
),
"locate_status": receipt_locate_status if receipt_applies else "",
"candidate_confirmation_required": bool(
receipt_applies and "requires_confirmation" in receipt_locate_status
),
"public_lan_topology_redacted": True,
"redacted_public_display_only": True,
"raw_path_output": False,
"path_fingerprint_only": True,
"file_content_read": False,
"file_content_read": bool(
receipt_applies and receipt.get("identity_metadata_read") is True
),
"bounded_identity_metadata_only": bool(
receipt_applies and receipt.get("bounded_identity_metadata_only") is True
),
"secret_value_read": False,
"password_prompt_allowed": False,
"remote_write_performed": False,
@@ -165,7 +263,12 @@ def build_windows99_vmx_source_locator_readback(
"operation_boundaries": {
"secret_value_read": False,
"password_prompt_allowed": False,
"file_content_read": False,
"file_content_read": bool(
receipt_applies and receipt.get("identity_metadata_read") is True
),
"bounded_identity_metadata_only": bool(
receipt_applies and receipt.get("bounded_identity_metadata_only") is True
),
"raw_path_output": False,
"remote_write_performed": False,
"host_reboot_performed": False,
@@ -177,7 +280,12 @@ def build_windows99_vmx_source_locator_readback(
"allowed": locator_check_mode_ready,
"effective_collect_ready": effective_locator_collect_ready,
"path_existence_only": True,
"vmx_file_content_read": False,
"vmx_file_content_read": bool(
receipt_applies and receipt.get("identity_metadata_read") is True
),
"bounded_identity_metadata_only": bool(
receipt_applies and receipt.get("bounded_identity_metadata_only") is True
),
"raw_path_output": False,
"path_fingerprint_only": True,
"collector_command": safe_next_step if locator_check_mode_ready else "",