182 lines
5.9 KiB
Python
182 lines
5.9 KiB
Python
#!/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())
|