150 lines
6.6 KiB
Python
150 lines
6.6 KiB
Python
# --- routers/diagnostics.py ---
|
|
import re
|
|
import asyncssh
|
|
import logging
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from typing import Dict, Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
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:
|
|
async with asyncssh.connect(host, username=username, password=password, known_hosts=None) as conn:
|
|
print("\n🚀 [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"
|
|
}
|
|
|
|
print("✅ [Diagnostics] 解析結果: " + str(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)) |