初始化
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
"""SSH 连接服务"""
|
||||
import paramiko
|
||||
import re
|
||||
import time
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
@dataclass
|
||||
class ONUInfo:
|
||||
"""ONU 设备完整信息"""
|
||||
mac_address: str
|
||||
status: str # online/offline
|
||||
distance_m: Optional[int] = None # 距离(米)
|
||||
distance_str: Optional[str] = None # 距离原始字符串,如 "<1000"
|
||||
slot_number: Optional[int] = None # 插槽号
|
||||
port_number: Optional[int] = None # 端口号
|
||||
port_id: Optional[str] = None # 完整端口标识,如 "1/0/1:1"
|
||||
loid: Optional[str] = None # LOID
|
||||
model: Optional[str] = None # 设备型号
|
||||
|
||||
|
||||
class SSHService:
|
||||
"""SSH 连接和命令执行服务"""
|
||||
|
||||
def __init__(self, host: str, username: str, password: str, port: int = 22):
|
||||
self.host = host
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.port = port
|
||||
self.client: Optional[paramiko.SSHClient] = None
|
||||
self.shell = None
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""建立 SSH 连接,等待初始 banner 输出完毕"""
|
||||
try:
|
||||
self.client = paramiko.SSHClient()
|
||||
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
self.client.connect(
|
||||
hostname=self.host,
|
||||
port=self.port,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
timeout=30
|
||||
)
|
||||
self.shell = self.client.invoke_shell(width=200, height=50)
|
||||
# 等待登录 banner 输出完毕,直到出现命令提示符 ">"
|
||||
deadline = time.time() + 10
|
||||
buf = ""
|
||||
while time.time() < deadline:
|
||||
if self.shell.recv_ready():
|
||||
buf += self.shell.recv(4096).decode('utf-8', errors='ignore')
|
||||
if ">" in buf:
|
||||
break
|
||||
else:
|
||||
time.sleep(0.2)
|
||||
return True
|
||||
except Exception as e:
|
||||
raise Exception(f"SSH 连接失败: {str(e)}")
|
||||
|
||||
def execute_command(self, command: str) -> str:
|
||||
"""执行命令并处理 More 分页,等待命令提示符出现后返回"""
|
||||
if not self.shell:
|
||||
raise Exception("SSH 未连接")
|
||||
|
||||
self.shell.send(command + "\n")
|
||||
output = ""
|
||||
# 等待命令回显出现,再开始收集输出
|
||||
time.sleep(0.5)
|
||||
|
||||
deadline = time.time() + 60
|
||||
while time.time() < deadline:
|
||||
if self.shell.recv_ready():
|
||||
chunk = self.shell.recv(4096).decode('utf-8', errors='ignore')
|
||||
output += chunk
|
||||
if "---- More ----" in chunk:
|
||||
self.shell.send(" ")
|
||||
time.sleep(0.3)
|
||||
elif ">" in chunk:
|
||||
# 命令提示符出现,说明输出完毕
|
||||
break
|
||||
else:
|
||||
time.sleep(0.2)
|
||||
|
||||
return output
|
||||
|
||||
def clear_onu_port(self, port_id: str) -> bool:
|
||||
"""清除指定端口的 ONU 配置(恢复默认)
|
||||
流程: system-view -> interface Onu{port_id} -> default -> Y
|
||||
"""
|
||||
if not self.shell:
|
||||
raise Exception("SSH 未连接")
|
||||
|
||||
def send_and_wait(cmd: str, expect: str, timeout: int = 10) -> str:
|
||||
self.shell.send(cmd + "\n")
|
||||
buf = ""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
if self.shell.recv_ready():
|
||||
buf += self.shell.recv(4096).decode('utf-8', errors='ignore')
|
||||
if expect in buf:
|
||||
return buf
|
||||
else:
|
||||
time.sleep(0.2)
|
||||
return buf
|
||||
|
||||
# 进入系统视图
|
||||
out = send_and_wait("system-view", "]")
|
||||
if "]" not in out:
|
||||
raise Exception("进入 system-view 失败")
|
||||
|
||||
# 进入端口
|
||||
out = send_and_wait(f"interface Onu{port_id}", "]")
|
||||
if "]" not in out:
|
||||
raise Exception(f"进入端口 Onu{port_id} 失败")
|
||||
|
||||
# 执行 default,等待确认提示
|
||||
self.shell.send("default\n")
|
||||
buf = ""
|
||||
deadline = time.time() + 10
|
||||
while time.time() < deadline:
|
||||
if self.shell.recv_ready():
|
||||
buf += self.shell.recv(4096).decode('utf-8', errors='ignore')
|
||||
if "[Y/N]" in buf or "[y/n]" in buf:
|
||||
break
|
||||
else:
|
||||
time.sleep(0.2)
|
||||
|
||||
if "[Y/N]" not in buf and "[y/n]" not in buf:
|
||||
raise Exception("未收到确认提示")
|
||||
|
||||
# 确认
|
||||
self.shell.send("Y\n")
|
||||
time.sleep(1)
|
||||
# 排空缓冲区
|
||||
if self.shell.recv_ready():
|
||||
self.shell.recv(4096)
|
||||
|
||||
# 退出到用户视图
|
||||
send_and_wait("quit", "]", timeout=5)
|
||||
send_and_wait("quit", ">", timeout=5)
|
||||
|
||||
return True
|
||||
|
||||
def detect_loopback(self) -> dict:
|
||||
"""执行环路检测,返回 {has_loop: bool, interfaces: [str]}"""
|
||||
output = self.execute_command("display loopback-detection")
|
||||
has_loop = "Loop is detected on following interfaces" in output
|
||||
interfaces = []
|
||||
if has_loop:
|
||||
for line in output.splitlines():
|
||||
m = re.match(r'\s+(Onu\S+)\s+', line)
|
||||
if m:
|
||||
interfaces.append(m.group(1))
|
||||
return {"has_loop": has_loop, "interfaces": interfaces, "raw": output}
|
||||
|
||||
def parse_onu_status(self, output: str) -> Dict[str, str]:
|
||||
"""解析 ONU 状态输出"""
|
||||
devices = {}
|
||||
lines = output.split('\n')
|
||||
|
||||
for line in lines:
|
||||
# 匹配包含 MAC 地址的行
|
||||
mac_match = re.search(r'([0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4})', line, re.IGNORECASE)
|
||||
if mac_match:
|
||||
mac = mac_match.group(1).lower()
|
||||
# 提取状态字段
|
||||
if 'Up' in line:
|
||||
devices[mac] = 'online'
|
||||
elif 'Offline' in line:
|
||||
devices[mac] = 'offline'
|
||||
|
||||
return devices
|
||||
|
||||
def parse_onu_info(self, output: str) -> Tuple[Dict[str, ONUInfo], Dict[str, List[ONUInfo]]]:
|
||||
"""增强解析:提取完整 ONU 信息
|
||||
返回: (unique_devices, duplicate_devices)
|
||||
- unique_devices: MAC -> ONUInfo(每个 MAC 只保留最新端口)
|
||||
- duplicate_devices: MAC -> [ONUInfo, ...] (出现在多个端口的 MAC)
|
||||
"""
|
||||
all_records: Dict[str, List[ONUInfo]] = {}
|
||||
lines = output.split('\n')
|
||||
|
||||
current_slot = None
|
||||
|
||||
for line in lines:
|
||||
# 检测新的插槽区域: Olt1/0/1
|
||||
slot_match = re.search(r'Olt(\d+)/(\d+)/(\d+)', line)
|
||||
if slot_match:
|
||||
current_slot = int(slot_match.group(3))
|
||||
continue
|
||||
|
||||
# 跳过表头行和空行
|
||||
if 'MAC' in line and 'LOID' in line:
|
||||
continue
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
# 解析设备行
|
||||
device = self._parse_device_line(line, current_slot)
|
||||
if device:
|
||||
if device.mac_address not in all_records:
|
||||
all_records[device.mac_address] = []
|
||||
all_records[device.mac_address].append(device)
|
||||
|
||||
unique_devices: Dict[str, ONUInfo] = {}
|
||||
duplicate_devices: Dict[str, List[ONUInfo]] = {}
|
||||
|
||||
for mac, records in all_records.items():
|
||||
if len(records) == 1:
|
||||
unique_devices[mac] = records[0]
|
||||
else:
|
||||
duplicate_devices[mac] = records
|
||||
# unique 中保留在线的,若都离线则保留最后一条
|
||||
online = [r for r in records if r.status == 'online']
|
||||
unique_devices[mac] = online[0] if online else records[-1]
|
||||
|
||||
return unique_devices, duplicate_devices
|
||||
|
||||
def _parse_device_line(self, line: str, slot: Optional[int]) -> Optional[ONUInfo]:
|
||||
"""解析单行设备信息"""
|
||||
# 匹配 MAC 地址
|
||||
mac_match = re.search(r'([0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4})', line, re.IGNORECASE)
|
||||
if not mac_match:
|
||||
return None
|
||||
|
||||
mac = mac_match.group(1).lower()
|
||||
|
||||
# 提取状态
|
||||
status = 'offline'
|
||||
if 'Up' in line:
|
||||
status = 'online'
|
||||
|
||||
# 提取端口信息: Onu1/0/2:1 -> slot=2, port=1, port_id="1/0/2:1"
|
||||
slot_num, port_num, port_id = None, None, None
|
||||
port_match = re.search(r'Onu(\d+)/(\d+)/(\d+):(\d+)', line)
|
||||
if port_match:
|
||||
slot_num = int(port_match.group(3)) # 第三段数字为槽位
|
||||
port_num = int(port_match.group(4)) # 冒号后为端口号
|
||||
port_id = f"{port_match.group(1)}/{port_match.group(2)}/{port_match.group(3)}:{port_match.group(4)}"
|
||||
|
||||
# 提取距离 - Port 列前的字段,如 "<1000" 或 "N/A"
|
||||
distance_str = None
|
||||
dist_match = re.search(r'(\S+)\s+Onu\d+/\d+/\d+:\d+', line)
|
||||
if dist_match:
|
||||
val = dist_match.group(1)
|
||||
if val != 'N/A':
|
||||
distance_str = val
|
||||
|
||||
# 提取 LOID - MAC 后第一个非空字段
|
||||
loid = None
|
||||
loid_match = re.search(r'[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}\s+(\S+)', line, re.IGNORECASE)
|
||||
if loid_match:
|
||||
loid_val = loid_match.group(1)
|
||||
if loid_val != 'N/A' and loid_val.isdigit():
|
||||
loid = loid_val
|
||||
|
||||
# 提取设备型号 - Port 列之后的第一个字段,如 "WA6520H-EGPON/A"
|
||||
model = None
|
||||
model_match = re.search(r'Onu\d+/\d+/\d+:\d+\s+(\S+)', line)
|
||||
if model_match:
|
||||
potential_model = model_match.group(1)
|
||||
if potential_model != 'N/A':
|
||||
model = potential_model
|
||||
|
||||
return ONUInfo(
|
||||
mac_address=mac,
|
||||
status=status,
|
||||
distance_m=None,
|
||||
distance_str=distance_str,
|
||||
slot_number=slot_num or slot,
|
||||
port_number=port_num,
|
||||
port_id=port_id,
|
||||
loid=loid,
|
||||
model=model
|
||||
)
|
||||
|
||||
def close(self):
|
||||
"""关闭 SSH 连接"""
|
||||
if self.client:
|
||||
self.client.close()
|
||||
Reference in New Issue
Block a user