""" Playbook Seed Service — 從 alert_rules.yaml 初始化 Playbook 資料 ================================================================= 職責: - 啟動時讀取 alert_rules.yaml - 將每條規則轉換為 APPROVED Playbook 寫入 DB(冪等:已存在則跳過) - 確保自動修復鏈路有資料可用 呼叫方: main.py lifespan (asyncio.create_task — 非阻塞) 2026-04-10 Claude Sonnet 4.6 Asia/Taipei """ from __future__ import annotations from pathlib import Path import structlog import yaml logger = structlog.get_logger(__name__) _RULES_PATH = Path(__file__).parent.parent.parent / "alert_rules.yaml" async def seed_playbooks_from_rules() -> None: """從 alert_rules.yaml 匯入 APPROVED Playbook(冪等)""" try: if not _RULES_PATH.exists(): logger.warning("playbook_seed_rules_not_found", path=str(_RULES_PATH)) return data = yaml.safe_load(_RULES_PATH.read_text()) rules = data.get("rules", []) if not rules: return from src.models.playbook import ( ActionType, Playbook, PlaybookSource, PlaybookStatus, RepairStep, RiskLevel, SymptomPattern, ) from src.repositories.playbook_repository import get_playbook_repository repo = get_playbook_repository() # 取得現有 YAML_RULE playbook,依 name 去重避免重複建立 # 2026-04-15 ogt: 不再用 list_playbooks(舊格式 repair_steps 會 validation error) # 改用 raw SQL 只撈 name 欄位,更穩健 # 2026-04-24 ogt + Claude Sonnet 4.6: 修復重複建立/封存迴圈 # 舊邏輯 status != 'deprecated' 導致:deprecated 歷史記錄永遠存在 → # 每次啟動都重建同名 Playbook(C1 保護後不再封存,但 deprecated 不清除) # 修:不管 status,同名 yaml_rule 任何狀態存在即視為已存在,不重建 from src.db.base import get_db_context from sqlalchemy import text as sa_text async with get_db_context() as db: rows = await db.execute( sa_text( "SELECT name FROM playbooks WHERE source = 'yaml_rule'" ) ) existing_names = {r[0] for r in rows.fetchall()} seeded = 0 for rule in rules: rule_id = rule.get("id", "") rule_name = rule.get("description", rule_id) if rule_name in existing_names: continue resp = rule.get("response", {}) kubectl_cmd = resp.get("kubectl_command", "").strip() if not kubectl_cmd: continue risk_str = resp.get("risk", "medium").lower() risk_map = {"low": RiskLevel.LOW, "medium": RiskLevel.MEDIUM, "critical": RiskLevel.HIGH} risk = risk_map.get(risk_str, RiskLevel.MEDIUM) alertnames = rule.get("match", {}).get("alertname", []) action_type = ActionType.KUBECTL if kubectl_cmd.startswith("ssh"): action_type = ActionType.SSH_COMMAND playbook = Playbook( name=rule.get("description", rule_id), description=resp.get("description", rule.get("description", "")), status=PlaybookStatus.APPROVED, source=PlaybookSource.YAML_RULE, symptom_pattern=SymptomPattern( alert_names=alertnames, affected_services=[], severity_range=["P2", "P3"], ), repair_steps=[ RepairStep( step_number=1, action_type=action_type, command=kubectl_cmd, expected_result=resp.get("action_title", ""), risk_level=risk, requires_approval=False, ) ], ai_confidence=1.0, approved_by="alert_rules_yaml", ) try: await repo.create(playbook) seeded += 1 logger.info("playbook_seeded", rule_id=rule_id, name=playbook.name) except Exception as e: logger.warning("playbook_seed_failed", rule_id=rule_id, error=str(e)) logger.info("playbook_seed_complete", seeded=seeded, total=len(rules)) except Exception as e: logger.error("playbook_seed_error", error=str(e))