Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
seed 啟動時靜默失敗的根因: from src.db.session import get_db_context ← 模組不存在 from src.db.base import get_db_context ← 正確路徑 此 bug 導致 yaml_rule playbooks 完全無法建立。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
112 lines
4.1 KiB
Python
112 lines
4.1 KiB
Python
"""
|
||
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 欄位,更穩健
|
||
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))
|