fix(cr): Code Review P0/P1 全修補 — 積木化+SSH路由+安全守衛順序
Some checks failed
CD Pipeline / build-and-deploy (push) Failing after 2m30s

P0-1: classify_alert_early 移至 incident_service (Service層),webhooks.py import 修正
P0-2: _ssh_execute() 改用 self._ssh,移除冗餘 SSHProvider() 實例化
P1-1: infrastructure SSH routing 移至 kubectl safety guard 之前,docker指令不再被攔截
P1-2: alert_rule_engine 新增 get_risk_for_alertname() public API
P1-3: classify_notification() docstring 修正 ORM→Pydantic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-12 14:51:12 +08:00
parent 47db80f495
commit c09521a1c6
5 changed files with 72 additions and 49 deletions

View File

@@ -55,6 +55,7 @@ from src.services.approval_db import get_approval_service
# Phase 17 P0: Service 層 (消除 Router 直接存取 Redis)
# C2 修正 (首席架構師審查 2026-04-10): create_incident_for_approval + extract_affected_services 已移入 Service 層
from src.services.incident_service import (
classify_alert_early,
create_incident_for_approval,
extract_affected_services,
get_incident_service,
@@ -970,30 +971,6 @@ class AlertmanagerPayload(BaseModel):
alerts: list[AlertmanagerAlert]
def classify_alert_early(alertname: str, severity: str, _labels: dict) -> tuple[str, str]:
"""
ADR-073 Phase 2-2: 早期分診,在 LLM 分析前決定 alert_category + notification_type。
2026-04-12 ogt: 防止 HostBackupFailed 等被誤路由到 K8s executor。
Returns:
tuple[str, str]: (alert_category, notification_type)
"""
alertname_lower = alertname.lower()
if alertname in ("ConfigurationDrift", "KubeConfigDrift"):
return "config_drift", "TYPE-4D"
if severity in ("info", "none"):
return "info", "TYPE-1"
if any(kw in alertname_lower for kw in ("backup", "heartbeat")):
return "backup", "TYPE-1"
if alertname.startswith(("Docker", "Host")):
return "infrastructure", "TYPE-3"
if alertname.startswith(("Kube", "Pod", "Deploy", "Node", "Velero", "ArgoCD")):
return "kubernetes", "TYPE-3"
if alertname.startswith(("Postgres", "Redis")):
return "database", "TYPE-3"
return "general", "TYPE-3"
def is_internal_ip(client_ip: str) -> bool:
"""檢查是否為內網 IP"""
import ipaddress

View File

@@ -145,6 +145,20 @@ def _is_generic(rule: dict) -> bool:
# ── 公開 API ────────────────────────────────────────────────
def get_risk_for_alertname(alertname: str) -> str | None:
"""
根據 alertname 從 alert_rules.yaml 取得 risk 等級。
ADR-073 Phase 3-6: 供 decision_manager 做 YAML risk override。
Returns:
risk 字串 ("low"/"medium"/"critical") 或 None無匹配規則
"""
for rule in _load_rules():
if _matches(rule, alertname, "", "", ""):
return rule.get("response", {}).get("risk")
return None
def match_rule(alert_context: dict) -> dict[str, Any] | None:
"""
根據 alert_context 匹配規則,回傳填充後的 response dict。

View File

@@ -1246,6 +1246,15 @@ class DecisionManager:
action = _re.sub(r"<deployment_name>", _target, action)
action = _re.sub(r"<[^>]+>", _target, action)
# ADR-073 Phase 3-2: infrastructure 告警 (Docker/Host) → SSH MCP routing (2026-04-12 ogt)
# alert_category = "infrastructure" 表示 Docker/Host 告警,不走 K8s executor
# action 格式應為 "docker restart <container>" 或 "systemctl restart <service>"
# P1-1 fix 2026-04-12: 必須在 kubectl safety guard 之前 routing否則 docker 指令被 _action_safe=False 攔截
_alert_category = getattr(incident, "alert_category", None) or ""
if _alert_category == "infrastructure" and action and not action.startswith("kubectl"):
await self._ssh_execute(incident, token, action, _target)
return
# 安全守衛: 替換後仍含 "unknown" 或未替換的 <...>/{...} → 拒絕執行
# 另外:若 target 等於 alertname代表 LLM 把告警名稱填入 deployment_name也拒絕
_alertname = incident.signals[0].labels.get("alertname", "") if incident.signals else ""
@@ -1274,14 +1283,6 @@ class DecisionManager:
)
return
# ADR-073 Phase 3-2: infrastructure 告警 (Docker/Host) → SSH MCP routing (2026-04-12 ogt)
# alert_category = "infrastructure" 表示 Docker/Host 告警,不走 K8s executor
# action 格式應為 "docker restart <container>" 或 "systemctl restart <service>"
_alert_category = getattr(incident, "alert_category", None) or ""
if _alert_category == "infrastructure" and action and not action.startswith("kubectl"):
await self._ssh_execute(incident, token, action, _target)
return
# BUG-003 修復 2026-04-11: 加入 K8s deployment 存在性驗證,
# 避免 LLM 產生的無效 deployment name<placeholder>/alertname/unknown通過 safety guard
# 但仍對 K8s 發出錯誤指令
@@ -1596,24 +1597,21 @@ class DecisionManager:
# ADR-073 Phase 3-6: YAML rule risk_level 優先於 LLM 輸出 (2026-04-12 ogt)
# LLM 有時把 critical 告警估為 mediumYAML 規則是由人工審閱過的,優先採用
try:
from src.services.alert_rule_engine import _load_rules, _matches
from src.services.alert_rule_engine import get_risk_for_alertname
_alertname_for_risk = (
incident.signals[0].labels.get("alertname", "")
if incident.signals else ""
)
if _alertname_for_risk:
for _rule in _load_rules():
if _matches(_rule, _alertname_for_risk, "", "", ""):
_yaml_risk = _rule.get("response", {}).get("risk")
if _yaml_risk and _yaml_risk != result.get("risk_level"):
logger.info(
"risk_level_yaml_override",
incident_id=incident.incident_id,
llm_risk=result.get("risk_level"),
yaml_risk=_yaml_risk,
)
result["risk_level"] = _yaml_risk
break
_yaml_risk = get_risk_for_alertname(_alertname_for_risk)
if _yaml_risk and _yaml_risk != result.get("risk_level"):
logger.info(
"risk_level_yaml_override",
incident_id=incident.incident_id,
llm_risk=result.get("risk_level"),
yaml_risk=_yaml_risk,
)
result["risk_level"] = _yaml_risk
except Exception as _re:
logger.debug("risk_level_yaml_override_failed", error=str(_re))
@@ -2030,7 +2028,6 @@ class DecisionManager:
- systemctl restart <service>
- docker rm -f <container> (含 docker start)
"""
from src.plugins.mcp.providers.ssh_provider import SSHProvider
import os as _os
# 取得主機 — 從 instance label 或 SSH_MCP_ALLOWED_HOSTS 第一台
@@ -2076,7 +2073,6 @@ class DecisionManager:
_fire_and_forget(_push_decision_to_telegram(incident, token.proposal_data))
return
ssh = SSHProvider()
params: dict = {"host": _host}
if _tool == "docker_restart":
params["container"] = _container
@@ -2084,7 +2080,7 @@ class DecisionManager:
params["service"] = _service
try:
result = await ssh.execute(tool_name=_tool, parameters=params)
result = await self._ssh.execute(tool_name=_tool, parameters=params)
success = result.success
logger.info(

View File

@@ -101,6 +101,42 @@ def extract_affected_services(labels: dict, target_resource: str) -> list[str]:
return []
def classify_alert_early(alertname: str, severity: str, _labels: dict) -> tuple[str, str]:
"""
ADR-073 Phase 2-2: 早期分診,在 LLM 分析前決定 alert_category + notification_type。
防止 HostBackupFailed 等告警被誤路由到 K8s executor。
規則優先順序(由高到低):
1. ConfigurationDrift / KubeConfigDrift → TYPE-4D (Config Drift 卡片)
2. severity=info/none → TYPE-1 (純資訊,無按鈕)
3. backup/heartbeat 關鍵字 → TYPE-1
4. Docker/Host 前綴 → infrastructure TYPE-3
5. Kube/Pod/Deploy/Node/Velero/ArgoCD 前綴 → kubernetes TYPE-3
6. Postgres/Redis 前綴 → database TYPE-3
7. 預設 → general TYPE-3
C3 修正 (首席架構師 CR 2026-04-13): 從 Router 層 (webhooks.py) 移入 Service 層
原違規: 業務邏輯函數定義在 api/v1/webhooks.py
Returns:
tuple[str, str]: (alert_category, notification_type)
"""
alertname_lower = alertname.lower()
if alertname in ("ConfigurationDrift", "KubeConfigDrift"):
return "config_drift", "TYPE-4D"
if severity in ("info", "none"):
return "info", "TYPE-1"
if any(kw in alertname_lower for kw in ("backup", "heartbeat")):
return "backup", "TYPE-1"
if alertname.startswith(("Docker", "Host")):
return "infrastructure", "TYPE-3"
if alertname.startswith(("Kube", "Pod", "Deploy", "Node", "Velero", "ArgoCD")):
return "kubernetes", "TYPE-3"
if alertname.startswith(("Postgres", "Redis")):
return "database", "TYPE-3"
return "general", "TYPE-3"
async def create_incident_for_approval(
approval_id: str,
risk_level: str,

View File

@@ -1127,7 +1127,7 @@ def classify_notification(
TYPE-4D > TYPE-1 > TYPE-2 > TYPE-4 > TYPE-3(預設)
Args:
incident: Incident ORM 物件(需要 signals[].labels + title
incident: Incident Pydantic 模型(需要 signals[].labels + title
confidence: AI 決策信心值 (0.0~1.0)
auto_executed: 是否已自動修復執行完成
mcp_all_failed: 所有 MCP provider 是否全失敗