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 记录
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
from fastapi import APIRouter
|
||||
from app.api.v1 import networks, ips, scan, enhanced_scan, async_scan, snmp, alerts, auth, audit
|
||||
|
||||
api_router = APIRouter(prefix="/api/v1")
|
||||
|
||||
api_router.include_router(networks.router)
|
||||
api_router.include_router(ips.router)
|
||||
api_router.include_router(scan.router)
|
||||
api_router.include_router(enhanced_scan.router)
|
||||
api_router.include_router(async_scan.router)
|
||||
api_router.include_router(snmp.router)
|
||||
api_router.include_router(alerts.router)
|
||||
api_router.include_router(auth.router)
|
||||
api_router.include_router(audit.router)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,182 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.services.alert_service import AlertService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/alerts",
|
||||
tags=["告警管理"],
|
||||
dependencies=[Depends(get_current_user)],
|
||||
)
|
||||
|
||||
|
||||
@router.get("", summary="获取告警列表")
|
||||
def get_alerts(
|
||||
status: Optional[str] = None,
|
||||
severity: Optional[str] = None,
|
||||
alert_type: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取告警列表"""
|
||||
from app.models.alert import Alert
|
||||
|
||||
query = db.query(Alert)
|
||||
|
||||
if status:
|
||||
query = query.filter(Alert.status == status)
|
||||
if severity:
|
||||
query = query.filter(Alert.severity == severity)
|
||||
if alert_type:
|
||||
query = query.filter(Alert.alert_type == alert_type)
|
||||
|
||||
total = query.count()
|
||||
items = query.order_by(Alert.created_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
return {"total": total, "items": items}
|
||||
|
||||
|
||||
@router.get("/{alert_id}", summary="获取告警详情")
|
||||
def get_alert(alert_id: int, db: Session = Depends(get_db)):
|
||||
from app.models.alert import Alert
|
||||
|
||||
alert = db.query(Alert).filter(Alert.id == alert_id).first()
|
||||
if not alert:
|
||||
raise HTTPException(status_code=404, detail="告警不存在")
|
||||
return alert
|
||||
|
||||
|
||||
@router.post("/{alert_id}/acknowledge", summary="确认告警")
|
||||
def acknowledge_alert(alert_id: int, acknowledged_by: str = "system", db: Session = Depends(get_db)):
|
||||
"""确认告警"""
|
||||
alert = AlertService.acknowledge_alert(db, alert_id, acknowledged_by)
|
||||
if not alert:
|
||||
raise HTTPException(status_code=404, detail="告警不存在")
|
||||
return {"message": "告警已确认", "alert": alert}
|
||||
|
||||
|
||||
@router.post("/{alert_id}/resolve", summary="解决告警")
|
||||
def resolve_alert(alert_id: int, resolved_by: str = "system", notes: str = "", db: Session = Depends(get_db)):
|
||||
"""标记告警为已解决"""
|
||||
alert = AlertService.resolve_alert(db, alert_id, resolved_by, notes)
|
||||
if not alert:
|
||||
raise HTTPException(status_code=404, detail="告警不存在")
|
||||
return {"message": "告警已解决", "alert": alert}
|
||||
|
||||
|
||||
@router.post("/{alert_id}/ignore", summary="忽略告警")
|
||||
def ignore_alert(alert_id: int, db: Session = Depends(get_db)):
|
||||
"""忽略告警"""
|
||||
alert = AlertService.ignore_alert(db, alert_id)
|
||||
if not alert:
|
||||
raise HTTPException(status_code=404, detail="告警不存在")
|
||||
return {"message": "告警已忽略", "alert": alert}
|
||||
|
||||
|
||||
@router.post("/detect/run", summary="运行所有检测")
|
||||
def run_detection(db: Session = Depends(get_db)):
|
||||
"""立即运行所有告警检测"""
|
||||
results = AlertService.run_all_detections(db)
|
||||
return {"message": "检测完成", "results": results}
|
||||
|
||||
|
||||
@router.get("/statistics/summary", summary="告警统计摘要")
|
||||
def get_alert_statistics(db: Session = Depends(get_db)):
|
||||
"""获取告警统计信息"""
|
||||
stats = AlertService.get_statistics(db)
|
||||
return stats
|
||||
|
||||
|
||||
# ========== MAC 白名单管理 ==========
|
||||
|
||||
@router.get("/whitelist/macs", summary="获取 MAC 白名单")
|
||||
def get_mac_whitelist(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取 MAC 地址白名单"""
|
||||
total, items = AlertService.get_mac_whitelist(db, skip=skip, limit=limit)
|
||||
return {"total": total, "items": items}
|
||||
|
||||
|
||||
@router.post("/whitelist/macs", summary="添加 MAC 到白名单")
|
||||
def add_mac_to_whitelist(
|
||||
mac_address: str,
|
||||
description: Optional[str] = None,
|
||||
owner: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""添加 MAC 地址到白名单"""
|
||||
whitelist_mac = AlertService.add_mac_to_whitelist(
|
||||
db,
|
||||
mac_address=mac_address,
|
||||
description=description,
|
||||
owner=owner
|
||||
)
|
||||
return {"message": "MAC 已添加到白名单", "item": whitelist_mac}
|
||||
|
||||
|
||||
@router.delete("/whitelist/macs/{mac_id}", summary="从白名单移除 MAC")
|
||||
def remove_mac_from_whitelist(mac_id: int, db: Session = Depends(get_db)):
|
||||
"""从白名单移除 MAC 地址"""
|
||||
success = AlertService.remove_mac_from_whitelist(db, mac_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="MAC 不存在")
|
||||
return {"message": "MAC 已从白名单移除"}
|
||||
|
||||
|
||||
# ========== 单独检测接口 ==========
|
||||
|
||||
@router.post("/detect/ip-conflicts", summary="检测 IP 冲突")
|
||||
def detect_ip_conflicts(db: Session = Depends(get_db)):
|
||||
"""仅检测 IP 冲突"""
|
||||
conflicts = AlertService.detect_ip_conflicts(db)
|
||||
return {
|
||||
"count": len(conflicts),
|
||||
"conflicts": conflicts
|
||||
}
|
||||
|
||||
|
||||
@router.post("/detect/unauthorized", summary="检测未授权接入")
|
||||
def detect_unauthorized_access(db: Session = Depends(get_db)):
|
||||
"""仅检测未授权接入"""
|
||||
unauthorized = AlertService.detect_unauthorized_access(db)
|
||||
return {
|
||||
"count": len(unauthorized),
|
||||
"devices": unauthorized
|
||||
}
|
||||
|
||||
|
||||
@router.post("/detect/subnet-exhaustion", summary="检测网段耗尽")
|
||||
def detect_subnet_exhaustion(db: Session = Depends(get_db)):
|
||||
"""仅检测网段耗尽"""
|
||||
exhaustion = AlertService.detect_subnet_exhaustion(db)
|
||||
return {
|
||||
"count": len(exhaustion),
|
||||
"subnets": exhaustion
|
||||
}
|
||||
|
||||
|
||||
@router.post("/detect/new-devices", summary="检测新设备")
|
||||
def detect_new_devices(db: Session = Depends(get_db)):
|
||||
"""仅检测新发现的设备"""
|
||||
new_devices = AlertService.detect_new_devices(db)
|
||||
return {
|
||||
"count": len(new_devices),
|
||||
"devices": new_devices
|
||||
}
|
||||
|
||||
|
||||
@router.post("/detect/offline-devices", summary="检测离线设备")
|
||||
def detect_offline_devices(db: Session = Depends(get_db)):
|
||||
"""仅检测离线设备"""
|
||||
offline_devices = AlertService.detect_device_offline(db)
|
||||
return {
|
||||
"count": len(offline_devices),
|
||||
"devices": offline_devices
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.schemas.network import ScanTask
|
||||
from app.models.network import TaskStatus
|
||||
from app.services.network_service import NetworkService
|
||||
from app.services.scan_service import ScanService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/async-scan",
|
||||
tags=["异步扫描"],
|
||||
dependencies=[Depends(get_current_user)],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/network/{network_id}", response_model=ScanTask, summary="异步扫描网段")
|
||||
def async_scan_network(
|
||||
network_id: int,
|
||||
enable_ping: bool = True,
|
||||
enable_arp: bool = True,
|
||||
enable_dns: bool = True,
|
||||
timeout: int = 2,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
提交异步网段扫描任务,立即返回任务ID,后台执行
|
||||
"""
|
||||
network = NetworkService.get_by_id(db, network_id)
|
||||
if not network:
|
||||
raise HTTPException(status_code=404, detail="网段不存在")
|
||||
|
||||
# 创建数据库任务记录
|
||||
db_task = ScanService.create_scan_task(db, 'ping', network_id)
|
||||
|
||||
# 提交 Celery 任务
|
||||
from app.tasks.celery_app import scan_network_task
|
||||
celery_task = scan_network_task.delay(
|
||||
network_id=network_id,
|
||||
enable_ping=enable_ping,
|
||||
enable_arp=enable_arp,
|
||||
enable_dns=enable_dns,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
# 记录 Celery 任务ID
|
||||
db_task.celery_task_id = celery_task.id
|
||||
db.commit()
|
||||
db.refresh(db_task)
|
||||
|
||||
return db_task
|
||||
|
||||
|
||||
@router.post("/ip/{ip_address}", summary="异步扫描单个IP")
|
||||
def async_scan_ip(
|
||||
ip_address: str,
|
||||
enable_ping: bool = True,
|
||||
enable_arp: bool = True,
|
||||
enable_dns: bool = True
|
||||
):
|
||||
"""
|
||||
提交异步单个IP扫描任务
|
||||
"""
|
||||
from app.tasks.celery_app import scan_single_ip_task
|
||||
celery_task = scan_single_ip_task.delay(
|
||||
ip_address=ip_address,
|
||||
enable_ping=enable_ping,
|
||||
enable_arp=enable_arp,
|
||||
enable_dns=enable_dns
|
||||
)
|
||||
|
||||
return {
|
||||
"task_id": celery_task.id,
|
||||
"ip_address": ip_address,
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/quick", summary="快速批量IP状态更新")
|
||||
def quick_scan_ips(ip_addresses: List[str]):
|
||||
"""
|
||||
快速批量更新IP状态(只做Ping+ARP,不做DNS)
|
||||
"""
|
||||
from app.tasks.celery_app import quick_status_update
|
||||
celery_task = quick_status_update.delay(ip_addresses=ip_addresses)
|
||||
|
||||
return {
|
||||
"task_id": celery_task.id,
|
||||
"ips_count": len(ip_addresses),
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/task/{task_id}", summary="查询异步任务状态")
|
||||
def get_async_task_status(task_id: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
根据 Celery 任务ID 查询任务状态
|
||||
"""
|
||||
# 先查询数据库任务
|
||||
from app.models.network import ScanTask as DbScanTask
|
||||
db_task = db.query(DbScanTask).filter(DbScanTask.celery_task_id == task_id).first()
|
||||
|
||||
# 直接查询 Celery
|
||||
from celery.result import AsyncResult
|
||||
result = AsyncResult(task_id)
|
||||
|
||||
response = {
|
||||
"task_id": task_id,
|
||||
"celery_status": result.status
|
||||
}
|
||||
|
||||
if db_task:
|
||||
response["db_status"] = db_task.status.value
|
||||
response["progress"] = db_task.progress
|
||||
response["total_count"] = db_task.total_count
|
||||
response["success_count"] = db_task.success_count
|
||||
response["started_at"] = db_task.started_at
|
||||
response["completed_at"] = db_task.completed_at
|
||||
|
||||
if result.status == 'SUCCESS':
|
||||
response["result"] = result.result
|
||||
elif result.status == 'FAILURE':
|
||||
response["error"] = str(result.info)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/tasks/running", summary="获取运行中的任务列表")
|
||||
def get_running_tasks(db: Session = Depends(get_db)):
|
||||
"""
|
||||
获取所有正在运行的扫描任务
|
||||
"""
|
||||
from app.models.network import ScanTask as DbScanTask
|
||||
running = db.query(DbScanTask).filter(DbScanTask.status == TaskStatus.RUNNING).all()
|
||||
pending = db.query(DbScanTask).filter(DbScanTask.status == TaskStatus.PENDING).all()
|
||||
|
||||
return {
|
||||
"running_count": len(running),
|
||||
"pending_count": len(pending),
|
||||
"running_tasks": running,
|
||||
"pending_tasks": pending
|
||||
}
|
||||
|
||||
|
||||
@router.post("/task/{task_id}/cancel", summary="取消异步任务")
|
||||
def cancel_async_task(task_id: str):
|
||||
"""
|
||||
取消正在运行的异步任务
|
||||
"""
|
||||
from celery.result import AsyncResult
|
||||
result = AsyncResult(task_id)
|
||||
|
||||
if result.status in ['PENDING', 'RUNNING']:
|
||||
result.revoke(terminate=True)
|
||||
return {"status": "cancelled", "task_id": task_id}
|
||||
else:
|
||||
return {"status": "not_running", "current_status": result.status}
|
||||
|
||||
|
||||
@router.post("/full-scan", summary="触发全量扫描")
|
||||
def trigger_full_scan(
|
||||
enable_ping: bool = True,
|
||||
enable_arp: bool = True,
|
||||
enable_dns: bool = True
|
||||
):
|
||||
"""
|
||||
触发全量网段扫描(所有网段)
|
||||
"""
|
||||
from app.tasks.celery_app import full_network_scan
|
||||
celery_task = full_network_scan.delay(
|
||||
enable_ping=enable_ping,
|
||||
enable_arp=enable_arp,
|
||||
enable_dns=enable_dns
|
||||
)
|
||||
|
||||
return {
|
||||
"task_id": celery_task.id,
|
||||
"status": "pending",
|
||||
"message": "全量扫描任务已提交"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/celery/inspect", summary="Celery Worker 状态检查")
|
||||
def inspect_celery():
|
||||
"""
|
||||
检查 Celery Worker 状态
|
||||
"""
|
||||
from app.tasks.celery_app import celery_app
|
||||
|
||||
try:
|
||||
inspector = celery_app.control.inspect()
|
||||
active = inspector.active()
|
||||
scheduled = inspector.scheduled()
|
||||
reserved = inspector.reserved()
|
||||
stats = inspector.stats()
|
||||
|
||||
return {
|
||||
"status": "healthy" if stats else "no_workers",
|
||||
"active_tasks": active,
|
||||
"scheduled_tasks": scheduled,
|
||||
"reserved_tasks": reserved,
|
||||
"worker_stats": stats
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"message": "Celery Worker 可能未启动"
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
import csv
|
||||
import io
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.auth import User
|
||||
from app.services.audit_service import AuditService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/audit",
|
||||
tags=["审计日志"],
|
||||
dependencies=[Depends(get_current_user)],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/logs", summary="查询审计日志")
|
||||
def get_audit_logs(
|
||||
user_id: Optional[int] = None,
|
||||
username: Optional[str] = None,
|
||||
action: Optional[str] = None,
|
||||
resource_type: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
# 当前用户必须是 admin 或 super_admin 才能看审计
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
# 角色检查:super_admin / admin 可以看所有人的,operator/viewer 只能看自己的
|
||||
if current_user.role not in ("super_admin", "admin"):
|
||||
user_id = current_user.id
|
||||
|
||||
total, items = AuditService.get_logs(
|
||||
db,
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
status=status,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
serialized = []
|
||||
for log in items:
|
||||
serialized.append({
|
||||
"id": log.id,
|
||||
"user_id": log.user_id,
|
||||
"username": log.username,
|
||||
"action": log.action,
|
||||
"resource_type": log.resource_type,
|
||||
"resource_id": log.resource_id,
|
||||
"resource_name": log.resource_name,
|
||||
"method": log.method,
|
||||
"path": log.path,
|
||||
"ip_address": log.ip_address,
|
||||
"user_agent": log.user_agent,
|
||||
"status": log.status,
|
||||
"detail": log.detail,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
})
|
||||
|
||||
return {"total": total, "items": serialized}
|
||||
|
||||
|
||||
@router.get("/logs/export", summary="导出审计日志为 CSV")
|
||||
def export_audit_logs(
|
||||
user_id: Optional[int] = None,
|
||||
username: Optional[str] = None,
|
||||
action: Optional[str] = None,
|
||||
resource_type: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出 CSV 流。admin/super_admin 看全部;operator/viewer 仅能导出自己的。"""
|
||||
if current_user.role not in ("super_admin", "admin"):
|
||||
user_id = current_user.id
|
||||
|
||||
# 导出上限 50000 行,避免一次拉太多 OOM
|
||||
total, items = AuditService.get_logs(
|
||||
db,
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
status=status,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
skip=0,
|
||||
limit=50000,
|
||||
)
|
||||
|
||||
output = io.StringIO()
|
||||
# 写入 UTF-8 BOM 让 Excel 直接打开不乱码
|
||||
output.write("\ufeff")
|
||||
writer = csv.writer(output)
|
||||
writer.writerow([
|
||||
"ID", "时间", "用户", "操作", "资源类型", "资源ID", "资源名称",
|
||||
"HTTP方法", "路径", "客户端IP", "状态", "详情"
|
||||
])
|
||||
for log in items:
|
||||
writer.writerow([
|
||||
log.id,
|
||||
log.created_at.isoformat() if log.created_at else "",
|
||||
log.username or "",
|
||||
log.action or "",
|
||||
log.resource_type or "",
|
||||
log.resource_id or "",
|
||||
log.resource_name or "",
|
||||
log.method or "",
|
||||
log.path or "",
|
||||
log.ip_address or "",
|
||||
log.status or "",
|
||||
(log.detail or "").replace("\n", " ")[:500],
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
filename = f"audit_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv; charset=utf-8",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats", summary="审计日志统计")
|
||||
def get_audit_stats(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
if current_user.role not in ("super_admin", "admin"):
|
||||
# operator/viewer 只能看自己的简单计数
|
||||
from app.models.audit import AuditLog
|
||||
from sqlalchemy import func
|
||||
my_count = db.query(func.count(AuditLog.id)).filter(AuditLog.user_id == current_user.id).scalar()
|
||||
return {"recent_24h": 0, "by_action_24h": {}, "my_total": int(my_count or 0)}
|
||||
|
||||
return AuditService.get_stats(db)
|
||||
|
||||
|
||||
@router.get("/actions", summary="获取所有审计操作类型(用于前端过滤下拉框)")
|
||||
def get_action_types():
|
||||
"""硬编码返回常用 action 列表,避免每次都查 DB"""
|
||||
from app.services.audit_service import AuditAction
|
||||
actions = []
|
||||
for name in dir(AuditAction):
|
||||
if name.startswith("_") or name.isupper() and not name.startswith("__"):
|
||||
continue
|
||||
val = getattr(AuditAction, name, None)
|
||||
if isinstance(val, str):
|
||||
actions.append({"code": val, "name": name})
|
||||
return actions
|
||||
@@ -0,0 +1,359 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user, require_permission
|
||||
from app.models.auth import User, UserRole, UserStatus
|
||||
from app.services.user_service import UserService, TokenService
|
||||
from app.services.audit_service import AuditService, AuditAction
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["认证管理"])
|
||||
|
||||
|
||||
@router.post("/login", summary="用户登录")
|
||||
def login(
|
||||
request: Request,
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""用户登录,获取访问令牌"""
|
||||
user = UserService.authenticate(db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
# 记录失败尝试
|
||||
AuditService.record(
|
||||
db,
|
||||
action=AuditAction.USER_LOGIN_FAILED,
|
||||
username=form_data.username,
|
||||
method="POST",
|
||||
path=str(request.url.path),
|
||||
ip_address=request.client.host if request.client else "",
|
||||
user_agent=(request.headers.get("user-agent") or "")[:500],
|
||||
status="failed",
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 更新登录信息
|
||||
UserService.update_login_info(db, user.id, ip_address=request.client.host if request.client else None)
|
||||
|
||||
# 创建令牌
|
||||
tokens = TokenService.create_tokens(
|
||||
db, user,
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=(request.headers.get("user-agent") or "")[:500],
|
||||
)
|
||||
|
||||
AuditService.record(
|
||||
db,
|
||||
action=AuditAction.USER_LOGIN,
|
||||
user=user,
|
||||
method="POST",
|
||||
path=str(request.url.path),
|
||||
ip_address=request.client.host if request.client else "",
|
||||
user_agent=(request.headers.get("user-agent") or "")[:500],
|
||||
)
|
||||
|
||||
return {
|
||||
**tokens,
|
||||
"user_info": {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"real_name": user.real_name,
|
||||
"role": user.role.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("/refresh", summary="刷新访问令牌")
|
||||
def refresh_token(refresh_token: str, db: Session = Depends(get_db)):
|
||||
"""使用刷新令牌获取新的访问令牌"""
|
||||
result = TokenService.refresh_access_token(db, refresh_token)
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的刷新令牌或已过期",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/logout", summary="登出")
|
||||
def logout(
|
||||
request: Request,
|
||||
refresh_token: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""用户登出,撤销刷新令牌"""
|
||||
TokenService.revoke_token(db, refresh_token)
|
||||
AuditService.record(
|
||||
db,
|
||||
action=AuditAction.USER_LOGOUT,
|
||||
user=current_user,
|
||||
method="POST",
|
||||
path=str(request.url.path),
|
||||
ip_address=request.client.host if request.client else "",
|
||||
user_agent=(request.headers.get("user-agent") or "")[:500],
|
||||
)
|
||||
return {"message": "登出成功"}
|
||||
|
||||
|
||||
@router.get("/me", summary="获取当前用户信息")
|
||||
def get_current_user_info(current_user: User = Depends(get_current_user)):
|
||||
"""获取当前登录用户信息"""
|
||||
return {
|
||||
"id": current_user.id,
|
||||
"username": current_user.username,
|
||||
"email": current_user.email,
|
||||
"phone": current_user.phone,
|
||||
"real_name": current_user.real_name,
|
||||
"role": current_user.role.value,
|
||||
"status": current_user.status.value,
|
||||
"last_login_at": current_user.last_login_at,
|
||||
"email_notification": current_user.email_notification,
|
||||
"wechat_notification": current_user.wechat_notification,
|
||||
"dingtalk_notification": current_user.dingtalk_notification
|
||||
}
|
||||
|
||||
|
||||
@router.post("/change-password", summary="修改密码")
|
||||
def change_password(
|
||||
old_password: str,
|
||||
new_password: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""用户修改自己的密码"""
|
||||
if len(new_password) < 6:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="新密码长度不能少于6位"
|
||||
)
|
||||
|
||||
try:
|
||||
UserService.change_password(db, current_user.id, old_password, new_password)
|
||||
return {"message": "密码修改成功,请重新登录"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"密码修改失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# ========== 用户管理 API (需要管理员权限) ==========
|
||||
|
||||
@router.get("/users", summary="获取用户列表")
|
||||
def get_users(
|
||||
status: Optional[UserStatus] = None,
|
||||
role: Optional[UserRole] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
current_user: User = Depends(require_permission("user:view")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取用户列表(需要管理员权限)"""
|
||||
total, items = UserService.get_users(db, skip=skip, limit=limit, status=status, role=role)
|
||||
|
||||
# 返回脱敏的用户信息
|
||||
user_list = []
|
||||
for user in items:
|
||||
user_list.append({
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"phone": user.phone,
|
||||
"real_name": user.real_name,
|
||||
"role": user.role.value,
|
||||
"status": user.status.value,
|
||||
"last_login_at": user.last_login_at,
|
||||
"created_at": user.created_at
|
||||
})
|
||||
|
||||
return {"total": total, "items": user_list}
|
||||
|
||||
|
||||
@router.get("/users/{user_id}", summary="获取用户详情")
|
||||
def get_user(
|
||||
user_id: int,
|
||||
current_user: User = Depends(require_permission("user:view")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取用户详情(需要管理员权限)"""
|
||||
user = UserService.get_user_by_id(db, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"phone": user.phone,
|
||||
"real_name": user.real_name,
|
||||
"role": user.role.value,
|
||||
"status": user.status.value,
|
||||
"last_login_at": user.last_login_at,
|
||||
"last_login_ip": user.last_login_ip,
|
||||
"login_failed_count": user.login_failed_count,
|
||||
"created_at": user.created_at,
|
||||
"email_notification": user.email_notification,
|
||||
"wechat_notification": user.wechat_notification,
|
||||
"dingtalk_notification": user.dingtalk_notification
|
||||
}
|
||||
|
||||
|
||||
@router.post("/users", summary="创建用户", status_code=status.HTTP_201_CREATED)
|
||||
def create_user(
|
||||
username: str,
|
||||
password: str,
|
||||
email: Optional[str] = None,
|
||||
phone: Optional[str] = None,
|
||||
real_name: Optional[str] = None,
|
||||
role: UserRole = UserRole.VIEWER,
|
||||
current_user: User = Depends(require_permission("user:create")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""创建新用户(需要超级管理员权限)"""
|
||||
# 只有超级管理员才能创建管理员
|
||||
if role in [UserRole.SUPER_ADMIN, UserRole.ADMIN] and current_user.role != UserRole.SUPER_ADMIN:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权创建管理员账户"
|
||||
)
|
||||
|
||||
if len(password) < 6:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="密码长度不能少于6位"
|
||||
)
|
||||
|
||||
user = UserService.create_user(
|
||||
db,
|
||||
username=username,
|
||||
password=password,
|
||||
email=email,
|
||||
phone=phone,
|
||||
real_name=real_name,
|
||||
role=role,
|
||||
created_by=current_user.id
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "用户创建成功",
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"role": user.role.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.put("/users/{user_id}/status", summary="更新用户状态")
|
||||
def update_user_status(
|
||||
user_id: int,
|
||||
status: UserStatus,
|
||||
current_user: User = Depends(require_permission("user:edit")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""启用/禁用用户"""
|
||||
# 不能禁用自己
|
||||
if user_id == current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="不能禁用自己的账户"
|
||||
)
|
||||
|
||||
user = UserService.update_user_status(db, user_id, status)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
return {"message": "用户状态更新成功", "new_status": status.value}
|
||||
|
||||
|
||||
@router.post("/users/{user_id}/unlock", summary="解锁用户")
|
||||
def unlock_user(
|
||||
user_id: int,
|
||||
current_user: User = Depends(require_permission("user:edit")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""解锁被锁定的用户"""
|
||||
user = UserService.unlock_user(db, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
return {"message": "用户已解锁"}
|
||||
|
||||
|
||||
@router.post("/users/{user_id}/reset-password", summary="重置用户密码")
|
||||
def reset_user_password(
|
||||
user_id: int,
|
||||
new_password: str,
|
||||
current_user: User = Depends(require_permission("user:edit")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""管理员重置用户密码"""
|
||||
if len(new_password) < 6:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="密码长度不能少于6位"
|
||||
)
|
||||
|
||||
# 只有超级管理员才能重置其他管理员的密码
|
||||
target_user = UserService.get_user_by_id(db, user_id)
|
||||
if not target_user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
if target_user.role in [UserRole.SUPER_ADMIN, UserRole.ADMIN] and current_user.role != UserRole.SUPER_ADMIN:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权重置管理员密码"
|
||||
)
|
||||
|
||||
success = UserService.reset_password(db, user_id, new_password)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 撤销该用户所有现有令牌,迫使其重新登录
|
||||
TokenService.revoke_all_user_tokens(db, user_id)
|
||||
|
||||
return {"message": "密码重置成功,用户所有会话已失效"}
|
||||
|
||||
|
||||
@router.delete("/users/{user_id}", summary="删除用户")
|
||||
def delete_user(
|
||||
user_id: int,
|
||||
current_user: User = Depends(require_permission("user:delete")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除用户(软删除,标记为禁用)"""
|
||||
# 不能删除自己
|
||||
if user_id == current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="不能删除自己的账户"
|
||||
)
|
||||
|
||||
# 只有超级管理员才能删除管理员
|
||||
target_user = UserService.get_user_by_id(db, user_id)
|
||||
if target_user and target_user.role in [UserRole.SUPER_ADMIN, UserRole.ADMIN]:
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权删除管理员账户"
|
||||
)
|
||||
|
||||
success = UserService.delete_user(db, user_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 撤销所有令牌
|
||||
TokenService.revoke_all_user_tokens(db, user_id)
|
||||
|
||||
return {"message": "用户已删除"}
|
||||
@@ -0,0 +1,209 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.schemas.network import ScanTask, IPAddress
|
||||
from app.models.network import TaskType, TaskStatus
|
||||
from app.services.network_service import NetworkService
|
||||
from app.services.enhanced_scan_service import EnhancedScanService
|
||||
from app.services.scan_service import ScanService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/scan",
|
||||
tags=["增强扫描"],
|
||||
dependencies=[Depends(get_current_user)],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/comprehensive/{network_id}", response_model=ScanTask, summary="综合扫描网段")
|
||||
def comprehensive_scan(
|
||||
network_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
enable_ping: bool = True,
|
||||
enable_arp: bool = True,
|
||||
enable_dns: bool = True,
|
||||
timeout: int = 2,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
对指定网段执行综合扫描
|
||||
- Ping 扫描检测在线状态
|
||||
- ARP 扫描获取MAC地址
|
||||
- 反向DNS解析获取主机名
|
||||
- MAC厂商识别
|
||||
"""
|
||||
network = NetworkService.get_by_id(db, network_id)
|
||||
if not network:
|
||||
raise HTTPException(status_code=404, detail="网段不存在")
|
||||
|
||||
# 创建扫描任务
|
||||
task = ScanService.create_scan_task(db, TaskType.PING, network_id=network_id)
|
||||
|
||||
try:
|
||||
# 立即执行扫描(后续改为Celery异步)
|
||||
ScanService.update_task_status(db, task.id, TaskStatus.RUNNING, progress=0)
|
||||
|
||||
# 执行综合扫描
|
||||
results = EnhancedScanService.scan_network(
|
||||
network.cidr,
|
||||
enable_ping=enable_ping,
|
||||
enable_arp=enable_arp,
|
||||
enable_dns=enable_dns,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
# 更新IP状态
|
||||
stats = EnhancedScanService.bulk_update_ips_from_scan(db, results)
|
||||
|
||||
# 刷新网段统计字段(used_ips/reserved_ips),保证网段列表利用率能即时反映
|
||||
NetworkService.get_stats(db, network_id)
|
||||
|
||||
# 更新任务状态
|
||||
ScanService.update_task_status(
|
||||
db,
|
||||
task.id,
|
||||
TaskStatus.COMPLETED,
|
||||
progress=100,
|
||||
total_count=stats['total_scanned'],
|
||||
success_count=stats['online_count']
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
ScanService.update_task_status(
|
||||
db,
|
||||
task.id,
|
||||
TaskStatus.FAILED,
|
||||
error_message=str(e)
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=f"扫描失败: {str(e)}")
|
||||
|
||||
# 重新获取任务状态
|
||||
task = ScanService.get_task_by_id(db, task.id)
|
||||
return task
|
||||
|
||||
|
||||
@router.post("/comprehensive/ip/{ip_address}", summary="单个IP综合扫描")
|
||||
def comprehensive_scan_ip(
|
||||
ip_address: str,
|
||||
enable_ping: bool = True,
|
||||
enable_arp: bool = True,
|
||||
enable_dns: bool = True,
|
||||
timeout: int = 2,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
对单个IP执行综合扫描
|
||||
"""
|
||||
result = EnhancedScanService.comprehensive_scan(
|
||||
ip_address,
|
||||
enable_ping=enable_ping,
|
||||
enable_arp=enable_arp,
|
||||
enable_dns=enable_dns,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
# 如果IP在数据库中,更新信息
|
||||
EnhancedScanService.update_ip_from_scan_result(db, result)
|
||||
|
||||
# 刷新所属网段统计字段(used_ips/reserved_ips),保证网段列表利用率能即时反映
|
||||
db_ip = db.query(IPAddress).filter(IPAddress.ip_address == ip_address).first()
|
||||
if db_ip:
|
||||
NetworkService.get_stats(db, db_ip.network_id)
|
||||
|
||||
db.commit()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/mac/vendor/{mac_address}", summary="MAC地址厂商识别")
|
||||
def get_mac_vendor(mac_address: str):
|
||||
"""
|
||||
根据MAC地址识别厂商
|
||||
"""
|
||||
vendor = EnhancedScanService.get_mac_vendor(mac_address)
|
||||
return {
|
||||
"mac_address": mac_address,
|
||||
"vendor": vendor or "Unknown"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/dns/reverse/{ip_address}", summary="反向DNS解析")
|
||||
def reverse_dns(ip_address: str, timeout: int = 2):
|
||||
"""
|
||||
反向DNS解析获取主机名
|
||||
"""
|
||||
hostname = EnhancedScanService.reverse_dns_lookup(ip_address, timeout)
|
||||
return {
|
||||
"ip_address": ip_address,
|
||||
"hostname": hostname or "Unknown"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/online/ips", summary="获取所有在线IP列表")
|
||||
def get_online_ips(
|
||||
network_id: Optional[int] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
获取所有在线的IP地址列表
|
||||
"""
|
||||
from app.models.network import IPAddress as IPAddressModel, IPStatus
|
||||
|
||||
query = db.query(IPAddressModel).filter(IPAddressModel.status == IPStatus.ONLINE)
|
||||
|
||||
if network_id:
|
||||
query = query.filter(IPAddressModel.network_id == network_id)
|
||||
|
||||
total = query.count()
|
||||
items = query.order_by(IPAddressModel.ip_address).offset(skip).limit(limit).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"online_count": total,
|
||||
"items": items
|
||||
}
|
||||
|
||||
|
||||
@router.get("/statistics/summary", summary="扫描统计摘要")
|
||||
def get_scan_statistics(db: Session = Depends(get_db)):
|
||||
"""
|
||||
获取扫描统计摘要信息
|
||||
"""
|
||||
from app.models.network import IPAddress as IPAddressModel, IPStatus
|
||||
from app.models.network import ScanTask, TaskStatus
|
||||
|
||||
# IP 统计
|
||||
total_ips = db.query(IPAddressModel).count()
|
||||
online_ips = db.query(IPAddressModel).filter(IPAddressModel.status == IPStatus.ONLINE).count()
|
||||
offline_ips = db.query(IPAddressModel).filter(IPAddressModel.status == IPStatus.OFFLINE).count()
|
||||
reserved_ips = db.query(IPAddressModel).filter(IPAddressModel.status == IPStatus.RESERVED).count()
|
||||
with_mac = db.query(IPAddressModel).filter(IPAddressModel.mac_address.isnot(None)).count()
|
||||
with_hostname = db.query(IPAddressModel).filter(IPAddressModel.hostname.isnot(None)).count()
|
||||
|
||||
# 任务统计
|
||||
total_tasks = db.query(ScanTask).count()
|
||||
completed_tasks = db.query(ScanTask).filter(ScanTask.status == TaskStatus.COMPLETED).count()
|
||||
failed_tasks = db.query(ScanTask).filter(ScanTask.status == TaskStatus.FAILED).count()
|
||||
|
||||
return {
|
||||
"ip_statistics": {
|
||||
"total": total_ips,
|
||||
"online": online_ips,
|
||||
"offline": offline_ips,
|
||||
"reserved": reserved_ips,
|
||||
"available": total_ips - online_ips - offline_ips - reserved_ips,
|
||||
"with_mac": with_mac,
|
||||
"with_hostname": with_hostname,
|
||||
"online_rate": round(online_ips / total_ips * 100, 2) if total_ips > 0 else 0
|
||||
},
|
||||
"task_statistics": {
|
||||
"total": total_tasks,
|
||||
"completed": completed_tasks,
|
||||
"failed": failed_tasks,
|
||||
"success_rate": round(completed_tasks / total_tasks * 100, 2) if total_tasks > 0 else 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.auth import User
|
||||
from app.schemas.network import IPAddressListResponse, IPAddressUpdate, IPAddress
|
||||
from app.models.network import IPStatus
|
||||
from app.services.ip_service import IPService
|
||||
from app.services.audit_service import AuditService, AuditAction
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/ips",
|
||||
tags=["IP地址管理"],
|
||||
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("", response_model=IPAddressListResponse, summary="获取IP列表")
|
||||
def get_ips(
|
||||
network_id: Optional[int] = None,
|
||||
status: Optional[IPStatus] = None,
|
||||
search: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取IP地址列表,支持过滤和搜索"""
|
||||
total, items = IPService.get_list(
|
||||
db,
|
||||
network_id=network_id,
|
||||
status=status,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
search=search
|
||||
)
|
||||
return {"total": total, "items": items}
|
||||
|
||||
|
||||
@router.get("/{ip_id}", response_model=IPAddress, summary="获取IP详情")
|
||||
def get_ip(ip_id: int, db: Session = Depends(get_db)):
|
||||
"""根据ID获取IP详情"""
|
||||
ip = IPService.get_by_id(db, ip_id)
|
||||
if not ip:
|
||||
raise HTTPException(status_code=404, detail="IP不存在")
|
||||
return ip
|
||||
|
||||
|
||||
@router.get("/address/{ip_address}", response_model=IPAddress, summary="根据IP地址查询")
|
||||
def get_ip_by_address(ip_address: str, db: Session = Depends(get_db)):
|
||||
"""根据IP地址查询详细信息"""
|
||||
ip = IPService.get_by_ip(db, ip_address)
|
||||
if not ip:
|
||||
raise HTTPException(status_code=404, detail="IP不存在")
|
||||
return ip
|
||||
|
||||
|
||||
@router.put("/{ip_id}", response_model=IPAddress, summary="更新IP信息")
|
||||
def update_ip(
|
||||
ip_id: int,
|
||||
ip_in: IPAddressUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""更新IP地址的属性信息"""
|
||||
old = IPService.get_by_id(db, ip_id)
|
||||
if not old:
|
||||
raise HTTPException(status_code=404, detail="IP不存在")
|
||||
|
||||
ip = IPService.update(db, ip_id, ip_in)
|
||||
if not ip:
|
||||
raise HTTPException(status_code=404, detail="IP不存在")
|
||||
|
||||
c, u = _client_info(request)
|
||||
AuditService.record(
|
||||
db,
|
||||
action=AuditAction.IP_UPDATE,
|
||||
user=current_user,
|
||||
resource_type="ip",
|
||||
resource_id=str(ip.id),
|
||||
resource_name=ip.ip_address,
|
||||
method="PUT",
|
||||
path=str(request.url.path),
|
||||
ip_address=c,
|
||||
user_agent=u,
|
||||
detail={
|
||||
"before": {
|
||||
"owner": old.owner, "hostname": old.hostname, "mac_address": old.mac_address,
|
||||
"vendor": old.vendor, "status": old.status.value if old.status else None,
|
||||
"notes": old.notes, "switch_name": old.switch_name, "switch_port": old.switch_port,
|
||||
},
|
||||
"after": {
|
||||
"owner": ip.owner, "hostname": ip.hostname, "mac_address": ip.mac_address,
|
||||
"vendor": ip.vendor, "status": ip.status.value if ip.status else None,
|
||||
"notes": ip.notes, "switch_name": ip.switch_name, "switch_port": ip.switch_port,
|
||||
},
|
||||
},
|
||||
)
|
||||
return ip
|
||||
|
||||
|
||||
@router.post("/{ip_id}/reserve", response_model=IPAddress, summary="标记为保留IP")
|
||||
def reserve_ip(
|
||||
ip_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""将IP标记为保留状态,禁止分配"""
|
||||
ip = IPService.set_reserved(db, ip_id, reserved=True)
|
||||
if not ip:
|
||||
raise HTTPException(status_code=404, detail="IP不存在")
|
||||
c, u = _client_info(request)
|
||||
AuditService.record(
|
||||
db,
|
||||
action=AuditAction.IP_RESERVE,
|
||||
user=current_user,
|
||||
resource_type="ip",
|
||||
resource_id=str(ip.id),
|
||||
resource_name=ip.ip_address,
|
||||
method="POST",
|
||||
path=str(request.url.path),
|
||||
ip_address=c,
|
||||
user_agent=u,
|
||||
)
|
||||
return ip
|
||||
|
||||
|
||||
@router.post("/{ip_id}/unreserve", response_model=IPAddress, summary="取消保留IP")
|
||||
def unreserve_ip(
|
||||
ip_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""取消IP的保留状态"""
|
||||
ip = IPService.set_reserved(db, ip_id, reserved=False)
|
||||
if not ip:
|
||||
raise HTTPException(status_code=404, detail="IP不存在")
|
||||
c, u = _client_info(request)
|
||||
AuditService.record(
|
||||
db,
|
||||
action=AuditAction.IP_UNRESERVE,
|
||||
user=current_user,
|
||||
resource_type="ip",
|
||||
resource_id=str(ip.id),
|
||||
resource_name=ip.ip_address,
|
||||
method="POST",
|
||||
path=str(request.url.path),
|
||||
ip_address=c,
|
||||
user_agent=u,
|
||||
)
|
||||
return ip
|
||||
@@ -0,0 +1,180 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.auth import User
|
||||
from app.schemas.network import (
|
||||
NetworkCreate, NetworkUpdate, Network, NetworkListResponse,
|
||||
NetworkStats, IPAddressListResponse, IPAddressUpdate, IPAddress
|
||||
)
|
||||
from app.services.network_service import NetworkService
|
||||
from app.services.audit_service import AuditService, AuditAction
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/networks",
|
||||
tags=["网段管理"],
|
||||
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("", response_model=NetworkListResponse, summary="获取网段列表")
|
||||
def get_networks(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
group_name: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取所有网段列表"""
|
||||
total, items = NetworkService.get_list(db, skip=skip, limit=limit, group_name=group_name)
|
||||
return {"total": total, "items": items}
|
||||
|
||||
|
||||
@router.get("/stats", response_model=list[NetworkStats], summary="获取所有网段统计")
|
||||
def get_all_network_stats(db: Session = Depends(get_db)):
|
||||
"""获取所有网段的统计信息"""
|
||||
return NetworkService.get_all_stats(db)
|
||||
|
||||
|
||||
@router.get("/{network_id}", response_model=Network, summary="获取网段详情")
|
||||
def get_network(network_id: int, db: Session = Depends(get_db)):
|
||||
"""根据ID获取网段详情"""
|
||||
network = NetworkService.get_by_id(db, network_id)
|
||||
if not network:
|
||||
raise HTTPException(status_code=404, detail="网段不存在")
|
||||
return network
|
||||
|
||||
|
||||
@router.get("/{network_id}/stats", response_model=NetworkStats, summary="获取网段统计")
|
||||
def get_network_stats(network_id: int, db: Session = Depends(get_db)):
|
||||
"""获取指定网段的统计信息"""
|
||||
stats = NetworkService.get_stats(db, network_id)
|
||||
if not stats:
|
||||
raise HTTPException(status_code=404, detail="网段不存在")
|
||||
return stats
|
||||
|
||||
|
||||
@router.get("/{network_id}/ips", response_model=IPAddressListResponse, summary="获取网段下的IP列表")
|
||||
def get_network_ips(
|
||||
network_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 200,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取指定网段下的所有IP地址"""
|
||||
from app.services.ip_service import IPService
|
||||
total, items = IPService.get_list(db, network_id=network_id, skip=skip, limit=limit)
|
||||
return {"total": total, "items": items}
|
||||
|
||||
|
||||
@router.post("", response_model=Network, status_code=201, summary="创建网段")
|
||||
def create_network(
|
||||
network_in: NetworkCreate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创建新网段,支持CIDR格式"""
|
||||
existing = NetworkService.get_by_cidr(db, network_in.cidr)
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="该网段已存在")
|
||||
|
||||
ip, ua = _client_info(request)
|
||||
try:
|
||||
network = NetworkService.create(db, network_in)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
AuditService.record(
|
||||
db,
|
||||
action=AuditAction.NETWORK_CREATE,
|
||||
user=current_user,
|
||||
resource_type="network",
|
||||
resource_id=network.id,
|
||||
resource_name=network.cidr,
|
||||
method="POST",
|
||||
path=str(request.url.path),
|
||||
ip_address=ip,
|
||||
user_agent=ua,
|
||||
detail={"cidr": network.cidr, "name": network.name},
|
||||
)
|
||||
return network
|
||||
|
||||
|
||||
@router.put("/{network_id}", response_model=Network, summary="更新网段")
|
||||
def update_network(
|
||||
network_id: int,
|
||||
network_in: NetworkUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""更新网段信息"""
|
||||
old = NetworkService.get_by_id(db, network_id)
|
||||
if not old:
|
||||
raise HTTPException(status_code=404, detail="网段不存在")
|
||||
|
||||
network = NetworkService.update(db, network_id, network_in)
|
||||
if not network:
|
||||
raise HTTPException(status_code=404, detail="网段不存在")
|
||||
|
||||
ip, ua = _client_info(request)
|
||||
AuditService.record(
|
||||
db,
|
||||
action=AuditAction.NETWORK_UPDATE,
|
||||
user=current_user,
|
||||
resource_type="network",
|
||||
resource_id=network.id,
|
||||
resource_name=network.cidr,
|
||||
method="PUT",
|
||||
path=str(request.url.path),
|
||||
ip_address=ip,
|
||||
user_agent=ua,
|
||||
detail={
|
||||
"before": {"name": old.name, "gateway": old.gateway, "group_name": old.group_name, "vlan_id": old.vlan_id, "description": old.description},
|
||||
"after": {"name": network.name, "gateway": network.gateway, "group_name": network.group_name, "vlan_id": network.vlan_id, "description": network.description},
|
||||
},
|
||||
)
|
||||
return network
|
||||
|
||||
|
||||
@router.delete("/{network_id}", summary="删除网段")
|
||||
def delete_network(
|
||||
network_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""删除网段及其下的所有IP记录"""
|
||||
network = NetworkService.get_by_id(db, network_id)
|
||||
if not network:
|
||||
raise HTTPException(status_code=404, detail="网段不存在")
|
||||
cidr_snapshot = network.cidr
|
||||
|
||||
success = NetworkService.delete(db, network_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="网段不存在")
|
||||
|
||||
ip, ua = _client_info(request)
|
||||
AuditService.record(
|
||||
db,
|
||||
action=AuditAction.NETWORK_DELETE,
|
||||
user=current_user,
|
||||
resource_type="network",
|
||||
resource_id=network_id,
|
||||
resource_name=cidr_snapshot,
|
||||
method="DELETE",
|
||||
path=str(request.url.path),
|
||||
ip_address=ip,
|
||||
user_agent=ua,
|
||||
detail={"cidr": cidr_snapshot},
|
||||
)
|
||||
return {"message": "删除成功"}
|
||||
@@ -0,0 +1,108 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.schemas.network import ScanTask, ScanTaskListResponse, ScanResult
|
||||
from app.models.network import TaskType, TaskStatus
|
||||
from app.services.scan_service import ScanService
|
||||
from app.services.network_service import NetworkService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/scan",
|
||||
tags=["扫描管理"],
|
||||
dependencies=[Depends(get_current_user)],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tasks", response_model=ScanTaskListResponse, summary="获取扫描任务列表")
|
||||
def get_scan_tasks(
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取所有扫描任务"""
|
||||
total, items = ScanService.get_task_list(db, skip=skip, limit=limit)
|
||||
return {"total": total, "items": items}
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}", response_model=ScanTask, summary="获取扫描任务状态")
|
||||
def get_scan_task(task_id: int, db: Session = Depends(get_db)):
|
||||
"""获取指定扫描任务的状态"""
|
||||
task = ScanService.get_task_by_id(db, task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
return task
|
||||
|
||||
|
||||
@router.post("/ping/{network_id}", response_model=ScanTask, status_code=201, summary="触发网段Ping扫描")
|
||||
def trigger_ping_scan(network_id: int, db: Session = Depends(get_db)):
|
||||
"""对指定网段触发ICMP Ping扫描"""
|
||||
network = NetworkService.get_by_id(db, network_id)
|
||||
if not network:
|
||||
raise HTTPException(status_code=404, detail="网段不存在")
|
||||
|
||||
# 创建扫描任务
|
||||
task = ScanService.create_scan_task(db, TaskType.PING, network_id=network_id)
|
||||
|
||||
# 立即执行扫描(简单版,后续用Celery)
|
||||
try:
|
||||
ScanService.update_task_status(db, task.id, TaskStatus.RUNNING, progress=0)
|
||||
|
||||
# 执行扫描
|
||||
results = ScanService.ping_network(network.cidr)
|
||||
|
||||
# 更新IP状态
|
||||
online_count, offline_count = ScanService.update_ip_status_from_scan_results(db, results)
|
||||
|
||||
# 刷新网段统计字段(used_ips/reserved_ips),保证网段列表利用率能即时反映
|
||||
NetworkService.get_stats(db, network_id)
|
||||
|
||||
# 更新任务状态
|
||||
ScanService.update_task_status(
|
||||
db,
|
||||
task.id,
|
||||
TaskStatus.COMPLETED,
|
||||
progress=100,
|
||||
total_count=len(results),
|
||||
success_count=online_count,
|
||||
failed_count=offline_count
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
ScanService.update_task_status(
|
||||
db,
|
||||
task.id,
|
||||
TaskStatus.FAILED,
|
||||
error_message=str(e)
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=f"扫描失败: {str(e)}")
|
||||
|
||||
# 重新获取任务状态
|
||||
task = ScanService.get_task_by_id(db, task.id)
|
||||
return task
|
||||
|
||||
|
||||
@router.post("/ping/single/{ip_address}", response_model=ScanResult, summary="单个IP Ping测试")
|
||||
def ping_single_ip(ip_address: str):
|
||||
"""对单个IP执行Ping测试"""
|
||||
result = ScanService.ping_single_ip(ip_address)
|
||||
return {
|
||||
"ip_address": result["ip_address"],
|
||||
"status": result["status"],
|
||||
"mac_address": None,
|
||||
"hostname": None,
|
||||
"response_time": result.get("response_time")
|
||||
}
|
||||
|
||||
|
||||
@router.post("/refresh/stats", summary="刷新所有网段统计")
|
||||
def refresh_all_stats(db: Session = Depends(get_db)):
|
||||
"""刷新所有网段的统计数据"""
|
||||
stats = NetworkService.get_all_stats(db)
|
||||
return {
|
||||
"message": "统计更新完成",
|
||||
"networks_updated": len(stats),
|
||||
"stats": stats
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user