PingWatch 网络设备离线监控系统
- FastAPI 后端 + Vue 3 前端 - Docker Compose 一键部署 - Casdoor OAuth 认证集成 - LogHive 集中式日志 - 设备批量 CSV 导入/导出 - WebSocket 实时状态推送 - 企业微信告警通知 - fping 高性能并发 Ping 检测 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
"""设备管理 API"""
|
||||
import csv
|
||||
import io
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import select, func, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db
|
||||
from app.core.auth import get_current_user, require_admin
|
||||
from app.models.device import Device, DeviceTypeEnum
|
||||
from app.models.user import User
|
||||
from app.schemas.device import DeviceCreate, DeviceUpdate, DeviceOut
|
||||
|
||||
logger = logging.getLogger("pingwatch.devices")
|
||||
router = APIRouter(prefix="/api/devices", tags=["设备管理"])
|
||||
|
||||
# CSV 模板列
|
||||
IMPORT_COLUMNS = [
|
||||
("name", "设备名称(必填)"),
|
||||
("ip", "IP地址(必填)"),
|
||||
("device_type", "设备类型(server/olt/switch/firewall/other)"),
|
||||
("location", "物理位置"),
|
||||
("project_name", "所属项目"),
|
||||
("tags", "标签(逗号分隔)"),
|
||||
("ping_interval", "Ping间隔(秒)"),
|
||||
("alert_threshold", "告警阈值(次)"),
|
||||
]
|
||||
|
||||
|
||||
@router.get("", response_model=list[DeviceOut])
|
||||
async def list_devices(
|
||||
is_enabled: Optional[bool] = None,
|
||||
device_type: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取设备列表,支持过滤和搜索"""
|
||||
query = select(Device)
|
||||
|
||||
if is_enabled is not None:
|
||||
query = query.where(Device.is_enabled == is_enabled)
|
||||
if device_type:
|
||||
query = query.where(Device.device_type == device_type)
|
||||
if search:
|
||||
like = f"%{search}%"
|
||||
query = query.where(
|
||||
Device.name.ilike(like) | Device.ip.ilike(like) | Device.location.ilike(like)
|
||||
)
|
||||
|
||||
query = query.order_by(Device.id)
|
||||
result = await db.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.get("/{device_id}", response_model=DeviceOut)
|
||||
async def get_device(
|
||||
device_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
result = await db.execute(select(Device).where(Device.id == device_id))
|
||||
device = result.scalar_one_or_none()
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail="设备不存在")
|
||||
return device
|
||||
|
||||
|
||||
@router.post("", response_model=DeviceOut)
|
||||
async def create_device(
|
||||
data: DeviceCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: User = Depends(require_admin),
|
||||
):
|
||||
device = Device(**data.model_dump())
|
||||
db.add(device)
|
||||
await db.commit()
|
||||
await db.refresh(device)
|
||||
return device
|
||||
|
||||
|
||||
@router.put("/{device_id}", response_model=DeviceOut)
|
||||
async def update_device(
|
||||
device_id: int,
|
||||
data: DeviceUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: User = Depends(require_admin),
|
||||
):
|
||||
result = await db.execute(select(Device).where(Device.id == device_id))
|
||||
device = result.scalar_one_or_none()
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail="设备不存在")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
if update_data:
|
||||
update_data["updated_at"] = datetime.now()
|
||||
await db.execute(update(Device).where(Device.id == device_id).values(**update_data))
|
||||
await db.commit()
|
||||
await db.refresh(device)
|
||||
return device
|
||||
|
||||
|
||||
@router.delete("/{device_id}")
|
||||
async def delete_device(
|
||||
device_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: User = Depends(require_admin),
|
||||
):
|
||||
result = await db.execute(select(Device).where(Device.id == device_id))
|
||||
device = result.scalar_one_or_none()
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail="设备不存在")
|
||||
await db.delete(device)
|
||||
await db.commit()
|
||||
return {"message": "删除成功"}
|
||||
|
||||
|
||||
@router.get("/template/download")
|
||||
async def download_template(
|
||||
admin: User = Depends(require_admin),
|
||||
):
|
||||
"""下载 CSV 导入模板"""
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow([col[1] for col in IMPORT_COLUMNS])
|
||||
# 写入一行示例数据
|
||||
writer.writerow([
|
||||
"示例设备", "192.168.1.1", "server", "机房A", "项目1", "核心,生产", "30", "5"
|
||||
])
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv; charset=utf-8-sig",
|
||||
headers={"Content-Disposition": "attachment; filename=device_template.csv"},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/import")
|
||||
async def import_devices(
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin: User = Depends(require_admin),
|
||||
):
|
||||
"""批量导入设备(CSV)"""
|
||||
if not file.filename or not file.filename.endswith(".csv"):
|
||||
raise HTTPException(status_code=400, detail="请上传 .csv 文件")
|
||||
|
||||
content = await file.read()
|
||||
# 处理 BOM
|
||||
text = content.decode("utf-8-sig")
|
||||
reader = csv.reader(io.StringIO(text))
|
||||
rows = list(reader)
|
||||
|
||||
if len(rows) < 2:
|
||||
raise HTTPException(status_code=400, detail="CSV 至少需要表头行 + 一行数据")
|
||||
|
||||
devices_added = 0
|
||||
errors = []
|
||||
|
||||
for i, row in enumerate(rows[1:], start=1):
|
||||
if not any(cell.strip() for cell in row):
|
||||
continue # 跳过空行
|
||||
|
||||
if len(row) < 2:
|
||||
errors.append(f"第 {i + 1} 行: 缺少必填列")
|
||||
continue
|
||||
|
||||
name = row[0].strip() if len(row) > 0 else ""
|
||||
ip = row[1].strip() if len(row) > 1 else ""
|
||||
|
||||
if not name or not ip:
|
||||
errors.append(f"第 {i + 1} 行: 设备名称和 IP 地址为必填")
|
||||
continue
|
||||
|
||||
# 检查 IP 是否重复
|
||||
existing = await db.execute(select(Device).where(Device.ip == ip))
|
||||
if existing.scalar_one_or_none():
|
||||
errors.append(f"第 {i + 1} 行: IP {ip} 已存在")
|
||||
continue
|
||||
|
||||
device_type = row[2].strip().lower() if len(row) > 2 and row[2].strip() else "other"
|
||||
if device_type not in (e.value for e in DeviceTypeEnum):
|
||||
device_type = "other"
|
||||
|
||||
device = Device(
|
||||
name=name,
|
||||
ip=ip,
|
||||
device_type=device_type,
|
||||
location=row[3].strip() if len(row) > 3 else "",
|
||||
project_name=row[4].strip() if len(row) > 4 else "",
|
||||
tags=row[5].strip() if len(row) > 5 else "",
|
||||
ping_interval=int(row[6]) if len(row) > 6 and row[6].strip().isdigit() else 30,
|
||||
alert_threshold=int(row[7]) if len(row) > 7 and row[7].strip().isdigit() else 5,
|
||||
)
|
||||
db.add(device)
|
||||
devices_added += 1
|
||||
|
||||
await db.commit()
|
||||
|
||||
result = {"devices_added": devices_added}
|
||||
if errors:
|
||||
result["errors"] = errors
|
||||
logger.info(f"批量导入完成: 成功 {devices_added} 台, 错误 {len(errors)} 条")
|
||||
return result
|
||||
Reference in New Issue
Block a user