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-04-29 10:00:40 +00:00
|
|
|
|
|
|
|
|
|
|
DEBUG_MODE = False
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
"""
|
|
|
|
|
|
將 CMTS 的 CLI 配置文字,根據縮排轉換為巢狀 JSON (Dictionary)
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 過濾掉空行與註解 (!)
|
|
|
|
|
|
lines = [line for line in cli_text.splitlines() if line.strip() and line.strip() != '!']
|
|
|
|
|
|
|
|
|
|
|
|
def parse_block(start_idx: int, base_indent: int):
|
|
|
|
|
|
block = {}
|
|
|
|
|
|
i = start_idx
|
|
|
|
|
|
while i < len(lines):
|
|
|
|
|
|
line = lines[i]
|
|
|
|
|
|
indent = len(line) - len(line.lstrip())
|
|
|
|
|
|
|
|
|
|
|
|
if indent < base_indent:
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
content = line.strip()
|
|
|
|
|
|
|
|
|
|
|
|
has_children = False
|
|
|
|
|
|
if i + 1 < len(lines):
|
|
|
|
|
|
next_indent = len(lines[i+1]) - len(lines[i+1].lstrip())
|
|
|
|
|
|
if next_indent > base_indent:
|
|
|
|
|
|
has_children = True
|
|
|
|
|
|
|
|
|
|
|
|
if has_children:
|
|
|
|
|
|
child_block, next_i = parse_block(i + 1, next_indent)
|
|
|
|
|
|
block[content] = child_block
|
|
|
|
|
|
i = next_i
|
|
|
|
|
|
else:
|
|
|
|
|
|
parts = content.split(maxsplit=1)
|
|
|
|
|
|
if len(parts) == 2:
|
|
|
|
|
|
block[parts[0]] = parts[1]
|
|
|
|
|
|
else:
|
|
|
|
|
|
block[parts[0]] = ""
|
|
|
|
|
|
i += 1
|
|
|
|
|
|
|
|
|
|
|
|
return block, i
|
|
|
|
|
|
|
|
|
|
|
|
tree, _ = parse_block(0, 0)
|
|
|
|
|
|
return tree
|
|
|
|
|
|
|
|
|
|
|
|
def deep_split_tree(data):
|
|
|
|
|
|
"""將平坦的 CLI 字典,依照空白字元拆分成深層巢狀結構 (具備型態衝突保護)"""
|
|
|
|
|
|
if not isinstance(data, dict):
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
nested_tree = {}
|
|
|
|
|
|
for key, value in data.items():
|
|
|
|
|
|
# 1. 遞迴處理子節點
|
|
|
|
|
|
processed_value = deep_split_tree(value)
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 將當前的 key 依照空白拆分
|
|
|
|
|
|
parts = str(key).strip().split()
|
|
|
|
|
|
if not parts:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 依序建立深層結構
|
|
|
|
|
|
current = nested_tree
|
|
|
|
|
|
for i, part in enumerate(parts):
|
|
|
|
|
|
if i == len(parts) - 1:
|
|
|
|
|
|
# 抵達最後一個單字,準備塞入值
|
|
|
|
|
|
if part not in current:
|
|
|
|
|
|
current[part] = processed_value
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 💡 衝突保護:如果節點已存在,根據型態進行安全合併
|
|
|
|
|
|
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):
|
|
|
|
|
|
# 原本是資料夾,新來的是字串 -> 保留資料夾
|
|
|
|
|
|
pass
|
|
|
|
|
|
elif not isinstance(current[part], dict) and isinstance(processed_value, dict):
|
|
|
|
|
|
# 原本是字串,新來的是資料夾 -> 升級成資料夾
|
|
|
|
|
|
current[part] = processed_value
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 兩者都是字串,直接覆蓋
|
|
|
|
|
|
current[part] = processed_value
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 建立中間的階層
|
|
|
|
|
|
if part not in current:
|
|
|
|
|
|
current[part] = {}
|
|
|
|
|
|
elif not isinstance(current[part], dict):
|
|
|
|
|
|
# 💡 衝突保護:如果中間節點原本被當作字串,強制升級為字典(資料夾)以便容納子節點
|
|
|
|
|
|
current[part] = {}
|
|
|
|
|
|
|
|
|
|
|
|
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()
|