fix(reboot): route slo blockers to action matrix
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 59s
CD Pipeline / build-and-deploy (push) Successful in 4m40s
CD Pipeline / post-deploy-checks (push) Has been cancelled

This commit is contained in:
Your Name
2026-07-03 00:33:27 +08:00
parent f700489bb9
commit fd79a8cd0c
8 changed files with 630 additions and 10 deletions

View File

@@ -487,8 +487,28 @@ def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]:
readiness_percent=readiness_percent,
can_claim_slo=can_claim_slo,
)
active_blocker_action_matrix = _build_active_blocker_action_matrix(
active_blockers=active_blockers,
primary_blocker=sop_progress["primary_blocker"],
)
rollups = {
"active_blocker_count": active_blocker_count,
"active_blocker_action_count": active_blocker_action_matrix["item_count"],
"active_blocker_action_category_counts": active_blocker_action_matrix[
"category_counts"
],
"active_blocker_action_owner_lane_counts": active_blocker_action_matrix[
"owner_lane_counts"
],
"primary_blocker_owner_lane": active_blocker_action_matrix[
"primary_blocker_action"
].get("owner_lane", ""),
"primary_blocker_action_category": active_blocker_action_matrix[
"primary_blocker_action"
].get("category", ""),
"telegram_active_blocker_alert_required_count": active_blocker_action_matrix[
"telegram_alert_required_count"
],
"readiness_percent": readiness_percent,
"completed_check_count": completed_check_count,
"required_check_count": len(required_checks),
@@ -694,7 +714,17 @@ def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]:
"safe_next_step": safe_next_step,
"next_safe_action": sop_progress["next_safe_action"],
"primary_blocker": sop_progress["primary_blocker"],
"primary_blocker_owner_lane": active_blocker_action_matrix[
"primary_blocker_action"
].get("owner_lane", ""),
"primary_blocker_action_category": active_blocker_action_matrix[
"primary_blocker_action"
].get("category", ""),
"active_blocker_count": active_blocker_count,
"active_blocker_action_count": active_blocker_action_matrix["item_count"],
"telegram_active_blocker_alert_required_count": (
active_blocker_action_matrix["telegram_alert_required_count"]
),
"readiness_percent": readiness_percent,
"blocked_by_fresh_reboot_window_only": blocked_by_fresh_reboot_window_only,
"latest_verify_only_metric_present": latest_verify_only_metric_present,
@@ -740,6 +770,7 @@ def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]:
],
},
"reboot_sop_progress": sop_progress,
"active_blocker_action_matrix": active_blocker_action_matrix,
"controlled_service_data_backup_readback": (
controlled_service_data_backup_readback
),
@@ -1080,8 +1111,8 @@ def _reboot_sop_current_phase(active_blockers: list[str], can_claim_slo: bool) -
"reboot_event_required_host_unreachable",
"fresh_all_host_reboot_event_missing",
"all_required_hosts_not_in_10_minute_reboot_window",
"windows99_vmware_autostart_readback_missing",
"windows99_remote_execution_channel_unavailable",
"windows99_vmware_autostart_readback_missing",
"windows99_vmrun_missing",
"windows99_vmware_vmx_missing",
"windows99_vmware_autostart_config_not_ready",
@@ -1129,8 +1160,8 @@ def _reboot_sop_primary_blocker(active_blockers: list[str]) -> str:
"all_required_hosts_not_in_10_minute_reboot_window",
"host_boot_observation_older_than_target_window",
"host_uptime_unknown",
"windows99_vmware_autostart_readback_missing",
"windows99_remote_execution_channel_unavailable",
"windows99_vmware_autostart_readback_missing",
"windows99_vmrun_missing",
"windows99_vmware_vmx_missing",
"windows99_vmware_autostart_config_not_ready",
@@ -1185,6 +1216,228 @@ def _reboot_sop_eta_or_wait_reason(
return "eta_unavailable"
def _build_active_blocker_action_matrix(
*,
active_blockers: list[str],
primary_blocker: str,
) -> dict[str, Any]:
items = [
_active_blocker_action_row(
blocker=blocker,
priority_order=index,
is_primary=blocker == primary_blocker,
)
for index, blocker in enumerate(active_blockers, start=1)
]
primary_action = next(
(item for item in items if item.get("is_primary") is True),
{},
)
return {
"schema_version": "reboot_active_blocker_action_matrix_v1",
"item_count": len(items),
"telegram_alert_required_count": sum(
1 for item in items if item.get("telegram_alert_required") is True
),
"category_counts": _count_by(items, "category"),
"owner_lane_counts": _count_by(items, "owner_lane"),
"primary_blocker_action": primary_action,
"items": items,
}
def _active_blocker_action_row(
*,
blocker: str,
priority_order: int,
is_primary: bool,
) -> dict[str, Any]:
category = "reboot_slo_unknown"
owner_lane = "sre_reboot_slo_scorecard"
severity = "critical"
next_safe_action = (
"read_active_blocker_evidence_then_rerun_reboot_slo_scorecard_no_reboot"
)
post_verifier = (
"scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py "
"--output <scorecard.json>"
)
evidence_inputs = ["scorecard.active_blockers"]
controlled_apply_mode = "verifier_only_from_reboot_slo_lane"
forbidden_actions = [
"host_reboot",
"service_restart",
"docker_daemon_restart",
"node_drain",
"database_write_or_restore",
"secret_value_read",
"workflow_dispatch",
"github_api",
"force_push_or_ref_delete",
]
if blocker.startswith("windows99_"):
category = "windows99_vmware_autostart"
owner_lane = "windows99_console_or_no_secret_management_channel"
next_safe_action = (
"collect_windows99_vmware_autostart_verify_readback_then_rerun_"
"reboot_scorecard_no_secret_no_reboot"
)
if blocker == "windows99_remote_execution_channel_unavailable":
next_safe_action = (
"restore_windows99_no_secret_management_channel_or_collect_local_"
"console_verify_readback_then_rerun_reboot_scorecard_no_reboot"
)
post_verifier = (
"bash scripts/reboot-recovery/collect-windows99-vmware-verify.sh "
"--check && rerun_reboot_auto_recovery_slo_scorecard"
)
evidence_inputs = [
"windows99_management_channel",
"windows99_vmware_autostart",
"windows99_verify_collection",
]
controlled_apply_mode = "console_verify_or_no_secret_collector_only"
forbidden_actions.extend(
[
"windows_password_or_secret_collection",
"vm_power_change",
"windows_update_policy_apply_from_scorecard",
"manual_registry_edit",
]
)
elif blocker in {
"all_host_reboot_detection_missing",
"stateful_reboot_event_detection_missing",
"host_boot_probe_missing_hosts",
"host_unreachable_after_reboot",
"host_boot_observation_older_than_target_window",
"host_uptime_unknown",
"reboot_event_missing_required_hosts",
"reboot_event_required_host_unreachable",
"fresh_all_host_reboot_event_missing",
"all_required_hosts_not_in_10_minute_reboot_window",
}:
category = "host_boot_detection"
owner_lane = "reboot_event_detector_and_host_probe"
next_safe_action = (
"rerun_reboot_event_detector_and_host_probe_verify_only_no_reboot"
)
post_verifier = (
"scripts/reboot-recovery/reboot-auto-recovery-host-probe.sh && "
"scripts/reboot-recovery/reboot-event-detector.py"
)
evidence_inputs = ["host_boot_detection", "reboot_event_detection"]
elif blocker in {
"post_reboot_summary_missing",
"post_start_blocked_not_zero",
"service_green_not_1",
"host_188_service_green_not_1",
}:
category = "post_reboot_service_readiness"
owner_lane = "service_readiness_and_k3s_workloads"
next_safe_action = (
"collect_post_reboot_readiness_summary_and_public_health_no_restart"
)
post_verifier = "scripts/reboot-recovery/post-reboot-readiness-summary.sh"
evidence_inputs = ["post_reboot_readiness", "public_health_readback"]
elif blocker == "backup_core_green_not_1" or "backup" in blocker:
category = "backup_observability"
owner_lane = "backup_health_and_restore_drill"
next_safe_action = (
"collect_backup_health_restore_drill_and_offsite_readback_no_restore"
)
post_verifier = "scripts/ops/backup-health-textfile-exporter.py --help"
evidence_inputs = [
"post_reboot_readiness.backup_core_green",
"backup_health_textfile",
"restore_drill_readback",
]
elif blocker == "wazuh_dashboard_degraded":
category = "security_observability"
owner_lane = "wazuh_dashboard_and_security_readback"
severity = "warning"
next_safe_action = "collect_wazuh_dashboard_readback_no_service_restart"
post_verifier = "read_wazuh_dashboard_or_registry_health_readback"
evidence_inputs = ["post_reboot_readiness.wazuh_dashboard_degraded"]
elif blocker in _PUBLIC_MAINTENANCE_BLOCKERS or blocker.startswith("public_"):
category = "public_maintenance_fallback"
owner_lane = "public_route_maintenance_fallback"
next_safe_action = (
"run_public_maintenance_fallback_probe_and_rerun_scorecard_no_reload"
)
post_verifier = (
"python3 scripts/reboot-recovery/public-maintenance-fallback-probe.py"
)
evidence_inputs = ["public_maintenance_fallback"]
controlled_apply_mode = "check_mode_then_separate_edge_or_nginx_apply"
elif "stockplatform" in blocker or "product_data" in blocker:
category = "product_data_freshness"
owner_lane = "stockplatform_freshness_and_ingestion"
next_safe_action = (
"inspect_stockplatform_freshness_ingestion_readback_then_check_mode_"
"recovery_if_required_no_manual_db_write"
)
post_verifier = "stockplatform_public_api_freshness_and_ingestion_readback"
evidence_inputs = ["stockplatform_data_freshness"]
controlled_apply_mode = "check_mode_then_controlled_data_recovery_lane"
forbidden_actions.extend(
["manual_zero_fill", "copy_previous_trading_day_data"]
)
elif blocker in {
"host_pressure_high_load",
"host_container_cpu_attribution_stale",
"host_110_gitea_cpu_pressure",
"host_188_postgres_cpu_pressure",
"awooop_conversation_event_hot_path_index_drift_detected",
}:
category = "host_cpu_pressure"
owner_lane = "host_pressure_controller"
next_safe_action = (
"collect_sanitized_host_pressure_and_apply_allowed_playbook_if_"
"verifier_green_no_restart"
)
post_verifier = "host_pressure_readback_and_reboot_slo_scorecard"
evidence_inputs = ["host_pressure"]
controlled_apply_mode = "playbook_check_mode_then_controlled_apply_if_low_risk"
forbidden_actions.extend(
["unmask_legacy_runner_or_restore_generic_labels", "kill_process_without_gate"]
)
elif blocker == "local_disk_free_below_minimum":
category = "host_capacity"
owner_lane = "capacity_and_disk_hygiene"
next_safe_action = "collect_disk_usage_inventory_and_prepare_cleanup_dry_run"
post_verifier = "df_and_capacity_scorecard_readback"
evidence_inputs = ["capacity"]
controlled_apply_mode = "cleanup_dry_run_then_controlled_apply"
return {
"blocker": blocker,
"priority_order": priority_order,
"is_primary": is_primary,
"category": category,
"owner_lane": owner_lane,
"telegram_alert": "RebootAutoRecoveryActiveBlocker",
"telegram_alert_required": True,
"telegram_severity": severity,
"metric_name": "awoooi_reboot_auto_recovery_slo_active_blocker",
"evidence_inputs": evidence_inputs,
"next_safe_action": next_safe_action,
"post_verifier": post_verifier,
"controlled_apply_mode": controlled_apply_mode,
"controlled_apply_authorized_by_scorecard": False,
"forbidden_actions": _unique_strings(forbidden_actions),
}
def _count_by(items: list[dict[str, Any]], key: str) -> dict[str, int]:
counts: dict[str, int] = {}
for item in items:
value = str(item.get(key) or "unknown")
counts[value] = counts.get(value, 0) + 1
return counts
def _require_operation_boundaries(payload: dict[str, Any], label: str) -> None:
boundaries = _dict(payload.get("operation_boundaries"))
forbidden_true = [