Files
ipam/backend/app/api/v1/snmp.py
T
Your Name 1e1e5957e8 fix(ipam): 修复IP管理页面网段筛选不生效及下拉框过窄
- Ips.vue: el-select 添加 width: 280px,修复网段下拉框过窄
- Ips.vue: query 参数 networkId → network_id(与 FastAPI snake_case 一致)
- Scan.vue: el-select 用 network.id 代替整个 network 对象作为 value,修复切换网段不生效
- enhanced_scan_service.py: 替换失效的 scapy ARP 扫描为 arp-scan 命令;扫描结果自动创建缺失 IP 记录
2026-07-18 09:45:30 +08:00

555 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
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
}
}