1e1e5957e8
- 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 记录
555 lines
18 KiB
Python
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
|
|
}
|
|
} |