From 6813710d0412c6e8ab00ef9ca864f9a65aa56a8d Mon Sep 17 00:00:00 2001 From: ogt Date: Thu, 2 Jul 2026 15:07:55 +0800 Subject: [PATCH] fix(reboot): expose windows99 verify collection packet --- .../reboot_auto_recovery_slo_scorecard.py | 116 +++++++++++++++++ ..._reboot_auto_recovery_slo_scorecard_api.py | 35 +++++ docs/LOGBOOK.md | 16 +++ ...r-inserted-requirements-priority-ledger.md | 8 +- .../reboot-auto-recovery-slo-scorecard.py | 120 ++++++++++++++++++ ...test_reboot_auto_recovery_slo_scorecard.py | 41 ++++++ 6 files changed, 332 insertions(+), 4 deletions(-) diff --git a/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py b/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py index c252a447..8e9f8e39 100644 --- a/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py +++ b/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py @@ -91,6 +91,10 @@ def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]: active_blockers=active_blockers, ) ) + windows99_verify_collection = _build_windows99_verify_collection_packet( + windows99=windows99, + host_boot_detection=host_boot_detection, + ) blocked_by_fresh_reboot_window_only = active_blockers == [ "host_boot_observation_older_than_target_window" ] @@ -180,6 +184,19 @@ def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]: "windows99_vmware_powered_off_count": len( _strings(windows99.get("powered_off_aliases")) ), + "windows99_verify_collection_status": windows99_verify_collection["status"], + "windows99_verify_collection_can_collect_no_secret": ( + windows99_verify_collection["can_collect_no_secret_verify"] is True + ), + "windows99_verify_collection_blocker_count": len( + _strings(windows99_verify_collection.get("collection_blockers")) + ), + "windows99_host99_reachable": ( + windows99_verify_collection["host99_reachable"] is True + ), + "windows99_host99_uptime_known": ( + windows99_verify_collection["host99_uptime_known"] is True + ), } return { "schema_version": _API_SCHEMA_VERSION, @@ -250,6 +267,12 @@ def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]: "windows99_update_no_auto_reboot_ready": rollups[ "windows99_update_no_auto_reboot_ready" ], + "windows99_verify_collection_status": rollups[ + "windows99_verify_collection_status" + ], + "windows99_verify_collection_can_collect_no_secret": rollups[ + "windows99_verify_collection_can_collect_no_secret" + ], }, "reboot_sop_progress": sop_progress, "controlled_service_data_backup_readback": ( @@ -259,6 +282,7 @@ def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]: "post_reboot_readiness": post_reboot_readiness, "stockplatform_data_freshness": stockplatform, "windows99_vmware_autostart": windows99, + "windows99_verify_collection": windows99_verify_collection, "source_controls": source_controls, "active_blockers": active_blockers, "required_checks": required_checks, @@ -382,6 +406,98 @@ def _build_controlled_service_data_backup_readback( } +def _build_windows99_verify_collection_packet( + *, + windows99: dict[str, Any], + host_boot_detection: dict[str, Any], +) -> dict[str, Any]: + host99 = _host_row_by_alias(host_boot_detection, "99") + host99_reachable = host99.get("reachable") is True + host99_uptime_known = bool(host99) and _int(host99.get("uptime_seconds")) >= 0 + readback_present = windows99.get("readback_present") is True + verify_ready = windows99.get("verify_ready") is True + collection_blockers: list[str] = [] + if not host99_reachable: + collection_blockers.append("windows99_host_not_reachable_for_verify_collection") + if not readback_present: + collection_blockers.append("windows99_vmware_autostart_readback_missing") + for blocker in _strings(windows99.get("blockers")): + if blocker not in collection_blockers: + collection_blockers.append(blocker) + if not host99_uptime_known: + collection_blockers.append("windows99_uptime_unknown") + + return { + "schema_version": "windows99_vmware_verify_collection_packet_v1", + "status": ( + "ready_windows99_vmware_verify_readback_green" + if verify_ready + else ( + "blocked_windows99_verify_output_missing_host_reachable" + if host99_reachable and not readback_present + else "blocked_windows99_verify_collection_not_ready" + ) + ), + "target_host_alias": "99", + "target_host": "192.168.0.99", + "host99_reachable": host99_reachable, + "host99_uptime_known": host99_uptime_known, + "readback_present": readback_present, + "verify_ready": verify_ready, + "can_collect_no_secret_verify": host99_reachable and not verify_ready, + "required_vm_aliases": _strings(windows99.get("required_vm_aliases")) + or ["111", "112", "120", "121", "188"], + "expected_no_secret_output_fields": [ + "VMRUN_PRESENT", + "VMX alias= present=<0|1>", + "VMWARE_SERVICE name= ok=<0|1>", + "VMWARE_AUTOSTART_TASK name=AWOOOI-Start-VMware-VMs ok=<0|1>", + "WINDOWS_UPDATE_POLICY name= ok=<0|1>", + "VM_POWER alias= running=<0|1>", + "VMWARE_AUTOSTART_CONFIG_READY", + "VMWARE_AUTOSTART_POWER_READY", + "WINDOWS_UPDATE_NO_AUTO_REBOOT_READY", + "VMWARE_AUTOSTART_VERIFY_READY", + ], + "no_secret_verify_command": ( + "powershell -ExecutionPolicy Bypass -File " + ".\\windows99-vmware-autostart.ps1 -Mode Verify" + ), + "post_verifier": ( + "rerun_reboot_auto_recovery_slo_scorecard_with_" + "windows99_vmware_file_no_secret_no_reboot" + ), + "collection_blockers": collection_blockers, + "safe_collection_channels": [ + "authorized_windows99_console_verify_stdout_only", + "existing_management_channel_verify_mode_only", + "committed_no_secret_artifact_file_then_scorecard_rerun", + ], + "forbidden_actions": [ + "windows_password_or_secret_collection", + "host_reboot", + "vm_power_change", + "windows_update_policy_apply", + "manual_registry_edit", + "service_restart", + "github_api", + ], + } + + +def _host_row_by_alias( + host_boot_detection: dict[str, Any], + alias: str, +) -> dict[str, Any]: + rows = host_boot_detection.get("host_rows") + if not isinstance(rows, list): + return {} + for item in rows: + if isinstance(item, dict) and str(item.get("alias") or "") == alias: + return item + return {} + + def _build_reboot_sop_progress( *, scorecard: dict[str, Any], diff --git a/apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py b/apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py index e7821f36..771234b3 100644 --- a/apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py +++ b/apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py @@ -134,6 +134,13 @@ def _assert_reboot_slo_payload(payload: dict): assert payload["readback"]["latest_verify_only_metric_present"] is False assert payload["readback"]["windows99_vmware_verify_ready"] is False assert payload["readback"]["windows99_update_no_auto_reboot_ready"] is False + assert payload["readback"]["windows99_verify_collection_status"] == ( + "blocked_windows99_verify_output_missing_host_reachable" + ) + assert ( + payload["readback"]["windows99_verify_collection_can_collect_no_secret"] + is True + ) assert payload["rollups"]["active_blocker_count"] == 13 assert payload["rollups"]["readiness_percent"] == 15 assert payload["rollups"]["observed_host_count"] == 7 @@ -167,6 +174,16 @@ def _assert_reboot_slo_payload(payload: dict): assert payload["rollups"]["windows99_vmware_readback_present"] is False assert payload["rollups"]["windows99_vmware_verify_ready"] is False assert payload["rollups"]["windows99_update_no_auto_reboot_ready"] is False + assert payload["rollups"]["windows99_verify_collection_status"] == ( + "blocked_windows99_verify_output_missing_host_reachable" + ) + assert ( + payload["rollups"]["windows99_verify_collection_can_collect_no_secret"] + is True + ) + assert payload["rollups"]["windows99_verify_collection_blocker_count"] == 2 + assert payload["rollups"]["windows99_host99_reachable"] is True + assert payload["rollups"]["windows99_host99_uptime_known"] is False assert payload["rollups"]["stockplatform_final_retry_window_passed"] is False assert ( payload["rollups"]["stockplatform_controlled_recovery_gate_required"] @@ -230,6 +247,24 @@ def _assert_reboot_slo_payload(payload: dict): assert windows99["readback_present"] is False assert windows99["required_vm_aliases"] == ["111", "112", "120", "121", "188"] assert windows99["blockers"] == ["windows99_vmware_autostart_readback_missing"] + collection = payload["windows99_verify_collection"] + assert collection["schema_version"] == ( + "windows99_vmware_verify_collection_packet_v1" + ) + assert collection["target_host"] in {"192.168.0.99", "host:internal-node"} + assert collection["host99_reachable"] is True + assert collection["host99_uptime_known"] is False + assert collection["readback_present"] is False + assert collection["verify_ready"] is False + assert collection["can_collect_no_secret_verify"] is True + assert collection["collection_blockers"] == [ + "windows99_vmware_autostart_readback_missing", + "windows99_uptime_unknown", + ] + assert "VMRUN_PRESENT" in collection["expected_no_secret_output_fields"] + assert "-Mode Verify" in collection["no_secret_verify_command"] + assert "host_reboot" in collection["forbidden_actions"] + assert "windows_password_or_secret_collection" in collection["forbidden_actions"] stockplatform = payload["stockplatform_data_freshness"] assert stockplatform["freshness_endpoint_readback_present"] is True assert stockplatform["ingestion_endpoint_readback_present"] is True diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index f0d57ff3..100b31da 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,19 @@ +## 2026-07-02 — 15:20 P0-006 Windows 99 Verify collection packet 產品化 + +**完成內容**: +- `scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py` 新增 `windows99_verify_collection`,把 99 VMware / Windows Update no-secret Verify 的下一步拆成機器可讀 collection packet。 +- `/api/v1/agents/reboot-auto-recovery-slo-scorecard` loader 即使讀舊 snapshot,也會從 `host_boot_detection` 與 `windows99_vmware_autostart` 推導 `windows99_verify_collection`、`rollups.windows99_verify_collection_status`、`can_collect_no_secret`、`host99_reachable` 與 `host99_uptime_known`。 +- 目前 production truth 仍應維持 fail-closed:99 host 可達但缺 live `Verify` output,`collection_blockers=["windows99_vmware_autostart_readback_missing","windows99_uptime_unknown"]`;不得宣稱 VM autostart / Windows Update policy 已驗證。 +- collection packet 明確列出 expected no-secret output fields、`-Mode Verify` command、post-verifier 與 forbidden actions:不得讀 Windows 密碼 / secret,不得重啟 host,不得改 VM power,不得套用 Windows Update policy,不得 service restart,不得使用 GitHub API。 + +**驗證**: +- `python3.11 -m py_compile scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py apps/api/src/services/reboot_auto_recovery_slo_scorecard.py`:通過。 +- `DATABASE_URL=sqlite:///tmp/awoooi-test.db python3.11 -m pytest -q scripts/reboot-recovery/tests/test_reboot_auto_recovery_slo_scorecard.py apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py`:`14 passed`。 +- 本機 API loader readback:`status=blocked_reboot_auto_recovery_slo_not_ready`、`collection=blocked_windows99_verify_output_missing_host_reachable`、`can_collect=True`、`blockers=windows99_vmware_autostart_readback_missing,windows99_uptime_unknown`。 + +**仍維持**: +- 未執行 Windows / VMware console;未讀密碼、token、secret、`.env`、raw sessions / SQLite / auth;未重啟主機 / VM / Docker / Nginx / K3s / DB / firewall;未觸發 workflow;未使用 GitHub / `gh` / GitHub API。 + ## 2026-07-02 — 14:46 CD controlled-runtime classifier 補上 Windows 99 VMware verifier source **完成內容**: diff --git a/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md b/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md index 67413079..7bdf573f 100644 --- a/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md +++ b/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md @@ -58,9 +58,9 @@ | 順序 | ID | 優先序 | 使用者插入要求 | 正規化工作項 | 目前狀態 | 下一個可驗證動作 | | --- | --- | --- | --- | --- | --- | --- | | 1 | CIR-P0-RBT-001 | P0 | 「主機重啟後 10 分鐘內全部恢復,且要自動判斷所有主機被重啟」 | 建立 99/110/111/112/120/121/188 reboot event detector + 10 分鐘 SLO scorecard + fixed triage order | 部分已有 `reboot_recovery_slo_alerts`、scorecard、textfile;仍需要 fresh all-host reboot/drill 證明 | 產生最新 reboot SLO scorecard readback;若缺 fresh event,標 `awaiting_next_reboot_or_approved_drill`,不可宣稱 10 分鐘 SLA 已證明 | -| 2 | CIR-P0-RBT-002 | P0 | 「沒有偵測到主機重啟」 | 修正 host reboot/shutdown/up detection:boot_id / uptime / node exporter / Windows exporter / VMware VM power state 都要進同一事件 | Source verifier 已補:scorecard 可解析 Windows99 VMware readback;live 99 verify output 仍缺 | 收集 `windows99-vmware-autostart.ps1 -Mode Verify` no-secret output 後 rerun scorecard;缺 99 時不得把 110/120/121/188 green 當全主機 green | -| 3 | CIR-P0-RBT-003 | P0 | 「192.168.0.99 VMWare 要自動啟動,裡面 111/188/120/121/112 也自動啟動」 | Windows 99 VMware host autostart + guest VM autostart contract;VM host 111/188/120/121/112 開機順序與 readback | Source verifier / parser / API readback 已完成;snapshot active blocker=`windows99_vmware_autostart_readback_missing` | 從 99 取得 no-secret Verify output,確認 `VMRUN_PRESENT`、scheduled task、VMware services、VM power、VMX present 全綠 | -| 4 | CIR-P0-RBT-004 | P0 | 「192.168.0.99 不可因 Windows Update 無預警重開」 | Windows Update reboot policy:active hours / no auto-restart / maintenance window / update notification audit | Source verifier 已補 `WINDOWS_UPDATE_POLICY` 與 `WINDOWS_UPDATE_NO_AUTO_REBOOT_READY`;live 99 policy readback 仍缺 | 從 99 取得 Verify output;若 policy 不綠,再走 controlled apply,禁止要求或記錄 Windows 密碼 | +| 2 | CIR-P0-RBT-002 | P0 | 「沒有偵測到主機重啟」 | 修正 host reboot/shutdown/up detection:boot_id / uptime / node exporter / Windows exporter / VMware VM power state 都要進同一事件 | Source verifier 已補:scorecard 可解析 Windows99 VMware readback;`windows99_verify_collection` 已補 no-secret collection packet;live 99 verify output 仍缺 | 收集 `windows99-vmware-autostart.ps1 -Mode Verify` no-secret output 後 rerun scorecard;缺 99 時不得把 110/120/121/188 green 當全主機 green | +| 3 | CIR-P0-RBT-003 | P0 | 「192.168.0.99 VMWare 要自動啟動,裡面 111/188/120/121/112 也自動啟動」 | Windows 99 VMware host autostart + guest VM autostart contract;VM host 111/188/120/121/112 開機順序與 readback | Source verifier / parser / API readback / collection packet 已完成;snapshot active blocker=`windows99_vmware_autostart_readback_missing` | 從 99 取得 no-secret Verify output,確認 `VMRUN_PRESENT`、scheduled task、VMware services、VM power、VMX present 全綠 | +| 4 | CIR-P0-RBT-004 | P0 | 「192.168.0.99 不可因 Windows Update 無預警重開」 | Windows Update reboot policy:active hours / no auto-restart / maintenance window / update notification audit | Source verifier 已補 `WINDOWS_UPDATE_POLICY` 與 `WINDOWS_UPDATE_NO_AUTO_REBOOT_READY`;collection packet 已列 forbidden actions;live 99 policy readback 仍缺 | 從 99 取得 Verify output;若 policy 不綠,再走 controlled apply,禁止要求或記錄 Windows 密碼 | | 5 | CIR-P0-RBT-005 | P0 | 「網站重啟後 502 嚴重影響體驗,要維護頁,外部雲端或專業做法」 | Public maintenance fallback:Nginx / edge / external static maintenance page / status page / fail-open UX,避免 502 直出 | 尚未完整落地;目前是需求缺口 | 產生 `public_maintenance_fallback` decision record:DNS/edge/外部雲端/本地 Nginx fallback 風險比較,先做不切流量的 check-mode | | 6 | CIR-P0-RBT-006 | P0 | 「所有主機關機立刻 Telegram 告警,重啟後也要告警,其他告警一併完整思考」 | Down / shutdown suspected / reboot detected / reboot recovered / SLO missed / backup failed / freshness stale / CPU pressure / Gitea queue 告警矩陣 | 部分已有 Alertmanager rule 與 Telegram receipt 補強;仍缺完整 shutdown/up E2E receipt | 建立 Telegram alert matrix + receipt verifier,逐項讀回 Alertmanager active/resolved 與 outbound receipt,不送測試 secret | | 7 | CIR-P0-RBT-007 | P0 | 「所有備份包含主機、DB、網站、服務、套件、工具、日誌都沒有監控告警」 | Backup observability coverage:backup job inventory、last success、freshness、offsite、restore drill、Telegram receipt | 部分已有 backup health exporter / alert rules;全域 coverage 與 restore drill 未全綠 | 建立 backup coverage matrix:host / DB / website / service config / package list / tool scripts / logs,每列有 metric、alert、last_success、restore_verifier | @@ -130,7 +130,7 @@ | OpenClaw / Gather-style 持續動畫工作室 | route 已存在,已列為 P1 工作項 | 補 production desktop/mobile smoke、AwoooP 導流與截圖證據 | | AI 專業 UI / 非文字牆 cockpit | 已列為 P2 UX 驗收 | 將長文字區塊收斂成 first-viewport cockpit、cards、flow rows 與 expandable details | | 10 分鐘 reboot auto-recovery SLA | scorecard source / snapshot / API 已補固定 phase / ETA / blocker / next action 欄位;仍缺 fresh all-host reboot/drill proof | 補 production API readback,缺事件則明確標等待下一次 reboot 或 approved drill | -| 99 Windows / VMware autostart | Source verifier / parser / API readback 已完成;live 99 Verify output 尚未收集,scorecard 以 `windows99_vmware_autostart_readback_missing` fail-closed | 收集 99 no-secret Verify output,確認 VM 111/188/120/121/112 running、scheduled task / services / Windows Update policy 全綠 | +| 99 Windows / VMware autostart | Source verifier / parser / API readback / collection packet 已完成;live 99 Verify output 尚未收集,scorecard 以 `windows99_vmware_autostart_readback_missing` fail-closed;collection packet 顯示 99 reachable、可收 no-secret Verify,但 uptime unknown | 收集 99 no-secret Verify output,確認 VM 111/188/120/121/112 running、scheduled task / services / Windows Update policy 全綠 | | 502 maintenance fallback | 尚未完成外部維護頁 / edge fallback 決策與實作 | 先做 no-write decision record + smoke verifier | | 全備份監控告警 coverage | 部分 exporter/rule 已存在,但 host/DB/site/service/package/tool/log coverage 未全列 | 建立 backup coverage matrix 與 restore drill verifier | | Stock/Postgres hot pressure | 110 live 已導向 Stock/Postgres playbook;尚未完成 hot query / backup export playbook closure | 下一步執行 read-only Stock/Postgres evidence 與 source freshness / query attribution | diff --git a/scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py b/scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py index f398dcc0..fe58253b 100755 --- a/scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py +++ b/scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py @@ -703,6 +703,96 @@ def build_host_pressure_readback(payload: dict[str, Any]) -> dict[str, Any]: } +def host_row_by_alias(host_boot_detection: dict[str, Any], alias: str) -> dict[str, Any]: + rows = host_boot_detection.get("host_rows") + if not isinstance(rows, list): + return {} + for item in rows: + if isinstance(item, dict) and str(item.get("alias") or "") == alias: + return item + return {} + + +def build_windows99_verify_collection_packet( + *, + windows99: dict[str, Any], + host_boot_detection: dict[str, Any], +) -> dict[str, Any]: + """Describe the next no-secret Windows 99 verifier collection step.""" + host99 = host_row_by_alias(host_boot_detection, "99") + host99_reachable = host99.get("reachable") is True + host99_uptime_known = int_value(host99.get("uptime_seconds"), -1) >= 0 + readback_present = windows99.get("readback_present") is True + verify_ready = windows99.get("verify_ready") is True + blockers = strings(windows99.get("blockers")) + collection_blockers: list[str] = [] + if not host99_reachable: + collection_blockers.append("windows99_host_not_reachable_for_verify_collection") + if not readback_present: + collection_blockers.append("windows99_vmware_autostart_readback_missing") + collection_blockers.extend( + blocker for blocker in blockers if blocker not in collection_blockers + ) + if not host99_uptime_known: + collection_blockers.append("windows99_uptime_unknown") + + status = "ready_windows99_vmware_verify_readback_green" + if not verify_ready: + status = ( + "blocked_windows99_verify_output_missing_host_reachable" + if host99_reachable and not readback_present + else "blocked_windows99_verify_collection_not_ready" + ) + return { + "schema_version": "windows99_vmware_verify_collection_packet_v1", + "status": status, + "target_host_alias": "99", + "target_host": "192.168.0.99", + "host99_reachable": host99_reachable, + "host99_uptime_known": host99_uptime_known, + "readback_present": readback_present, + "verify_ready": verify_ready, + "can_collect_no_secret_verify": host99_reachable and not verify_ready, + "required_vm_aliases": strings(windows99.get("required_vm_aliases")) + or sorted(WINDOWS99_REQUIRED_VM_ALIASES), + "expected_no_secret_output_fields": [ + "VMRUN_PRESENT", + "VMX alias= present=<0|1>", + "VMWARE_SERVICE name= ok=<0|1>", + "VMWARE_AUTOSTART_TASK name=AWOOOI-Start-VMware-VMs ok=<0|1>", + "WINDOWS_UPDATE_POLICY name= ok=<0|1>", + "VM_POWER alias= running=<0|1>", + "VMWARE_AUTOSTART_CONFIG_READY", + "VMWARE_AUTOSTART_POWER_READY", + "WINDOWS_UPDATE_NO_AUTO_REBOOT_READY", + "VMWARE_AUTOSTART_VERIFY_READY", + ], + "no_secret_verify_command": ( + "powershell -ExecutionPolicy Bypass -File " + ".\\windows99-vmware-autostart.ps1 -Mode Verify" + ), + "post_verifier": ( + "rerun_reboot_auto_recovery_slo_scorecard_with_" + "windows99_vmware_file_no_secret_no_reboot" + ), + "collection_blockers": collection_blockers, + "safe_collection_channels": [ + "authorized_windows99_console_verify_stdout_only", + "existing_management_channel_verify_mode_only", + "committed_no_secret_artifact_file_then_scorecard_rerun", + ], + "forbidden_actions": [ + "windows_password_or_secret_collection", + "host_reboot", + "vm_power_change", + "windows_update_policy_apply", + "manual_registry_edit", + "service_restart", + "github_api", + ], + } + + def choose_safe_next_step( *, blockers: list[str], @@ -998,6 +1088,10 @@ def enrich_machine_readback(payload: dict[str, Any]) -> dict[str, Any]: source_controls_present = ( bool(controls) and source_control_ready_count == len(controls) ) + windows99_verify_collection = build_windows99_verify_collection_packet( + windows99=windows99, + host_boot_detection=host_boot_detection, + ) rollups = { "active_blocker_count": len(active_blockers), @@ -1064,6 +1158,19 @@ def enrich_machine_readback(payload: dict[str, Any]) -> dict[str, Any]: "windows99_vmware_powered_off_count": len( strings(windows99.get("powered_off_aliases")) ), + "windows99_verify_collection_status": windows99_verify_collection["status"], + "windows99_verify_collection_can_collect_no_secret": ( + windows99_verify_collection["can_collect_no_secret_verify"] is True + ), + "windows99_verify_collection_blocker_count": len( + strings(windows99_verify_collection.get("collection_blockers")) + ), + "windows99_host99_reachable": ( + windows99_verify_collection["host99_reachable"] is True + ), + "windows99_host99_uptime_known": ( + windows99_verify_collection["host99_uptime_known"] is True + ), "capacity_checked": capacity.get("checked") is True, "capacity_free_gib": capacity.get("free_gib"), "capacity_min_free_gib": capacity.get("min_free_gib"), @@ -1091,6 +1198,12 @@ def enrich_machine_readback(payload: dict[str, Any]) -> dict[str, Any]: "windows99_update_no_auto_reboot_ready": rollups[ "windows99_update_no_auto_reboot_ready" ], + "windows99_verify_collection_status": rollups[ + "windows99_verify_collection_status" + ], + "windows99_verify_collection_can_collect_no_secret": rollups[ + "windows99_verify_collection_can_collect_no_secret" + ], "runtime_write_authorized_by_this_scorecard": False, "host_reboot_authorized_by_this_scorecard": False, "workflow_trigger_authorized_by_this_scorecard": False, @@ -1133,6 +1246,12 @@ def enrich_machine_readback(payload: dict[str, Any]) -> dict[str, Any]: "reboot_auto_recovery_windows99_update_no_auto_reboot_ready": rollups[ "windows99_update_no_auto_reboot_ready" ], + "reboot_auto_recovery_windows99_verify_collection_status": rollups[ + "windows99_verify_collection_status" + ], + "reboot_auto_recovery_windows99_verify_collection_can_collect_no_secret": ( + rollups["windows99_verify_collection_can_collect_no_secret"] + ), "secret_values_collected": False, "github_api_used": False, "workflow_trigger_performed": False, @@ -1148,6 +1267,7 @@ def enrich_machine_readback(payload: dict[str, Any]) -> dict[str, Any]: payload["readback"] = readback payload["rollups"] = rollups payload["summary"] = summary + payload["windows99_verify_collection"] = windows99_verify_collection return payload diff --git a/scripts/reboot-recovery/tests/test_reboot_auto_recovery_slo_scorecard.py b/scripts/reboot-recovery/tests/test_reboot_auto_recovery_slo_scorecard.py index cbe461ed..b8534850 100644 --- a/scripts/reboot-recovery/tests/test_reboot_auto_recovery_slo_scorecard.py +++ b/scripts/reboot-recovery/tests/test_reboot_auto_recovery_slo_scorecard.py @@ -232,9 +232,26 @@ def test_green_summary_and_recent_all_host_probe_can_claim_slo(tmp_path: Path) - assert payload["readback"]["runtime_write_authorized_by_this_scorecard"] is False assert payload["readback"]["windows99_vmware_verify_ready"] is True assert payload["readback"]["windows99_update_no_auto_reboot_ready"] is True + assert payload["readback"]["windows99_verify_collection_status"] == ( + "ready_windows99_vmware_verify_readback_green" + ) + assert ( + payload["readback"]["windows99_verify_collection_can_collect_no_secret"] + is False + ) assert payload["rollups"]["source_controls_present"] is True assert payload["rollups"]["windows99_vmware_verify_ready"] is True assert payload["rollups"]["windows99_update_no_auto_reboot_ready"] is True + assert payload["rollups"]["windows99_verify_collection_status"] == ( + "ready_windows99_vmware_verify_readback_green" + ) + assert ( + payload["rollups"]["windows99_verify_collection_can_collect_no_secret"] + is False + ) + assert payload["rollups"]["windows99_verify_collection_blocker_count"] == 0 + assert payload["rollups"]["windows99_host99_reachable"] is True + assert payload["rollups"]["windows99_host99_uptime_known"] is True assert payload["rollups"]["readiness_percent"] == 100 assert payload["summary"]["reboot_auto_recovery_workplan_id"] == "P0-006" assert payload["summary"]["reboot_auto_recovery_current_phase"] == "slo_ready" @@ -256,6 +273,11 @@ def test_green_summary_and_recent_all_host_probe_can_claim_slo(tmp_path: Path) - "188", ] assert payload["windows99_vmware_autostart"]["verify_ready"] is True + assert payload["windows99_verify_collection"]["collection_blockers"] == [] + assert ( + payload["windows99_verify_collection"]["post_verifier"] + == "rerun_reboot_auto_recovery_slo_scorecard_with_windows99_vmware_file_no_secret_no_reboot" + ) assert payload["active_blockers"] == [] assert payload["source_controls"][ "conversation_event_hot_path_index_migration_source_present" @@ -285,6 +307,25 @@ def test_missing_windows99_vmware_readback_fails_closed(tmp_path: Path) -> None: assert payload["windows99_vmware_autostart"]["readback_present"] is False assert payload["readback"]["windows99_vmware_verify_ready"] is False assert payload["readback"]["windows99_update_no_auto_reboot_ready"] is False + assert payload["readback"]["windows99_verify_collection_status"] == ( + "blocked_windows99_verify_output_missing_host_reachable" + ) + assert ( + payload["readback"]["windows99_verify_collection_can_collect_no_secret"] + is True + ) + assert payload["windows99_verify_collection"]["collection_blockers"] == [ + "windows99_vmware_autostart_readback_missing" + ] + assert "VMRUN_PRESENT" in payload["windows99_verify_collection"][ + "expected_no_secret_output_fields" + ] + assert "-Mode Verify" in payload["windows99_verify_collection"][ + "no_secret_verify_command" + ] + assert "windows_password_or_secret_collection" in payload[ + "windows99_verify_collection" + ]["forbidden_actions"] assert payload["safe_next_step"] == ( "collect_windows99_vmware_autostart_verify_readback_then_rerun_all_host_" "reboot_scorecard_no_secret_no_reboot"