scm-harmonic-cmts-admin/routers/diagnostics.py

158 lines
6.8 KiB
Python
Raw Permalink Normal View History

# --- routers/diagnostics.py ---
import asyncio
import re
import asyncssh
from logger import get_logger
from fastapi import APIRouter, HTTPException, Query
from typing import Dict, Any
logger = get_logger("app.diagnostics")
router = APIRouter(prefix="/cm-diagnostics", tags=["Diagnostics"])
@router.get("/")
async def get_cm_diagnostics(
host: str = Query(...),
username: str = Query(...),
password: str = Query(...),
mac: str = Query(..., description="Cable Modem MAC Address")
) -> Dict[str, Any]:
mac = mac.strip().lower()
if not mac:
raise HTTPException(status_code=400, detail="MAC Address is required")
result_data = {
"mac": mac,
"ip": "N/A",
"state": "N/A",
"cpe_count": 0,
"phy": {
"tx_power": None,
"rx_power": None
},
"upstreams": [],
"mer_channels": {}
}
try:
# 先用 wait_for 取得連線 (超過 10 秒會拋出 asyncio.TimeoutError)
conn = await asyncio.wait_for(
asyncssh.connect(host, username=username, password=password, known_hosts=None),
timeout=10.0
)
# 再進入 async with 確保資源會被自動關閉
async with conn:
logger.debug(f"🚀 [Diagnostics] 開始診斷 MAC: {mac}")
# ==========================================
# 1. 查詢基本狀態
# ==========================================
cmd_base = "show cable modem " + mac + " | nomore"
res_base = await conn.run(cmd_base, check=False)
if res_base.exit_status == 0 and res_base.stdout:
for line in res_base.stdout.splitlines():
if mac in line.lower():
tokens = line.split()
for t in tokens:
if re.match(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$", t):
result_data["ip"] = t
elif re.match(r"^[a-z]-online|online|offline|init.*|reject", t, re.IGNORECASE):
result_data["state"] = t
nums = [t for t in tokens if t.isdigit()]
if nums:
result_data["cpe_count"] = int(nums[-1])
break
# ==========================================
# 2. 查詢 PHY 狀態 (💡 修復:移除寫死的 us/oad 判斷,改用通用特徵)
# ==========================================
cmd_phy = "show cable modem " + mac + " phy | nomore"
res_phy = await conn.run(cmd_phy, check=False)
if res_phy.exit_status == 0 and res_phy.stdout:
tx_list = []
rx_list = []
for line in res_phy.stdout.splitlines():
line_lower = line.lower()
if mac in line_lower:
tokens = line.split()
if len(tokens) >= 6:
ch_name = tokens[1]
# 💡 只要名稱包含 ":" 和 "/",就認定是合法的通道 (例如 Oa32, Oad32, Us32)
if ":" in ch_name and "/" in ch_name:
try:
snr_val = float(tokens[4])
result_data["upstreams"].append({"channel": ch_name, "snr": snr_val})
except:
pass
try:
tx_val = float(tokens[3].split('/')[0])
rx_list_val = float(tokens[5].split('/')[0])
tx_list.append(tx_val)
rx_list.append(rx_list_val)
except:
pass
if tx_list: result_data["phy"]["tx_power"] = round(sum(tx_list)/len(tx_list), 2)
if rx_list: result_data["phy"]["rx_power"] = round(sum(rx_list)/len(rx_list), 2)
# ==========================================
# 3. 尋找所有 OFDM 通道
# ==========================================
cmd_help = "show cable modem " + mac + " ofdm-channel ?"
res_help = await conn.run(cmd_help, check=False)
text_to_search = (res_help.stdout or "") + "\n" + (res_help.stderr or "")
cmd_verb = "show cable modem " + mac + " verbose | nomore"
res_verb = await conn.run(cmd_verb, check=False)
text_to_search += "\n" + (res_verb.stdout or "")
matches = re.findall(r"\b(Of(?:dm)?\d*[\d/:]+)\b", text_to_search, re.IGNORECASE)
ofdm_channels = []
for m in matches:
clean_m = m.lower()
if clean_m.startswith("of"):
clean_m = "Of" + clean_m[2:]
if clean_m not in ofdm_channels:
ofdm_channels.append(clean_m)
ofdm_channels.sort()
# ==========================================
# 4. 對每一個 OFDM 通道查詢 MER
# ==========================================
for ch in ofdm_channels:
cmd_mer = "show cable modem " + mac + " ofdm-channel " + ch + " mer | nomore"
res_mer = await conn.run(cmd_mer, check=False)
histogram = {}
min_mer = 999
if res_mer.exit_status == 0 and res_mer.stdout:
mer_lines = re.finditer(r"(\d+)\s*(?:db|dB)?\s*\|[^|]*\|?\s*(\**)", res_mer.stdout, re.IGNORECASE)
for match in mer_lines:
db_val = match.group(1)
stars = match.group(2)
if stars:
histogram[db_val] = len(stars) * 100
db_int = int(db_val)
if db_int < min_mer:
min_mer = db_int
health = "unknown"
if histogram and min_mer != 999:
if min_mer >= 41: health = "good"
elif min_mer >= 38: health = "warning"
else: health = "critical"
result_data["mer_channels"][ch] = {
"histogram": histogram,
"health": health,
"min_mer": min_mer if min_mer != 999 else "N/A"
}
logger.debug(f"✅ [Diagnostics] 解析結果: {result_data}")
return {"status": "success", "data": result_data}
except Exception as e:
logger.error("CM Diagnostics Error: " + str(e))
raise HTTPException(status_code=500, detail="設備連線或查詢失敗: " + str(e))