Files
ipam/backend/app/api/v1/snmp.py
T
Your Name 094d2cc112 fix(snmp): 修复异步函数同步调用导致'coroutine object is not an iterator'错误
问题原因:
- pysnmp.hlapi.asyncio 的 getCmd/bulkCmd/nextCmd 都是 async def 协程函数
- 返回的是 coroutine 对象,需要用 await 获取结果
- 原代码用 next() 或 for...in 同步迭代协程对象,报错:
  'coroutine' object is not an iterator

解决方案:
1. snmp_service.py: 所有 SNMP 操作改为 async def
   - test_connection(): getCmd 改为 await getCmd()
   - get_arp_table(): bulkCmd 改为 await bulkCmd()
   - get_mac_address_table(): bulkCmd 改为 await bulkCmd()
   - get_interfaces(): bulkCmd 改为 await bulkCmd()
   - poll_device(): 所有子调用加 await
2. api/v1/snmp.py: API 路由改为 async def + await

bulkCmd await 后返回 (errorIndication, errorStatus, errorIndex, varBindTable)
其中 varBindTable 是列表的列表(table 形式),需双层循环解析
2026-07-23 15:01:18 +08:00

550 lines
18 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from typing import Optional, List
from app.core.database import get_db
from app.core.security import get_current_user
from app.models.auth import User
from app.services.snmp_service import SNMPService
from app.services.audit_service import AuditService, AuditAction
from app.schemas._tz_util import serialize_dt_fields, serialize_dt_list
router = APIRouter(
prefix="/snmp",
tags=["SNMP 管理"],
dependencies=[Depends(get_current_user)],
)
def _client_info(request: Request) -> tuple[str, str]:
return (
request.client.host if request.client else "",
(request.headers.get("user-agent") or "")[:500],
)
# ========== 凭据管理 ==========
@router.get("/credentials", summary="获取 SNMP 凭据列表")
def get_snmp_credentials(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db)
):
from app.models.snmp import SNMPCredential
query = db.query(SNMPCredential)
total = query.count()
items = query.order_by(SNMPCredential.id.desc()).offset(skip).limit(limit).all()
return {"total": total, "items": serialize_dt_list(items)}
@router.get("/credentials/{credential_id}", summary="获取凭据详情")
def get_snmp_credential(credential_id: int, db: Session = Depends(get_db)):
from app.models.snmp import SNMPCredential
credential = db.query(SNMPCredential).filter(SNMPCredential.id == credential_id).first()
if not credential:
raise HTTPException(status_code=404, detail="凭据不存在")
return serialize_dt_fields(credential)
@router.post("/credentials", summary="创建 SNMP 凭据")
def create_snmp_credential(
name: str,
version: str,
community_string: Optional[str] = None,
username: Optional[str] = None,
auth_password: Optional[str] = None,
auth_protocol: Optional[str] = None,
priv_password: Optional[str] = None,
priv_protocol: Optional[str] = None,
description: Optional[str] = None,
timeout: int = 3,
retries: int = 2,
request: Request = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
from app.models.snmp import SNMPCredential
existing = db.query(SNMPCredential).filter(SNMPCredential.name == name).first()
if existing:
raise HTTPException(status_code=400, detail="凭据名称已存在")
credential = SNMPCredential(
name=name,
version=version,
community_string=community_string,
username=username,
auth_password=auth_password,
auth_protocol=auth_protocol,
priv_password=priv_password,
priv_protocol=priv_protocol,
description=description,
timeout=timeout,
retries=retries
)
db.add(credential)
db.commit()
db.refresh(credential)
c, u = _client_info(request)
AuditService.record(
db,
action=AuditAction.SNMP_CRED_CREATE,
user=current_user,
resource_type="snmp_credential",
resource_id=credential.id,
resource_name=credential.name,
method="POST",
path=str(request.url.path),
ip_address=c,
user_agent=u,
detail={"version": credential.version, "name": credential.name},
)
return serialize_dt_fields(credential)
@router.put("/credentials/{credential_id}", summary="更新 SNMP 凭据")
def update_snmp_credential(
credential_id: int,
name: Optional[str] = None,
community_string: Optional[str] = None,
username: Optional[str] = None,
auth_password: Optional[str] = None,
auth_protocol: Optional[str] = None,
priv_password: Optional[str] = None,
priv_protocol: Optional[str] = None,
description: Optional[str] = None,
timeout: Optional[int] = None,
retries: Optional[int] = None,
is_active: Optional[bool] = None,
request: Request = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
from app.models.snmp import SNMPCredential
credential = db.query(SNMPCredential).filter(SNMPCredential.id == credential_id).first()
if not credential:
raise HTTPException(status_code=404, detail="凭据不存在")
before = {"name": credential.name, "description": credential.description, "is_active": credential.is_active, "timeout": credential.timeout, "retries": credential.retries}
if name:
credential.name = name
if community_string is not None:
credential.community_string = community_string
if username is not None:
credential.username = username
if auth_password is not None:
credential.auth_password = auth_password
if auth_protocol is not None:
credential.auth_protocol = auth_protocol
if priv_password is not None:
credential.priv_password = priv_password
if priv_protocol is not None:
credential.priv_protocol = priv_protocol
if description is not None:
credential.description = description
if timeout is not None:
credential.timeout = timeout
if retries is not None:
credential.retries = retries
if is_active is not None:
credential.is_active = is_active
db.commit()
db.refresh(credential)
c, u = _client_info(request)
AuditService.record(
db,
action=AuditAction.SNMP_CRED_UPDATE,
user=current_user,
resource_type="snmp_credential",
resource_id=credential.id,
resource_name=credential.name,
method="PUT",
path=str(request.url.path),
ip_address=c,
user_agent=u,
detail={
"before": before,
"after": {"name": credential.name, "description": credential.description, "is_active": credential.is_active, "timeout": credential.timeout, "retries": credential.retries},
},
)
return serialize_dt_fields(credential)
@router.delete("/credentials/{credential_id}", summary="删除 SNMP 凭据")
def delete_snmp_credential(
credential_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
from app.models.snmp import SNMPCredential, NetworkDevice
credential = db.query(SNMPCredential).filter(SNMPCredential.id == credential_id).first()
if not credential:
raise HTTPException(status_code=404, detail="凭据不存在")
devices = db.query(NetworkDevice).filter(NetworkDevice.snmp_credential_id == credential_id).count()
if devices > 0:
raise HTTPException(status_code=400, detail=f"{devices} 个设备正在使用此凭据,无法删除")
name_snapshot = credential.name
db.delete(credential)
db.commit()
c, u = _client_info(request)
AuditService.record(
db,
action=AuditAction.SNMP_CRED_DELETE,
user=current_user,
resource_type="snmp_credential",
resource_id=credential_id,
resource_name=name_snapshot,
method="DELETE",
path=str(request.url.path),
ip_address=c,
user_agent=u,
)
return {"message": "删除成功"}
# ========== 设备管理 ==========
@router.get("/devices", summary="获取网络设备列表")
def get_network_devices(
device_type: Optional[str] = None,
is_active: Optional[bool] = None,
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db)
):
from app.models.snmp import NetworkDevice, SNMPCredential
query = db.query(NetworkDevice)
if device_type:
query = query.filter(NetworkDevice.device_type == device_type)
if is_active is not None:
query = query.filter(NetworkDevice.is_active == is_active)
total = query.count()
items = query.order_by(NetworkDevice.id.desc()).offset(skip).limit(limit).all()
# 附带凭据名称
credential_ids = {d.snmp_credential_id for d in items if d.snmp_credential_id}
cred_map = {}
if credential_ids:
for c in db.query(SNMPCredential).filter(SNMPCredential.id.in_(credential_ids)).all():
cred_map[c.id] = c.name
serialized = []
for d in items:
item = serialize_dt_fields(d)
# 补充手写的关联字段(datetime 已经由 serialize_dt_fields 序列化)
item["device_type"] = d.device_type.value if hasattr(d.device_type, 'value') else d.device_type
item["credential_name"] = cred_map.get(d.snmp_credential_id)
item["snmp_credential_id"] = d.snmp_credential_id # 显式添加,确保前端能拿到
item["last_polled_at"] = item.get("last_polled_at") # 已是 ISO 字符串
item["last_successful_poll"] = item.get("last_successful_poll")
item["arp_poll_interval"] = d.arp_poll_interval
item["mac_poll_interval"] = d.mac_poll_interval
item["interface_poll_interval"] = d.interface_poll_interval
serialized.append(item)
return {"total": total, "items": serialized}
@router.get("/devices/{device_id}", summary="获取网络设备详情")
def get_network_device(device_id: int, db: Session = Depends(get_db)):
from app.models.snmp import NetworkDevice
device = db.query(NetworkDevice).filter(NetworkDevice.id == device_id).first()
if not device:
raise HTTPException(status_code=404, detail="设备不存在")
return serialize_dt_fields(device)
@router.post("/devices", summary="创建网络设备")
def create_network_device(
name: str,
ip_address: str,
snmp_credential_id: Optional[int] = None, # 前端字段名(与 ORM 列名一致)
port: int = 161,
device_type: str = "other",
description: Optional[str] = None,
location: Optional[str] = None,
request: Request = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
from app.models.snmp import NetworkDevice
existing = db.query(NetworkDevice).filter(NetworkDevice.name == name).first()
if existing:
raise HTTPException(status_code=400, detail="设备名称已存在")
device = NetworkDevice(
name=name,
ip_address=ip_address,
port=port,
device_type=device_type,
snmp_credential_id=snmp_credential_id,
description=description,
location=location,
is_active=True
)
db.add(device)
db.commit()
db.refresh(device)
c, u = _client_info(request)
AuditService.record(
db,
action=AuditAction.SNMP_DEVICE_CREATE,
user=current_user,
resource_type="snmp_device",
resource_id=device.id,
resource_name=device.name,
method="POST",
path=str(request.url.path),
ip_address=c,
user_agent=u,
detail={"ip_address": device.ip_address, "port": device.port, "device_type": device.device_type.value if hasattr(device.device_type, 'value') else device.device_type},
)
return serialize_dt_fields(device)
@router.put("/devices/{device_id}", summary="更新网络设备")
def update_network_device(
device_id: int,
name: Optional[str] = None,
ip_address: Optional[str] = None,
snmp_credential_id: Optional[int] = None,
clear_snmp_credential: bool = False, # 新增:专门的标志
port: Optional[int] = None,
device_type: Optional[str] = None,
description: Optional[str] = None,
location: Optional[str] = None,
is_active: Optional[bool] = None,
request: Request = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
from app.models.snmp import NetworkDevice
device = db.query(NetworkDevice).filter(NetworkDevice.id == device_id).first()
if not device:
raise HTTPException(status_code=404, detail="设备不存在")
before = {
"name": device.name, "ip_address": device.ip_address, "port": device.port,
"snmp_credential_id": device.snmp_credential_id, "device_type": device.device_type.value if hasattr(device.device_type, 'value') else device.device_type,
"description": device.description, "location": device.location, "is_active": device.is_active,
}
if name:
device.name = name
if ip_address:
device.ip_address = ip_address
# 处理 SNMP 凭据:
# - clear_snmp_credential=True → 清除凭据
# - snmp_credential_id > 0 → 设置为该值
# - 否则(都不做任何操作
if clear_snmp_credential:
device.snmp_credential_id = None
elif snmp_credential_id is not None and snmp_credential_id > 0:
device.snmp_credential_id = snmp_credential_id
if port:
device.port = port
if device_type:
device.device_type = device_type
if description is not None:
device.description = description
if location is not None:
device.location = location
if is_active is not None:
device.is_active = is_active
db.commit()
db.refresh(device)
c, u = _client_info(request)
AuditService.record(
db,
action=AuditAction.SNMP_DEVICE_UPDATE,
user=current_user,
resource_type="snmp_device",
resource_id=device.id,
resource_name=device.name,
method="PUT",
path=str(request.url.path),
ip_address=c,
user_agent=u,
detail={
"before": before,
"after": {
"name": device.name, "ip_address": device.ip_address, "port": device.port,
"snmp_credential_id": device.snmp_credential_id, "device_type": device.device_type.value if hasattr(device.device_type, 'value') else device.device_type,
"description": device.description, "location": device.location, "is_active": device.is_active,
},
},
)
return serialize_dt_fields(device)
@router.delete("/devices/{device_id}", summary="删除网络设备")
def delete_network_device(
device_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
from app.models.snmp import NetworkDevice
device = db.query(NetworkDevice).filter(NetworkDevice.id == device_id).first()
if not device:
raise HTTPException(status_code=404, detail="设备不存在")
name_snapshot = device.name
ip_snapshot = device.ip_address
db.delete(device)
db.commit()
c, u = _client_info(request)
AuditService.record(
db,
action=AuditAction.SNMP_DEVICE_DELETE,
user=current_user,
resource_type="snmp_device",
resource_id=device_id,
resource_name=name_snapshot,
method="DELETE",
path=str(request.url.path),
ip_address=c,
user_agent=u,
detail={"ip_address": ip_snapshot},
)
return {"message": "删除成功"}
# ========== SNMP 操作 ==========
@router.post("/devices/{device_id}/test", summary="测试 SNMP 连接")
async def test_snmp_connection(device_id: int, db: Session = Depends(get_db)):
"""测试设备的 SNMP 连接(不写审计,避免刷屏)"""
from app.models.snmp import NetworkDevice
device = db.query(NetworkDevice).filter(NetworkDevice.id == device_id).first()
if not device:
raise HTTPException(status_code=404, detail="设备不存在")
if not device.snmp_credential:
raise HTTPException(status_code=400, detail="设备未配置 SNMP 凭据")
success, info = await SNMPService.test_connection(device, device.snmp_credential)
return {
"device_id": device_id,
"device_name": device.name,
"success": success,
"info": info
}
@router.post("/devices/{device_id}/poll", summary="立即轮询设备")
async def poll_device_now(device_id: int, db: Session = Depends(get_db)):
"""立即轮询设备,获取 ARP 表、MAC 地址表、接口信息(不写审计,常规操作)"""
result = await SNMPService.poll_device(db, device_id)
if 'error' in result:
raise HTTPException(status_code=400, detail=result['error'])
return result
@router.post("/devices/{device_id}/arp", summary="获取设备 ARP 表")
async def get_device_arp_table(device_id: int, db: Session = Depends(get_db)):
"""获取设备的 ARP 表"""
from app.models.snmp import NetworkDevice, ARPEntry
device = db.query(NetworkDevice).filter(NetworkDevice.id == device_id).first()
if not device:
raise HTTPException(status_code=404, detail="设备不存在")
if not device.snmp_credential:
raise HTTPException(status_code=400, detail="设备未配置 SNMP 凭据")
entries = await SNMPService.get_arp_table(device, device.snmp_credential, db)
return {
"device_id": device_id,
"device_name": device.name,
"entries": entries,
"count": len(entries)
}
@router.get("/arp-entries", summary="查询所有 ARP 记录")
def get_all_arp_entries(
device_id: Optional[int] = None,
ip_address: Optional[str] = None,
mac_address: Optional[str] = None,
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db)
):
"""查询 ARP 表数据"""
from app.models.snmp import ARPEntry
query = db.query(ARPEntry)
if device_id:
query = query.filter(ARPEntry.device_id == device_id)
if ip_address:
query = query.filter(ARPEntry.ip_address.like(f'%{ip_address}%'))
if mac_address:
query = query.filter(ARPEntry.mac_address.like(f'%{mac_address}%'))
total = query.count()
items = query.order_by(ARPEntry.last_seen.desc()).offset(skip).limit(limit).all()
return {"total": total, "items": items}
@router.get("/statistics/summary", summary="SNMP 统计摘要")
def get_snmp_statistics(db: Session = Depends(get_db)):
"""获取 SNMP 相关统计"""
from app.models.snmp import SNMPCredential, NetworkDevice, ARPEntry
credential_count = db.query(SNMPCredential).filter(SNMPCredential.is_active == True).count()
device_count = db.query(NetworkDevice).filter(NetworkDevice.is_active == True).count()
arp_count = db.query(ARPEntry).count()
unique_macs = db.query(ARPEntry.mac_address).distinct().count()
from datetime import datetime, timedelta
one_hour_ago = datetime.utcnow() - timedelta(hours=1)
online_devices = db.query(NetworkDevice).filter(
NetworkDevice.is_active == True,
NetworkDevice.last_successful_poll >= one_hour_ago
).count()
return {
"credentials": {
"total": credential_count
},
"devices": {
"total": device_count,
"online_last_hour": online_devices
},
"data": {
"arp_entries": arp_count,
"unique_mac_addresses": unique_macs
}
}