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

258 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# --- routers/config.py ---
import re
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List
from netmiko import ConnectHandler
# 💡 確保從 shared 引入 deep_split_tree
from shared import CMTS_DEVICE, cmts_config_lock, parse_cli_to_tree, deep_split_tree, load_settings, save_settings
from collections import defaultdict
router = APIRouter()
class ConfigRequest(BaseModel):
script: str
host: str
username: str
password: str
# 定義前端傳來的 Diff 資料結構
class DiffItem(BaseModel):
path: str # 例如: "cable.ds-rf-port.1:9/0.down-channel.0.frequency"
old_val: str
new_val: str
class GenerateCliRequest(BaseModel):
diffs: List[DiffItem]
@router.post("/cmts-config")
async def execute_cmts_config(req: ConfigRequest):
# 使用非同步鎖,確保同一時間只有一個寫入任務執行
async with cmts_config_lock:
try:
device = CMTS_DEVICE.copy()
device.update({'host': req.host, 'username': req.username, 'password': req.password})
# 過濾掉空行與註解 (! 開頭的行)
commands = [cmd.strip() for cmd in req.script.splitlines() if cmd.strip() and not cmd.strip().startswith('!')]
net_connect = ConnectHandler(**device)
# 🌟 1. 手動進入設定模式 (恢復您原本正確的寫法)
net_connect.send_command_timing("config")
# 🌟 2. 批次送出設定指令
# 加上 enter_config_mode=False 與 exit_config_mode=False
# 這樣 Netmiko 就不會自作主張去打 config terminal 了
output = net_connect.send_config_set(
commands,
read_timeout=60,
cmd_verify=False,
enter_config_mode=False,
exit_config_mode=False
)
# 🌟 3. 手動退出設定模式
net_connect.send_command_timing("exit")
net_connect.disconnect()
# 🌟 新增:檢查設備回傳的文字中是否包含拒絕或錯誤的關鍵字
lower_output = output.lower()
if "aborted" in lower_output or "error" in lower_output or "invalid" in lower_output:
# 回傳錯誤狀態,前端就會顯示紅色字體且「不會」自動關閉視窗
return {
"status": "error",
"message": "CMTS 拒絕了設定 (Commit Aborted)",
"detail": output
}
return {"status": "success", "data": output}
except Exception as e:
raise HTTPException(status_code=500, detail=f"CMTS Config Error: {str(e)}")
@router.post("/cmts-generate-cli")
async def generate_cli(req: GenerateCliRequest):
"""
將前端的 JSON Diff 轉譯為 Harmonic 單行 CLI 腳本
並自動處理 admin-state 的安全生命週期 (先 down 後 up)
"""
# 用來將同一個介面的變更群組化
# 結構: { "cable ds-rf-port 1:9/0 down-channel 0": [ ("frequency", "350000000"), ... ] }
interface_groups = defaultdict(list)
# 用來記錄該介面最終的 admin-state 應該是什麼 (預設為不變,若有改動則更新)
interface_admin_states = {}
for diff in req.diffs:
# 將路徑切分:例如 ['cable', 'ds-rf-port', '1:9/0', 'down-channel', '0', 'frequency']
parts = diff.path.split('::')
# 由於 Harmonic 的樹狀圖結構與 CLI 結構高度一致
# 我們可以假設「最後一個元素」是參數名稱,「前面的所有元素」組合起來就是介面路徑
param_name = parts[-1]
interface_path = " ".join(parts[:-1]) # 例如: "cable ds-rf-port 1:9/0 down-channel 0"
if param_name == "admin-state":
# 如果使用者直接修改了 admin-state我們記錄下來作為最後的狀態
interface_admin_states[interface_path] = diff.new_val
else:
# 一般參數變更,加入群組
interface_groups[interface_path].append((param_name, diff.new_val))
# 開始組裝最終的 CLI 腳本
cli_lines = []
# 為了視覺清晰,我們可以加上註解 (您的執行 API 已經會自動過濾 ! 開頭的行)
cli_lines.append("! --- Auto-Generated Configuration Script ---")
for interface, changes in interface_groups.items():
cli_lines.append(f"! Configuring: {interface}")
# 1. 安全機制:先強制將介面 admin-state down
cli_lines.append(f"{interface} admin-state down")
# 2. 寫入所有被修改的參數
for param, new_val in changes:
if new_val == "":
# 處理清空數值的情況 (通常是加上 no)
cli_lines.append(f"no {interface} {param}")
else:
cli_lines.append(f"{interface} {param} {new_val}")
# 3. 恢復狀態:如果使用者有特別設定 admin-state 就用使用者的,否則預設恢復為 up
final_state = interface_admin_states.get(interface, "up")
cli_lines.append(f"{interface} admin-state {final_state}")
# 🌟 新增Harmonic 必須要有 commit 才會生效
cli_lines.append("commit")
cli_lines.append("!") # 空行分隔
# 將獨立修改 admin-state (沒有修改其他參數) 的情況也補上
for interface, state in interface_admin_states.items():
if interface not in interface_groups:
cli_lines.append(f"! Configuring state only: {interface}")
cli_lines.append(f"{interface} admin-state {state}")
# 🌟 新增Harmonic 必須要有 commit 才會生效
cli_lines.append("commit")
cli_lines.append("!")
# 將陣列組合成字串回傳
final_script = "\n".join(cli_lines)
return {"status": "success", "data": final_script}
# ==========================================
# 💡 以下為 DS RF Port 動態樹狀查詢 API
# ==========================================
@router.get("/cmts-ds-rf-port-list")
async def get_ds_rf_port_list(host: str, username: str, password: str = ""):
"""獲取設備上所有的 DS RF Port 清單"""
try:
device = CMTS_DEVICE.copy()
device.update({'host': host, 'username': username, 'password': password})
net_connect = ConnectHandler(**device)
# 使用 include 只抓取關鍵字,並把等待時間延長至 60 秒
command = 'show running-config | include "cable ds-rf-port"'
raw_output = net_connect.send_command(command, read_timeout=60)
net_connect.disconnect()
# 使用正規表達式擷取 port 號碼 (例如 62:0/0)
pattern = r"cable ds-rf-port\s+([0-9:/]+)"
ports = re.findall(pattern, raw_output)
unique_ports = sorted(list(set(ports)))
return {
"status": "success",
"data": unique_ports,
"raw_output": raw_output
}
except Exception as e:
return {"status": "error", "message": str(e)}
@router.get("/cmts-ds-rf-port-config")
async def get_ds_rf_port_config(target: str, host: str, username: str, password: str = ""):
"""獲取特定 DS RF Port 的配置,並轉換為樹狀 JSON"""
try:
device = CMTS_DEVICE.copy()
device.update({'host': host, 'username': username, 'password': password})
net_connect = ConnectHandler(**device)
command = f"show running-config interface cable ds-rf-port {target} | nomore"
raw_output = net_connect.send_command(command, read_timeout=60)
net_connect.disconnect()
# 1. 初步解析縮排
base_tree = parse_cli_to_tree(raw_output)
# 2. 💡 深層解析空白字元,建立完美樹狀結構
perfect_tree = deep_split_tree(base_tree)
return {
"status": "success",
"data": perfect_tree, # 回傳完美的樹狀結構
"raw_cli": raw_output.strip()
}
except Exception as e:
return {"status": "error", "message": str(e)}
@router.get("/cmts-full-config")
async def get_full_config(host: str, username: str, password: str = "", skip_filter: bool = False):
"""獲取整台 CMTS 的完整配置,並轉換為大型樹狀 JSON"""
try:
device = CMTS_DEVICE.copy()
device.update({'host': host, 'username': username, 'password': password})
net_connect = ConnectHandler(**device)
command = "show running-config | nomore"
raw_output = net_connect.send_command(command, read_timeout=60)
net_connect.disconnect()
base_tree = parse_cli_to_tree(raw_output)
perfect_tree = deep_split_tree(base_tree)
# ==========================================
# 💡 深度過濾攔截器:支援多層級路徑 (如 cable.mac-domain)
# ==========================================
if not skip_filter:
settings = load_settings()
hidden_keys = settings.get("hidden_keys", [])
for path in hidden_keys:
keys = path.split('.')
current = perfect_tree
# 走到倒數第二層
for k in keys[:-1]:
if isinstance(current, dict) and k in current:
current = current[k]
else:
current = None
break
# 刪除最後一層的目標節點
if isinstance(current, dict) and keys[-1] in current:
current.pop(keys[-1], None)
# ==========================================
return {"status": "success", "data": perfect_tree}
except Exception as e:
return {"status": "error", "message": str(e)}
# ==========================================
# 💡 系統設定 API (System Settings)
# ==========================================
class SettingsRequest(BaseModel):
hidden_keys: list[str]
@router.get("/settings/tree-filters")
async def get_tree_filters():
"""獲取目前的樹狀圖隱藏名單"""
settings = load_settings()
return {"status": "success", "data": settings["hidden_keys"]}
@router.post("/settings/tree-filters")
async def update_tree_filters(req: SettingsRequest):
"""更新樹狀圖隱藏名單並存檔"""
save_settings({"hidden_keys": req.hidden_keys})
return {"status": "success", "message": "系統設定已更新"}