2026-04-29 10:00:40 +00:00
|
|
|
|
# --- shared.py ---
|
|
|
|
|
|
import asyncio
|
2026-04-30 08:27:18 +00:00
|
|
|
|
import json
|
|
|
|
|
|
import os
|
2026-05-11 10:16:03 +00:00
|
|
|
|
import re
|
|
|
|
|
|
from collections import defaultdict
|
2026-04-29 10:00:40 +00:00
|
|
|
|
|
|
|
|
|
|
DEBUG_MODE = False
|
2026-05-20 09:19:40 +00:00
|
|
|
|
USE_DB = True # PostgreSQL Feature Toggle
|
2026-04-29 10:00:40 +00:00
|
|
|
|
|
|
|
|
|
|
def debug_print(msg: str):
|
|
|
|
|
|
if DEBUG_MODE:
|
|
|
|
|
|
print(msg)
|
|
|
|
|
|
|
|
|
|
|
|
# 預設的 CMTS 連線樣板
|
|
|
|
|
|
CMTS_DEVICE = {
|
|
|
|
|
|
'device_type': 'cisco_ios',
|
|
|
|
|
|
'host': '10.14.110.4',
|
|
|
|
|
|
'username': 'admin',
|
|
|
|
|
|
'password': 'nsgadmin',
|
|
|
|
|
|
'port': 22,
|
|
|
|
|
|
'fast_cli': False,
|
|
|
|
|
|
'global_delay_factor': 2
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 08:27:18 +00:00
|
|
|
|
def parse_cli_to_tree(cli_text: str) -> dict:
|
|
|
|
|
|
"""
|
2026-05-11 10:16:03 +00:00
|
|
|
|
將 CMTS 的 CLI 配置文字轉換為完美的邏輯巢狀 JSON (兩階段解析法)
|
2026-04-30 08:27:18 +00:00
|
|
|
|
"""
|
2026-05-13 02:32:26 +00:00
|
|
|
|
# ==========================================
|
|
|
|
|
|
# 🌟 階段 0:預處理 (修復終端機 200 字元自動折行問題)
|
|
|
|
|
|
# ==========================================
|
|
|
|
|
|
raw_lines = cli_text.split('\n')
|
|
|
|
|
|
fixed_lines = []
|
|
|
|
|
|
|
|
|
|
|
|
# 定義常見的 Root 指令關鍵字,避免把正常的指令誤判為斷行
|
|
|
|
|
|
root_keywords = (
|
|
|
|
|
|
'alias', 'ssh', 'hostname', 'logging', 'cable', 'ipdr',
|
|
|
|
|
|
'snmp-server', 'aaa', 'radius-server', 'privilege', 'cli', 'packetcable',
|
|
|
|
|
|
'system', 'network', 'clock', 'no'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
for line in raw_lines:
|
|
|
|
|
|
line = line.replace('\r', '')
|
|
|
|
|
|
if not line.strip():
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 💡 判斷是否為被截斷的行:
|
|
|
|
|
|
# 1. 前一行長度非常長 (大於 190 字元,根據您的截圖,極限是 199)
|
|
|
|
|
|
# 2. 當前行「沒有縮排」(終端機自動折行會把字元推到最左邊第 0 格)
|
|
|
|
|
|
# 3. 當前行不是註解 '!'
|
|
|
|
|
|
if fixed_lines and len(fixed_lines[-1]) > 190 and not line.startswith(' ') and not line.startswith('!'):
|
|
|
|
|
|
# 取得當前行的第一個單字
|
|
|
|
|
|
first_word = line.strip().split()[0] if line.strip() else ""
|
|
|
|
|
|
|
|
|
|
|
|
# 確保它不是一個正常的 Root 指令
|
|
|
|
|
|
if first_word not in root_keywords:
|
|
|
|
|
|
# ✅ 觸發黏合邏輯:將這行直接接在上一行後面
|
|
|
|
|
|
fixed_lines[-1] = fixed_lines[-1] + line
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
fixed_lines.append(line)
|
|
|
|
|
|
|
|
|
|
|
|
# 統一去除右側多餘空白,準備進入階段 1
|
|
|
|
|
|
lines = [line.rstrip() for line in fixed_lines if line.strip()]
|
2026-05-11 10:16:03 +00:00
|
|
|
|
|
|
|
|
|
|
# ==========================================
|
2026-05-13 02:32:26 +00:00
|
|
|
|
# 階段 1:建立實體樹 (嚴格比對縮排與 '!' 的對應關係)
|
2026-05-11 10:16:03 +00:00
|
|
|
|
# ==========================================
|
|
|
|
|
|
def build_raw_tree(start_idx, current_indent):
|
|
|
|
|
|
nodes = []
|
2026-04-30 08:27:18 +00:00
|
|
|
|
i = start_idx
|
|
|
|
|
|
while i < len(lines):
|
|
|
|
|
|
line = lines[i]
|
2026-05-11 10:16:03 +00:00
|
|
|
|
stripped = line.strip()
|
2026-04-30 08:27:18 +00:00
|
|
|
|
indent = len(line) - len(line.lstrip())
|
|
|
|
|
|
|
2026-05-28 05:28:39 +00:00
|
|
|
|
# 🌟 嚴格判斷 1:遇到「真實指令」縮排退回,才代表區塊結束
|
|
|
|
|
|
# (加上 stripped != '!',防止設備偶發的「0 縮排 !」導致區塊被誤殺)
|
|
|
|
|
|
if indent < current_indent and stripped != '!':
|
2026-05-11 10:16:03 +00:00
|
|
|
|
return nodes, i
|
2026-05-13 02:32:26 +00:00
|
|
|
|
|
2026-05-28 05:28:39 +00:00
|
|
|
|
# 🌟 嚴格判斷 2:處理 ! (無論縮排多少,都當純分隔符消耗掉)
|
2026-05-13 02:32:26 +00:00
|
|
|
|
if stripped == '!':
|
|
|
|
|
|
i += 1
|
|
|
|
|
|
continue
|
2026-04-30 08:27:18 +00:00
|
|
|
|
|
2026-05-28 05:28:39 +00:00
|
|
|
|
# 🌟 探測是否有子節點 (強化版:往下尋找第一個「非 !」的真實指令來判斷)
|
2026-04-30 08:27:18 +00:00
|
|
|
|
has_children = False
|
2026-05-28 05:28:39 +00:00
|
|
|
|
next_i = i + 1
|
|
|
|
|
|
while next_i < len(lines) and lines[next_i].strip() == '!':
|
|
|
|
|
|
next_i += 1
|
|
|
|
|
|
|
|
|
|
|
|
if next_i < len(lines):
|
|
|
|
|
|
next_line = lines[next_i]
|
|
|
|
|
|
next_indent = len(next_line) - len(next_line.lstrip())
|
|
|
|
|
|
if next_indent > indent:
|
|
|
|
|
|
has_children = True
|
2026-04-30 08:27:18 +00:00
|
|
|
|
|
|
|
|
|
|
if has_children:
|
2026-05-11 10:16:03 +00:00
|
|
|
|
children, next_i = build_raw_tree(i + 1, next_indent)
|
|
|
|
|
|
nodes.append({"type": "folder", "content": stripped, "children": children})
|
|
|
|
|
|
i = next_i
|
|
|
|
|
|
else:
|
|
|
|
|
|
nodes.append({"type": "leaf", "content": stripped})
|
|
|
|
|
|
i += 1
|
|
|
|
|
|
return nodes, i
|
|
|
|
|
|
|
|
|
|
|
|
raw_tree, _ = build_raw_tree(0, 0)
|
|
|
|
|
|
|
|
|
|
|
|
# ==========================================
|
|
|
|
|
|
# 階段 2:邏輯群組化與字典合併
|
|
|
|
|
|
# ==========================================
|
|
|
|
|
|
def deep_merge(dict1, dict2):
|
|
|
|
|
|
"""深度合併兩個字典 (解決同名資料夾與指令群組的融合)"""
|
|
|
|
|
|
for k, v in dict2.items():
|
|
|
|
|
|
if k in dict1:
|
|
|
|
|
|
if isinstance(dict1[k], dict) and isinstance(v, dict):
|
|
|
|
|
|
deep_merge(dict1[k], v)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 發生碰撞時的安全機制
|
2026-05-08 08:50:12 +00:00
|
|
|
|
idx = 1
|
2026-05-11 10:16:03 +00:00
|
|
|
|
while f"{k} [{idx}]" in dict1:
|
2026-05-08 08:50:12 +00:00
|
|
|
|
idx += 1
|
2026-05-11 10:16:03 +00:00
|
|
|
|
dict1[f"{k} [{idx}]"] = v
|
|
|
|
|
|
else:
|
|
|
|
|
|
dict1[k] = v
|
|
|
|
|
|
|
|
|
|
|
|
def nest_folder_key(key_string, value_dict, target_dict):
|
|
|
|
|
|
"""將 'cable mac-domain 13' 拆解為巢狀字典"""
|
|
|
|
|
|
parts = key_string.split()
|
|
|
|
|
|
current = target_dict
|
|
|
|
|
|
for i, part in enumerate(parts):
|
|
|
|
|
|
if i == len(parts) - 1:
|
|
|
|
|
|
if part not in current:
|
|
|
|
|
|
current[part] = value_dict
|
2026-05-08 08:50:12 +00:00
|
|
|
|
else:
|
2026-05-11 10:16:03 +00:00
|
|
|
|
if isinstance(current[part], dict) and isinstance(value_dict, dict):
|
|
|
|
|
|
deep_merge(current[part], value_dict)
|
|
|
|
|
|
else:
|
|
|
|
|
|
current[f"{part} (資料夾)"] = value_dict
|
2026-04-30 08:27:18 +00:00
|
|
|
|
else:
|
2026-05-11 10:16:03 +00:00
|
|
|
|
if part not in current or not isinstance(current[part], dict):
|
|
|
|
|
|
current[part] = {}
|
|
|
|
|
|
current = current[part]
|
|
|
|
|
|
|
|
|
|
|
|
def group_leaves(leaf_strings):
|
|
|
|
|
|
"""將單行指令依據共同前綴進行遞迴群組化"""
|
|
|
|
|
|
grouped = {}
|
|
|
|
|
|
first_word_map = defaultdict(list)
|
|
|
|
|
|
|
|
|
|
|
|
for s in leaf_strings:
|
|
|
|
|
|
parts = s.split(maxsplit=1)
|
|
|
|
|
|
first_word = parts[0]
|
|
|
|
|
|
remainder = parts[1] if len(parts) > 1 else ""
|
|
|
|
|
|
first_word_map[first_word].append(remainder)
|
|
|
|
|
|
|
|
|
|
|
|
for prefix, remainders in first_word_map.items():
|
|
|
|
|
|
if len(remainders) == 1:
|
|
|
|
|
|
grouped[prefix] = remainders[0]
|
|
|
|
|
|
else:
|
|
|
|
|
|
valid_remainders = [r for r in remainders if r]
|
|
|
|
|
|
empty_count = len(remainders) - len(valid_remainders)
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-11 10:16:03 +00:00
|
|
|
|
group_dict = {}
|
|
|
|
|
|
if valid_remainders:
|
|
|
|
|
|
sub_grouped = group_leaves(valid_remainders)
|
|
|
|
|
|
|
|
|
|
|
|
# 判斷是否全為純陣列值 (例如 IP 列表)
|
|
|
|
|
|
is_all_empty_vals = all(not isinstance(v, dict) and v == "" for v in sub_grouped.values())
|
|
|
|
|
|
|
|
|
|
|
|
if is_all_empty_vals:
|
|
|
|
|
|
for i, k in enumerate(sub_grouped.keys()):
|
|
|
|
|
|
group_dict[f"[{i}]"] = k
|
|
|
|
|
|
else:
|
|
|
|
|
|
idx = 0
|
|
|
|
|
|
for k, v in sub_grouped.items():
|
|
|
|
|
|
if isinstance(v, dict):
|
|
|
|
|
|
group_dict[k] = v
|
|
|
|
|
|
elif v == "":
|
|
|
|
|
|
group_dict[f"[{idx}]"] = k
|
|
|
|
|
|
idx += 1
|
|
|
|
|
|
else:
|
|
|
|
|
|
group_dict[k] = v
|
|
|
|
|
|
|
|
|
|
|
|
# 補回完全沒有後續參數的指令
|
|
|
|
|
|
for _ in range(empty_count):
|
|
|
|
|
|
idx = 0
|
|
|
|
|
|
while f"[{idx}]" in group_dict:
|
2026-05-08 08:50:12 +00:00
|
|
|
|
idx += 1
|
2026-05-11 10:16:03 +00:00
|
|
|
|
group_dict[f"[{idx}]"] = ""
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-11 10:16:03 +00:00
|
|
|
|
grouped[prefix] = group_dict
|
2026-04-30 08:27:18 +00:00
|
|
|
|
|
2026-05-11 10:16:03 +00:00
|
|
|
|
return grouped
|
|
|
|
|
|
|
|
|
|
|
|
def fold_nodes(nodes):
|
|
|
|
|
|
result = {}
|
|
|
|
|
|
# 1. 處理資料夾 (Folders)
|
|
|
|
|
|
for node in [n for n in nodes if n["type"] == "folder"]:
|
|
|
|
|
|
folded_children = fold_nodes(node["children"])
|
|
|
|
|
|
nest_folder_key(node["content"], folded_children, result)
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 處理單行指令 (Leaves)
|
|
|
|
|
|
leaves = [n["content"] for n in nodes if n["type"] == "leaf"]
|
|
|
|
|
|
if leaves:
|
|
|
|
|
|
leaf_dict = group_leaves(leaves)
|
|
|
|
|
|
deep_merge(result, leaf_dict)
|
|
|
|
|
|
return result
|
2026-04-30 08:27:18 +00:00
|
|
|
|
|
2026-05-11 10:16:03 +00:00
|
|
|
|
return fold_nodes(raw_tree)
|
2026-04-30 08:27:18 +00:00
|
|
|
|
|
|
|
|
|
|
def deep_split_tree(data):
|
2026-05-08 08:50:12 +00:00
|
|
|
|
"""將平坦的 CLI 字典,依照空白字元拆分成深層巢狀結構 (具備終極型態衝突保護)"""
|
2026-04-30 08:27:18 +00:00
|
|
|
|
if not isinstance(data, dict):
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
nested_tree = {}
|
|
|
|
|
|
for key, value in data.items():
|
|
|
|
|
|
processed_value = deep_split_tree(value)
|
|
|
|
|
|
|
|
|
|
|
|
parts = str(key).strip().split()
|
|
|
|
|
|
if not parts:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
current = nested_tree
|
|
|
|
|
|
for i, part in enumerate(parts):
|
|
|
|
|
|
if i == len(parts) - 1:
|
|
|
|
|
|
if part not in current:
|
|
|
|
|
|
current[part] = processed_value
|
|
|
|
|
|
else:
|
2026-05-08 08:50:12 +00:00
|
|
|
|
# 💡 終極衝突保護:安全轉型,絕不遺失資料
|
2026-04-30 08:27:18 +00:00
|
|
|
|
if isinstance(current[part], dict) and isinstance(processed_value, dict):
|
|
|
|
|
|
current[part].update(processed_value)
|
|
|
|
|
|
elif isinstance(current[part], dict) and not isinstance(processed_value, dict):
|
2026-05-08 08:50:12 +00:00
|
|
|
|
# 原本是資料夾,新來的是字串 -> 塞入虛擬 key [0], [1]
|
|
|
|
|
|
idx = 0
|
|
|
|
|
|
while f"[{idx}]" in current[part]: idx += 1
|
|
|
|
|
|
current[part][f"[{idx}]"] = processed_value
|
2026-04-30 08:27:18 +00:00
|
|
|
|
elif not isinstance(current[part], dict) and isinstance(processed_value, dict):
|
2026-05-08 08:50:12 +00:00
|
|
|
|
# 原本是字串,新來的是資料夾 -> 升級成資料夾,並保留原字串至 [0]
|
|
|
|
|
|
old_val = current[part]
|
2026-04-30 08:27:18 +00:00
|
|
|
|
current[part] = processed_value
|
2026-05-08 08:50:12 +00:00
|
|
|
|
idx = 0
|
|
|
|
|
|
while f"[{idx}]" in current[part]: idx += 1
|
|
|
|
|
|
current[part][f"[{idx}]"] = old_val
|
2026-04-30 08:27:18 +00:00
|
|
|
|
else:
|
2026-05-08 08:50:12 +00:00
|
|
|
|
# 兩者都是字串 -> 升級成資料夾,包含兩個字串
|
|
|
|
|
|
old_val = current[part]
|
|
|
|
|
|
current[part] = {"[0]": old_val, "[1]": processed_value}
|
2026-04-30 08:27:18 +00:00
|
|
|
|
else:
|
|
|
|
|
|
if part not in current:
|
|
|
|
|
|
current[part] = {}
|
|
|
|
|
|
elif not isinstance(current[part], dict):
|
2026-05-08 08:50:12 +00:00
|
|
|
|
# 中間節點原本是字串,必須升級為資料夾,並保留原字串
|
|
|
|
|
|
old_val = current[part]
|
|
|
|
|
|
current[part] = {"[0]": old_val}
|
2026-04-30 08:27:18 +00:00
|
|
|
|
current = current[part]
|
|
|
|
|
|
|
|
|
|
|
|
return nested_tree
|
|
|
|
|
|
|
|
|
|
|
|
# ==========================================
|
|
|
|
|
|
# 💡 系統設定管理 (System Settings Manager)
|
|
|
|
|
|
# ==========================================
|
|
|
|
|
|
SETTINGS_FILE = "system_settings.json"
|
|
|
|
|
|
|
|
|
|
|
|
def load_settings():
|
|
|
|
|
|
"""讀取系統設定,若檔案不存在則回傳預設的黑名單"""
|
|
|
|
|
|
if os.path.exists(SETTINGS_FILE):
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(SETTINGS_FILE, "r", encoding="utf-8") as f:
|
|
|
|
|
|
return json.load(f)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
# 預設隱藏這些比較雜亂且不常修改的系統層級設定
|
|
|
|
|
|
return {"hidden_keys": ["logging", "privilege", "aaa", "certificate", "line"]}
|
|
|
|
|
|
|
|
|
|
|
|
def save_settings(settings_data):
|
|
|
|
|
|
"""將系統設定寫入 JSON 檔案"""
|
|
|
|
|
|
with open(SETTINGS_FILE, "w", encoding="utf-8") as f:
|
|
|
|
|
|
json.dump(settings_data, f, indent=4, ensure_ascii=False)
|
|
|
|
|
|
|
2026-04-29 10:00:40 +00:00
|
|
|
|
# 全域非同步鎖:確保同一時間只有一人能執行設定寫入
|
|
|
|
|
|
cmts_config_lock = asyncio.Lock()
|