diff --git a/backend/app/api/v1/snmp.py b/backend/app/api/v1/snmp.py index 9863e42..70557b7 100644 --- a/backend/app/api/v1/snmp.py +++ b/backend/app/api/v1/snmp.py @@ -437,7 +437,7 @@ def delete_network_device( # ========== SNMP 操作 ========== @router.post("/devices/{device_id}/test", summary="测试 SNMP 连接") -def test_snmp_connection(device_id: int, db: Session = Depends(get_db)): +async def test_snmp_connection(device_id: int, db: Session = Depends(get_db)): """测试设备的 SNMP 连接(不写审计,避免刷屏)""" from app.models.snmp import NetworkDevice @@ -448,7 +448,7 @@ def test_snmp_connection(device_id: int, db: Session = Depends(get_db)): if not device.snmp_credential: raise HTTPException(status_code=400, detail="设备未配置 SNMP 凭据") - success, info = SNMPService.test_connection(device, device.snmp_credential) + success, info = await SNMPService.test_connection(device, device.snmp_credential) return { "device_id": device_id, @@ -459,9 +459,9 @@ def test_snmp_connection(device_id: int, db: Session = Depends(get_db)): @router.post("/devices/{device_id}/poll", summary="立即轮询设备") -def poll_device_now(device_id: int, db: Session = Depends(get_db)): +async def poll_device_now(device_id: int, db: Session = Depends(get_db)): """立即轮询设备,获取 ARP 表、MAC 地址表、接口信息(不写审计,常规操作)""" - result = SNMPService.poll_device(db, device_id) + result = await SNMPService.poll_device(db, device_id) if 'error' in result: raise HTTPException(status_code=400, detail=result['error']) @@ -470,7 +470,7 @@ def poll_device_now(device_id: int, db: Session = Depends(get_db)): @router.post("/devices/{device_id}/arp", summary="获取设备 ARP 表") -def get_device_arp_table(device_id: int, db: Session = Depends(get_db)): +async def get_device_arp_table(device_id: int, db: Session = Depends(get_db)): """获取设备的 ARP 表""" from app.models.snmp import NetworkDevice, ARPEntry @@ -481,7 +481,7 @@ def get_device_arp_table(device_id: int, db: Session = Depends(get_db)): if not device.snmp_credential: raise HTTPException(status_code=400, detail="设备未配置 SNMP 凭据") - entries = SNMPService.get_arp_table(device, device.snmp_credential, db) + entries = await SNMPService.get_arp_table(device, device.snmp_credential, db) return { "device_id": device_id, diff --git a/backend/app/services/snmp_service.py b/backend/app/services/snmp_service.py index 894bb6e..bfe24f8 100644 --- a/backend/app/services/snmp_service.py +++ b/backend/app/services/snmp_service.py @@ -3,6 +3,7 @@ from sqlalchemy.orm import Session from datetime import datetime import ipaddress import logging +import asyncio from pysnmp.hlapi.asyncio import ( SnmpEngine, CommunityData, UsmUserData, @@ -117,7 +118,7 @@ class SNMPService: return None @staticmethod - def test_connection(device: NetworkDevice, credential: SNMPCredential) -> Tuple[bool, Dict[str, Any]]: + async def test_connection(device: NetworkDevice, credential: SNMPCredential) -> Tuple[bool, Dict[str, Any]]: """ 测试 SNMP 连接 返回: (成功, 设备信息字典) @@ -131,11 +132,11 @@ class SNMPService: retries=credential.retries ) - errorIndication, errorStatus, errorIndex, varBinds = next( - getCmd(SnmpEngine(), auth_data, transport, ContextData(), - ObjectType(ObjectIdentity(SNMPService.OID_SYS_DESCR)), - ObjectType(ObjectIdentity(SNMPService.OID_SYS_NAME)), - ObjectType(ObjectIdentity(SNMPService.OID_SYS_LOCATION))) + errorIndication, errorStatus, errorIndex, varBinds = await getCmd( + SnmpEngine(), auth_data, transport, ContextData(), + ObjectType(ObjectIdentity(SNMPService.OID_SYS_DESCR)), + ObjectType(ObjectIdentity(SNMPService.OID_SYS_NAME)), + ObjectType(ObjectIdentity(SNMPService.OID_SYS_LOCATION)) ) if errorIndication: @@ -162,7 +163,7 @@ class SNMPService: return False, {'error': str(e)} @staticmethod - def get_arp_table(device: NetworkDevice, credential: SNMPCredential, db: Session) -> List[Dict[str, Any]]: + async def get_arp_table(device: NetworkDevice, credential: SNMPCredential, db: Session) -> List[Dict[str, Any]]: """ 获取 ARP 表 (IP -> MAC 映射) """ @@ -178,61 +179,60 @@ class SNMPService: ) # 使用 bulkCmd 获取 ARP 表 - for errorIndication, errorStatus, errorIndex, varBinds in bulkCmd( + errorIndication, errorStatus, errorIndex, varBindTable = await bulkCmd( SnmpEngine(), auth_data, transport, ContextData(), 0, 50, # nonRepeaters, maxRepetitions ObjectType(ObjectIdentity(SNMPService.OID_IP_NET_TO_MEDIA_PHYS_ADDRESS)), ObjectType(ObjectIdentity(SNMPService.OID_IP_NET_TO_MEDIA_NET_ADDRESS)), ObjectType(ObjectIdentity(SNMPService.OID_IP_NET_TO_MEDIA_IF_INDEX)), lexicographicMode=False - ): - if errorIndication: - logger.error(f"SNMP 错误: {errorIndication}") - break + ) + if errorIndication: + logger.error(f"SNMP 错误: {errorIndication}") + elif errorStatus: + logger.error(f"SNMP 错误: {errorStatus}") + else: + # varBindTable 是列表的列表,每行是一条 SNMP 响应记录 + for varBinds in varBindTable: + # 解析结果 + arp_data = {} + for varBind in varBinds: + oid = str(varBind[0]) + value = varBind[1] - if errorStatus: - logger.error(f"SNMP 错误: {errorStatus}") - break + # 提取 IP 地址 (OID 后缀: ifIndex.IP) + parts = oid.split('.') + if len(parts) >= 4: + # 从 OID 中提取 IP 地址 + ip_parts = parts[-4:] + ip_address = '.'.join(ip_parts) - # 解析结果 - arp_data = {} - for varBind in varBinds: - oid = str(varBind[0]) - value = varBind[1] + if SNMPService.OID_IP_NET_TO_MEDIA_PHYS_ADDRESS in oid: + mac = SNMPService._normalize_mac(value) + if mac: + if ip_address not in arp_data: + arp_data[ip_address] = {} + arp_data[ip_address]['mac_address'] = mac + elif SNMPService.OID_IP_NET_TO_MEDIA_IF_INDEX in oid: + if isinstance(value, Integer32): + if_index = int(value) + if ip_address not in arp_data: + arp_data[ip_address] = {} + arp_data[ip_address]['if_index'] = if_index - # 提取 IP 地址 (OID 后缀: ifIndex.IP) - parts = oid.split('.') - if len(parts) >= 4: - # 从 OID 中提取 IP 地址 - ip_parts = parts[-4:] - ip_address = '.'.join(ip_parts) - - if SNMPService.OID_IP_NET_TO_MEDIA_PHYS_ADDRESS in oid: - mac = SNMPService._normalize_mac(value) - if mac: - if ip_address not in arp_data: - arp_data[ip_address] = {} - arp_data[ip_address]['mac_address'] = mac - elif SNMPService.OID_IP_NET_TO_MEDIA_IF_INDEX in oid: - if isinstance(value, Integer32): - if_index = int(value) - if ip_address not in arp_data: - arp_data[ip_address] = {} - arp_data[ip_address]['if_index'] = if_index - - # 转换为列表 - now = datetime.utcnow() - for ip, data in arp_data.items(): - if 'mac_address' in data: - entry = { - 'ip_address': ip, - 'mac_address': data.get('mac_address'), - 'interface': str(data.get('if_index', '')), - 'device_id': device.id, - 'last_seen': now.isoformat(), - 'discovered_at': now.isoformat() - } - arp_entries.append(entry) + # 转换为列表 + now = datetime.utcnow() + for ip, data in arp_data.items(): + if 'mac_address' in data: + entry = { + 'ip_address': ip, + 'mac_address': data.get('mac_address'), + 'interface': str(data.get('if_index', '')), + 'device_id': device.id, + 'last_seen': now.isoformat(), + 'discovered_at': now.isoformat() + } + arp_entries.append(entry) # 保存到数据库 for entry_data in arp_entries: @@ -281,7 +281,7 @@ class SNMPService: return arp_entries @staticmethod - def get_mac_address_table(device: NetworkDevice, credential: SNMPCredential, db: Session, vlan_id: Optional[int] = None) -> List[Dict[str, Any]]: + async def get_mac_address_table(device: NetworkDevice, credential: SNMPCredential, db: Session, vlan_id: Optional[int] = None) -> List[Dict[str, Any]]: """ 获取 MAC 地址表 (MAC -> 端口 映射) """ @@ -297,39 +297,39 @@ class SNMPService: ) # 获取 MAC 地址表 - for errorIndication, errorStatus, errorIndex, varBinds in bulkCmd( + errorIndication, errorStatus, errorIndex, varBindTable = await bulkCmd( SnmpEngine(), auth_data, transport, ContextData(), 0, 100, ObjectType(ObjectIdentity(SNMPService.OID_DOT1D_TP_FDB_PORT)), ObjectType(ObjectIdentity(SNMPService.OID_DOT1D_TP_FDB_STATUS)), lexicographicMode=False - ): - if errorIndication: - break + ) + if errorIndication: + logger.error(f"获取 MAC 地址表失败: {errorIndication}") + elif errorStatus: + logger.error(f"获取 MAC 地址表失败: {errorStatus}") + else: + for varBinds in varBindTable: + for varBind in varBinds: + oid = str(varBind[0]) + value = varBind[1] - if errorStatus: - break + # OID 后缀是 MAC 地址: .a.b.c.d.e.f + parts = oid.split('.') + if len(parts) >= 6: + mac_parts = parts[-6:] + mac_address = ':'.join(f'{int(p):02X}' for p in mac_parts) - for varBind in varBinds: - oid = str(varBind[0]) - value = varBind[1] + if SNMPService.OID_DOT1D_TP_FDB_PORT in oid: + port = int(value) if isinstance(value, Integer32) else None - # OID 后缀是 MAC 地址: .a.b.c.d.e.f - parts = oid.split('.') - if len(parts) >= 6: - mac_parts = parts[-6:] - mac_address = ':'.join(f'{int(p):02X}' for p in mac_parts) - - if SNMPService.OID_DOT1D_TP_FDB_PORT in oid: - port = int(value) if isinstance(value, Integer32) else None - - entry = { - 'mac_address': mac_address, - 'port_number': port, - 'device_id': device.id, - 'vlan_id': vlan_id - } - mac_entries.append(entry) + entry = { + 'mac_address': mac_address, + 'port_number': port, + 'device_id': device.id, + 'vlan_id': vlan_id + } + mac_entries.append(entry) # 保存到数据库 now = datetime.utcnow() @@ -361,7 +361,7 @@ class SNMPService: return mac_entries @staticmethod - def get_interfaces(device: NetworkDevice, credential: SNMPCredential, db: Session) -> List[Dict[str, Any]]: + async def get_interfaces(device: NetworkDevice, credential: SNMPCredential, db: Session) -> List[Dict[str, Any]]: """ 获取设备接口信息 """ @@ -379,7 +379,7 @@ class SNMPService: # 获取接口信息 interface_data = {} - for errorIndication, errorStatus, errorIndex, varBinds in bulkCmd( + errorIndication, errorStatus, errorIndex, varBindTable = await bulkCmd( SnmpEngine(), auth_data, transport, ContextData(), 0, 100, ObjectType(ObjectIdentity(SNMPService.OID_IF_DESCR)), @@ -390,35 +390,34 @@ class SNMPService: ObjectType(ObjectIdentity(SNMPService.OID_IF_ADMIN_STATUS)), ObjectType(ObjectIdentity(SNMPService.OID_IF_OPER_STATUS)), lexicographicMode=False - ): - if errorIndication or errorStatus: - break + ) + if not errorIndication and not errorStatus: + for varBinds in varBindTable: + for varBind in varBinds: + oid = str(varBind[0]) + value = varBind[1] + parts = oid.split('.') + if_index = parts[-1] - for varBind in varBinds: - oid = str(varBind[0]) - value = varBind[1] - parts = oid.split('.') - if_index = parts[-1] + if if_index not in interface_data: + interface_data[if_index] = {'if_index': int(if_index)} - if if_index not in interface_data: - interface_data[if_index] = {'if_index': int(if_index)} - - if SNMPService.OID_IF_DESCR in oid: - interface_data[if_index]['if_descr'] = value.prettyPrint() - elif SNMPService.OID_IF_TYPE in oid: - interface_data[if_index]['if_type'] = str(value) - elif SNMPService.OID_IF_MTU in oid: - interface_data[if_index]['if_mtu'] = int(value) if isinstance(value, Integer32) else None - elif SNMPService.OID_IF_SPEED in oid: - interface_data[if_index]['if_speed'] = int(value) if isinstance(value, (Integer32, Gauge32)) else None - elif SNMPService.OID_IF_PHYS_ADDRESS in oid: - interface_data[if_index]['if_phys_address'] = SNMPService._normalize_mac(value) - elif SNMPService.OID_IF_ADMIN_STATUS in oid: - status_map = {1: 'up', 2: 'down', 3: 'testing'} - interface_data[if_index]['if_admin_status'] = status_map.get(int(value), 'unknown') - elif SNMPService.OID_IF_OPER_STATUS in oid: - status_map = {1: 'up', 2: 'down', 3: 'testing'} - interface_data[if_index]['if_oper_status'] = status_map.get(int(value), 'unknown') + if SNMPService.OID_IF_DESCR in oid: + interface_data[if_index]['if_descr'] = value.prettyPrint() + elif SNMPService.OID_IF_TYPE in oid: + interface_data[if_index]['if_type'] = str(value) + elif SNMPService.OID_IF_MTU in oid: + interface_data[if_index]['if_mtu'] = int(value) if isinstance(value, Integer32) else None + elif SNMPService.OID_IF_SPEED in oid: + interface_data[if_index]['if_speed'] = int(value) if isinstance(value, (Integer32, Gauge32)) else None + elif SNMPService.OID_IF_PHYS_ADDRESS in oid: + interface_data[if_index]['if_phys_address'] = SNMPService._normalize_mac(value) + elif SNMPService.OID_IF_ADMIN_STATUS in oid: + status_map = {1: 'up', 2: 'down', 3: 'testing'} + interface_data[if_index]['if_admin_status'] = status_map.get(int(value), 'unknown') + elif SNMPService.OID_IF_OPER_STATUS in oid: + status_map = {1: 'up', 2: 'down', 3: 'testing'} + interface_data[if_index]['if_oper_status'] = status_map.get(int(value), 'unknown') # 转换为列表并保存 now = datetime.utcnow() @@ -485,7 +484,7 @@ class SNMPService: return device @staticmethod - def poll_device(db: Session, device_id: int) -> Dict[str, Any]: + async def poll_device(db: Session, device_id: int) -> Dict[str, Any]: """轮询设备,获取所有信息""" device = db.query(NetworkDevice).filter(NetworkDevice.id == device_id).first() if not device: @@ -505,17 +504,17 @@ class SNMPService: } # 获取 ARP 表 - arp_entries = SNMPService.get_arp_table(device, credential, db) + arp_entries = await SNMPService.get_arp_table(device, credential, db) result['arp_entries'] = arp_entries result['arp_count'] = len(arp_entries) # 获取 MAC 地址表 - mac_entries = SNMPService.get_mac_address_table(device, credential, db) + mac_entries = await SNMPService.get_mac_address_table(device, credential, db) result['mac_entries'] = mac_entries result['mac_count'] = len(mac_entries) # 获取接口信息 - interfaces = SNMPService.get_interfaces(device, credential, db) + interfaces = await SNMPService.get_interfaces(device, credential, db) result['interfaces'] = interfaces result['interface_count'] = len(interfaces)