Files
k8smanager-cli/backend/main.py
T
cnbugs 8109204843 fix: 修复集群不可达时误报100分的严重bug
- 新增 check_connectivity() 连通性预检: kubectl存在性/kubeconfig/API Server连接
- 连不上集群直接返回0分+明确错误信息+排查建议(不再静默跳过)
- 每个检查项kubectl失败时必须报critical问题(不再忽略error)
- 前端: 连接失败时显示红色错误页+错误详情+排查建议+重新诊断按钮
- 智能识别常见错误: connection refused/证书过期/超时/DNS解析失败等
2026-07-25 11:07:03 +08:00

232 lines
8.3 KiB
Python

"""K8S 智能诊断平台 - FastAPI 主应用"""
import os
import json
import asyncio
from datetime import datetime
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import Optional
from backend.config import PORT
from backend.diagnosis import (
run_full_diagnosis, build_diagnosis_report, ALL_CHECKS,
check_connectivity,
check_cluster_info, check_pods, check_nodes, check_events,
check_deployments, check_services, check_pvc, check_resource_usage,
check_network_policies,
)
from backend.ai_agent import analyze_with_ai, chat_with_ai
app = FastAPI(title="K8S 智能诊断平台", version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# 存储最近诊断结果 (内存)
_last_diagnosis: Optional[dict] = None
_last_report: str = ""
# 诊断项执行顺序 (带中文标题)
CHECK_ORDER = [
("cluster_info", "集群信息", check_cluster_info),
("nodes", "Node 状态检查", check_nodes),
("pods", "Pod 状态检查", check_pods),
("deployments", "Deployment 状态检查", check_deployments),
("services", "Service/Endpoint 检查", check_services),
("events", "异常事件检查", check_events),
("pvc", "PVC 存储检查", check_pvc),
("resource_usage", "资源使用率", check_resource_usage),
("network", "网络策略检查", check_network_policies),
]
class DiagnoseRequest(BaseModel):
namespace: str = ""
checks: Optional[list[str]] = None
class AnalyzeRequest(BaseModel):
question: str = ""
class ChatRequest(BaseModel):
messages: list[dict]
@app.get("/api/health")
async def health():
return {"status": "ok", "time": datetime.now().isoformat()}
@app.get("/api/checks")
async def list_checks():
"""列出所有可用诊断项"""
return {
"checks": [
{"name": name, "title": title}
for name, title, _ in CHECK_ORDER
]
}
@app.get("/api/diagnose/stream")
async def diagnose_stream(namespace: str = ""):
"""SSE 流式诊断 - 实时推送每一步进度"""
global _last_diagnosis, _last_report
async def event_generator():
start = datetime.now()
# ★ 第一步: 连通性检查
yield f"data: {json.dumps({'type': 'progress', 'step': 0, 'total': len(CHECK_ORDER) + 1, 'name': 'connectivity', 'title': '集群连通性检查', 'status': 'running'}, ensure_ascii=False)}\n\n"
conn = await check_connectivity()
if not conn["connected"]:
yield f"data: {json.dumps({'type': 'progress', 'step': 0, 'total': len(CHECK_ORDER) + 1, 'name': 'connectivity', 'title': '集群连通性检查', 'status': 'done', 'issues': 1}, ensure_ascii=False)}\n\n"
diagnosis = {
"timestamp": start.isoformat(),
"elapsed_seconds": round((datetime.now() - start).total_seconds(), 2),
"namespace": namespace or "all",
"score": 0,
"status": "critical",
"connected": False,
"connectivity_error": conn["error"],
"connectivity_suggestion": conn.get("suggestion", ""),
"critical_count": 1,
"warning_count": 0,
"issues": [{
"level": "critical",
"check": "集群连通性",
"resource": "API Server",
"message": f"无法连接到集群: {conn['error'][:300]}",
}],
"checks": {},
}
_last_diagnosis = diagnosis
_last_report = build_diagnosis_report(diagnosis)
yield f"data: {json.dumps({'type': 'complete', 'diagnosis': diagnosis}, ensure_ascii=False)}\n\n"
return
yield f"data: {json.dumps({'type': 'progress', 'step': 0, 'total': len(CHECK_ORDER) + 1, 'name': 'connectivity', 'title': '集群连通性检查', 'status': 'done', 'issues': 0}, ensure_ascii=False)}\n\n"
# ★ 第二步: 执行各项检查
results = {}
all_issues = []
total = len(CHECK_ORDER) + 1
for idx, (name, title, fn) in enumerate(CHECK_ORDER, 1):
yield f"data: {json.dumps({'type': 'progress', 'step': idx, 'total': total, 'name': name, 'title': title, 'status': 'running'}, ensure_ascii=False)}\n\n"
try:
if name in ("pods", "events", "deployments", "services", "pvc", "network"):
result = await fn(namespace)
else:
result = await fn()
results[name] = result
issue_count = len(result.get("issues", []))
for issue in result.get("issues", []):
issue["check"] = title
all_issues.append(issue)
except Exception as e:
results[name] = {
"name": name, "title": title, "error": str(e),
"issues": [{"level": "critical", "resource": name, "message": f"检查执行异常: {str(e)}"}],
}
issue_count = 1
all_issues.append({"level": "critical", "check": title, "resource": name, "message": f"检查执行异常: {str(e)}"})
yield f"data: {json.dumps({'type': 'progress', 'step': idx, 'total': total, 'name': name, 'title': title, 'status': 'done', 'issues': issue_count}, ensure_ascii=False)}\n\n"
# 汇总
critical = [i for i in all_issues if i["level"] == "critical"]
warning = [i for i in all_issues if i["level"] == "warning"]
elapsed = (datetime.now() - start).total_seconds()
score = max(0, 100 - len(critical) * 15 - len(warning) * 5)
status = "healthy" if score >= 90 else ("warning" if score >= 60 else "critical")
diagnosis = {
"timestamp": start.isoformat(),
"elapsed_seconds": round(elapsed, 2),
"namespace": namespace or "all",
"score": score,
"status": status,
"connected": True,
"critical_count": len(critical),
"warning_count": len(warning),
"issues": all_issues,
"checks": results,
}
_last_diagnosis = diagnosis
_last_report = build_diagnosis_report(diagnosis)
yield f"data: {json.dumps({'type': 'complete', 'diagnosis': diagnosis}, ensure_ascii=False)}\n\n"
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
@app.post("/api/diagnose")
async def diagnose(req: DiagnoseRequest):
"""执行一键诊断 (非流式,兼容旧接口)"""
global _last_diagnosis, _last_report
_last_diagnosis = await run_full_diagnosis(namespace=req.namespace, checks=req.checks)
_last_report = build_diagnosis_report(_last_diagnosis)
return _last_diagnosis
@app.get("/api/diagnosis/latest")
async def get_latest():
"""获取最近一次诊断结果"""
if _last_diagnosis is None:
raise HTTPException(status_code=404, detail="暂无诊断记录,请先执行诊断")
return _last_diagnosis
@app.post("/api/analyze")
async def analyze(req: AnalyzeRequest):
"""AI 分析诊断结果"""
global _last_diagnosis, _last_report
if _last_diagnosis is None:
_last_diagnosis = await run_full_diagnosis()
_last_report = build_diagnosis_report(_last_diagnosis)
result = await analyze_with_ai(_last_report, question=req.question)
return result
@app.post("/api/chat")
async def chat(req: ChatRequest):
"""AI 多轮对话 (基于诊断上下文追问)"""
result = await chat_with_ai(req.messages, context=_last_report)
return result
@app.get("/api/report")
async def get_report():
"""获取诊断报告文本"""
if not _last_report:
raise HTTPException(status_code=404, detail="暂无诊断报告")
return {"report": _last_report}
# 静态文件服务 (前端)
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend", "dist")
if os.path.isdir(static_dir):
app.mount("/", StaticFiles(directory=static_dir, html=True), name="static")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=PORT)