fix(api): enqueue repair candidates for ansible check mode
Some checks failed
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m40s
CD Pipeline / build-and-deploy (push) Successful in 6m32s
CD Pipeline / post-deploy-checks (push) Successful in 1m38s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
Some checks failed
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m40s
CD Pipeline / build-and-deploy (push) Successful in 6m32s
CD Pipeline / post-deploy-checks (push) Successful in 1m38s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
This commit is contained in:
@@ -542,10 +542,15 @@ def build_ansible_decision_audit_payload(
|
||||
or ""
|
||||
)[:240],
|
||||
}
|
||||
controlled_queue = decision_path == "repair_candidate_controlled_queue"
|
||||
output_payload = {
|
||||
"not_used_reason": not_used_reason,
|
||||
"decision_effect": "audit_only",
|
||||
"next_required_step": "wire approval_execution to Ansible check-mode before apply",
|
||||
"decision_effect": "check_mode_queue_ready" if controlled_queue else "audit_only",
|
||||
"next_required_step": (
|
||||
"awooop_ansible_check_mode_worker_claims_candidate"
|
||||
if controlled_queue
|
||||
else "wire approval_execution to Ansible check-mode before apply"
|
||||
),
|
||||
}
|
||||
return {
|
||||
"operation_type": "ansible_candidate_matched",
|
||||
|
||||
@@ -31,6 +31,10 @@ from src.models.playbook import RiskLevel as PlaybookRiskLevel
|
||||
from src.repositories.playbook_repository import get_playbook_repository
|
||||
from src.services.action_parser import ActionKind, parse_kubectl_action
|
||||
from src.services.auto_repair_service import AutoRepairService
|
||||
from src.services.awooop_ansible_audit_service import (
|
||||
build_ansible_decision_audit_payload,
|
||||
record_ansible_decision_audit,
|
||||
)
|
||||
from src.services.awooop_deeplinks import work_item_url
|
||||
from src.services.evidence_snapshot import EvidenceSnapshot
|
||||
from src.services.incident_service import get_incident_service
|
||||
@@ -71,11 +75,15 @@ class RepairCandidateService:
|
||||
investigator: Any | None = None,
|
||||
playbook_repository: Any | None = None,
|
||||
auto_repair_service: AutoRepairService | None = None,
|
||||
ansible_audit_builder: Any | None = None,
|
||||
ansible_audit_recorder: Any | None = None,
|
||||
) -> None:
|
||||
self._incident_service = incident_service or get_incident_service()
|
||||
self._investigator = investigator or get_pre_decision_investigator()
|
||||
self._playbook_repository = playbook_repository or get_playbook_repository()
|
||||
self._auto_repair = auto_repair_service or AutoRepairService()
|
||||
self._ansible_audit_builder = ansible_audit_builder or build_ansible_decision_audit_payload
|
||||
self._ansible_audit_recorder = ansible_audit_recorder or record_ansible_decision_audit
|
||||
|
||||
async def build_from_incident_id(
|
||||
self,
|
||||
@@ -139,7 +147,7 @@ class RepairCandidateService:
|
||||
)
|
||||
if not playbook_id:
|
||||
blockers.append("playbook_not_matched")
|
||||
return self._blocked_result(
|
||||
return await self._blocked_result_with_controlled_handoff(
|
||||
blockers=blockers,
|
||||
metadata=metadata,
|
||||
evidence=evidence,
|
||||
@@ -148,12 +156,13 @@ class RepairCandidateService:
|
||||
alertname=alertname,
|
||||
target_resource=target_resource,
|
||||
namespace=namespace,
|
||||
severity=severity,
|
||||
)
|
||||
|
||||
playbook = await self._playbook_repository.get_by_id(playbook_id)
|
||||
if not playbook:
|
||||
blockers.append("playbook_not_found")
|
||||
return self._blocked_result(
|
||||
return await self._blocked_result_with_controlled_handoff(
|
||||
blockers=blockers,
|
||||
metadata=metadata,
|
||||
evidence=evidence,
|
||||
@@ -162,6 +171,7 @@ class RepairCandidateService:
|
||||
alertname=alertname,
|
||||
target_resource=target_resource,
|
||||
namespace=namespace,
|
||||
severity=severity,
|
||||
)
|
||||
|
||||
metadata["playbook_trust"] = {
|
||||
@@ -181,7 +191,7 @@ class RepairCandidateService:
|
||||
step, step_blockers = self._select_executable_step(incident, playbook)
|
||||
blockers.extend(step_blockers)
|
||||
if blockers or step is None:
|
||||
return self._blocked_result(
|
||||
return await self._blocked_result_with_controlled_handoff(
|
||||
blockers=blockers,
|
||||
metadata=metadata,
|
||||
evidence=evidence,
|
||||
@@ -191,6 +201,7 @@ class RepairCandidateService:
|
||||
alertname=alertname,
|
||||
target_resource=target_resource,
|
||||
namespace=namespace,
|
||||
severity=severity,
|
||||
)
|
||||
|
||||
metadata["repair_candidate"] = {
|
||||
@@ -255,6 +266,116 @@ class RepairCandidateService:
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
async def _blocked_result_with_controlled_handoff(
|
||||
self,
|
||||
*,
|
||||
blockers: list[str],
|
||||
metadata: dict[str, Any],
|
||||
fallback_action: str,
|
||||
evidence: EvidenceSnapshot | None = None,
|
||||
playbook: Playbook | None = None,
|
||||
incident: Incident | None = None,
|
||||
alertname: str = "",
|
||||
target_resource: str = "",
|
||||
namespace: str = "",
|
||||
severity: str | None = None,
|
||||
) -> RepairCandidateResult:
|
||||
result = self._blocked_result(
|
||||
blockers=blockers,
|
||||
metadata=metadata,
|
||||
evidence=evidence,
|
||||
playbook=playbook,
|
||||
fallback_action=fallback_action,
|
||||
incident=incident,
|
||||
alertname=alertname,
|
||||
target_resource=target_resource,
|
||||
namespace=namespace,
|
||||
)
|
||||
await self._maybe_record_controlled_executor_handoff(
|
||||
result=result,
|
||||
incident=incident,
|
||||
severity=severity,
|
||||
)
|
||||
return result
|
||||
|
||||
async def _maybe_record_controlled_executor_handoff(
|
||||
self,
|
||||
*,
|
||||
result: RepairCandidateResult,
|
||||
incident: Incident | None,
|
||||
severity: str | None = None,
|
||||
) -> None:
|
||||
if incident is None or not result.metadata.get("repair_candidate_draft_ready"):
|
||||
return
|
||||
draft_package = result.metadata.get("repair_candidate_draft_package")
|
||||
if not isinstance(draft_package, dict):
|
||||
return
|
||||
promotion_contract = draft_package.get("candidate_promotion_contract")
|
||||
if not isinstance(promotion_contract, dict):
|
||||
return
|
||||
|
||||
proposal_data = {
|
||||
"source": "repair_candidate_controlled_queue",
|
||||
"risk_level": severity or getattr(getattr(incident, "severity", None), "value", ""),
|
||||
"action": promotion_contract.get("repair_command_template") or "",
|
||||
"matched_playbook_id": draft_package.get("matched_playbook_id"),
|
||||
"repair_candidate_status": result.metadata.get("repair_candidate_status"),
|
||||
"route_id": promotion_contract.get("route_id"),
|
||||
"controlled_playbook_queue": True,
|
||||
}
|
||||
payload = self._ansible_audit_builder(
|
||||
incident=incident,
|
||||
proposal_data=proposal_data,
|
||||
decision_path="repair_candidate_controlled_queue",
|
||||
not_used_reason=(
|
||||
"repair candidate controlled queue ready; "
|
||||
"Ansible check-mode worker should claim candidate"
|
||||
),
|
||||
)
|
||||
if payload is None:
|
||||
handoff = {
|
||||
"schema_version": "repair_candidate_controlled_executor_handoff_v1",
|
||||
"status": "no_ansible_catalog_candidate",
|
||||
"operation_type": "ansible_candidate_matched",
|
||||
"candidate_count": 0,
|
||||
"check_mode_worker": "awooop_ansible_check_mode_worker",
|
||||
"check_mode_queued": False,
|
||||
"runtime_execution_authorized": False,
|
||||
}
|
||||
else:
|
||||
written = await self._ansible_audit_recorder(
|
||||
incident=incident,
|
||||
proposal_data=proposal_data,
|
||||
decision_path="repair_candidate_controlled_queue",
|
||||
not_used_reason=(
|
||||
"repair candidate controlled queue ready; "
|
||||
"Ansible check-mode worker should claim candidate"
|
||||
),
|
||||
)
|
||||
candidates = payload.get("input", {}).get("executor_candidates")
|
||||
candidate_count = len(candidates) if isinstance(candidates, list) else 0
|
||||
handoff = {
|
||||
"schema_version": "repair_candidate_controlled_executor_handoff_v1",
|
||||
"status": (
|
||||
"ansible_candidate_matched_queued"
|
||||
if written
|
||||
else "ansible_candidate_matched_existing_or_write_skipped"
|
||||
),
|
||||
"operation_type": payload.get("operation_type"),
|
||||
"decision_effect": payload.get("output", {}).get("decision_effect"),
|
||||
"next_required_step": payload.get("output", {}).get("next_required_step"),
|
||||
"candidate_count": candidate_count,
|
||||
"check_mode_worker": "awooop_ansible_check_mode_worker",
|
||||
"check_mode_queued": True,
|
||||
"runtime_execution_authorized": False,
|
||||
}
|
||||
|
||||
result.metadata["controlled_executor_handoff"] = handoff
|
||||
draft_package["controlled_executor_handoff"] = handoff
|
||||
work_item = draft_package.get("awooop_work_item")
|
||||
if isinstance(work_item, dict):
|
||||
work_item["controlled_executor_handoff"] = handoff
|
||||
|
||||
async def _collect_evidence(self, incident: Incident) -> EvidenceSnapshot | None:
|
||||
try:
|
||||
return await self._investigator.investigate(incident)
|
||||
|
||||
Reference in New Issue
Block a user