初始化
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
"""设备状态检查服务"""
|
||||
from typing import List, Dict, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from app.services.ssh_service import SSHService
|
||||
from app.models.device import OLTDevice, ONUDevice, DeviceStatusHistory, DuplicateMac, NewDevice
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
|
||||
def parse_distance(distance_str: Optional[str]) -> Optional[int]:
|
||||
"""将距离字符串转为整数,如 '<1000' -> 1000, '1234' -> 1234"""
|
||||
if not distance_str:
|
||||
return None
|
||||
m = re.search(r'\d+', distance_str)
|
||||
return int(m.group()) if m else None
|
||||
|
||||
|
||||
class CheckService:
|
||||
"""设备状态检查服务"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def update_status_only(self, olt_id: int) -> Dict:
|
||||
"""扫描 OLT,仅更新已有设备的在线状态和距离(按全局 MAC 匹配)。
|
||||
不修改 olt_id/端口等字段,不标记 unknown,不入库新设备。
|
||||
"""
|
||||
olt = self.db.query(OLTDevice).filter(OLTDevice.id == olt_id).first()
|
||||
if not olt:
|
||||
raise Exception(f"OLT 设备不存在: {olt_id}")
|
||||
|
||||
ssh = SSHService(olt.ip_address, olt.username, olt.password)
|
||||
try:
|
||||
ssh.connect()
|
||||
output = ssh.execute_command(olt.slot_command)
|
||||
onu_info_dict, _ = ssh.parse_onu_info(output)
|
||||
|
||||
checked_at = datetime.utcnow()
|
||||
online_count = 0
|
||||
offline_count = 0
|
||||
|
||||
# 全局 MAC → id 索引,只取需要的字段
|
||||
existing = {
|
||||
row.mac_address.lower(): row.id
|
||||
for row in self.db.query(ONUDevice.mac_address, ONUDevice.id).all()
|
||||
}
|
||||
|
||||
for mac, onu_info in onu_info_dict.items():
|
||||
onu_id = existing.get(mac)
|
||||
if onu_id is None:
|
||||
continue # 不在库中,跳过
|
||||
|
||||
status = onu_info.status
|
||||
distance_m = parse_distance(onu_info.distance_str)
|
||||
if status == 'online':
|
||||
online_count += 1
|
||||
else:
|
||||
offline_count += 1
|
||||
|
||||
self.db.add(DeviceStatusHistory(
|
||||
onu_device_id=onu_id,
|
||||
status=status,
|
||||
distance_m=distance_m,
|
||||
checked_at=checked_at,
|
||||
response_data=None,
|
||||
))
|
||||
|
||||
self.db.commit()
|
||||
return {
|
||||
"online": online_count,
|
||||
"offline": offline_count,
|
||||
}
|
||||
finally:
|
||||
ssh.close()
|
||||
|
||||
def check_single_device(self, device_id: int) -> Dict:
|
||||
"""通过 SSH 单独查询一台 ONU 设备的当前状态和距离。
|
||||
命令格式: display onu slot {slot} | include {mac}
|
||||
"""
|
||||
device = self.db.query(ONUDevice).filter(ONUDevice.id == device_id).first()
|
||||
if not device:
|
||||
raise Exception("设备不存在")
|
||||
if not device.olt_id:
|
||||
raise Exception("该设备未关联 OLT,无法查询")
|
||||
|
||||
olt = self.db.query(OLTDevice).filter(OLTDevice.id == device.olt_id).first()
|
||||
if not olt:
|
||||
raise Exception("关联的 OLT 不存在")
|
||||
|
||||
mac = device.mac_address.lower()
|
||||
base_cmd = olt.slot_command # e.g. "display onu slot"
|
||||
if device.slot_number is not None:
|
||||
cmd = f"{base_cmd} {device.slot_number} | include {mac}"
|
||||
else:
|
||||
cmd = f"{base_cmd} | include {mac}"
|
||||
|
||||
ssh = SSHService(olt.ip_address, olt.username, olt.password)
|
||||
try:
|
||||
ssh.connect()
|
||||
output = ssh.execute_command(cmd)
|
||||
|
||||
# 从输出中找到包含该 MAC 的行并解析
|
||||
info = None
|
||||
for line in output.splitlines():
|
||||
if mac in line.lower():
|
||||
info = ssh._parse_device_line(line, device.slot_number)
|
||||
if info:
|
||||
break
|
||||
|
||||
if info is None:
|
||||
# 未找到该 MAC,视为离线
|
||||
status = 'offline'
|
||||
distance_m = None
|
||||
else:
|
||||
status = info.status
|
||||
distance_m = parse_distance(info.distance_str)
|
||||
|
||||
self.db.add(DeviceStatusHistory(
|
||||
onu_device_id=device.id,
|
||||
status=status,
|
||||
distance_m=distance_m,
|
||||
checked_at=datetime.utcnow(),
|
||||
response_data=None,
|
||||
))
|
||||
self.db.commit()
|
||||
return {"status": status, "distance_m": distance_m}
|
||||
finally:
|
||||
ssh.close()
|
||||
|
||||
def scan_and_discover(self, olt_id: int) -> Dict:
|
||||
"""扫描 OLT,更新已有设备状态,并将新发现的在线设备入库。
|
||||
不标记 unknown,不修改已有设备的 olt_id/端口以外的字段。
|
||||
"""
|
||||
olt = self.db.query(OLTDevice).filter(OLTDevice.id == olt_id).first()
|
||||
if not olt:
|
||||
raise Exception(f"OLT 设备不存在: {olt_id}")
|
||||
|
||||
ssh = SSHService(olt.ip_address, olt.username, olt.password)
|
||||
try:
|
||||
ssh.connect()
|
||||
output = ssh.execute_command(olt.slot_command)
|
||||
onu_info_dict, duplicate_dict = ssh.parse_onu_info(output)
|
||||
|
||||
checked_at = datetime.utcnow()
|
||||
online_count = 0
|
||||
offline_count = 0
|
||||
new_count = 0
|
||||
|
||||
existing = {
|
||||
row.mac_address.lower(): row.id
|
||||
for row in self.db.query(ONUDevice.mac_address, ONUDevice.id).all()
|
||||
}
|
||||
|
||||
for mac, onu_info in onu_info_dict.items():
|
||||
onu_id = existing.get(mac)
|
||||
|
||||
if onu_id is None:
|
||||
# 新设备:只入库在线的
|
||||
if onu_info.status != 'online':
|
||||
continue
|
||||
onu = ONUDevice(
|
||||
mac_address=mac,
|
||||
olt_id=olt_id,
|
||||
slot_number=onu_info.slot_number,
|
||||
port_number=onu_info.port_number,
|
||||
port_id=onu_info.port_id,
|
||||
distance_m=parse_distance(onu_info.distance_str),
|
||||
loid=onu_info.loid,
|
||||
model=onu_info.model,
|
||||
)
|
||||
self.db.add(onu)
|
||||
self.db.flush()
|
||||
self.db.add(NewDevice(onu_device_id=onu.id, olt_id=olt_id))
|
||||
onu_id = onu.id
|
||||
new_count += 1
|
||||
|
||||
status = onu_info.status
|
||||
distance_m = parse_distance(onu_info.distance_str)
|
||||
if status == 'online':
|
||||
online_count += 1
|
||||
else:
|
||||
offline_count += 1
|
||||
|
||||
self.db.add(DeviceStatusHistory(
|
||||
onu_device_id=onu_id,
|
||||
status=status,
|
||||
distance_m=distance_m,
|
||||
checked_at=checked_at,
|
||||
response_data=None,
|
||||
))
|
||||
|
||||
self._save_duplicate_macs(olt_id, duplicate_dict)
|
||||
self.db.commit()
|
||||
return {
|
||||
"online": online_count,
|
||||
"offline": offline_count,
|
||||
"new_discovered": new_count,
|
||||
}
|
||||
finally:
|
||||
ssh.close()
|
||||
|
||||
async def scan_olt(self, olt_id: int) -> Dict:
|
||||
"""仅扫描 OLT,返回发现的设备列表(不写入数据库)"""
|
||||
olt = self.db.query(OLTDevice).filter(OLTDevice.id == olt_id).first()
|
||||
if not olt:
|
||||
raise Exception(f"OLT 设备不存在: {olt_id}")
|
||||
|
||||
ssh = SSHService(olt.ip_address, olt.username, olt.password)
|
||||
try:
|
||||
ssh.connect()
|
||||
output = ssh.execute_command(olt.slot_command)
|
||||
onu_info_dict, duplicate_dict = ssh.parse_onu_info(output)
|
||||
|
||||
# 对比全局 MAC
|
||||
existing_macs = {
|
||||
onu.mac_address.lower()
|
||||
for onu in self.db.query(ONUDevice.mac_address).all()
|
||||
}
|
||||
|
||||
devices = []
|
||||
for mac, info in onu_info_dict.items():
|
||||
devices.append({
|
||||
"mac_address": mac,
|
||||
"status": info.status,
|
||||
"distance_m": info.distance_str,
|
||||
"slot_number": info.slot_number,
|
||||
"port_number": info.port_number,
|
||||
"port_id": info.port_id,
|
||||
"loid": info.loid,
|
||||
"model": info.model,
|
||||
"is_new": mac not in existing_macs,
|
||||
})
|
||||
|
||||
duplicates = []
|
||||
for mac, records in duplicate_dict.items():
|
||||
duplicates.append({
|
||||
"mac_address": mac,
|
||||
"ports": [{"port_id": r.port_id, "status": r.status} for r in records],
|
||||
})
|
||||
|
||||
return {
|
||||
"olt_id": olt_id,
|
||||
"olt_ip": olt.ip_address,
|
||||
"total": len(devices),
|
||||
"new": sum(1 for d in devices if d["is_new"]),
|
||||
"devices": devices,
|
||||
"duplicates": duplicates,
|
||||
}
|
||||
finally:
|
||||
ssh.close()
|
||||
|
||||
def _save_duplicate_macs(self, olt_id: int, duplicate_dict: dict):
|
||||
for mac, records in duplicate_dict.items():
|
||||
ports = [{"port_id": r.port_id, "status": r.status} for r in records]
|
||||
existing = self.db.query(DuplicateMac).filter(
|
||||
DuplicateMac.olt_id == olt_id,
|
||||
DuplicateMac.mac_address == mac
|
||||
).first()
|
||||
if existing:
|
||||
existing.ports = ports
|
||||
existing.last_seen_at = datetime.utcnow()
|
||||
else:
|
||||
self.db.add(DuplicateMac(olt_id=olt_id, mac_address=mac, ports=ports))
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Excel 导入服务"""
|
||||
import pandas as pd
|
||||
from typing import List, Dict
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.device import ONUDevice
|
||||
import math
|
||||
|
||||
# 定义字段映射:Excel列名 -> 数据库字段名
|
||||
FIELD_MAPPING = {
|
||||
'mac_address': 'mac_address',
|
||||
'region': 'region',
|
||||
'school_name': 'school_name',
|
||||
'building': 'building',
|
||||
'place_type': 'place_type',
|
||||
'room_number': 'room_number',
|
||||
'notes': 'notes',
|
||||
}
|
||||
|
||||
|
||||
def clean_value(value) -> str:
|
||||
"""清理单元格值,处理NaN和None"""
|
||||
if value is None:
|
||||
return ''
|
||||
if isinstance(value, float) and math.isnan(value):
|
||||
return ''
|
||||
return str(value).strip()
|
||||
|
||||
|
||||
class ImportService:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def parse_excel(self, file_path: str) -> List[Dict]:
|
||||
"""解析 Excel 文件"""
|
||||
df = pd.read_excel(file_path)
|
||||
# 标准化列名(去除空格,转小写)
|
||||
df.columns = [col.strip().lower() for col in df.columns]
|
||||
# 转换为记录列表
|
||||
records = []
|
||||
for _, row in df.iterrows():
|
||||
record = {}
|
||||
for col_name, db_field in FIELD_MAPPING.items():
|
||||
if col_name in row:
|
||||
record[db_field] = clean_value(row[col_name])
|
||||
records.append(record)
|
||||
return records
|
||||
|
||||
def validate_data(self, records: List[Dict]) -> Dict:
|
||||
"""验证数据"""
|
||||
valid = []
|
||||
invalid = []
|
||||
|
||||
for idx, record in enumerate(records):
|
||||
mac = record.get('mac_address', '').strip()
|
||||
if not mac:
|
||||
invalid.append({'record': record, 'error': f'第{idx+2}行: MAC地址缺失'})
|
||||
continue
|
||||
|
||||
# 标准化MAC地址:统一使用横杠分隔小写格式
|
||||
# 支持格式:AA:BB:CC:DD:EE:FF, AA-BB-CC-DD-EE-FF, AABBCCDDEEFF, aa:bb:cc:dd:ee:ff
|
||||
mac_clean = mac.upper().replace(':', '-')
|
||||
|
||||
# 验证基本格式:12个十六进制字符(可能有分隔符)
|
||||
hex_chars = mac_clean.replace('-', '')
|
||||
if len(hex_chars) != 12 or not all(c in '0123456789ABCDEF' for c in hex_chars):
|
||||
invalid.append({'record': record, 'error': f'第{idx+2}行: MAC地址格式错误 "{mac}"'})
|
||||
continue
|
||||
|
||||
# 转换为标准格式 34dc-99c8-56e0(小写4位分组)
|
||||
mac_formatted = '-'.join([hex_chars[i:i+4].lower() for i in range(0, 12, 4)])
|
||||
record['mac_address'] = mac_formatted
|
||||
valid.append(record)
|
||||
|
||||
return {'valid': valid, 'invalid': invalid}
|
||||
|
||||
def import_devices(self, records: List[Dict], olt_id: int = None) -> Dict:
|
||||
"""批量导入设备,存在则更新,不存在则新增"""
|
||||
success_count = 0
|
||||
skip_count = 0
|
||||
invalid_count = 0
|
||||
|
||||
for record in records:
|
||||
mac = record.get('mac_address', '')
|
||||
if not mac:
|
||||
skip_count += 1
|
||||
continue
|
||||
|
||||
# 查询是否已存在该 MAC 地址
|
||||
existing = self.db.query(ONUDevice).filter(ONUDevice.mac_address == mac).first()
|
||||
|
||||
if existing:
|
||||
# 更新现有记录
|
||||
existing.region = record.get('region', '')
|
||||
existing.school_name = record.get('school_name', '')
|
||||
existing.building = record.get('building') or None
|
||||
existing.place_type = record.get('place_type') or None
|
||||
existing.room_number = record.get('room_number') or None
|
||||
existing.notes = record.get('notes') or None
|
||||
success_count += 1
|
||||
else:
|
||||
# 新增记录
|
||||
device = ONUDevice(
|
||||
mac_address=mac,
|
||||
olt_id=olt_id,
|
||||
region=record.get('region', ''),
|
||||
school_name=record.get('school_name', ''),
|
||||
building=record.get('building') or None,
|
||||
place_type=record.get('place_type') or None,
|
||||
room_number=record.get('room_number') or None,
|
||||
notes=record.get('notes') or None
|
||||
)
|
||||
self.db.add(device)
|
||||
success_count += 1
|
||||
|
||||
self.db.commit()
|
||||
return {'success': success_count}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""业务下发服务"""
|
||||
from typing import Dict
|
||||
from sqlalchemy.orm import Session
|
||||
from app.services.ssh_service import SSHService
|
||||
from app.models.device import ONUDevice, OLTDevice
|
||||
import time
|
||||
|
||||
|
||||
class ProvisionService:
|
||||
"""业务下发服务"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def provision_device(self, device_id: int) -> Dict[str, any]:
|
||||
"""
|
||||
业务下发流程(参照下发业务流程.md):
|
||||
1. system-view
|
||||
2. interface Onu{port_id}
|
||||
3. uni 1 vlan-mode trunk pvid 4094 2000 to 2000 3000 to 3010
|
||||
4. port link-type trunk
|
||||
5. undo port trunk permit vlan 1
|
||||
6. port trunk permit vlan 2000 3000 to 3010 4094
|
||||
7. save force
|
||||
"""
|
||||
device = self.db.query(ONUDevice).filter(ONUDevice.id == device_id).first()
|
||||
if not device:
|
||||
return {"success": False, "error": f"设备不存在: {device_id}"}
|
||||
|
||||
if not device.olt_id:
|
||||
return {"success": False, "error": "设备未关联 OLT,请先进行扫描"}
|
||||
|
||||
olt = self.db.query(OLTDevice).filter(OLTDevice.id == device.olt_id).first()
|
||||
if not olt:
|
||||
return {"success": False, "error": "关联的 OLT 设备不存在"}
|
||||
|
||||
if not device.port_id:
|
||||
return {"success": False, "error": "设备端口信息不完整,请先对 OLT 执行扫描"}
|
||||
|
||||
port_name = f"Onu{device.port_id}"
|
||||
ssh = SSHService(olt.ip_address, olt.username, olt.password)
|
||||
|
||||
try:
|
||||
ssh.connect()
|
||||
|
||||
def send_and_wait(cmd: str, expect: str, timeout: int = 15) -> str:
|
||||
ssh.shell.send(cmd + "\n")
|
||||
buf = ""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
if ssh.shell.recv_ready():
|
||||
buf += ssh.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 {port_name}", "]")
|
||||
if "]" not in out:
|
||||
raise Exception(f"进入端口 {port_name} 失败")
|
||||
|
||||
# 配置 VLAN
|
||||
send_and_wait("uni 1 vlan-mode trunk pvid 4094 2000 to 2000 3000 to 3010", "]")
|
||||
send_and_wait("port link-type trunk", "]")
|
||||
send_and_wait("undo port trunk permit vlan 1", "]")
|
||||
send_and_wait("port trunk permit vlan 2000 3000 to 3010 4094", "]")
|
||||
|
||||
# 保存配置(等待 "successfully" 出现)
|
||||
save_out = send_and_wait("save force", "successfully", timeout=30)
|
||||
if "successfully" not in save_out:
|
||||
raise Exception("save force 未确认成功,请检查 OLT 日志")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"device_id": device_id,
|
||||
"mac_address": device.mac_address,
|
||||
"olt_ip": olt.ip_address,
|
||||
"port": port_name,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"device_id": device_id,
|
||||
"mac_address": device.mac_address,
|
||||
"error": str(e),
|
||||
}
|
||||
finally:
|
||||
ssh.close()
|
||||
|
||||
def batch_provision(self, device_ids: list) -> Dict[str, any]:
|
||||
results = []
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
for device_id in device_ids:
|
||||
result = self.provision_device(device_id)
|
||||
results.append(result)
|
||||
if result.get("success"):
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
return {
|
||||
"total": len(device_ids),
|
||||
"success": success_count,
|
||||
"failed": fail_count,
|
||||
"results": results,
|
||||
}
|
||||
@@ -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