848f804169
- FastAPI 后端 + Vue 3 前端 - Docker Compose 一键部署 - Casdoor OAuth 认证集成 - LogHive 集中式日志 - 设备批量 CSV 导入/导出 - WebSocket 实时状态推送 - 企业微信告警通知 - fping 高性能并发 Ping 检测 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
210 lines
6.6 KiB
Python
210 lines
6.6 KiB
Python
"""设备管理 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
|