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 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": 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 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 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 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 = { "id": d.id, "name": d.name, "description": d.description, "ip_address": d.ip_address, "port": d.port, "device_type": d.device_type.value if hasattr(d.device_type, 'value') else d.device_type, "vendor": d.vendor, "model": d.model, "firmware_version": d.firmware_version, "serial_number": d.serial_number, "location": d.location, "snmp_credential_id": d.snmp_credential_id, "credential_name": cred_map.get(d.snmp_credential_id), "is_active": d.is_active, "last_polled_at": d.last_polled_at.isoformat() if d.last_polled_at else None, "last_successful_poll": d.last_successful_poll.isoformat() if d.last_successful_poll else None, "arp_poll_interval": d.arp_poll_interval, "mac_poll_interval": d.mac_poll_interval, "interface_poll_interval": d.interface_poll_interval, "created_at": d.created_at.isoformat() if d.created_at else None, "updated_at": d.updated_at.isoformat() if d.updated_at else None, } 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 device @router.post("/devices", summary="创建网络设备") def create_network_device( name: str, ip_address: str, credential_id: Optional[int] = None, 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=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 device @router.put("/devices/{device_id}", summary="更新网络设备") def update_network_device( device_id: int, name: Optional[str] = None, ip_address: Optional[str] = None, credential_id: Optional[int] = None, 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 if credential_id is not None: device.snmp_credential_id = 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 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 连接") 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 = 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="立即轮询设备") def poll_device_now(device_id: int, db: Session = Depends(get_db)): """立即轮询设备,获取 ARP 表、MAC 地址表、接口信息(不写审计,常规操作)""" result = 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 表") 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 = 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 } }