403 lines
14 KiB
Python
403 lines
14 KiB
Python
"""OLT 设备管理 API"""
|
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel
|
|
from app.core.database import get_db
|
|
from app.models.device import OLTDevice
|
|
import pandas as pd
|
|
import io
|
|
|
|
router = APIRouter(prefix="/api/olt", tags=["OLT设备"])
|
|
|
|
|
|
class OLTCreate(BaseModel):
|
|
ip_address: str
|
|
username: str
|
|
password: str
|
|
slot_command: str = "display onu slot"
|
|
location: str = ""
|
|
description: str = ""
|
|
|
|
|
|
class OLTEdit(BaseModel):
|
|
username: str
|
|
password: str = None
|
|
slot_command: str = "display onu slot"
|
|
location: str = ""
|
|
description: str = ""
|
|
|
|
|
|
@router.get("/devices")
|
|
def get_devices(db: Session = Depends(get_db)):
|
|
return db.query(OLTDevice).all()
|
|
|
|
|
|
@router.post("/devices")
|
|
def create_device(device: OLTCreate, db: Session = Depends(get_db)):
|
|
db_device = OLTDevice(**device.dict())
|
|
db.add(db_device)
|
|
db.commit()
|
|
return {"message": "创建成功"}
|
|
|
|
|
|
@router.put("/devices/{ip_address}")
|
|
def update_device(ip_address: str, device: OLTEdit, db: Session = Depends(get_db)):
|
|
db_device = db.query(OLTDevice).filter(OLTDevice.ip_address == ip_address).first()
|
|
if not db_device:
|
|
raise HTTPException(status_code=404, detail="设备不存在")
|
|
|
|
db_device.username = device.username
|
|
if device.password:
|
|
db_device.password = device.password
|
|
db_device.slot_command = device.slot_command
|
|
db_device.location = device.location
|
|
db_device.description = device.description
|
|
|
|
db.commit()
|
|
return {"message": "更新成功"}
|
|
|
|
|
|
@router.delete("/devices/{ip_address}")
|
|
def delete_device(ip_address: str, db: Session = Depends(get_db)):
|
|
from app.models.device import ONUDevice
|
|
|
|
device = db.query(OLTDevice).filter(OLTDevice.ip_address == ip_address).first()
|
|
if not device:
|
|
raise HTTPException(status_code=404, detail="设备不存在")
|
|
|
|
# 检查是否有关联的 ONU 设备
|
|
onu_count = db.query(ONUDevice).filter(ONUDevice.olt_id == device.id).count()
|
|
if onu_count > 0:
|
|
raise HTTPException(status_code=400, detail=f"该 OLT 设备下还有 {onu_count} 个 ONU 设备,无法删除")
|
|
|
|
db.delete(device)
|
|
db.commit()
|
|
return {"message": "删除成功"}
|
|
|
|
|
|
@router.post("/import")
|
|
async def import_devices(file: UploadFile = File(...), db: Session = Depends(get_db)):
|
|
try:
|
|
content = await file.read()
|
|
df = pd.read_excel(io.BytesIO(content))
|
|
# 标准化列名
|
|
df.columns = [str(c).strip() for c in df.columns]
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"文件解析失败: {str(e)}")
|
|
|
|
required_cols = ['IP地址', '用户名', '密码']
|
|
missing = [c for c in required_cols if c not in df.columns]
|
|
if missing:
|
|
raise HTTPException(status_code=400, detail=f"缺少必填列: {', '.join(missing)},当前列: {', '.join(df.columns.tolist())}")
|
|
|
|
success, failed = 0, []
|
|
for idx, row in df.iterrows():
|
|
try:
|
|
ip = str(row['IP地址']).strip()
|
|
if not ip or ip == 'nan':
|
|
continue
|
|
existing = db.query(OLTDevice).filter(OLTDevice.ip_address == ip).first()
|
|
if existing:
|
|
failed.append({"row": idx + 2, "ip": ip, "reason": "IP 已存在"})
|
|
continue
|
|
device = OLTDevice(
|
|
ip_address=ip,
|
|
username=str(row['用户名']).strip(),
|
|
password=str(row['密码']).strip(),
|
|
slot_command=str(row['槽位命令']).strip() if '槽位命令' in df.columns and str(row['槽位命令']) != 'nan' else 'display onu slot',
|
|
location=str(row['安装位置']).strip() if '安装位置' in df.columns and str(row['安装位置']) != 'nan' else '',
|
|
description=str(row['描述']).strip() if '描述' in df.columns and str(row['描述']) != 'nan' else '',
|
|
)
|
|
db.add(device)
|
|
success += 1
|
|
except Exception as e:
|
|
failed.append({"row": idx + 2, "ip": str(row.get('IP地址', '')), "reason": str(e)})
|
|
|
|
db.commit()
|
|
return {"message": f"成功导入 {success} 条记录", "success": success, "failed": failed}
|
|
|
|
|
|
@router.get("/template")
|
|
def download_template():
|
|
from fastapi.responses import FileResponse
|
|
return FileResponse(
|
|
path="/home/v6ole/pyproject/H3ConuMS2/backend/templates/OLT设备导入模板.xlsx",
|
|
filename="OLT设备导入模板.xlsx"
|
|
)
|
|
|
|
|
|
@router.get("/duplicate-macs")
|
|
def get_duplicate_macs(olt_id: int = None, db: Session = Depends(get_db)):
|
|
"""查询重复 MAC 地址记录"""
|
|
from app.models.device import DuplicateMac
|
|
query = db.query(DuplicateMac)
|
|
if olt_id:
|
|
query = query.filter(DuplicateMac.olt_id == olt_id)
|
|
records = query.order_by(DuplicateMac.last_seen_at.desc()).all()
|
|
return [
|
|
{
|
|
"id": r.id,
|
|
"olt_id": r.olt_id,
|
|
"mac_address": r.mac_address,
|
|
"ports": r.ports,
|
|
"first_seen_at": r.first_seen_at,
|
|
"last_seen_at": r.last_seen_at,
|
|
}
|
|
for r in records
|
|
]
|
|
|
|
|
|
@router.delete("/duplicate-macs/{record_id}")
|
|
def delete_duplicate_mac(record_id: int, db: Session = Depends(get_db)):
|
|
"""删除重复 MAC 记录(已处理后清除)"""
|
|
from app.models.device import DuplicateMac
|
|
record = db.query(DuplicateMac).filter(DuplicateMac.id == record_id).first()
|
|
if not record:
|
|
raise HTTPException(status_code=404, detail="记录不存在")
|
|
db.delete(record)
|
|
db.commit()
|
|
return {"message": "已删除"}
|
|
|
|
|
|
class ClearPortRequest(BaseModel):
|
|
port_id: str # 如 "1/0/1:3"
|
|
|
|
|
|
@router.post("/duplicate-macs/{record_id}/clear-port")
|
|
def clear_onu_port(record_id: int, body: ClearPortRequest, db: Session = Depends(get_db)):
|
|
"""通过 SSH 清除指定端口的 ONU 配置,并从 ports 列表中移除该端口"""
|
|
from app.models.device import DuplicateMac
|
|
from app.services.ssh_service import SSHService
|
|
|
|
record = db.query(DuplicateMac).filter(DuplicateMac.id == record_id).first()
|
|
if not record:
|
|
raise HTTPException(status_code=404, detail="记录不存在")
|
|
|
|
olt = db.query(OLTDevice).filter(OLTDevice.id == record.olt_id).first()
|
|
if not olt:
|
|
raise HTTPException(status_code=404, detail="OLT 设备不存在")
|
|
|
|
# 验证 port_id 在记录中
|
|
port_ids = [p["port_id"] for p in (record.ports or [])]
|
|
if body.port_id not in port_ids:
|
|
raise HTTPException(status_code=400, detail="端口不在重复记录中")
|
|
|
|
ssh = SSHService(olt.ip_address, olt.username, olt.password)
|
|
try:
|
|
ssh.connect()
|
|
ssh.clear_onu_port(body.port_id)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"清除失败: {str(e)}")
|
|
finally:
|
|
ssh.close()
|
|
|
|
# 从 ports 列表移除已清除的端口
|
|
remaining = [p for p in record.ports if p["port_id"] != body.port_id]
|
|
if remaining:
|
|
record.ports = remaining
|
|
else:
|
|
# 所有端口都清除了,删除整条记录
|
|
db.delete(record)
|
|
db.commit()
|
|
|
|
return {"message": f"端口 Onu{body.port_id} 已清除", "remaining_ports": remaining}
|
|
|
|
|
|
@router.get("/new-devices")
|
|
def get_new_devices(db: Session = Depends(get_db)):
|
|
"""查询新发现的设备列表(待补全信息)"""
|
|
from app.models.device import NewDevice, ONUDevice
|
|
rows = (
|
|
db.query(NewDevice, ONUDevice)
|
|
.join(ONUDevice, NewDevice.onu_device_id == ONUDevice.id)
|
|
.all()
|
|
)
|
|
return [
|
|
{
|
|
"id": nd.id,
|
|
"onu_device_id": nd.onu_device_id,
|
|
"olt_id": nd.olt_id,
|
|
"discovered_at": nd.discovered_at,
|
|
"mac_address": onu.mac_address,
|
|
"port_id": f"{onu.slot_number}/{onu.port_number}" if onu.slot_number else None,
|
|
"loid": onu.loid,
|
|
"model": onu.model,
|
|
"region": onu.region,
|
|
"school_name": onu.school_name,
|
|
"building": onu.building,
|
|
"room_number": onu.room_number,
|
|
}
|
|
for nd, onu in rows
|
|
]
|
|
|
|
|
|
class NewDeviceUpdate(BaseModel):
|
|
region: str
|
|
school_name: str
|
|
building: str = ""
|
|
place_type: str = ""
|
|
room_number: str = ""
|
|
notes: str = ""
|
|
|
|
|
|
@router.put("/new-devices/{record_id}")
|
|
def update_new_device(record_id: int, body: NewDeviceUpdate, db: Session = Depends(get_db)):
|
|
"""补全新设备信息,完成后从 new_devices 移除"""
|
|
from app.models.device import NewDevice, ONUDevice
|
|
record = db.query(NewDevice).filter(NewDevice.id == record_id).first()
|
|
if not record:
|
|
raise HTTPException(status_code=404, detail="记录不存在")
|
|
|
|
onu = db.query(ONUDevice).filter(ONUDevice.id == record.onu_device_id).first()
|
|
if not onu:
|
|
raise HTTPException(status_code=404, detail="ONU 设备不存在")
|
|
|
|
onu.region = body.region
|
|
onu.school_name = body.school_name
|
|
onu.building = body.building or None
|
|
onu.place_type = body.place_type or None
|
|
onu.room_number = body.room_number or None
|
|
onu.notes = body.notes or None
|
|
|
|
db.delete(record)
|
|
db.commit()
|
|
return {"message": "信息已补全"}
|
|
|
|
|
|
@router.delete("/new-devices/{record_id}")
|
|
def dismiss_new_device(record_id: int, db: Session = Depends(get_db)):
|
|
"""忽略新设备(不补全信息,仅从待处理列表移除)"""
|
|
from app.models.device import NewDevice
|
|
record = db.query(NewDevice).filter(NewDevice.id == record_id).first()
|
|
if not record:
|
|
raise HTTPException(status_code=404, detail="记录不存在")
|
|
db.delete(record)
|
|
db.commit()
|
|
return {"message": "已忽略"}
|
|
|
|
|
|
@router.post("/quick-scan")
|
|
def quick_scan(db: Session = Depends(get_db)):
|
|
"""多线程对所有 OLT 同时执行扫描,更新已有设备状态"""
|
|
from app.services.check_service import CheckService
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
olts = db.query(OLTDevice).all()
|
|
|
|
def scan_one(olt):
|
|
from app.core.database import SessionLocal
|
|
thread_db = SessionLocal()
|
|
try:
|
|
service = CheckService(thread_db)
|
|
result = service.scan_and_discover(olt.id)
|
|
return {"olt_id": olt.id, "olt_location": olt.location or olt.ip_address,
|
|
"online": result.get("online", 0), "offline": result.get("offline", 0),
|
|
"new_discovered": result.get("new_discovered", 0),
|
|
"error": None}
|
|
except Exception as e:
|
|
return {"olt_id": olt.id, "olt_location": olt.location or olt.ip_address,
|
|
"online": 0, "offline": 0, "new_discovered": 0, "error": str(e)}
|
|
finally:
|
|
thread_db.close()
|
|
|
|
olt_results = {}
|
|
with ThreadPoolExecutor(max_workers=len(olts) or 1) as executor:
|
|
futures = {executor.submit(scan_one, olt): olt.id for olt in olts}
|
|
for future in as_completed(futures):
|
|
r = future.result()
|
|
olt_results[r["olt_id"]] = r
|
|
|
|
results = []
|
|
errors = []
|
|
total_online = 0
|
|
total_offline = 0
|
|
total_new = 0
|
|
for olt in olts:
|
|
r = olt_results.get(olt.id, {})
|
|
if r.get("error"):
|
|
errors.append({"olt_location": r["olt_location"], "error": r["error"]})
|
|
else:
|
|
total_online += r.get("online", 0)
|
|
total_offline += r.get("offline", 0)
|
|
total_new += r.get("new_discovered", 0)
|
|
results.append({
|
|
"olt_location": r.get("olt_location", olt.location or olt.ip_address),
|
|
"online": r.get("online", 0),
|
|
"offline": r.get("offline", 0),
|
|
"new_discovered": r.get("new_discovered", 0),
|
|
"success": not r.get("error"),
|
|
"error": r.get("error"),
|
|
})
|
|
|
|
return {
|
|
"total_online": total_online,
|
|
"total_offline": total_offline,
|
|
"total_new": total_new,
|
|
"results": results,
|
|
"errors": errors,
|
|
}
|
|
|
|
|
|
@router.post("/loopback-detection")
|
|
def loopback_detection(db: Session = Depends(get_db)):
|
|
"""对所有 OLT 并发执行环路检测,返回有环路的端口及对应设备信息"""
|
|
from app.models.device import ONUDevice
|
|
from app.services.ssh_service import SSHService
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
olts = db.query(OLTDevice).all()
|
|
|
|
# 预加载所有 ONU 设备,按 (olt_id, port_id) 索引,避免多线程操作 Session
|
|
all_onus = db.query(ONUDevice).all()
|
|
onu_map = {(o.olt_id, o.port_id): o for o in all_onus if o.port_id}
|
|
|
|
def check_one(olt):
|
|
ssh = SSHService(olt.ip_address, olt.username, olt.password)
|
|
try:
|
|
ssh.connect()
|
|
detection = ssh.detect_loopback()
|
|
except Exception as e:
|
|
return {
|
|
"olt_id": olt.id,
|
|
"olt_ip": olt.ip_address,
|
|
"olt_location": olt.location or olt.ip_address,
|
|
"error": str(e),
|
|
"has_loop": False,
|
|
"loop_interfaces": [],
|
|
}
|
|
finally:
|
|
ssh.close()
|
|
|
|
loop_interfaces = []
|
|
for iface in detection.get("interfaces", []):
|
|
port_id = iface.removeprefix("Onu")
|
|
onu = onu_map.get((olt.id, port_id))
|
|
loop_interfaces.append({
|
|
"interface": iface,
|
|
"port_id": port_id,
|
|
"mac_address": onu.mac_address if onu else None,
|
|
"region": onu.region if onu else None,
|
|
"school_name": onu.school_name if onu else None,
|
|
"building": onu.building if onu else None,
|
|
"room_number": onu.room_number if onu else None,
|
|
})
|
|
|
|
return {
|
|
"olt_id": olt.id,
|
|
"olt_ip": olt.ip_address,
|
|
"olt_location": olt.location or olt.ip_address,
|
|
"has_loop": detection["has_loop"],
|
|
"loop_interfaces": loop_interfaces,
|
|
"error": None,
|
|
}
|
|
|
|
results_map = {}
|
|
with ThreadPoolExecutor(max_workers=len(olts) or 1) as executor:
|
|
futures = {executor.submit(check_one, olt): olt.id for olt in olts}
|
|
for future in as_completed(futures):
|
|
olt_id = futures[future]
|
|
results_map[olt_id] = future.result()
|
|
|
|
# 按原始顺序返回
|
|
return [results_map[olt.id] for olt in olts]
|