Files
awoooi/scripts/ops/awooop-seed-auto-repair-canary-playbook.py
Your Name a0a0731cd6
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / tests (push) Successful in 5m46s
CD Pipeline / build-and-deploy (push) Successful in 4m6s
CD Pipeline / post-deploy-checks (push) Successful in 1m28s
fix(auto-repair): preserve exact playbook candidates
2026-05-13 23:38:19 +08:00

182 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Seed the AwoooP T16 auto-repair canary Playbook.
Run from an API pod so it uses the same PostgreSQL/Redis context as production:
python scripts/ops/awooop-seed-auto-repair-canary-playbook.py
The Playbook is intentionally scoped to the no-traffic
``awoooi-auto-repair-canary`` Deployment. It exists to prove the automation
chain with real execution evidence; it is not an organic symptom rule.
"""
from __future__ import annotations
import argparse
import asyncio
import json
import re
import sys
from dataclasses import asdict, dataclass
from pathlib import Path
_IMAGE_API_ROOT = Path(__file__).resolve().parents[2]
_REPO_API_ROOT = _IMAGE_API_ROOT / "apps" / "api"
for _api_root in (_IMAGE_API_ROOT, _REPO_API_ROOT):
if (_api_root / "src").exists():
sys.path.insert(0, str(_api_root))
break
from src.models.playbook import ( # noqa: E402
ActionType,
Playbook,
PlaybookSource,
PlaybookStatus,
RepairStep,
RiskLevel,
SymptomPattern,
)
from src.core.redis_client import close_redis_pool, init_redis_pool # noqa: E402
from src.repositories.playbook_repository import get_playbook_repository # noqa: E402
from src.utils.timezone import now_taipei # noqa: E402
DEFAULT_ALERTNAME = "AwoooPAutoRepairCanaryT16"
DEFAULT_TARGET = "awoooi-auto-repair-canary"
DEFAULT_NAMESPACE = "awoooi-prod"
@dataclass(frozen=True)
class SeedResult:
playbook_id: str
alertname: str
target: str
namespace: str
status: str
source: str
command: str
preserved_success_count: int
preserved_failure_count: int
trust_score: float
def _playbook_id_for_alertname(alertname: str) -> str:
if alertname == DEFAULT_ALERTNAME:
return "PB-AWOOOP-T16-CANARY"
prefix = "PB-AWOOOP-CANARY-"
suffix = re.sub(r"[^A-Z0-9]+", "-", alertname.upper()).strip("-")
suffix = suffix.replace("AWOOOP-AUTO-REPAIR-CANARY-", "")
suffix = suffix[: 32 - len(prefix)] or "T16"
return f"{prefix}{suffix}"
async def seed_canary_playbook(
*,
alertname: str,
target: str,
namespace: str,
) -> SeedResult:
repo = get_playbook_repository()
playbook_id = _playbook_id_for_alertname(alertname)
existing = await repo.get_by_id(playbook_id)
command = f"kubectl rollout restart deployment/{target} -n {namespace}"
playbook = Playbook(
playbook_id=playbook_id,
name="AwoooP T16 auto-repair canary rollout restart",
description=(
"低風險 live-fire canary用於驗證 alert -> Playbook -> executor "
"-> verifier -> learning/KM -> truth-chain 的真實自動修復閉環。"
),
status=PlaybookStatus.APPROVED,
source=PlaybookSource.MANUAL,
symptom_pattern=SymptomPattern(
alert_names=[alertname],
affected_services=[target],
severity_range=["P2"],
label_patterns={"component": target, "namespace": namespace},
keywords=["auto repair canary", "live-fire"],
),
repair_steps=[
RepairStep(
step_number=1,
action_type=ActionType.KUBECTL,
command=command,
expected_result=f"Deployment {target} receives restartedAt annotation and remains Available.",
rollback_command=f"kubectl rollout undo deployment/{target} -n {namespace}",
requires_approval=False,
risk_level=RiskLevel.LOW,
)
],
estimated_duration_minutes=1,
source_incident_ids=(existing.source_incident_ids if existing else []),
ai_confidence=1.0,
success_count=(existing.success_count if existing else 0),
failure_count=(existing.failure_count if existing else 0),
last_used_at=(existing.last_used_at if existing else None),
trust_score=(existing.trust_score if existing else 0.6),
approved_by="codex-t16-live-fire",
approved_at=(existing.approved_at if existing and existing.approved_at else now_taipei()),
tags=["awooop", "t16", "auto-repair", "canary", "live-fire"],
notes=(
"Synthetic low-risk canary for AwoooP automation verification. "
"This does not authorize organic production remediation rules; "
"it only proves the closed-loop plumbing with a no-traffic target."
),
created_at=(existing.created_at if existing else now_taipei()),
)
await repo.create(playbook)
return SeedResult(
playbook_id=playbook_id,
alertname=alertname,
target=target,
namespace=namespace,
status=playbook.status.value,
source=playbook.source.value,
command=command,
preserved_success_count=playbook.success_count,
preserved_failure_count=playbook.failure_count,
trust_score=playbook.trust_score,
)
async def _amain() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--alertname", default=DEFAULT_ALERTNAME)
parser.add_argument("--target", default=DEFAULT_TARGET)
parser.add_argument("--namespace", default=DEFAULT_NAMESPACE)
args = parser.parse_args()
redis_initialized = False
try:
await init_redis_pool()
redis_initialized = True
except Exception as exc:
print(
json.dumps(
{
"warning": "redis_pool_init_failed_pg_seed_continues",
"error": str(exc),
},
ensure_ascii=False,
sort_keys=True,
),
file=sys.stderr,
)
try:
result = await seed_canary_playbook(
alertname=args.alertname,
target=args.target,
namespace=args.namespace,
)
print(json.dumps(asdict(result), ensure_ascii=False, sort_keys=True))
finally:
if redis_initialized:
await close_redis_pool()
if __name__ == "__main__":
asyncio.run(_amain())