292 lines
11 KiB
Python
292 lines
11 KiB
Python
# --- routers/config.py ---
|
||
import re
|
||
import json
|
||
import os
|
||
from fastapi import APIRouter, HTTPException
|
||
from pydantic import BaseModel
|
||
from typing import List
|
||
from netmiko import ConnectHandler
|
||
# 🌟 移除了舊的 load_settings, save_settings,改用專屬的雙軌機制
|
||
from shared import CMTS_DEVICE, cmts_config_lock, parse_cli_to_tree, deep_split_tree
|
||
from collections import defaultdict
|
||
|
||
router = APIRouter()
|
||
|
||
# ==========================================
|
||
# 🌟 雙軌過濾器檔案讀寫輔助函數
|
||
# ==========================================
|
||
def get_filter_file_path(config_type: str) -> str:
|
||
# 確保檔名安全,只允許 'running' 或 'full'
|
||
safe_type = "full" if config_type == "full" else "running"
|
||
return f"filters_{safe_type}.json"
|
||
|
||
def load_tree_filters(config_type: str) -> list:
|
||
file_path = get_filter_file_path(config_type)
|
||
if os.path.exists(file_path):
|
||
try:
|
||
with open(file_path, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
return data.get("hidden_keys", [])
|
||
except Exception as e:
|
||
print(f"讀取過濾器檔案失敗: {e}")
|
||
return []
|
||
return []
|
||
|
||
def save_tree_filters(config_type: str, hidden_keys: list):
|
||
file_path = get_filter_file_path(config_type)
|
||
try:
|
||
with open(file_path, 'w', encoding='utf-8') as f:
|
||
json.dump({"hidden_keys": hidden_keys}, f, ensure_ascii=False, indent=4)
|
||
except Exception as e:
|
||
print(f"儲存過濾器檔案失敗: {e}")
|
||
|
||
# ==========================================
|
||
|
||
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]
|
||
interfaces_with_admin_state: List[str] = []
|
||
|
||
@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. 批次送出設定指令
|
||
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)
|
||
"""
|
||
interface_groups = defaultdict(list)
|
||
interface_admin_states = {}
|
||
|
||
for diff in req.diffs:
|
||
parts = diff.path.split('::')
|
||
|
||
param_name = parts[-1]
|
||
interface_path = " ".join(parts[:-1])
|
||
|
||
# 全域清除路徑中間可能夾帶的虛擬資料夾索引
|
||
interface_path = re.sub(r"\s*\[\d+\]", "", interface_path)
|
||
|
||
if param_name == "admin-state":
|
||
interface_admin_states[interface_path] = diff.new_val
|
||
else:
|
||
interface_groups[interface_path].append((param_name, diff.old_val, diff.new_val))
|
||
|
||
cli_lines = []
|
||
cli_lines.append("! --- Auto-Generated Configuration Script ---")
|
||
|
||
for interface, changes in interface_groups.items():
|
||
cli_lines.append(f"! Configuring: {interface}")
|
||
|
||
supports_admin_state = interface in req.interfaces_with_admin_state
|
||
|
||
if supports_admin_state:
|
||
cli_lines.append(f"{interface} admin-state down")
|
||
|
||
for param, old_val, new_val in changes:
|
||
if re.match(r"^\[\d+\]$", param):
|
||
if new_val == "":
|
||
if old_val:
|
||
cli_lines.append(f"no {interface} {old_val}")
|
||
else:
|
||
if old_val and old_val != new_val:
|
||
cli_lines.append(f"no {interface} {old_val}")
|
||
cli_lines.append(f"{interface} {new_val}")
|
||
else:
|
||
if new_val == "":
|
||
cli_lines.append(f"no {interface} {param}")
|
||
else:
|
||
cli_lines.append(f"{interface} {param} {new_val}")
|
||
|
||
if supports_admin_state:
|
||
final_state = interface_admin_states.get(interface, "up")
|
||
cli_lines.append(f"{interface} admin-state {final_state}")
|
||
|
||
cli_lines.append("commit")
|
||
cli_lines.append("!")
|
||
|
||
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}")
|
||
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)
|
||
command = 'show running-config | include "cable ds-rf-port"'
|
||
raw_output = str(net_connect.send_command(command, read_timeout=60))
|
||
net_connect.disconnect()
|
||
|
||
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 = str(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)
|
||
|
||
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, config_type: str = "running"):
|
||
"""獲取整台 CMTS 的完整配置,並轉換為大型樹狀 JSON"""
|
||
try:
|
||
device = CMTS_DEVICE.copy()
|
||
device.update({'host': host, 'username': username, 'password': password})
|
||
|
||
net_connect = ConnectHandler(**device)
|
||
|
||
if config_type == "full":
|
||
net_connect.send_command_timing("config")
|
||
command = "show full-configuration | nomore"
|
||
raw_output = str(net_connect.send_command(command, read_timeout=180))
|
||
net_connect.send_command_timing("exit")
|
||
else:
|
||
command = "show running-config | nomore"
|
||
raw_output = str(net_connect.send_command(command, read_timeout=180))
|
||
|
||
net_connect.disconnect()
|
||
|
||
clean_output = re.sub(r"^(?:Building configuration\.\.\.|Current configuration.*?)\n+", "", raw_output, flags=re.IGNORECASE | re.MULTILINE)
|
||
clean_output = clean_output.lstrip()
|
||
|
||
base_tree = parse_cli_to_tree(clean_output)
|
||
perfect_tree = deep_split_tree(base_tree)
|
||
|
||
# ==========================================
|
||
# 🌟 深度過濾攔截器:改用雙軌過濾器讀取邏輯
|
||
# ==========================================
|
||
if not skip_filter:
|
||
hidden_keys = load_tree_filters(config_type)
|
||
|
||
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]
|
||
config_type: str = "running" # 🌟 加入 config_type 參數
|
||
|
||
@router.get("/settings/tree-filters")
|
||
async def get_tree_filters(config_type: str = "running"):
|
||
"""獲取目前的樹狀圖隱藏名單"""
|
||
# 🌟 根據 config_type 讀取對應的 JSON
|
||
hidden_keys = load_tree_filters(config_type)
|
||
return {"status": "success", "data": hidden_keys}
|
||
|
||
@router.post("/settings/tree-filters")
|
||
async def update_tree_filters(req: SettingsRequest):
|
||
"""更新樹狀圖隱藏名單並存檔"""
|
||
# 🌟 根據 config_type 寫入對應的 JSON
|
||
save_tree_filters(req.config_type, req.hidden_keys)
|
||
return {"status": "success", "message": f"系統設定 ({req.config_type}) 已更新"}
|