scm-harmonic-cmts-admin/shared.py

285 lines
11 KiB
Python
Raw Permalink 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.

# --- shared.py ---
import asyncio
import json
import os
import re
from collections import defaultdict
DEBUG_MODE = False
USE_DB = True # PostgreSQL Feature Toggle
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
}
def parse_cli_to_tree(cli_text: str) -> dict:
"""
將 CMTS 的 CLI 配置文字轉換為完美的邏輯巢狀 JSON (兩階段解析法)
"""
# ==========================================
# 🌟 階段 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()]
# ==========================================
# 階段 1建立實體樹 (嚴格比對縮排與 '!' 的對應關係)
# ==========================================
def build_raw_tree(start_idx, current_indent):
nodes = []
i = start_idx
while i < len(lines):
line = lines[i]
stripped = line.strip()
indent = len(line) - len(line.lstrip())
# 🌟 嚴格判斷 1如果遇到縮排退回 (包含縮排退回的 !),代表當前深度的區塊已結束
if indent < current_indent:
return nodes, i
# 🌟 嚴格判斷 2處理同層級的 !
if stripped == '!':
# 能走到這裡,代表 indent >= current_indent
# 這是對應當前層級的結束符號或分隔符,我們將其消耗掉並繼續,絕對不提早 return
i += 1
continue
# 探測是否有子節點
has_children = False
if i + 1 < len(lines):
next_line = lines[i+1]
next_stripped = next_line.strip()
# 只有當下一行不是 ! 且縮排大於當前行時,才判定為有子節點
if next_stripped != '!':
next_indent = len(next_line) - len(next_line.lstrip())
if next_indent > indent:
has_children = True
if has_children:
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:
# 發生碰撞時的安全機制
idx = 1
while f"{k} [{idx}]" in dict1:
idx += 1
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
else:
if isinstance(current[part], dict) and isinstance(value_dict, dict):
deep_merge(current[part], value_dict)
else:
current[f"{part} (資料夾)"] = value_dict
else:
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)
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:
idx += 1
group_dict[f"[{idx}]"] = ""
grouped[prefix] = group_dict
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
return fold_nodes(raw_tree)
def deep_split_tree(data):
"""將平坦的 CLI 字典,依照空白字元拆分成深層巢狀結構 (具備終極型態衝突保護)"""
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:
# 💡 終極衝突保護:安全轉型,絕不遺失資料
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):
# 原本是資料夾,新來的是字串 -> 塞入虛擬 key [0], [1]
idx = 0
while f"[{idx}]" in current[part]: idx += 1
current[part][f"[{idx}]"] = processed_value
elif not isinstance(current[part], dict) and isinstance(processed_value, dict):
# 原本是字串,新來的是資料夾 -> 升級成資料夾,並保留原字串至 [0]
old_val = current[part]
current[part] = processed_value
idx = 0
while f"[{idx}]" in current[part]: idx += 1
current[part][f"[{idx}]"] = old_val
else:
# 兩者都是字串 -> 升級成資料夾,包含兩個字串
old_val = current[part]
current[part] = {"[0]": old_val, "[1]": processed_value}
else:
if part not in current:
current[part] = {}
elif not isinstance(current[part], dict):
# 中間節點原本是字串,必須升級為資料夾,並保留原字串
old_val = current[part]
current[part] = {"[0]": old_val}
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)
# 全域非同步鎖:確保同一時間只有一人能執行設定寫入
cmts_config_lock = asyncio.Lock()