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,19 @@
|
||||
# 数据库配置
|
||||
MYSQL_ROOT_PASSWORD=ipam2024
|
||||
MYSQL_DATABASE=ipam
|
||||
MYSQL_USER=ipam
|
||||
MYSQL_PASSWORD=ipam2024
|
||||
|
||||
# 应用配置
|
||||
DATABASE_URL=mysql+pymysql://ipam:***@localhost:3306/ipam
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
CELERY_BROKER_URL=redis://localhost:6379/1
|
||||
CELERY_RESULT_BACKEND=redis://localhost:6379/2
|
||||
|
||||
# JWT配置
|
||||
SECRET_KEY=your-super-secret-key-here-change-in-production
|
||||
|
||||
# 扫描配置
|
||||
PING_TIMEOUT=2
|
||||
PING_RETRIES=2
|
||||
SCAN_CONCURRENCY=50
|
||||
@@ -0,0 +1,28 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
libffi-dev \
|
||||
libssl-dev \
|
||||
iputils-ping \
|
||||
net-tools \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装Python依赖
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制应用代码
|
||||
COPY . .
|
||||
|
||||
# 创建启动脚本
|
||||
RUN echo '#!/bin/bash\n\
|
||||
echo "Waiting for MySQL..."\n\
|
||||
sleep 10\n\
|
||||
echo "Starting FastAPI server..."\n\
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload' > /app/start.sh && chmod +x /app/start.sh
|
||||
|
||||
CMD ["/app/start.sh"]
|
||||
@@ -0,0 +1 @@
|
||||
# app/__init__.py
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# app/core/__init__.py
|
||||
from app.core.config import settings
|
||||
from app.core.database import Base, engine, get_db, SessionLocal
|
||||
|
||||
__all__ = ["settings", "Base", "engine", "get_db", "SessionLocal"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,35 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# 应用配置
|
||||
APP_NAME: str = "IPAM Management System"
|
||||
APP_VERSION: str = "0.1.0"
|
||||
DEBUG: bool = True
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL: str = "mysql+pymysql://ipam:***@localhost:3308/ipam"
|
||||
|
||||
# Redis配置
|
||||
REDIS_URL: str = "redis://localhost:6379/0"
|
||||
|
||||
# Celery配置
|
||||
CELERY_BROKER_URL: str = "redis://localhost:6379/1"
|
||||
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/2"
|
||||
|
||||
# JWT配置
|
||||
SECRET_KEY: str = "your-super-secret-key-here-change-in-production"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7
|
||||
|
||||
# 扫描配置
|
||||
PING_TIMEOUT: int = 2
|
||||
PING_RETRIES: int = 2
|
||||
SCAN_CONCURRENCY: int = 50
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,31 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.core.config import settings
|
||||
|
||||
# 创建数据库引擎
|
||||
engine = create_engine(
|
||||
settings.DATABASE_URL,
|
||||
pool_pre_ping=True,
|
||||
pool_recycle=3600,
|
||||
pool_size=10,
|
||||
max_overflow=20,
|
||||
echo=settings.DEBUG
|
||||
)
|
||||
|
||||
# 创建会话工厂
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# 创建基础模型类
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db():
|
||||
"""
|
||||
获取数据库会话的依赖项
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
@@ -0,0 +1,158 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.models.auth import User, UserStatus
|
||||
|
||||
# 密码加密上下文
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
# OAuth2 认证方案
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth/login")
|
||||
|
||||
|
||||
class SecurityUtils:
|
||||
"""安全工具类"""
|
||||
|
||||
@staticmethod
|
||||
def hash_password(password: str) -> str:
|
||||
"""密码哈希加密"""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
@staticmethod
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""验证密码"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
@staticmethod
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""创建访问令牌"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
|
||||
to_encode.update({"exp": expire, "type": "access"})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
@staticmethod
|
||||
def create_refresh_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""创建刷新令牌"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(days=7) # 刷新令牌7天过期
|
||||
|
||||
to_encode.update({"exp": expire, "type": "refresh"})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
@staticmethod
|
||||
def verify_token(token: str, credentials_exception=None) -> Optional[dict]:
|
||||
"""验证令牌"""
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
token_type: str = payload.get("type")
|
||||
|
||||
if username is None:
|
||||
raise credentials_exception or HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的认证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return payload
|
||||
except JWTError:
|
||||
raise credentials_exception or HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的认证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
|
||||
# 权限校验依赖
|
||||
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User:
|
||||
"""获取当前登录用户"""
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无法验证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
payload = SecurityUtils.verify_token(token, credentials_exception)
|
||||
username: str = payload.get("sub")
|
||||
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
# 检查用户状态
|
||||
if user.status != UserStatus.ACTIVE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="用户已被禁用或锁定"
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
|
||||
"""获取当前活跃用户(已废弃,保留兼容)"""
|
||||
return current_user
|
||||
|
||||
|
||||
class PermissionChecker:
|
||||
"""权限检查器"""
|
||||
|
||||
def __init__(self, required_permission: str):
|
||||
self.required_permission = required_permission
|
||||
|
||||
def __call__(self, current_user: User = Depends(get_current_user)) -> User:
|
||||
"""检查用户是否拥有指定权限"""
|
||||
# 超级管理员拥有所有权限
|
||||
if current_user.role == "super_admin":
|
||||
return current_user
|
||||
|
||||
# 基于角色的权限检查
|
||||
role_permissions = {
|
||||
"admin": {
|
||||
"network", "ip", "scan", "snmp", "alert", "report"
|
||||
},
|
||||
"operator": {
|
||||
"network:view", "ip:view", "ip:edit", "scan:run", "alert:view"
|
||||
},
|
||||
"viewer": {
|
||||
"network:view", "ip:view", "alert:view"
|
||||
}
|
||||
}
|
||||
|
||||
user_permissions = role_permissions.get(current_user.role, set())
|
||||
|
||||
# 检查是否有通配符权限(如 "network" 包含 "network:create")
|
||||
permission_category = self.required_permission.split(":")[0]
|
||||
if (
|
||||
self.required_permission not in user_permissions and
|
||||
permission_category not in user_permissions and
|
||||
"*" not in user_permissions
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"缺少权限: {self.required_permission}"
|
||||
)
|
||||
|
||||
return current_user
|
||||
|
||||
|
||||
# 快捷权限检查依赖
|
||||
def require_permission(permission: str):
|
||||
"""权限检查依赖函数"""
|
||||
return PermissionChecker(permission)
|
||||
@@ -0,0 +1,71 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import engine, Base, SessionLocal
|
||||
from app.api.v1 import api_router
|
||||
|
||||
# 导入所有模型确保创建表
|
||||
from app.models.network import Network, IPAddress, ScanTask
|
||||
from app.models.snmp import SNMPCredential, NetworkDevice, ARPEntry, MACAddressTable, DeviceInterface
|
||||
from app.models.alert import Alert, AlertRule, WhitelistedMAC
|
||||
from app.models.auth import User, UserRole, UserStatus, Permission, RolePermission, RefreshToken
|
||||
from app.models.audit import AuditLog
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理"""
|
||||
# 启动时创建数据库表
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# 初始化默认管理员账户 + 默认权限
|
||||
from app.services.user_service import UserService
|
||||
db = SessionLocal()
|
||||
try:
|
||||
UserService.init_default_admin(db)
|
||||
UserService.init_default_permissions(db)
|
||||
print("✅ 系统初始化完成,默认管理员账户: admin / admin123")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
yield
|
||||
# 关闭时清理
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.APP_VERSION,
|
||||
description="企业级IP地址管理系统",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# CORS 配置
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 注册 API 路由
|
||||
app.include_router(api_router)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根路径"""
|
||||
return {
|
||||
"name": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION,
|
||||
"docs": "/docs",
|
||||
"redoc": "/redoc"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查"""
|
||||
return {"status": "healthy"}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,120 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum, Boolean, Float
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.core.database import Base
|
||||
import enum
|
||||
|
||||
|
||||
class AlertType(str, enum.Enum):
|
||||
"""告警类型"""
|
||||
IP_CONFLICT = "ip_conflict" # IP 冲突
|
||||
UNAUTHORIZED_ACCESS = "unauthorized_access" # 未授权接入
|
||||
SUBNET_FULL = "subnet_full" # 网段耗尽
|
||||
SCAN_FAILED = "scan_failed" # 扫描失败
|
||||
DEVICE_OFFLINE = "device_offline" # 设备离线
|
||||
NEW_DEVICE_DETECTED = "new_device_detected" # 新设备发现
|
||||
MAC_CHANGED = "mac_changed" # MAC 地址变化
|
||||
|
||||
|
||||
class AlertSeverity(str, enum.Enum):
|
||||
"""告警级别"""
|
||||
INFO = "info" # 信息
|
||||
WARNING = "warning" # 警告
|
||||
ERROR = "error" # 错误
|
||||
CRITICAL = "critical" # 严重
|
||||
|
||||
|
||||
class AlertStatus(str, enum.Enum):
|
||||
"""告警状态"""
|
||||
ACTIVE = "active" # 活跃
|
||||
ACKNOWLEDGED = "acknowledged" # 已确认
|
||||
RESOLVED = "resolved" # 已解决
|
||||
IGNORED = "ignored" # 已忽略
|
||||
|
||||
|
||||
class Alert(Base):
|
||||
"""告警"""
|
||||
__tablename__ = "alerts"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
alert_type = Column(Enum(AlertType), nullable=False, index=True)
|
||||
severity = Column(Enum(AlertSeverity), default=AlertSeverity.WARNING, index=True)
|
||||
status = Column(Enum(AlertStatus), default=AlertStatus.ACTIVE, index=True)
|
||||
|
||||
title = Column(String(255), nullable=False)
|
||||
message = Column(Text)
|
||||
|
||||
# 关联对象
|
||||
network_id = Column(Integer, ForeignKey("networks.id"), nullable=True)
|
||||
ip_address_id = Column(Integer, ForeignKey("ip_addresses.id"), nullable=True)
|
||||
device_id = Column(Integer, ForeignKey("network_devices.id"), nullable=True)
|
||||
ip_address_str = Column(String(50), nullable=True) # 冗余存储便于查询
|
||||
|
||||
# 关联的 IP/MAC 信息
|
||||
mac_address = Column(String(50), nullable=True)
|
||||
conflicting_mac = Column(String(50), nullable=True) # 用于 IP 冲突告警
|
||||
|
||||
# 统计信息
|
||||
usage_percent = Column(Float, nullable=True) # 网段使用率告警时存储
|
||||
|
||||
# 处理信息
|
||||
acknowledged_by = Column(String(100), nullable=True)
|
||||
acknowledged_at = Column(DateTime(timezone=True), nullable=True)
|
||||
resolved_by = Column(String(100), nullable=True)
|
||||
resolved_at = Column(DateTime(timezone=True), nullable=True)
|
||||
resolution_notes = Column(Text, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
network = relationship("Network")
|
||||
ip_address = relationship("IPAddress")
|
||||
device = relationship("NetworkDevice")
|
||||
|
||||
|
||||
class AlertRule(Base):
|
||||
"""告警规则配置"""
|
||||
__tablename__ = "alert_rules"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), unique=True, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
alert_type = Column(Enum(AlertType), nullable=False)
|
||||
severity = Column(Enum(AlertSeverity), default=AlertSeverity.WARNING)
|
||||
|
||||
# 规则条件
|
||||
enabled = Column(Boolean, default=True)
|
||||
|
||||
# 阈值配置
|
||||
subnet_usage_threshold = Column(Float, default=90.0) # 网段使用率阈值 %
|
||||
scan_failure_threshold = Column(Integer, default=3) # 扫描失败次数阈值
|
||||
inactivity_threshold_minutes = Column(Integer, default=60) # 设备不活动阈值(分钟)
|
||||
|
||||
# MAC 白名单(用于未授权接入检测)
|
||||
mac_whitelist_enabled = Column(Boolean, default=False)
|
||||
mac_whitelist = Column(Text, nullable=True) # JSON 格式的 MAC 列表
|
||||
|
||||
# 通知配置
|
||||
notify_email = Column(Boolean, default=False)
|
||||
notify_webhook = Column(Boolean, default=False)
|
||||
notification_channels = Column(Text, nullable=True) # JSON 格式的通知渠道
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
|
||||
class WhitelistedMAC(Base):
|
||||
"""MAC 地址白名单"""
|
||||
__tablename__ = "whitelisted_macs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
mac_address = Column(String(50), unique=True, nullable=False, index=True)
|
||||
description = Column(String(255), nullable=True)
|
||||
owner = Column(String(100), nullable=True)
|
||||
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
@@ -0,0 +1,37 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Index
|
||||
from sqlalchemy.sql import func
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class AuditLog(Base):
|
||||
"""审计日志:记录用户的关键操作"""
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# 操作人(nullable:登录失败、token 失效等场景可能没有 user_id)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
username = Column(String(50), index=True, comment="冗余存储用户名,便于 user 被删除后仍能查看")
|
||||
|
||||
# 操作内容
|
||||
action = Column(String(50), nullable=False, index=True, comment="操作类型,如:network.create")
|
||||
resource_type = Column(String(50), nullable=True, index=True, comment="资源类型:network / ip / snmp / user / scan")
|
||||
resource_id = Column(String(50), nullable=True, comment="资源 ID(字符串以便兼容多种类型)")
|
||||
resource_name = Column(String(200), nullable=True, comment="资源名称/标识,便于阅读")
|
||||
|
||||
# 请求信息
|
||||
method = Column(String(10), comment="HTTP 方法")
|
||||
path = Column(String(500), comment="请求路径")
|
||||
ip_address = Column(String(50), comment="客户端 IP")
|
||||
user_agent = Column(String(500), comment="User-Agent")
|
||||
|
||||
# 状态
|
||||
status = Column(String(20), default="success", index=True, comment="success / failed")
|
||||
detail = Column(Text, comment="变更详情 / 错误信息 / JSON diff")
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
||||
|
||||
|
||||
# 复合索引:按时间+用户查、按时间+资源查更高效
|
||||
Index("idx_audit_logs_user_created", AuditLog.user_id, AuditLog.created_at.desc())
|
||||
Index("idx_audit_logs_resource", AuditLog.resource_type, AuditLog.resource_id, AuditLog.created_at.desc())
|
||||
@@ -0,0 +1,97 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.core.database import Base
|
||||
import enum
|
||||
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
"""用户角色枚举"""
|
||||
SUPER_ADMIN = "super_admin" # 超级管理员 - 所有权限
|
||||
ADMIN = "admin" # 管理员 - 除用户管理外的所有权限
|
||||
OPERATOR = "operator" # 操作员 - 可查看和操作,不能配置
|
||||
VIEWER = "viewer" # 只读用户 - 只能查看
|
||||
|
||||
|
||||
class UserStatus(str, enum.Enum):
|
||||
"""用户状态"""
|
||||
ACTIVE = "active" # 正常
|
||||
INACTIVE = "inactive" # 禁用
|
||||
LOCKED = "locked" # 锁定(登录失败过多)
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""用户表"""
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, index=True, nullable=False)
|
||||
email = Column(String(100), unique=True, index=True)
|
||||
phone = Column(String(20), index=True)
|
||||
real_name = Column(String(50), comment="真实姓名")
|
||||
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
|
||||
role = Column(Enum(UserRole), default=UserRole.VIEWER, nullable=False)
|
||||
status = Column(Enum(UserStatus), default=UserStatus.ACTIVE, nullable=False)
|
||||
|
||||
# 登录信息
|
||||
last_login_at = Column(DateTime(timezone=True))
|
||||
last_login_ip = Column(String(50))
|
||||
login_failed_count = Column(Integer, default=0)
|
||||
|
||||
# 通知配置
|
||||
email_notification = Column(Boolean, default=True)
|
||||
wechat_notification = Column(Boolean, default=False)
|
||||
dingtalk_notification = Column(Boolean, default=False)
|
||||
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
creator = relationship("User", remote_side=[id])
|
||||
|
||||
|
||||
class Permission(Base):
|
||||
"""权限项"""
|
||||
__tablename__ = "permissions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
code = Column(String(100), unique=True, nullable=False, comment="权限代码,如:network:create")
|
||||
name = Column(String(100), nullable=False, comment="权限名称")
|
||||
description = Column(Text)
|
||||
category = Column(String(50), comment="权限分类:network, ip, scan, snmp, alert, system")
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class RolePermission(Base):
|
||||
"""角色权限关联表"""
|
||||
__tablename__ = "role_permissions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
role = Column(Enum(UserRole), nullable=False)
|
||||
permission_id = Column(Integer, ForeignKey("permissions.id"), nullable=False)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class RefreshToken(Base):
|
||||
"""刷新令牌"""
|
||||
__tablename__ = "refresh_tokens"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
token = Column(String(500), unique=True, nullable=False)
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False)
|
||||
ip_address = Column(String(50))
|
||||
user_agent = Column(String(500))
|
||||
|
||||
revoked = Column(Boolean, default=False)
|
||||
revoked_at = Column(DateTime(timezone=True))
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# 关系
|
||||
user = relationship("User")
|
||||
@@ -0,0 +1,119 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, Enum, ForeignKey, JSON, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.core.database import Base
|
||||
import enum
|
||||
|
||||
|
||||
class NetworkGroup(str, enum.Enum):
|
||||
"""网段分组枚举"""
|
||||
PRODUCTION = "production"
|
||||
OFFICE = "office"
|
||||
TEST = "test"
|
||||
MANAGEMENT = "management"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class IPStatus(str, enum.Enum):
|
||||
"""IP状态枚举"""
|
||||
AVAILABLE = "available"
|
||||
ONLINE = "online"
|
||||
OFFLINE = "offline"
|
||||
RESERVED = "reserved"
|
||||
|
||||
|
||||
class TaskStatus(str, enum.Enum):
|
||||
"""任务状态枚举"""
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class TaskType(str, enum.Enum):
|
||||
"""任务类型枚举"""
|
||||
PING = "ping"
|
||||
ARP = "arp"
|
||||
SNMP = "snmp"
|
||||
|
||||
|
||||
class Network(Base):
|
||||
"""网段模型"""
|
||||
__tablename__ = "networks"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
cidr = Column(String(50), unique=True, index=True, nullable=False) # 如: 192.168.1.0/24
|
||||
name = Column(String(100), index=True)
|
||||
description = Column(Text, nullable=True)
|
||||
group_name = Column(String(100), index=True, nullable=True) # 分组名称
|
||||
gateway = Column(String(50), nullable=True)
|
||||
vlan_id = Column(Integer, nullable=True)
|
||||
total_ips = Column(Integer, default=0)
|
||||
used_ips = Column(Integer, default=0)
|
||||
reserved_ips = Column(Integer, default=0)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
ip_addresses = relationship("IPAddress", back_populates="network", cascade="all, delete-orphan")
|
||||
scan_tasks = relationship("ScanTask", back_populates="network")
|
||||
|
||||
|
||||
class IPAddress(Base):
|
||||
"""IP地址模型"""
|
||||
__tablename__ = "ip_addresses"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
network_id = Column(Integer, ForeignKey("networks.id"), nullable=False)
|
||||
ip_address = Column(String(50), unique=True, index=True, nullable=False)
|
||||
mac_address = Column(String(50), nullable=True, index=True)
|
||||
hostname = Column(String(255), nullable=True)
|
||||
status = Column(Enum(IPStatus), default=IPStatus.AVAILABLE)
|
||||
|
||||
# 业务属性
|
||||
owner = Column(String(100), nullable=True)
|
||||
business_type = Column(String(50), nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
custom_fields = Column(JSON, default=dict)
|
||||
|
||||
# 扫描信息
|
||||
last_seen = Column(DateTime(timezone=True), nullable=True)
|
||||
first_seen = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# 物理位置信息
|
||||
switch_name = Column(String(100), nullable=True)
|
||||
switch_port = Column(String(50), nullable=True)
|
||||
vendor = Column(String(100), nullable=True) # MAC OUI 解析的厂商
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
network = relationship("Network", back_populates="ip_addresses")
|
||||
|
||||
|
||||
class ScanTask(Base):
|
||||
"""扫描任务模型"""
|
||||
__tablename__ = "scan_tasks"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
network_id = Column(Integer, ForeignKey("networks.id"), nullable=True)
|
||||
task_type = Column(Enum(TaskType), nullable=False)
|
||||
status = Column(Enum(TaskStatus), default=TaskStatus.PENDING)
|
||||
progress = Column(Integer, default=0) # 0-100
|
||||
celery_task_id = Column(String(100), nullable=True)
|
||||
|
||||
# 统计信息
|
||||
total_count = Column(Integer, default=0)
|
||||
success_count = Column(Integer, default=0)
|
||||
failed_count = Column(Integer, default=0)
|
||||
|
||||
started_at = Column(DateTime(timezone=True), nullable=True)
|
||||
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
error_message = Column(Text, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# 关系
|
||||
network = relationship("Network", back_populates="scan_tasks")
|
||||
@@ -0,0 +1,190 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.core.database import Base
|
||||
import enum
|
||||
|
||||
|
||||
class SNMPVersion(str, enum.Enum):
|
||||
"""SNMP 版本"""
|
||||
V2C = "v2c"
|
||||
V3 = "v3"
|
||||
|
||||
|
||||
class SNMPAuthProtocol(str, enum.Enum):
|
||||
"""SNMP V3 认证协议"""
|
||||
MD5 = "md5"
|
||||
SHA = "sha"
|
||||
SHA256 = "sha256"
|
||||
|
||||
|
||||
class SNMPPrivProtocol(str, enum.Enum):
|
||||
"""SNMP V3 加密协议"""
|
||||
DES = "des"
|
||||
AES = "aes"
|
||||
AES256 = "aes256"
|
||||
|
||||
|
||||
class DeviceType(str, enum.Enum):
|
||||
"""设备类型"""
|
||||
ROUTER = "router"
|
||||
CORE_SWITCH = "core_switch"
|
||||
DISTRIBUTION_SWITCH = "distribution_switch"
|
||||
ACCESS_SWITCH = "access_switch"
|
||||
FIREWALL = "firewall"
|
||||
LOAD_BALANCER = "load_balancer"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class SNMPCredential(Base):
|
||||
"""SNMP 凭据"""
|
||||
__tablename__ = "snmp_credentials"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), unique=True, index=True, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
# 版本
|
||||
version = Column(Enum(SNMPVersion), default=SNMPVersion.V2C, nullable=False)
|
||||
|
||||
# V2C 配置
|
||||
community_string = Column(String(255), nullable=True)
|
||||
|
||||
# V3 配置
|
||||
username = Column(String(100), nullable=True)
|
||||
auth_password = Column(String(255), nullable=True)
|
||||
auth_protocol = Column(Enum(SNMPAuthProtocol), nullable=True)
|
||||
priv_password = Column(String(255), nullable=True)
|
||||
priv_protocol = Column(Enum(SNMPPrivProtocol), nullable=True)
|
||||
|
||||
# 超时和重试
|
||||
timeout = Column(Integer, default=3)
|
||||
retries = Column(Integer, default=2)
|
||||
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
devices = relationship("NetworkDevice", back_populates="snmp_credential")
|
||||
|
||||
|
||||
class NetworkDevice(Base):
|
||||
"""网络设备"""
|
||||
__tablename__ = "network_devices"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), unique=True, index=True, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
# 连接信息
|
||||
ip_address = Column(String(50), index=True, nullable=False)
|
||||
port = Column(Integer, default=161)
|
||||
|
||||
# 设备信息
|
||||
device_type = Column(Enum(DeviceType), default=DeviceType.OTHER)
|
||||
vendor = Column(String(100), nullable=True)
|
||||
model = Column(String(100), nullable=True)
|
||||
firmware_version = Column(String(100), nullable=True)
|
||||
serial_number = Column(String(100), nullable=True)
|
||||
location = Column(String(200), nullable=True)
|
||||
|
||||
# SNMP 凭据
|
||||
snmp_credential_id = Column(Integer, ForeignKey("snmp_credentials.id"), nullable=True)
|
||||
|
||||
# 状态
|
||||
is_active = Column(Boolean, default=True)
|
||||
last_polled_at = Column(DateTime(timezone=True), nullable=True)
|
||||
last_successful_poll = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# 采集配置
|
||||
arp_poll_interval = Column(Integer, default=300) # ARP表采集间隔(秒)
|
||||
mac_poll_interval = Column(Integer, default=300) # MAC表采集间隔(秒)
|
||||
interface_poll_interval = Column(Integer, default=300) # 接口信息采集间隔
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
snmp_credential = relationship("SNMPCredential", back_populates="devices")
|
||||
arp_entries = relationship("ARPEntry", back_populates="device", cascade="all, delete-orphan")
|
||||
mac_entries = relationship("MACAddressTable", back_populates="device", cascade="all, delete-orphan")
|
||||
interfaces = relationship("DeviceInterface", back_populates="device", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class ARPEntry(Base):
|
||||
"""ARP 表条目 (IP -> MAC 映射)"""
|
||||
__tablename__ = "arp_entries"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
device_id = Column(Integer, ForeignKey("network_devices.id"), nullable=False)
|
||||
|
||||
ip_address = Column(String(50), index=True, nullable=False)
|
||||
mac_address = Column(String(50), index=True, nullable=False)
|
||||
interface = Column(String(100), nullable=True) # 接口名称/索引
|
||||
vlan_id = Column(Integer, nullable=True)
|
||||
|
||||
# 老化信息
|
||||
age = Column(Integer, nullable=True) # 秒
|
||||
is_static = Column(Boolean, default=False)
|
||||
|
||||
discovered_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
last_seen = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# 关系
|
||||
device = relationship("NetworkDevice", back_populates="arp_entries")
|
||||
|
||||
|
||||
class MACAddressTable(Base):
|
||||
"""MAC 地址表 (MAC -> 端口 映射)"""
|
||||
__tablename__ = "mac_address_table"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
device_id = Column(Integer, ForeignKey("network_devices.id"), nullable=False)
|
||||
|
||||
mac_address = Column(String(50), index=True, nullable=False)
|
||||
vlan_id = Column(Integer, nullable=True)
|
||||
interface = Column(String(100), nullable=True) # 接口名称
|
||||
port_number = Column(Integer, nullable=True) # 端口号
|
||||
|
||||
# 类型: learned, static, management, etc.
|
||||
entry_type = Column(String(50), nullable=True)
|
||||
|
||||
discovered_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
last_seen = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# 关系
|
||||
device = relationship("NetworkDevice", back_populates="mac_entries")
|
||||
|
||||
|
||||
class DeviceInterface(Base):
|
||||
"""设备接口信息"""
|
||||
__tablename__ = "device_interfaces"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
device_id = Column(Integer, ForeignKey("network_devices.id"), nullable=False)
|
||||
|
||||
if_index = Column(Integer, nullable=False) # ifIndex
|
||||
if_name = Column(String(100), nullable=True) # ifName
|
||||
if_descr = Column(String(255), nullable=True) # ifDescr
|
||||
if_type = Column(String(50), nullable=True) # ifType
|
||||
if_mtu = Column(Integer, nullable=True) # ifMtu
|
||||
if_speed = Column(Integer, nullable=True) # ifSpeed (bps)
|
||||
if_phys_address = Column(String(50), nullable=True) # ifPhysAddress (MAC)
|
||||
if_admin_status = Column(String(50), nullable=True) # up/down/testing
|
||||
if_oper_status = Column(String(50), nullable=True) # up/down/testing
|
||||
|
||||
# IP 地址
|
||||
ip_address = Column(String(50), nullable=True)
|
||||
ip_netmask = Column(String(50), nullable=True)
|
||||
|
||||
# 统计
|
||||
if_in_octets = Column(Integer, nullable=True)
|
||||
if_out_octets = Column(Integer, nullable=True)
|
||||
if_in_errors = Column(Integer, nullable=True)
|
||||
if_out_errors = Column(Integer, nullable=True)
|
||||
|
||||
last_polled_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# 关系
|
||||
device = relationship("NetworkDevice", back_populates="interfaces")
|
||||
Binary file not shown.
@@ -0,0 +1,173 @@
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
import ipaddress
|
||||
from app.models.network import IPStatus, TaskStatus, TaskType
|
||||
|
||||
|
||||
# ========== 网段相关 Schemas ==========
|
||||
|
||||
class NetworkBase(BaseModel):
|
||||
cidr: str = Field(..., description="网段CIDR,如: 192.168.1.0/24")
|
||||
name: Optional[str] = Field(None, max_length=100, description="网段名称")
|
||||
description: Optional[str] = Field(None, description="描述")
|
||||
group_name: Optional[str] = Field(None, max_length=100, description="分组名称")
|
||||
gateway: Optional[str] = Field(None, max_length=50, description="网关地址")
|
||||
vlan_id: Optional[int] = Field(None, description="VLAN ID")
|
||||
|
||||
@field_validator('cidr')
|
||||
@classmethod
|
||||
def validate_cidr(cls, v):
|
||||
try:
|
||||
network = ipaddress.ip_network(v, strict=False)
|
||||
return str(network)
|
||||
except ValueError:
|
||||
raise ValueError(f"无效的CIDR格式: {v}")
|
||||
|
||||
@field_validator('gateway')
|
||||
@classmethod
|
||||
def validate_gateway(cls, v):
|
||||
if v:
|
||||
try:
|
||||
ipaddress.ip_address(v)
|
||||
except ValueError:
|
||||
raise ValueError(f"无效的IP地址: {v}")
|
||||
return v
|
||||
|
||||
|
||||
class NetworkCreate(NetworkBase):
|
||||
pass
|
||||
|
||||
|
||||
class NetworkUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
group_name: Optional[str] = None
|
||||
gateway: Optional[str] = None
|
||||
vlan_id: Optional[int] = None
|
||||
|
||||
|
||||
class Network(NetworkBase):
|
||||
id: int
|
||||
total_ips: int
|
||||
used_ips: int
|
||||
reserved_ips: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class NetworkStats(BaseModel):
|
||||
"""网段统计信息"""
|
||||
id: int
|
||||
cidr: str
|
||||
name: Optional[str]
|
||||
total_ips: int
|
||||
used_ips: int
|
||||
reserved_ips: int
|
||||
available_ips: int
|
||||
usage_percent: float
|
||||
group_name: Optional[str]
|
||||
vlan_id: Optional[int]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class NetworkListResponse(BaseModel):
|
||||
total: int
|
||||
items: List[Network]
|
||||
|
||||
|
||||
# ========== IP地址相关 Schemas ==========
|
||||
|
||||
class IPAddressBase(BaseModel):
|
||||
ip_address: str
|
||||
mac_address: Optional[str] = None
|
||||
hostname: Optional[str] = None
|
||||
owner: Optional[str] = None
|
||||
business_type: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
custom_fields: Dict[str, Any] = Field(default_factory=dict)
|
||||
switch_name: Optional[str] = None
|
||||
switch_port: Optional[str] = None
|
||||
vendor: Optional[str] = None
|
||||
|
||||
|
||||
class IPAddressCreate(IPAddressBase):
|
||||
network_id: int
|
||||
status: IPStatus = IPStatus.AVAILABLE
|
||||
|
||||
|
||||
class IPAddressUpdate(BaseModel):
|
||||
mac_address: Optional[str] = None
|
||||
hostname: Optional[str] = None
|
||||
status: Optional[IPStatus] = None
|
||||
owner: Optional[str] = None
|
||||
business_type: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
custom_fields: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class IPAddress(IPAddressBase):
|
||||
id: int
|
||||
network_id: int
|
||||
status: IPStatus
|
||||
last_seen: Optional[datetime]
|
||||
first_seen: datetime
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class IPAddressListResponse(BaseModel):
|
||||
total: int
|
||||
items: List[IPAddress]
|
||||
|
||||
|
||||
# ========== 扫描任务相关 Schemas ==========
|
||||
|
||||
class ScanTaskBase(BaseModel):
|
||||
task_type: TaskType
|
||||
network_id: Optional[int] = None
|
||||
|
||||
|
||||
class ScanTaskCreate(ScanTaskBase):
|
||||
pass
|
||||
|
||||
|
||||
class ScanTask(BaseModel):
|
||||
id: int
|
||||
network_id: Optional[int]
|
||||
task_type: TaskType
|
||||
status: TaskStatus
|
||||
progress: int
|
||||
celery_task_id: Optional[str]
|
||||
total_count: int
|
||||
success_count: int
|
||||
failed_count: int
|
||||
started_at: Optional[datetime]
|
||||
completed_at: Optional[datetime]
|
||||
error_message: Optional[str]
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ScanTaskListResponse(BaseModel):
|
||||
total: int
|
||||
items: List[ScanTask]
|
||||
|
||||
|
||||
class ScanResult(BaseModel):
|
||||
"""单次扫描结果"""
|
||||
ip_address: str
|
||||
status: str
|
||||
mac_address: Optional[str] = None
|
||||
hostname: Optional[str] = None
|
||||
response_time: Optional[float] = None
|
||||
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,496 @@
|
||||
from typing import List, Dict, Any, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import json
|
||||
|
||||
from app.models.alert import (
|
||||
Alert, AlertRule, WhitelistedMAC,
|
||||
AlertType, AlertSeverity, AlertStatus
|
||||
)
|
||||
from app.models.network import Network, IPAddress
|
||||
from app.models.snmp import NetworkDevice, ARPEntry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AlertService:
|
||||
"""告警检测与管理服务"""
|
||||
|
||||
@staticmethod
|
||||
def create_alert(db: Session,
|
||||
alert_type: AlertType,
|
||||
title: str,
|
||||
message: str = "",
|
||||
severity: AlertSeverity = AlertSeverity.WARNING,
|
||||
network_id: Optional[int] = None,
|
||||
ip_address_id: Optional[int] = None,
|
||||
device_id: Optional[int] = None,
|
||||
ip_address_str: Optional[str] = None,
|
||||
mac_address: Optional[str] = None,
|
||||
conflicting_mac: Optional[str] = None,
|
||||
usage_percent: Optional[float] = None) -> Alert:
|
||||
"""创建告警"""
|
||||
# 检查是否已有相同的活跃告警,避免重复告警
|
||||
existing = db.query(Alert).filter(
|
||||
Alert.alert_type == alert_type,
|
||||
Alert.status == AlertStatus.ACTIVE
|
||||
)
|
||||
|
||||
if ip_address_str:
|
||||
existing = existing.filter(Alert.ip_address_str == ip_address_str)
|
||||
if network_id:
|
||||
existing = existing.filter(Alert.network_id == network_id)
|
||||
if device_id:
|
||||
existing = existing.filter(Alert.device_id == device_id)
|
||||
|
||||
existing = existing.first()
|
||||
|
||||
if existing:
|
||||
logger.debug(f"已有相同的活跃告警: {alert_type} - {ip_address_str or network_id or device_id}")
|
||||
return existing
|
||||
|
||||
alert = Alert(
|
||||
alert_type=alert_type,
|
||||
title=title,
|
||||
message=message,
|
||||
severity=severity,
|
||||
network_id=network_id,
|
||||
ip_address_id=ip_address_id,
|
||||
device_id=device_id,
|
||||
ip_address_str=ip_address_str,
|
||||
mac_address=mac_address,
|
||||
conflicting_mac=conflicting_mac,
|
||||
usage_percent=usage_percent
|
||||
)
|
||||
|
||||
db.add(alert)
|
||||
db.commit()
|
||||
db.refresh(alert)
|
||||
|
||||
logger.info(f"创建告警: {alert_type} - {title}")
|
||||
return alert
|
||||
|
||||
@staticmethod
|
||||
def detect_ip_conflicts(db: Session) -> List[Dict[str, Any]]:
|
||||
"""检测 IP 冲突"""
|
||||
conflicts = []
|
||||
|
||||
# 检查 ARP 表中同一个 IP 对应多个不同 MAC 的情况
|
||||
subquery = db.query(
|
||||
ARPEntry.ip_address
|
||||
).group_by(ARPEntry.ip_address).having(
|
||||
db.func.count(db.func.distinct(ARPEntry.mac_address)) > 1
|
||||
).subquery()
|
||||
|
||||
conflict_entries = db.query(ARPEntry).filter(
|
||||
ARPEntry.ip_address.in_(subquery)
|
||||
).order_by(ARPEntry.ip_address).all()
|
||||
|
||||
# 按 IP 分组
|
||||
ip_macs = {}
|
||||
for entry in conflict_entries:
|
||||
if entry.ip_address not in ip_macs:
|
||||
ip_macs[entry.ip_address] = set()
|
||||
ip_macs[entry.ip_address].add(entry.mac_address)
|
||||
|
||||
# 为每个冲突创建告警
|
||||
for ip, macs in ip_macs.items():
|
||||
if len(macs) > 1:
|
||||
mac_list = list(macs)
|
||||
conflict_info = {
|
||||
'ip_address': ip,
|
||||
'mac_addresses': mac_list
|
||||
}
|
||||
conflicts.append(conflict_info)
|
||||
|
||||
# 创建告警
|
||||
AlertService.create_alert(
|
||||
db=db,
|
||||
alert_type=AlertType.IP_CONFLICT,
|
||||
title=f"IP 冲突检测: {ip}",
|
||||
message=f"IP 地址 {ip} 检测到多个 MAC 地址: {', '.join(mac_list)}",
|
||||
severity=AlertSeverity.ERROR,
|
||||
ip_address_str=ip,
|
||||
mac_address=mac_list[0],
|
||||
conflicting_mac=mac_list[1] if len(mac_list) > 1 else None
|
||||
)
|
||||
|
||||
return conflicts
|
||||
|
||||
@staticmethod
|
||||
def detect_unauthorized_access(db: Session) -> List[Dict[str, Any]]:
|
||||
"""检测未授权接入(新 MAC 地址不在白名单中)"""
|
||||
unauthorized = []
|
||||
|
||||
# 获取白名单 MAC
|
||||
whitelist = set()
|
||||
whitelist_entries = db.query(WhitelistedMAC).filter(
|
||||
WhitelistedMAC.is_active == True
|
||||
).all()
|
||||
for entry in whitelist_entries:
|
||||
whitelist.add(entry.mac_address.upper())
|
||||
|
||||
# 获取所有 ARP 表中的 MAC 地址
|
||||
arp_entries = db.query(ARPEntry).all()
|
||||
|
||||
for entry in arp_entries:
|
||||
if not entry.mac_address:
|
||||
continue
|
||||
|
||||
mac = entry.mac_address.upper()
|
||||
if mac and mac not in whitelist:
|
||||
# 检查是否是最近发现的(1小时内)
|
||||
is_recent = entry.discovered_at >= datetime.utcnow() - timedelta(hours=1)
|
||||
|
||||
if is_recent:
|
||||
unauth_info = {
|
||||
'ip_address': entry.ip_address,
|
||||
'mac_address': mac,
|
||||
'discovered_at': entry.discovered_at
|
||||
}
|
||||
unauthorized.append(unauth_info)
|
||||
|
||||
# 创建告警
|
||||
AlertService.create_alert(
|
||||
db=db,
|
||||
alert_type=AlertType.UNAUTHORIZED_ACCESS,
|
||||
title=f"未授权接入: {mac}",
|
||||
message=f"检测到未授权设备接入: IP {entry.ip_address}, MAC {mac}",
|
||||
severity=AlertSeverity.WARNING,
|
||||
ip_address_str=entry.ip_address,
|
||||
mac_address=mac
|
||||
)
|
||||
|
||||
return unauthorized
|
||||
|
||||
@staticmethod
|
||||
def detect_subnet_exhaustion(db: Session) -> List[Dict[str, Any]]:
|
||||
"""检测网段耗尽"""
|
||||
exhausted = []
|
||||
|
||||
# 获取所有网段的统计信息
|
||||
networks = db.query(Network).all()
|
||||
|
||||
for network in networks:
|
||||
if network.total_ips == 0:
|
||||
continue
|
||||
|
||||
# 计算使用中的 IP 数量(在线或已分配)
|
||||
used_count = db.query(IPAddress).filter(
|
||||
IPAddress.network_id == network.id
|
||||
).filter(
|
||||
(IPAddress.mac_address.isnot(None)) |
|
||||
(IPAddress.hostname.isnot(None))
|
||||
).count()
|
||||
|
||||
usage_percent = (used_count / network.total_ips) * 100
|
||||
|
||||
# 默认阈值 90%
|
||||
threshold = 90.0
|
||||
|
||||
if usage_percent >= threshold:
|
||||
exhaustion_info = {
|
||||
'network_id': network.id,
|
||||
'cidr': network.cidr,
|
||||
'name': network.name,
|
||||
'total_ips': network.total_ips,
|
||||
'used_ips': used_count,
|
||||
'usage_percent': round(usage_percent, 2)
|
||||
}
|
||||
exhausted.append(exhaustion_info)
|
||||
|
||||
# 创建告警
|
||||
AlertService.create_alert(
|
||||
db=db,
|
||||
alert_type=AlertType.SUBNET_FULL,
|
||||
title=f"网段使用率告警: {network.cidr}",
|
||||
message=f"网段 {network.cidr} ({network.name}) 使用率达到 {round(usage_percent, 2)}%,总IP {network.total_ips},已使用 {used_count}",
|
||||
severity=AlertSeverity.WARNING if usage_percent < 95 else AlertSeverity.CRITICAL,
|
||||
network_id=network.id,
|
||||
usage_percent=usage_percent
|
||||
)
|
||||
|
||||
return exhausted
|
||||
|
||||
@staticmethod
|
||||
def detect_new_devices(db: Session) -> List[Dict[str, Any]]:
|
||||
"""检测新发现的设备"""
|
||||
new_devices = []
|
||||
|
||||
# 查找最近 1 小时内发现的新 MAC 地址
|
||||
one_hour_ago = datetime.utcnow() - timedelta(hours=1)
|
||||
|
||||
recent_entries = db.query(ARPEntry).filter(
|
||||
ARPEntry.discovered_at >= one_hour_ago
|
||||
).all()
|
||||
|
||||
for entry in recent_entries:
|
||||
# 检查此 MAC 是否是新出现的
|
||||
older_entries = db.query(ARPEntry).filter(
|
||||
ARPEntry.mac_address == entry.mac_address,
|
||||
ARPEntry.discovered_at < one_hour_ago
|
||||
).count()
|
||||
|
||||
if older_entries == 0 and entry.mac_address:
|
||||
device_info = {
|
||||
'ip_address': entry.ip_address,
|
||||
'mac_address': entry.mac_address,
|
||||
'discovered_at': entry.discovered_at
|
||||
}
|
||||
new_devices.append(device_info)
|
||||
|
||||
# 创建告警
|
||||
AlertService.create_alert(
|
||||
db=db,
|
||||
alert_type=AlertType.NEW_DEVICE_DETECTED,
|
||||
title=f"新设备发现: {entry.ip_address}",
|
||||
message=f"检测到新设备: IP {entry.ip_address}, MAC {entry.mac_address}",
|
||||
severity=AlertSeverity.INFO,
|
||||
ip_address_str=entry.ip_address,
|
||||
mac_address=entry.mac_address
|
||||
)
|
||||
|
||||
return new_devices
|
||||
|
||||
@staticmethod
|
||||
def detect_device_offline(db: Session) -> List[Dict[str, Any]]:
|
||||
"""检测设备离线"""
|
||||
offline_devices = []
|
||||
|
||||
# 检查网络设备是否长时间没有成功轮询
|
||||
threshold_time = datetime.utcnow() - timedelta(hours=2)
|
||||
|
||||
offline_network_devices = db.query(NetworkDevice).filter(
|
||||
NetworkDevice.is_active == True,
|
||||
(NetworkDevice.last_successful_poll < threshold_time) |
|
||||
(NetworkDevice.last_successful_poll == None)
|
||||
).all()
|
||||
|
||||
for device in offline_network_devices:
|
||||
offline_info = {
|
||||
'device_id': device.id,
|
||||
'device_name': device.name,
|
||||
'ip_address': device.ip_address,
|
||||
'last_polled_at': device.last_polled_at,
|
||||
'last_successful_poll': device.last_successful_poll
|
||||
}
|
||||
offline_devices.append(offline_info)
|
||||
|
||||
# 创建告警
|
||||
AlertService.create_alert(
|
||||
db=db,
|
||||
alert_type=AlertType.DEVICE_OFFLINE,
|
||||
title=f"设备离线: {device.name}",
|
||||
message=f"网络设备 {device.name} ({device.ip_address}) 已超过 2 小时未成功轮询",
|
||||
severity=AlertSeverity.ERROR,
|
||||
device_id=device.id
|
||||
)
|
||||
|
||||
return offline_devices
|
||||
|
||||
@staticmethod
|
||||
def run_all_detections(db: Session) -> Dict[str, Any]:
|
||||
"""运行所有检测"""
|
||||
results = {}
|
||||
|
||||
logger.info("开始运行告警检测...")
|
||||
|
||||
# 检测 IP 冲突
|
||||
conflicts = AlertService.detect_ip_conflicts(db)
|
||||
results['ip_conflicts'] = {
|
||||
'count': len(conflicts),
|
||||
'items': conflicts
|
||||
}
|
||||
|
||||
# 检测未授权接入
|
||||
unauthorized = AlertService.detect_unauthorized_access(db)
|
||||
results['unauthorized_access'] = {
|
||||
'count': len(unauthorized),
|
||||
'items': unauthorized
|
||||
}
|
||||
|
||||
# 检测网段耗尽
|
||||
subnet_exhaustion = AlertService.detect_subnet_exhaustion(db)
|
||||
results['subnet_exhaustion'] = {
|
||||
'count': len(subnet_exhaustion),
|
||||
'items': subnet_exhaustion
|
||||
}
|
||||
|
||||
# 检测新设备
|
||||
new_devices = AlertService.detect_new_devices(db)
|
||||
results['new_devices'] = {
|
||||
'count': len(new_devices),
|
||||
'items': new_devices
|
||||
}
|
||||
|
||||
# 检测设备离线
|
||||
offline_devices = AlertService.detect_device_offline(db)
|
||||
results['offline_devices'] = {
|
||||
'count': len(offline_devices),
|
||||
'items': offline_devices
|
||||
}
|
||||
|
||||
total_alerts = sum(v['count'] for v in results.values())
|
||||
logger.info(f"告警检测完成,共发现 {total_alerts} 个问题")
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def get_alerts(db: Session,
|
||||
status: Optional[AlertStatus] = None,
|
||||
severity: Optional[AlertSeverity] = None,
|
||||
alert_type: Optional[AlertType] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100) -> tuple[int, List[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, items
|
||||
|
||||
@staticmethod
|
||||
def acknowledge_alert(db: Session, alert_id: int, acknowledged_by: str = "system") -> Optional[Alert]:
|
||||
"""确认告警"""
|
||||
alert = db.query(Alert).filter(Alert.id == alert_id).first()
|
||||
if not alert:
|
||||
return None
|
||||
|
||||
alert.status = AlertStatus.ACKNOWLEDGED
|
||||
alert.acknowledged_by = acknowledged_by
|
||||
alert.acknowledged_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(alert)
|
||||
|
||||
return alert
|
||||
|
||||
@staticmethod
|
||||
def resolve_alert(db: Session, alert_id: int, resolved_by: str = "system", notes: str = "") -> Optional[Alert]:
|
||||
"""解决告警"""
|
||||
alert = db.query(Alert).filter(Alert.id == alert_id).first()
|
||||
if not alert:
|
||||
return None
|
||||
|
||||
alert.status = AlertStatus.RESOLVED
|
||||
alert.resolved_by = resolved_by
|
||||
alert.resolved_at = datetime.utcnow()
|
||||
alert.resolution_notes = notes
|
||||
db.commit()
|
||||
db.refresh(alert)
|
||||
|
||||
return alert
|
||||
|
||||
@staticmethod
|
||||
def ignore_alert(db: Session, alert_id: int) -> Optional[Alert]:
|
||||
"""忽略告警"""
|
||||
alert = db.query(Alert).filter(Alert.id == alert_id).first()
|
||||
if not alert:
|
||||
return None
|
||||
|
||||
alert.status = AlertStatus.IGNORED
|
||||
db.commit()
|
||||
db.refresh(alert)
|
||||
|
||||
return alert
|
||||
|
||||
@staticmethod
|
||||
def get_statistics(db: Session) -> Dict[str, Any]:
|
||||
"""获取告警统计"""
|
||||
active_count = db.query(Alert).filter(Alert.status == AlertStatus.ACTIVE).count()
|
||||
acknowledged_count = db.query(Alert).filter(Alert.status == AlertStatus.ACKNOWLEDGED).count()
|
||||
resolved_count = db.query(Alert).filter(Alert.status == AlertStatus.RESOLVED).count()
|
||||
|
||||
# 按严重级别统计
|
||||
critical_count = db.query(Alert).filter(
|
||||
Alert.status == AlertStatus.ACTIVE,
|
||||
Alert.severity == AlertSeverity.CRITICAL
|
||||
).count()
|
||||
error_count = db.query(Alert).filter(
|
||||
Alert.status == AlertStatus.ACTIVE,
|
||||
Alert.severity == AlertSeverity.ERROR
|
||||
).count()
|
||||
warning_count = db.query(Alert).filter(
|
||||
Alert.status == AlertStatus.ACTIVE,
|
||||
Alert.severity == AlertSeverity.WARNING
|
||||
).count()
|
||||
info_count = db.query(Alert).filter(
|
||||
Alert.status == AlertStatus.ACTIVE,
|
||||
Alert.severity == AlertSeverity.INFO
|
||||
).count()
|
||||
|
||||
# 按类型统计
|
||||
type_stats = {}
|
||||
for alert_type in AlertType:
|
||||
count = db.query(Alert).filter(
|
||||
Alert.status == AlertStatus.ACTIVE,
|
||||
Alert.alert_type == alert_type
|
||||
).count()
|
||||
type_stats[alert_type.value] = count
|
||||
|
||||
return {
|
||||
'by_status': {
|
||||
'active': active_count,
|
||||
'acknowledged': acknowledged_count,
|
||||
'resolved': resolved_count
|
||||
},
|
||||
'by_severity': {
|
||||
'critical': critical_count,
|
||||
'error': error_count,
|
||||
'warning': warning_count,
|
||||
'info': info_count
|
||||
},
|
||||
'by_type': type_stats
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def add_mac_to_whitelist(db: Session, mac_address: str, description: str = "", owner: str = "") -> WhitelistedMAC:
|
||||
"""添加 MAC 地址到白名单"""
|
||||
existing = db.query(WhitelistedMAC).filter(WhitelistedMAC.mac_address == mac_address.upper()).first()
|
||||
if existing:
|
||||
existing.is_active = True
|
||||
if description:
|
||||
existing.description = description
|
||||
if owner:
|
||||
existing.owner = owner
|
||||
db.commit()
|
||||
db.refresh(existing)
|
||||
return existing
|
||||
|
||||
whitelisted_mac = WhitelistedMAC(
|
||||
mac_address=mac_address.upper(),
|
||||
description=description,
|
||||
owner=owner
|
||||
)
|
||||
db.add(whitelisted_mac)
|
||||
db.commit()
|
||||
db.refresh(whitelisted_mac)
|
||||
|
||||
return whitelisted_mac
|
||||
|
||||
@staticmethod
|
||||
def get_mac_whitelist(db: Session, skip: int = 0, limit: int = 100) -> tuple[int, List[WhitelistedMAC]]:
|
||||
"""获取 MAC 白名单"""
|
||||
query = db.query(WhitelistedMAC).filter(WhitelistedMAC.is_active == True)
|
||||
total = query.count()
|
||||
items = query.order_by(WhitelistedMAC.id.desc()).offset(skip).limit(limit).all()
|
||||
return total, items
|
||||
|
||||
@staticmethod
|
||||
def remove_mac_from_whitelist(db: Session, mac_id: int) -> bool:
|
||||
"""从白名单移除 MAC 地址"""
|
||||
mac = db.query(WhitelistedMAC).filter(WhitelistedMAC.id == mac_id).first()
|
||||
if not mac:
|
||||
return False
|
||||
|
||||
mac.is_active = False
|
||||
db.commit()
|
||||
return True
|
||||
@@ -0,0 +1,155 @@
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from app.models.audit import AuditLog
|
||||
from app.models.auth import User
|
||||
|
||||
|
||||
class AuditService:
|
||||
"""审计日志服务"""
|
||||
|
||||
@staticmethod
|
||||
def record(
|
||||
db: Session,
|
||||
*,
|
||||
action: str,
|
||||
user: Optional[User] = None,
|
||||
resource_type: Optional[str] = None,
|
||||
resource_id: Optional[str] = None,
|
||||
resource_name: Optional[str] = None,
|
||||
method: Optional[str] = None,
|
||||
path: Optional[str] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
user_agent: Optional[str] = None,
|
||||
status: str = "success",
|
||||
detail: Optional[Any] = None,
|
||||
) -> AuditLog:
|
||||
"""
|
||||
写入一条审计日志。任意字段缺失都安全降级(不抛异常),
|
||||
避免审计日志写入失败影响主业务流程。
|
||||
"""
|
||||
try:
|
||||
log = AuditLog(
|
||||
user_id=user.id if user else None,
|
||||
username=user.username if user else None,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=str(resource_id) if resource_id is not None else None,
|
||||
resource_name=resource_name,
|
||||
method=method,
|
||||
path=path,
|
||||
ip_address=ip_address,
|
||||
user_agent=(user_agent or "")[:500],
|
||||
status=status,
|
||||
detail=_serialize_detail(detail),
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
return log
|
||||
except Exception as e:
|
||||
# 写审计日志失败不能影响主业务,仅回滚
|
||||
db.rollback()
|
||||
# 用 print 而非 logger,避免循环依赖(logger 可能未初始化)
|
||||
print(f"[audit] failed to write log action={action}: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_logs(
|
||||
db: Session,
|
||||
*,
|
||||
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 = 0,
|
||||
limit: int = 50,
|
||||
) -> Tuple[int, List[AuditLog]]:
|
||||
query = db.query(AuditLog)
|
||||
|
||||
if user_id is not None:
|
||||
query = query.filter(AuditLog.user_id == user_id)
|
||||
if username:
|
||||
query = query.filter(AuditLog.username.like(f"%{username}%"))
|
||||
if action:
|
||||
query = query.filter(AuditLog.action.like(f"%{action}%"))
|
||||
if resource_type:
|
||||
query = query.filter(AuditLog.resource_type == resource_type)
|
||||
if status:
|
||||
query = query.filter(AuditLog.status == status)
|
||||
if start_time:
|
||||
query = query.filter(AuditLog.created_at >= start_time)
|
||||
if end_time:
|
||||
query = query.filter(AuditLog.created_at <= end_time)
|
||||
|
||||
total = query.count()
|
||||
items = query.order_by(desc(AuditLog.created_at)).offset(skip).limit(limit).all()
|
||||
return total, items
|
||||
|
||||
@staticmethod
|
||||
def get_stats(db: Session) -> Dict[str, Any]:
|
||||
"""首页/汇总用:最近 24h 操作数 + 按 action 分组"""
|
||||
from datetime import timedelta
|
||||
cutoff = datetime.utcnow() - timedelta(hours=24)
|
||||
recent = db.query(AuditLog).filter(AuditLog.created_at >= cutoff).count()
|
||||
by_action = db.query(AuditLog.action, db.query(AuditLog).filter(AuditLog.created_at >= cutoff).subquery()) # 占位避免循环
|
||||
# 简化:用 group by
|
||||
from sqlalchemy import func
|
||||
rows = db.query(AuditLog.action, func.count(AuditLog.id)).filter(
|
||||
AuditLog.created_at >= cutoff
|
||||
).group_by(AuditLog.action).all()
|
||||
return {
|
||||
"recent_24h": recent,
|
||||
"by_action_24h": {action: count for action, count in rows},
|
||||
}
|
||||
|
||||
|
||||
def _serialize_detail(detail: Any) -> Optional[str]:
|
||||
"""把 dict/list 等结构化数据序列化成字符串,便于存储和展示"""
|
||||
if detail is None:
|
||||
return None
|
||||
if isinstance(detail, str):
|
||||
return detail[:4000]
|
||||
try:
|
||||
return json.dumps(detail, ensure_ascii=False, default=str)[:4000]
|
||||
except (TypeError, ValueError):
|
||||
return str(detail)[:4000]
|
||||
|
||||
|
||||
# 常用 action 字符串集中管理(避免散落各处的魔法字符串)
|
||||
class AuditAction:
|
||||
# 网络
|
||||
NETWORK_CREATE = "network.create"
|
||||
NETWORK_UPDATE = "network.update"
|
||||
NETWORK_DELETE = "network.delete"
|
||||
# IP
|
||||
IP_UPDATE = "ip.update"
|
||||
IP_RESERVE = "ip.reserve"
|
||||
IP_UNRESERVE = "ip.unreserve"
|
||||
# 扫描
|
||||
SCAN_TRIGGER = "scan.trigger"
|
||||
# SNMP
|
||||
SNMP_CRED_CREATE = "snmp.credential.create"
|
||||
SNMP_CRED_UPDATE = "snmp.credential.update"
|
||||
SNMP_CRED_DELETE = "snmp.credential.delete"
|
||||
SNMP_DEVICE_CREATE = "snmp.device.create"
|
||||
SNMP_DEVICE_UPDATE = "snmp.device.update"
|
||||
SNMP_DEVICE_DELETE = "snmp.device.delete"
|
||||
# 告警
|
||||
ALERT_ACK = "alert.acknowledge"
|
||||
ALERT_RESOLVE = "alert.resolve"
|
||||
# 用户管理
|
||||
USER_LOGIN = "user.login"
|
||||
USER_LOGIN_FAILED = "user.login.failed"
|
||||
USER_LOGOUT = "user.logout"
|
||||
USER_CREATE = "user.create"
|
||||
USER_UPDATE_STATUS = "user.update_status"
|
||||
USER_UNLOCK = "user.unlock"
|
||||
USER_RESET_PASSWORD = "user.reset_password"
|
||||
USER_DELETE = "user.delete"
|
||||
USER_CHANGE_PASSWORD = "user.change_password"
|
||||
@@ -0,0 +1,381 @@
|
||||
from typing import List, Dict, Any, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import subprocess
|
||||
import socket
|
||||
import ipaddress
|
||||
import re
|
||||
|
||||
from app.models.network import ScanTask, Network, IPAddress
|
||||
from app.models.network import TaskStatus, TaskType, IPStatus
|
||||
from app.core.config import settings
|
||||
|
||||
logger = __import__('logging').getLogger(__name__)
|
||||
|
||||
|
||||
class EnhancedScanService:
|
||||
"""增强扫描服务 - 包含ARP、DNS解析、MAC厂商识别"""
|
||||
|
||||
# MAC OUI 厂商映射(常用厂商)
|
||||
OUI_MAP = {
|
||||
'00:1C:14': 'VMware',
|
||||
'00:0C:29': 'VMware',
|
||||
'00:50:56': 'VMware',
|
||||
'00:15:5D': 'Microsoft',
|
||||
'08:00:27': 'VirtualBox',
|
||||
'52:54:00': 'QEMU/KVM',
|
||||
'00:1A:2A': 'Dell',
|
||||
'00:21:9B': 'Dell',
|
||||
'00:26:B9': 'Dell',
|
||||
'00:1B:21': 'HP',
|
||||
'00:23:7D': 'HP',
|
||||
'00:1E:0B': 'Cisco',
|
||||
'00:21:55': 'Cisco',
|
||||
'00:23:04': 'Cisco',
|
||||
'00:24:14': 'Cisco',
|
||||
'00:1F:CA': 'Cisco',
|
||||
'00:19:BB': 'Huawei',
|
||||
'00:25:9E': 'Huawei',
|
||||
'00:E0:FC': 'Huawei',
|
||||
'AC:85:3D': 'Huawei',
|
||||
'28:6E:D4': 'Huawei',
|
||||
'00:16:6F': 'H3C',
|
||||
'00:23:89': 'H3C',
|
||||
'00:0F:E2': 'Intel',
|
||||
'00:1B:77': 'Intel',
|
||||
'00:21:6A': 'Intel',
|
||||
'1C:6F:65': 'Intel',
|
||||
'00:19:D2': 'Realtek',
|
||||
'00:24:1D': 'Realtek',
|
||||
'00:E0:4C': 'Realtek',
|
||||
'F4:6D:04': 'Apple',
|
||||
'E0:F8:47': 'Apple',
|
||||
'C8:BC:C8': 'Apple',
|
||||
'04:15:52': 'Apple',
|
||||
'28:CF:E9': 'Apple',
|
||||
'04:7D:7B': 'Xiaomi',
|
||||
'18:59:36': 'Xiaomi',
|
||||
'34:CE:00': 'Xiaomi',
|
||||
'64:09:80': 'TP-Link',
|
||||
'E8:94:F6': 'TP-Link',
|
||||
'50:FA:84': 'TP-Link',
|
||||
'00:24:01': 'TP-Link',
|
||||
'30:FC:68': 'NETGEAR',
|
||||
'00:09:5B': 'NETGEAR',
|
||||
'00:FF:FF': 'Broadcast',
|
||||
'FF:FF:FF': 'Broadcast',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_mac_vendor(mac_address: str) -> Optional[str]:
|
||||
"""根据MAC地址OUI识别厂商"""
|
||||
if not mac_address:
|
||||
return None
|
||||
|
||||
# 标准化MAC格式为 AA:BB:CC:DD:EE:FF
|
||||
mac = mac_address.upper().replace('-', ':')
|
||||
if len(mac) < 8:
|
||||
return None
|
||||
|
||||
# 提取前3字节 OUI
|
||||
oui = mac[:8]
|
||||
|
||||
# 精确匹配
|
||||
if oui in EnhancedScanService.OUI_MAP:
|
||||
return EnhancedScanService.OUI_MAP[oui]
|
||||
|
||||
# 尝试模糊匹配(前2字节)
|
||||
oui_short = mac[:5]
|
||||
for key, vendor in EnhancedScanService.OUI_MAP.items():
|
||||
if key.startswith(oui_short):
|
||||
return vendor
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def reverse_dns_lookup(ip_address: str, timeout: int = 2) -> Optional[str]:
|
||||
"""反向DNS解析获取主机名"""
|
||||
try:
|
||||
socket.setdefaulttimeout(timeout)
|
||||
hostname, _, _ = socket.gethostbyaddr(ip_address)
|
||||
return hostname
|
||||
except (socket.herror, socket.timeout, socket.gaierror):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def arp_scan_single(ip_address: str, timeout: int = 2) -> Optional[str]:
|
||||
"""
|
||||
对单个IP执行ARP扫描获取MAC地址
|
||||
返回: MAC地址 或 None
|
||||
"""
|
||||
# 方法1: 使用 arp-scan 命令(比 scapy 更可靠,不依赖 raw socket)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['arp-scan', '-I', 'auto', ip_address, '-q'],
|
||||
capture_output=True, text=True, timeout=timeout + 2
|
||||
)
|
||||
if result.returncode == 0:
|
||||
# arp-scan 输出格式: IP\tMAC\tVendor
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split('\t')
|
||||
if len(parts) >= 2:
|
||||
mac = parts[1].strip()
|
||||
if mac and mac != '<incomplete>':
|
||||
return mac.upper()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 方法2: 尝试读取系统 ARP 缓存
|
||||
return EnhancedScanService._get_arp_from_cache(ip_address)
|
||||
|
||||
@staticmethod
|
||||
def _get_arp_from_cache(ip_address: str) -> Optional[str]:
|
||||
"""从系统ARP缓存读取MAC地址"""
|
||||
try:
|
||||
# 读取 /proc/net/arp (Linux)
|
||||
with open('/proc/net/arp', 'r') as f:
|
||||
for line in f.readlines()[1:]: # 跳过表头
|
||||
parts = line.split()
|
||||
if len(parts) >= 4 and parts[0] == ip_address:
|
||||
mac = parts[3].upper()
|
||||
if mac != '00:00:00:00:00:00':
|
||||
return mac
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 尝试 arp -a 命令
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['arp', '-a', ip_address],
|
||||
capture_output=True, text=True,
|
||||
timeout=2
|
||||
)
|
||||
if result.returncode == 0:
|
||||
# 匹配 MAC 地址格式
|
||||
mac_pattern = r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})'
|
||||
match = re.search(mac_pattern, result.stdout)
|
||||
if match:
|
||||
return match.group().upper()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def comprehensive_scan(ip_address: str,
|
||||
enable_ping: bool = True,
|
||||
enable_arp: bool = True,
|
||||
enable_dns: bool = True,
|
||||
timeout: int = 2) -> Dict[str, Any]:
|
||||
"""
|
||||
综合扫描单个IP
|
||||
返回: {
|
||||
'ip_address': str,
|
||||
'status': 'online'|'offline',
|
||||
'mac_address': str|None,
|
||||
'hostname': str|None,
|
||||
'vendor': str|None,
|
||||
'response_time': float|None
|
||||
}
|
||||
"""
|
||||
result = {
|
||||
'ip_address': ip_address,
|
||||
'status': 'offline',
|
||||
'mac_address': None,
|
||||
'hostname': None,
|
||||
'vendor': None,
|
||||
'success': False
|
||||
}
|
||||
|
||||
# 1. Ping 扫描
|
||||
if enable_ping:
|
||||
ping_result = EnhancedScanService.ping_single_ip(ip_address, timeout)
|
||||
result['status'] = ping_result.get('status', 'offline')
|
||||
result['success'] = ping_result.get('success', False)
|
||||
result['response_time'] = ping_result.get('response_time')
|
||||
|
||||
# 2. ARP 扫描 (即使ping不通也尝试,有些设备禁ping)
|
||||
if enable_arp:
|
||||
mac = EnhancedScanService.arp_scan_single(ip_address, timeout)
|
||||
if mac:
|
||||
result['mac_address'] = mac
|
||||
result['vendor'] = EnhancedScanService.get_mac_vendor(mac)
|
||||
# 如果获取到MAC,说明设备实际上在线
|
||||
result['status'] = 'online'
|
||||
result['success'] = True
|
||||
|
||||
# 3. 反向 DNS 解析
|
||||
if enable_dns and result.get('success'):
|
||||
hostname = EnhancedScanService.reverse_dns_lookup(ip_address, timeout)
|
||||
if hostname:
|
||||
result['hostname'] = hostname
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def ping_single_ip(ip_address: str, timeout: int = 2) -> Dict[str, Any]:
|
||||
"""Ping 单个IP"""
|
||||
start_time = datetime.now()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['ping', '-c', '1', '-W', str(timeout), ip_address],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
is_alive = result.returncode == 0
|
||||
response_time = (datetime.now() - start_time).total_seconds() * 1000
|
||||
|
||||
return {
|
||||
'ip_address': ip_address,
|
||||
'status': 'online' if is_alive else 'offline',
|
||||
'response_time': round(response_time, 2),
|
||||
'success': is_alive
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'ip_address': ip_address,
|
||||
'status': 'error',
|
||||
'error': str(e),
|
||||
'success': False
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def scan_network(cidr: str,
|
||||
enable_ping: bool = True,
|
||||
enable_arp: bool = True,
|
||||
enable_dns: bool = True,
|
||||
timeout: int = 2,
|
||||
max_concurrent: int = 50) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
扫描整个网段(并发)
|
||||
"""
|
||||
network = ipaddress.ip_network(cidr, strict=False)
|
||||
ips = [str(ip) for ip in network.hosts()]
|
||||
|
||||
if not ips:
|
||||
return []
|
||||
|
||||
# 并发执行;并发数随网段大小自适应
|
||||
worker_count = min(max_concurrent, len(ips))
|
||||
results: List[Dict[str, Any]] = []
|
||||
with ThreadPoolExecutor(max_workers=worker_count) as executor:
|
||||
future_to_ip = {
|
||||
executor.submit(
|
||||
EnhancedScanService.comprehensive_scan,
|
||||
ip,
|
||||
enable_ping=enable_ping,
|
||||
enable_arp=enable_arp,
|
||||
enable_dns=enable_dns,
|
||||
timeout=timeout
|
||||
): ip
|
||||
for ip in ips
|
||||
}
|
||||
for future in as_completed(future_to_ip):
|
||||
try:
|
||||
results.append(future.result())
|
||||
except Exception as e:
|
||||
ip = future_to_ip[future]
|
||||
results.append({
|
||||
'ip_address': ip,
|
||||
'status': 'offline',
|
||||
'mac_address': None,
|
||||
'hostname': None,
|
||||
'vendor': None,
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def update_ip_from_scan_result(db: Session, scan_result: Dict[str, Any]):
|
||||
"""根据扫描结果更新IP信息"""
|
||||
ip_address = scan_result.get('ip_address')
|
||||
if not ip_address:
|
||||
return
|
||||
|
||||
db_ip = db.query(IPAddress).filter(IPAddress.ip_address == ip_address).first()
|
||||
|
||||
# 如果IP不存在,尝试自动创建
|
||||
if not db_ip:
|
||||
# 从ip_address推断network_id
|
||||
network = db.query(Network).filter(
|
||||
Network.cidr.op('@>')((ip_address + '/32').encode('utf-8'))
|
||||
).first()
|
||||
if not network:
|
||||
# 尝试精确匹配 network.cidr
|
||||
for net in db.query(Network).all():
|
||||
try:
|
||||
if ip_address in ipaddress.ip_network(net.cidr, strict=False):
|
||||
network = net
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if not network:
|
||||
logger.warning(f"无法为扫描到的IP {ip_address} 找到所属网段,跳过创建")
|
||||
return
|
||||
|
||||
db_ip = IPAddress(ip_address=ip_address, network_id=network.id)
|
||||
db.add(db_ip)
|
||||
db.commit()
|
||||
db.refresh(db_ip)
|
||||
|
||||
# 更新状态
|
||||
status = scan_result.get('status', 'offline')
|
||||
if status == 'online':
|
||||
db_ip.status = IPStatus.ONLINE
|
||||
elif status == 'offline' and db_ip.status == IPStatus.ONLINE:
|
||||
db_ip.status = IPStatus.OFFLINE
|
||||
|
||||
# 更新 MAC 地址
|
||||
mac = scan_result.get('mac_address')
|
||||
if mac:
|
||||
db_ip.mac_address = mac
|
||||
|
||||
# 更新主机名
|
||||
hostname = scan_result.get('hostname')
|
||||
if hostname:
|
||||
db_ip.hostname = hostname
|
||||
|
||||
# 更新厂商信息
|
||||
vendor = scan_result.get('vendor')
|
||||
if vendor:
|
||||
db_ip.vendor = vendor
|
||||
|
||||
# 更新最后发现时间
|
||||
if scan_result.get('success'):
|
||||
db_ip.last_seen = datetime.utcnow()
|
||||
|
||||
return db_ip
|
||||
|
||||
@staticmethod
|
||||
def bulk_update_ips_from_scan(db: Session, scan_results: List[Dict[str, Any]]):
|
||||
"""批量更新IP信息"""
|
||||
online_count = 0
|
||||
mac_found_count = 0
|
||||
hostname_found_count = 0
|
||||
created_count = 0
|
||||
|
||||
for result in scan_results:
|
||||
ip = EnhancedScanService.update_ip_from_scan_result(db, result)
|
||||
if ip:
|
||||
if result.get('success'):
|
||||
online_count += 1
|
||||
if result.get('mac_address'):
|
||||
mac_found_count += 1
|
||||
if result.get('hostname'):
|
||||
hostname_found_count += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
'online_count': online_count,
|
||||
'mac_found_count': mac_found_count,
|
||||
'hostname_found_count': hostname_found_count,
|
||||
'total_scanned': len(scan_results)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
from typing import List, Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
|
||||
from app.models.network import IPAddress as IPAddressModel
|
||||
from app.models.network import Network as NetworkModel
|
||||
from app.models.network import IPStatus
|
||||
from app.schemas.network import IPAddressUpdate, IPAddressCreate
|
||||
|
||||
|
||||
class IPService:
|
||||
"""IP地址管理服务"""
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, ip_id: int) -> Optional[IPAddressModel]:
|
||||
"""根据ID获取IP"""
|
||||
return db.query(IPAddressModel).filter(IPAddressModel.id == ip_id).first()
|
||||
|
||||
@staticmethod
|
||||
def get_by_ip(db: Session, ip_address: str) -> Optional[IPAddressModel]:
|
||||
"""根据IP地址获取"""
|
||||
return db.query(IPAddressModel).filter(IPAddressModel.ip_address == ip_address).first()
|
||||
|
||||
@staticmethod
|
||||
def get_list(
|
||||
db: Session,
|
||||
network_id: Optional[int] = None,
|
||||
status: Optional[IPStatus] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
search: Optional[str] = None
|
||||
) -> tuple[int, List[IPAddressModel]]:
|
||||
"""获取IP列表"""
|
||||
query = db.query(IPAddressModel)
|
||||
|
||||
if network_id:
|
||||
query = query.filter(IPAddressModel.network_id == network_id)
|
||||
|
||||
if status:
|
||||
query = query.filter(IPAddressModel.status == status)
|
||||
|
||||
if search:
|
||||
search_pattern = f"%{search}%"
|
||||
query = query.filter(
|
||||
(IPAddressModel.ip_address.like(search_pattern)) |
|
||||
(IPAddressModel.mac_address.like(search_pattern)) |
|
||||
(IPAddressModel.hostname.like(search_pattern)) |
|
||||
(IPAddressModel.owner.like(search_pattern))
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
items = query.order_by(IPAddressModel.ip_address).offset(skip).limit(limit).all()
|
||||
|
||||
return total, items
|
||||
|
||||
@staticmethod
|
||||
def create(db: Session, ip_in: IPAddressCreate) -> IPAddressModel:
|
||||
"""手动创建IP(通常由网段创建自动创建)"""
|
||||
db_ip = IPAddressModel(**ip_in.model_dump())
|
||||
db.add(db_ip)
|
||||
db.commit()
|
||||
db.refresh(db_ip)
|
||||
return db_ip
|
||||
|
||||
@staticmethod
|
||||
def update(db: Session, ip_id: int, ip_in: IPAddressUpdate) -> Optional[IPAddressModel]:
|
||||
"""更新IP信息"""
|
||||
db_ip = IPService.get_by_id(db, ip_id)
|
||||
if not db_ip:
|
||||
return None
|
||||
|
||||
update_data = ip_in.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(db_ip, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_ip)
|
||||
return db_ip
|
||||
|
||||
@staticmethod
|
||||
def update_status(db: Session, ip_address: str, status: IPStatus,
|
||||
mac_address: Optional[str] = None,
|
||||
hostname: Optional[str] = None) -> Optional[IPAddressModel]:
|
||||
"""更新IP状态和扫描信息"""
|
||||
db_ip = IPService.get_by_ip(db, ip_address)
|
||||
if not db_ip:
|
||||
return None
|
||||
|
||||
db_ip.status = status
|
||||
db_ip.last_seen = datetime.utcnow()
|
||||
|
||||
if mac_address:
|
||||
db_ip.mac_address = mac_address
|
||||
|
||||
if hostname:
|
||||
db_ip.hostname = hostname
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_ip)
|
||||
return db_ip
|
||||
|
||||
@staticmethod
|
||||
def set_reserved(db: Session, ip_id: int, reserved: bool = True) -> Optional[IPAddressModel]:
|
||||
"""设置/取消保留IP"""
|
||||
db_ip = IPService.get_by_id(db, ip_id)
|
||||
if not db_ip:
|
||||
return None
|
||||
|
||||
db_ip.status = IPStatus.RESERVED if reserved else IPStatus.AVAILABLE
|
||||
db.commit()
|
||||
db.refresh(db_ip)
|
||||
return db_ip
|
||||
|
||||
@staticmethod
|
||||
def bulk_update_status(db: Session, results: List[Dict[str, Any]]):
|
||||
"""批量更新IP状态
|
||||
|
||||
Args:
|
||||
results: 扫描结果列表,每个元素包含 ip_address, status, mac_address, hostname
|
||||
"""
|
||||
for result in results:
|
||||
ip_address = result.get('ip_address')
|
||||
status = result.get('status')
|
||||
mac_address = result.get('mac_address')
|
||||
hostname = result.get('hostname')
|
||||
|
||||
db_ip = IPService.get_by_ip(db, ip_address)
|
||||
if db_ip:
|
||||
db_ip.status = status
|
||||
db_ip.last_seen = datetime.utcnow()
|
||||
if mac_address:
|
||||
db_ip.mac_address = mac_address
|
||||
if hostname:
|
||||
db_ip.hostname = hostname
|
||||
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def get_by_network(db: Session, network_id: int) -> List[IPAddressModel]:
|
||||
"""获取指定网段下的所有IP"""
|
||||
return db.query(IPAddressModel).filter(
|
||||
IPAddressModel.network_id == network_id
|
||||
).order_by(IPAddressModel.ip_address).all()
|
||||
@@ -0,0 +1,177 @@
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, case
|
||||
import ipaddress
|
||||
|
||||
from app.models.network import Network as NetworkModel
|
||||
from app.models.network import IPAddress as IPAddressModel
|
||||
from app.models.network import IPStatus
|
||||
from app.schemas.network import NetworkCreate, NetworkUpdate, NetworkStats
|
||||
|
||||
|
||||
class NetworkService:
|
||||
"""网段管理服务"""
|
||||
|
||||
@staticmethod
|
||||
def calculate_total_ips(cidr: str) -> int:
|
||||
"""计算网段的总IP数量"""
|
||||
network = ipaddress.ip_network(cidr, strict=False)
|
||||
return network.num_addresses
|
||||
|
||||
@staticmethod
|
||||
def get_network_addresses(cidr: str) -> List[str]:
|
||||
"""获取网段内所有IP地址列表"""
|
||||
network = ipaddress.ip_network(cidr, strict=False)
|
||||
return [str(ip) for ip in network.hosts()]
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, network_id: int) -> Optional[NetworkModel]:
|
||||
"""根据ID获取网段"""
|
||||
return db.query(NetworkModel).filter(NetworkModel.id == network_id).first()
|
||||
|
||||
@staticmethod
|
||||
def get_by_cidr(db: Session, cidr: str) -> Optional[NetworkModel]:
|
||||
"""根据CIDR获取网段"""
|
||||
return db.query(NetworkModel).filter(NetworkModel.cidr == cidr).first()
|
||||
|
||||
@staticmethod
|
||||
def get_list(
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
group_name: Optional[str] = None
|
||||
) -> tuple[int, List[NetworkModel]]:
|
||||
"""获取网段列表"""
|
||||
query = db.query(NetworkModel)
|
||||
|
||||
if group_name:
|
||||
query = query.filter(NetworkModel.group_name == group_name)
|
||||
|
||||
total = query.count()
|
||||
items = query.order_by(NetworkModel.id.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
return total, items
|
||||
|
||||
@staticmethod
|
||||
def create(db: Session, network_in: NetworkCreate) -> NetworkModel:
|
||||
"""创建新网段"""
|
||||
total_ips = NetworkService.calculate_total_ips(network_in.cidr)
|
||||
|
||||
db_network = NetworkModel(
|
||||
cidr=network_in.cidr,
|
||||
name=network_in.name,
|
||||
description=network_in.description,
|
||||
group_name=network_in.group_name,
|
||||
gateway=network_in.gateway,
|
||||
vlan_id=network_in.vlan_id,
|
||||
total_ips=total_ips,
|
||||
used_ips=0,
|
||||
reserved_ips=0
|
||||
)
|
||||
|
||||
db.add(db_network)
|
||||
db.flush()
|
||||
|
||||
# 自动创建该网段下的所有IP记录
|
||||
NetworkService._create_ip_addresses(db, db_network)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_network)
|
||||
return db_network
|
||||
|
||||
@staticmethod
|
||||
def _create_ip_addresses(db: Session, network: NetworkModel):
|
||||
"""为网段创建所有IP记录"""
|
||||
ip_addresses = NetworkService.get_network_addresses(network.cidr)
|
||||
|
||||
ip_records = []
|
||||
for ip in ip_addresses:
|
||||
# 标记网关为保留状态
|
||||
status = IPStatus.RESERVED if ip == network.gateway else IPStatus.AVAILABLE
|
||||
ip_records.append(
|
||||
IPAddressModel(
|
||||
network_id=network.id,
|
||||
ip_address=ip,
|
||||
status=status
|
||||
)
|
||||
)
|
||||
|
||||
db.bulk_save_objects(ip_records)
|
||||
|
||||
# 更新统计
|
||||
reserved_count = sum(1 for ip in ip_addresses if ip == network.gateway)
|
||||
network.reserved_ips = reserved_count
|
||||
|
||||
@staticmethod
|
||||
def update(db: Session, network_id: int, network_in: NetworkUpdate) -> Optional[NetworkModel]:
|
||||
"""更新网段信息"""
|
||||
db_network = NetworkService.get_by_id(db, network_id)
|
||||
if not db_network:
|
||||
return None
|
||||
|
||||
update_data = network_in.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(db_network, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_network)
|
||||
return db_network
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, network_id: int) -> bool:
|
||||
"""删除网段"""
|
||||
db_network = NetworkService.get_by_id(db, network_id)
|
||||
if not db_network:
|
||||
return False
|
||||
|
||||
db.delete(db_network)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_stats(db: Session, network_id: int) -> Optional[NetworkStats]:
|
||||
"""获取网段统计信息"""
|
||||
db_network = NetworkService.get_by_id(db, network_id)
|
||||
if not db_network:
|
||||
return None
|
||||
|
||||
# 实时计算IP状态 - 使用SUM(CASE)兼容MySQL
|
||||
stats = db.query(
|
||||
func.sum(case((IPAddressModel.status == IPStatus.ONLINE, 1), else_=0)).label('online'),
|
||||
func.sum(case((IPAddressModel.status == IPStatus.OFFLINE, 1), else_=0)).label('offline'),
|
||||
func.sum(case((IPAddressModel.status == IPStatus.RESERVED, 1), else_=0)).label('reserved'),
|
||||
).filter(IPAddressModel.network_id == network_id).first()
|
||||
|
||||
used_ips = int(stats.online or 0) + int(stats.offline or 0)
|
||||
reserved_ips = int(stats.reserved or 0)
|
||||
available_ips = db_network.total_ips - used_ips - reserved_ips
|
||||
usage_percent = (used_ips / db_network.total_ips * 100) if db_network.total_ips > 0 else 0
|
||||
|
||||
# 更新数据库统计
|
||||
db_network.used_ips = used_ips
|
||||
db_network.reserved_ips = reserved_ips
|
||||
db.commit()
|
||||
|
||||
return NetworkStats(
|
||||
id=db_network.id,
|
||||
cidr=db_network.cidr,
|
||||
name=db_network.name,
|
||||
total_ips=db_network.total_ips,
|
||||
used_ips=used_ips,
|
||||
reserved_ips=reserved_ips,
|
||||
available_ips=available_ips,
|
||||
usage_percent=round(usage_percent, 2),
|
||||
group_name=db_network.group_name,
|
||||
vlan_id=db_network.vlan_id
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_all_stats(db: Session) -> List[NetworkStats]:
|
||||
"""获取所有网段的统计信息"""
|
||||
networks = db.query(NetworkModel).all()
|
||||
stats_list = []
|
||||
for network in networks:
|
||||
stats = NetworkService.get_stats(db, network.id)
|
||||
if stats:
|
||||
stats_list.append(stats)
|
||||
return stats_list
|
||||
@@ -0,0 +1,148 @@
|
||||
from typing import List, Dict, Any, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
import subprocess
|
||||
import ipaddress
|
||||
|
||||
from app.models.network import ScanTask, Network, IPAddress
|
||||
from app.models.network import TaskStatus, TaskType, IPStatus
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
class ScanService:
|
||||
"""扫描服务"""
|
||||
|
||||
@staticmethod
|
||||
def ping_single_ip(ip_address: str, timeout: int = 2) -> Dict[str, Any]:
|
||||
"""
|
||||
对单个IP执行Ping扫描
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['ping', '-c', '1', '-W', str(timeout), ip_address],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
is_alive = result.returncode == 0
|
||||
|
||||
return {
|
||||
'ip_address': ip_address,
|
||||
'status': 'online' if is_alive else 'offline',
|
||||
'response_time': None,
|
||||
'success': is_alive
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'ip_address': ip_address,
|
||||
'status': 'error',
|
||||
'error': str(e),
|
||||
'success': False
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def ping_network(cidr: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
对整个网段执行Ping扫描(同步版本)
|
||||
"""
|
||||
network = ipaddress.ip_network(cidr, strict=False)
|
||||
ips = [str(ip) for ip in network.hosts()]
|
||||
|
||||
results = []
|
||||
for ip in ips:
|
||||
result = ScanService.ping_single_ip(ip, settings.PING_TIMEOUT)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def create_scan_task(db: Session, task_type: TaskType,
|
||||
network_id: Optional[int] = None) -> ScanTask:
|
||||
"""
|
||||
创建扫描任务
|
||||
"""
|
||||
task = ScanTask(
|
||||
task_type=task_type,
|
||||
network_id=network_id,
|
||||
status=TaskStatus.PENDING,
|
||||
progress=0
|
||||
)
|
||||
db.add(task)
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def update_task_status(db: Session, task_id: int, status: TaskStatus,
|
||||
progress: int = None, total_count: int = None,
|
||||
success_count: int = None, failed_count: int = None,
|
||||
error_message: str = None):
|
||||
"""
|
||||
更新任务状态
|
||||
"""
|
||||
task = db.query(ScanTask).filter(ScanTask.id == task_id).first()
|
||||
if not task:
|
||||
return
|
||||
|
||||
task.status = status
|
||||
if progress is not None:
|
||||
task.progress = progress
|
||||
if total_count is not None:
|
||||
task.total_count = total_count
|
||||
if success_count is not None:
|
||||
task.success_count = success_count
|
||||
if failed_count is not None:
|
||||
task.failed_count = failed_count
|
||||
if error_message:
|
||||
task.error_message = error_message
|
||||
|
||||
if status == TaskStatus.RUNNING and task.started_at is None:
|
||||
task.started_at = datetime.utcnow()
|
||||
elif status in [TaskStatus.COMPLETED, TaskStatus.FAILED]:
|
||||
task.completed_at = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def get_task_by_id(db: Session, task_id: int) -> Optional[ScanTask]:
|
||||
"""
|
||||
获取任务信息
|
||||
"""
|
||||
return db.query(ScanTask).filter(ScanTask.id == task_id).first()
|
||||
|
||||
@staticmethod
|
||||
def get_task_list(db: Session, skip: int = 0, limit: int = 50) -> tuple[int, List[ScanTask]]:
|
||||
"""
|
||||
获取任务列表
|
||||
"""
|
||||
query = db.query(ScanTask)
|
||||
total = query.count()
|
||||
items = query.order_by(ScanTask.id.desc()).offset(skip).limit(limit).all()
|
||||
return total, items
|
||||
|
||||
@staticmethod
|
||||
def update_ip_status_from_scan_results(db: Session, results: List[Dict[str, Any]]):
|
||||
"""
|
||||
根据扫描结果更新IP状态
|
||||
"""
|
||||
online_count = 0
|
||||
offline_count = 0
|
||||
|
||||
for result in results:
|
||||
ip_address = result.get('ip_address')
|
||||
is_online = result.get('success', False)
|
||||
|
||||
db_ip = db.query(IPAddress).filter(IPAddress.ip_address == ip_address).first()
|
||||
if db_ip:
|
||||
if is_online:
|
||||
db_ip.status = IPStatus.ONLINE
|
||||
db_ip.last_seen = datetime.utcnow()
|
||||
online_count += 1
|
||||
elif db_ip.status == IPStatus.ONLINE:
|
||||
# 曾经在线,现在不在线,标记为离线
|
||||
db_ip.status = IPStatus.OFFLINE
|
||||
offline_count += 1
|
||||
|
||||
db.commit()
|
||||
return online_count, offline_count
|
||||
@@ -0,0 +1,520 @@
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
import ipaddress
|
||||
import logging
|
||||
|
||||
from pysnmp.hlapi import (
|
||||
SnmpEngine, CommunityData, UsmUserData,
|
||||
UdpTransportTarget, ContextData,
|
||||
ObjectType, ObjectIdentity,
|
||||
bulkCmd, getCmd, nextCmd
|
||||
)
|
||||
from pysnmp.proto.rfc1902 import OctetString, Integer32, Counter32, Counter64, Gauge32
|
||||
|
||||
from app.models.snmp import (
|
||||
SNMPCredential, NetworkDevice, ARPEntry, MACAddressTable, DeviceInterface,
|
||||
SNMPVersion, SNMPAuthProtocol, SNMPPrivProtocol
|
||||
)
|
||||
from app.services.enhanced_scan_service import EnhancedScanService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SNMPService:
|
||||
"""SNMP 服务"""
|
||||
|
||||
# OID 定义
|
||||
OID_SYSTEM = '1.3.6.1.2.1.1'
|
||||
OID_SYS_DESCR = '1.3.6.1.2.1.1.1.0'
|
||||
OID_SYS_NAME = '1.3.6.1.2.1.1.5.0'
|
||||
OID_SYS_LOCATION = '1.3.6.1.2.1.1.6.0'
|
||||
|
||||
# 接口相关 OID
|
||||
OID_IF_TABLE = '1.3.6.1.2.1.2.2'
|
||||
OID_IF_INDEX = '1.3.6.1.2.1.2.2.1.1'
|
||||
OID_IF_DESCR = '1.3.6.1.2.1.2.2.1.2'
|
||||
OID_IF_TYPE = '1.3.6.1.2.1.2.2.1.3'
|
||||
OID_IF_MTU = '1.3.6.1.2.1.2.2.1.4'
|
||||
OID_IF_SPEED = '1.3.6.1.2.1.2.2.1.5'
|
||||
OID_IF_PHYS_ADDRESS = '1.3.6.1.2.1.2.2.1.6'
|
||||
OID_IF_ADMIN_STATUS = '1.3.6.1.2.1.2.2.1.7'
|
||||
OID_IF_OPER_STATUS = '1.3.6.1.2.1.2.2.1.8'
|
||||
OID_IF_IN_OCTETS = '1.3.6.1.2.1.2.2.1.10'
|
||||
OID_IF_OUT_OCTETS = '1.3.6.1.2.1.2.2.1.16'
|
||||
OID_IF_IN_ERRORS = '1.3.6.1.2.1.2.2.1.14'
|
||||
OID_IF_OUT_ERRORS = '1.3.6.1.2.1.2.2.1.20'
|
||||
|
||||
# IP 地址表
|
||||
OID_IP_ADDR_TABLE = '1.3.6.1.2.1.4.20'
|
||||
OID_IP_ADDR_ENTRIES = '1.3.6.1.2.1.4.20.1.1'
|
||||
OID_IP_ADDR_IF_INDEX = '1.3.6.1.2.1.4.20.1.2'
|
||||
OID_IP_ADDR_NETMASK = '1.3.6.1.2.1.4.20.1.3'
|
||||
|
||||
# ARP 表 (ipNetToMediaTable)
|
||||
OID_IP_NET_TO_MEDIA_PHYS_ADDRESS = '1.3.6.1.2.1.4.22.1.2'
|
||||
OID_IP_NET_TO_MEDIA_IF_INDEX = '1.3.6.1.2.1.4.22.1.1'
|
||||
OID_IP_NET_TO_MEDIA_NET_ADDRESS = '1.3.6.1.2.1.4.22.1.3'
|
||||
OID_IP_NET_TO_MEDIA_TYPE = '1.3.6.1.2.1.4.22.1.4'
|
||||
|
||||
# DOT1D 桥接 MIB (MAC 地址表)
|
||||
OID_DOT1D_TP_FDB_PORT = '1.3.6.1.2.1.17.4.3.1.2'
|
||||
OID_DOT1D_TP_FDB_STATUS = '1.3.6.1.2.1.17.4.3.1.3'
|
||||
|
||||
# VLAN 相关 (Cisco 特定)
|
||||
OID_VTP_VLAN_STATE = '1.3.6.1.4.1.9.9.46.1.3.1.1.2'
|
||||
|
||||
@staticmethod
|
||||
def _create_auth_data(credential: SNMPCredential):
|
||||
"""创建 SNMP 认证数据"""
|
||||
if credential.version == SNMPVersion.V2C:
|
||||
if not credential.community_string:
|
||||
raise ValueError("SNMPv2c 凭据缺少 community_string")
|
||||
return CommunityData(credential.community_string)
|
||||
else: # V3
|
||||
if not credential.username:
|
||||
raise ValueError("SNMPv3 凭据缺少 username(V3 强制要求 1-32 字符)")
|
||||
|
||||
auth_protocol_map = {
|
||||
SNMPAuthProtocol.MD5: 'MD5',
|
||||
SNMPAuthProtocol.SHA: 'SHA',
|
||||
SNMPAuthProtocol.SHA256: 'SHA256',
|
||||
}
|
||||
priv_protocol_map = {
|
||||
SNMPPrivProtocol.DES: 'DES',
|
||||
SNMPPrivProtocol.AES: 'AES',
|
||||
SNMPPrivProtocol.AES256: 'AES256',
|
||||
}
|
||||
|
||||
auth_proto = auth_protocol_map.get(credential.auth_protocol) if credential.auth_protocol else None
|
||||
priv_proto = priv_protocol_map.get(credential.priv_protocol) if credential.priv_protocol else None
|
||||
|
||||
return UsmUserData(
|
||||
userName=credential.username,
|
||||
authProtocol=auth_proto,
|
||||
authKey=credential.auth_password,
|
||||
privProtocol=priv_proto,
|
||||
privKey=credential.priv_password
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_transport_target(ip_address: str, port: int = 161, timeout: int = 3, retries: int = 2):
|
||||
"""创建传输目标"""
|
||||
return UdpTransportTarget((ip_address, port), timeout=timeout, retries=retries)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_mac(mac_bytes) -> Optional[str]:
|
||||
"""将 MAC 地址字节转换为标准格式 AA:BB:CC:DD:EE:FF"""
|
||||
if not mac_bytes:
|
||||
return None
|
||||
|
||||
if isinstance(mac_bytes, OctetString):
|
||||
mac_bytes = mac_bytes.asOctets()
|
||||
|
||||
if len(mac_bytes) == 6:
|
||||
return ':'.join(f'{b:02X}' for b in mac_bytes)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def test_connection(device: NetworkDevice, credential: SNMPCredential) -> Tuple[bool, Dict[str, Any]]:
|
||||
"""
|
||||
测试 SNMP 连接
|
||||
返回: (成功, 设备信息字典)
|
||||
"""
|
||||
try:
|
||||
auth_data = SNMPService._create_auth_data(credential)
|
||||
transport = SNMPService._get_transport_target(
|
||||
device.ip_address,
|
||||
port=device.port,
|
||||
timeout=credential.timeout,
|
||||
retries=credential.retries
|
||||
)
|
||||
|
||||
errorIndication, errorStatus, errorIndex, varBinds = next(
|
||||
getCmd(SnmpEngine(), auth_data, transport, ContextData(),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_SYS_DESCR)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_SYS_NAME)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_SYS_LOCATION)))
|
||||
)
|
||||
|
||||
if errorIndication:
|
||||
return False, {'error': str(errorIndication)}
|
||||
if errorStatus:
|
||||
return False, {'error': f'{errorStatus.prettyPrint()} at {errorIndex and varBinds[int(errorIndex) - 1][0] or "?"}'}
|
||||
|
||||
sys_info = {}
|
||||
for varBind in varBinds:
|
||||
oid = str(varBind[0])
|
||||
value = varBind[1].prettyPrint()
|
||||
|
||||
if SNMPService.OID_SYS_DESCR in oid:
|
||||
sys_info['sys_descr'] = value
|
||||
elif SNMPService.OID_SYS_NAME in oid:
|
||||
sys_info['sys_name'] = value
|
||||
elif SNMPService.OID_SYS_LOCATION in oid:
|
||||
sys_info['sys_location'] = value
|
||||
|
||||
return True, sys_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"SNMP 连接测试失败: {e}")
|
||||
return False, {'error': str(e)}
|
||||
|
||||
@staticmethod
|
||||
def get_arp_table(device: NetworkDevice, credential: SNMPCredential, db: Session) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取 ARP 表 (IP -> MAC 映射)
|
||||
"""
|
||||
arp_entries = []
|
||||
|
||||
try:
|
||||
auth_data = SNMPService._create_auth_data(credential)
|
||||
transport = SNMPService._get_transport_target(
|
||||
device.ip_address,
|
||||
port=device.port,
|
||||
timeout=credential.timeout,
|
||||
retries=credential.retries
|
||||
)
|
||||
|
||||
# 使用 bulkCmd 获取 ARP 表
|
||||
for errorIndication, errorStatus, errorIndex, varBinds in bulkCmd(
|
||||
SnmpEngine(), auth_data, transport, ContextData(),
|
||||
0, 50, # nonRepeaters, maxRepetitions
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IP_NET_TO_MEDIA_PHYS_ADDRESS)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IP_NET_TO_MEDIA_NET_ADDRESS)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IP_NET_TO_MEDIA_IF_INDEX)),
|
||||
lexicographicMode=False
|
||||
):
|
||||
if errorIndication:
|
||||
logger.error(f"SNMP 错误: {errorIndication}")
|
||||
break
|
||||
|
||||
if errorStatus:
|
||||
logger.error(f"SNMP 错误: {errorStatus}")
|
||||
break
|
||||
|
||||
# 解析结果
|
||||
arp_data = {}
|
||||
for varBind in varBinds:
|
||||
oid = str(varBind[0])
|
||||
value = varBind[1]
|
||||
|
||||
# 提取 IP 地址 (OID 后缀: ifIndex.IP)
|
||||
parts = oid.split('.')
|
||||
if len(parts) >= 4:
|
||||
# 从 OID 中提取 IP 地址
|
||||
ip_parts = parts[-4:]
|
||||
ip_address = '.'.join(ip_parts)
|
||||
|
||||
if SNMPService.OID_IP_NET_TO_MEDIA_PHYS_ADDRESS in oid:
|
||||
mac = SNMPService._normalize_mac(value)
|
||||
if mac:
|
||||
if ip_address not in arp_data:
|
||||
arp_data[ip_address] = {}
|
||||
arp_data[ip_address]['mac_address'] = mac
|
||||
elif SNMPService.OID_IP_NET_TO_MEDIA_IF_INDEX in oid:
|
||||
if isinstance(value, Integer32):
|
||||
if_index = int(value)
|
||||
if ip_address not in arp_data:
|
||||
arp_data[ip_address] = {}
|
||||
arp_data[ip_address]['if_index'] = if_index
|
||||
|
||||
# 转换为列表
|
||||
for ip, data in arp_data.items():
|
||||
if 'mac_address' in data:
|
||||
entry = {
|
||||
'ip_address': ip,
|
||||
'mac_address': data.get('mac_address'),
|
||||
'interface': str(data.get('if_index', '')),
|
||||
'device_id': device.id
|
||||
}
|
||||
arp_entries.append(entry)
|
||||
|
||||
# 保存到数据库
|
||||
now = datetime.utcnow()
|
||||
for entry_data in arp_entries:
|
||||
# 查找是否已存在
|
||||
existing = db.query(ARPEntry).filter(
|
||||
ARPEntry.device_id == device.id,
|
||||
ARPEntry.ip_address == entry_data['ip_address']
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
existing.mac_address = entry_data['mac_address']
|
||||
existing.last_seen = now
|
||||
else:
|
||||
arp_entry = ARPEntry(
|
||||
device_id=device.id,
|
||||
ip_address=entry_data['ip_address'],
|
||||
mac_address=entry_data['mac_address'],
|
||||
interface=entry_data['interface'],
|
||||
discovered_at=now,
|
||||
last_seen=now
|
||||
)
|
||||
db.add(arp_entry)
|
||||
|
||||
db.commit()
|
||||
|
||||
# 更新 IP 资产台账中的 MAC 地址
|
||||
for entry in arp_entries:
|
||||
from app.models.network import IPAddress as IPAddressModel
|
||||
ip_addr = db.query(IPAddressModel).filter(
|
||||
IPAddressModel.ip_address == entry['ip_address']
|
||||
).first()
|
||||
if ip_addr and not ip_addr.mac_address:
|
||||
ip_addr.mac_address = entry['mac_address']
|
||||
# 识别厂商
|
||||
ip_addr.vendor = EnhancedScanService.get_mac_vendor(entry['mac_address'])
|
||||
|
||||
db.commit()
|
||||
device.last_successful_poll = now
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取 ARP 表失败: {e}", exc_info=True)
|
||||
|
||||
device.last_polled_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return arp_entries
|
||||
|
||||
@staticmethod
|
||||
def get_mac_address_table(device: NetworkDevice, credential: SNMPCredential, db: Session, vlan_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取 MAC 地址表 (MAC -> 端口 映射)
|
||||
"""
|
||||
mac_entries = []
|
||||
|
||||
try:
|
||||
auth_data = SNMPService._create_auth_data(credential)
|
||||
transport = SNMPService._get_transport_target(
|
||||
device.ip_address,
|
||||
port=device.port,
|
||||
timeout=credential.timeout,
|
||||
retries=credential.retries
|
||||
)
|
||||
|
||||
# 获取 MAC 地址表
|
||||
for errorIndication, errorStatus, errorIndex, varBinds in bulkCmd(
|
||||
SnmpEngine(), auth_data, transport, ContextData(),
|
||||
0, 100,
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_DOT1D_TP_FDB_PORT)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_DOT1D_TP_FDB_STATUS)),
|
||||
lexicographicMode=False
|
||||
):
|
||||
if errorIndication:
|
||||
break
|
||||
|
||||
if errorStatus:
|
||||
break
|
||||
|
||||
for varBind in varBinds:
|
||||
oid = str(varBind[0])
|
||||
value = varBind[1]
|
||||
|
||||
# OID 后缀是 MAC 地址: .a.b.c.d.e.f
|
||||
parts = oid.split('.')
|
||||
if len(parts) >= 6:
|
||||
mac_parts = parts[-6:]
|
||||
mac_address = ':'.join(f'{int(p):02X}' for p in mac_parts)
|
||||
|
||||
if SNMPService.OID_DOT1D_TP_FDB_PORT in oid:
|
||||
port = int(value) if isinstance(value, Integer32) else None
|
||||
|
||||
entry = {
|
||||
'mac_address': mac_address,
|
||||
'port_number': port,
|
||||
'device_id': device.id,
|
||||
'vlan_id': vlan_id
|
||||
}
|
||||
mac_entries.append(entry)
|
||||
|
||||
# 保存到数据库
|
||||
now = datetime.utcnow()
|
||||
for entry_data in mac_entries:
|
||||
existing = db.query(MACAddressTable).filter(
|
||||
MACAddressTable.device_id == device.id,
|
||||
MACAddressTable.mac_address == entry_data['mac_address']
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
existing.port_number = entry_data['port_number']
|
||||
existing.last_seen = now
|
||||
else:
|
||||
mac_entry = MACAddressTable(
|
||||
device_id=device.id,
|
||||
mac_address=entry_data['mac_address'],
|
||||
port_number=entry_data['port_number'],
|
||||
vlan_id=vlan_id,
|
||||
discovered_at=now,
|
||||
last_seen=now
|
||||
)
|
||||
db.add(mac_entry)
|
||||
|
||||
db.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取 MAC 地址表失败: {e}", exc_info=True)
|
||||
|
||||
return mac_entries
|
||||
|
||||
@staticmethod
|
||||
def get_interfaces(device: NetworkDevice, credential: SNMPCredential, db: Session) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取设备接口信息
|
||||
"""
|
||||
interfaces = []
|
||||
|
||||
try:
|
||||
auth_data = SNMPService._create_auth_data(credential)
|
||||
transport = SNMPService._get_transport_target(
|
||||
device.ip_address,
|
||||
port=device.port,
|
||||
timeout=credential.timeout,
|
||||
retries=credential.retries
|
||||
)
|
||||
|
||||
# 获取接口信息
|
||||
interface_data = {}
|
||||
|
||||
for errorIndication, errorStatus, errorIndex, varBinds in bulkCmd(
|
||||
SnmpEngine(), auth_data, transport, ContextData(),
|
||||
0, 100,
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IF_DESCR)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IF_TYPE)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IF_MTU)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IF_SPEED)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IF_PHYS_ADDRESS)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IF_ADMIN_STATUS)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IF_OPER_STATUS)),
|
||||
lexicographicMode=False
|
||||
):
|
||||
if errorIndication or errorStatus:
|
||||
break
|
||||
|
||||
for varBind in varBinds:
|
||||
oid = str(varBind[0])
|
||||
value = varBind[1]
|
||||
parts = oid.split('.')
|
||||
if_index = parts[-1]
|
||||
|
||||
if if_index not in interface_data:
|
||||
interface_data[if_index] = {'if_index': int(if_index)}
|
||||
|
||||
if SNMPService.OID_IF_DESCR in oid:
|
||||
interface_data[if_index]['if_descr'] = value.prettyPrint()
|
||||
elif SNMPService.OID_IF_TYPE in oid:
|
||||
interface_data[if_index]['if_type'] = str(value)
|
||||
elif SNMPService.OID_IF_MTU in oid:
|
||||
interface_data[if_index]['if_mtu'] = int(value) if isinstance(value, Integer32) else None
|
||||
elif SNMPService.OID_IF_SPEED in oid:
|
||||
interface_data[if_index]['if_speed'] = int(value) if isinstance(value, (Integer32, Gauge32)) else None
|
||||
elif SNMPService.OID_IF_PHYS_ADDRESS in oid:
|
||||
interface_data[if_index]['if_phys_address'] = SNMPService._normalize_mac(value)
|
||||
elif SNMPService.OID_IF_ADMIN_STATUS in oid:
|
||||
status_map = {1: 'up', 2: 'down', 3: 'testing'}
|
||||
interface_data[if_index]['if_admin_status'] = status_map.get(int(value), 'unknown')
|
||||
elif SNMPService.OID_IF_OPER_STATUS in oid:
|
||||
status_map = {1: 'up', 2: 'down', 3: 'testing'}
|
||||
interface_data[if_index]['if_oper_status'] = status_map.get(int(value), 'unknown')
|
||||
|
||||
# 转换为列表并保存
|
||||
now = datetime.utcnow()
|
||||
for if_index, data in interface_data.items():
|
||||
interfaces.append(data)
|
||||
|
||||
existing = db.query(DeviceInterface).filter(
|
||||
DeviceInterface.device_id == device.id,
|
||||
DeviceInterface.if_index == int(if_index)
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
for key, val in data.items():
|
||||
if hasattr(existing, key):
|
||||
setattr(existing, key, val)
|
||||
existing.last_polled_at = now
|
||||
else:
|
||||
iface = DeviceInterface(
|
||||
device_id=device.id,
|
||||
if_index=int(if_index),
|
||||
if_descr=data.get('if_descr'),
|
||||
if_type=data.get('if_type'),
|
||||
if_mtu=data.get('if_mtu'),
|
||||
if_speed=data.get('if_speed'),
|
||||
if_phys_address=data.get('if_phys_address'),
|
||||
if_admin_status=data.get('if_admin_status'),
|
||||
if_oper_status=data.get('if_oper_status'),
|
||||
last_polled_at=now
|
||||
)
|
||||
db.add(iface)
|
||||
|
||||
db.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取接口信息失败: {e}", exc_info=True)
|
||||
|
||||
return interfaces
|
||||
|
||||
@staticmethod
|
||||
def create_credential(db: Session, name: str, version: SNMPVersion, **kwargs) -> SNMPCredential:
|
||||
"""创建 SNMP 凭据"""
|
||||
credential = SNMPCredential(
|
||||
name=name,
|
||||
version=version,
|
||||
**kwargs
|
||||
)
|
||||
db.add(credential)
|
||||
db.commit()
|
||||
db.refresh(credential)
|
||||
return credential
|
||||
|
||||
@staticmethod
|
||||
def create_device(db: Session, name: str, ip_address: str, credential_id: Optional[int] = None, **kwargs) -> NetworkDevice:
|
||||
"""创建网络设备"""
|
||||
device = NetworkDevice(
|
||||
name=name,
|
||||
ip_address=ip_address,
|
||||
snmp_credential_id=credential_id,
|
||||
**kwargs
|
||||
)
|
||||
db.add(device)
|
||||
db.commit()
|
||||
db.refresh(device)
|
||||
return device
|
||||
|
||||
@staticmethod
|
||||
def poll_device(db: Session, device_id: int) -> Dict[str, Any]:
|
||||
"""轮询设备,获取所有信息"""
|
||||
device = db.query(NetworkDevice).filter(NetworkDevice.id == device_id).first()
|
||||
if not device:
|
||||
return {'error': '设备不存在'}
|
||||
|
||||
if not device.snmp_credential:
|
||||
return {'error': '设备未配置 SNMP 凭据'}
|
||||
|
||||
credential = device.snmp_credential
|
||||
|
||||
result = {
|
||||
'device_id': device_id,
|
||||
'device_name': device.name,
|
||||
'arp_entries': [],
|
||||
'mac_entries': [],
|
||||
'interfaces': []
|
||||
}
|
||||
|
||||
# 获取 ARP 表
|
||||
arp_entries = SNMPService.get_arp_table(device, credential, db)
|
||||
result['arp_entries'] = arp_entries
|
||||
result['arp_count'] = len(arp_entries)
|
||||
|
||||
# 获取 MAC 地址表
|
||||
mac_entries = SNMPService.get_mac_address_table(device, credential, db)
|
||||
result['mac_entries'] = mac_entries
|
||||
result['mac_count'] = len(mac_entries)
|
||||
|
||||
# 获取接口信息
|
||||
interfaces = SNMPService.get_interfaces(device, credential, db)
|
||||
result['interfaces'] = interfaces
|
||||
result['interface_count'] = len(interfaces)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,420 @@
|
||||
from typing import Optional, List
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.models.auth import User, UserRole, UserStatus, Permission, RolePermission, RefreshToken
|
||||
from app.core.security import SecurityUtils
|
||||
|
||||
|
||||
# 默认权限项 + 角色权限映射
|
||||
DEFAULT_PERMISSIONS = [
|
||||
# 网段
|
||||
("network:view", "查看网段", "network"),
|
||||
("network:create", "创建网段", "network"),
|
||||
("network:edit", "编辑网段", "network"),
|
||||
("network:delete", "删除网段", "network"),
|
||||
# IP
|
||||
("ip:view", "查看 IP", "ip"),
|
||||
("ip:edit", "编辑 IP", "ip"),
|
||||
# 扫描
|
||||
("scan:run", "执行扫描", "scan"),
|
||||
("scan:view", "查看扫描结果", "scan"),
|
||||
# SNMP
|
||||
("snmp:view", "查看 SNMP", "snmp"),
|
||||
("snmp:edit", "配置 SNMP", "snmp"),
|
||||
# 告警
|
||||
("alert:view", "查看告警", "alert"),
|
||||
("alert:ack", "确认告警", "alert"),
|
||||
("alert:resolve", "解决告警", "alert"),
|
||||
# 报告
|
||||
("report:export", "导出报告", "report"),
|
||||
# 用户管理
|
||||
("user:view", "查看用户", "system"),
|
||||
("user:create", "创建用户", "system"),
|
||||
("user:edit", "编辑用户", "system"),
|
||||
("user:delete", "删除用户", "system"),
|
||||
# 审计
|
||||
("audit:view", "查看审计日志", "system"),
|
||||
]
|
||||
|
||||
|
||||
# 角色 -> 权限代码集合
|
||||
DEFAULT_ROLE_PERMISSIONS = {
|
||||
UserRole.SUPER_ADMIN: {p[0] for p in DEFAULT_PERMISSIONS}, # 全部
|
||||
UserRole.ADMIN: {
|
||||
"network:view", "network:create", "network:edit", "network:delete",
|
||||
"ip:view", "ip:edit",
|
||||
"scan:run", "scan:view",
|
||||
"snmp:view", "snmp:edit",
|
||||
"alert:view", "alert:ack", "alert:resolve",
|
||||
"report:export",
|
||||
"user:view", "audit:view",
|
||||
},
|
||||
UserRole.OPERATOR: {
|
||||
"network:view",
|
||||
"ip:view", "ip:edit",
|
||||
"scan:run", "scan:view",
|
||||
"snmp:view",
|
||||
"alert:view", "alert:ack",
|
||||
"report:export",
|
||||
},
|
||||
UserRole.VIEWER: {
|
||||
"network:view",
|
||||
"ip:view",
|
||||
"scan:view",
|
||||
"snmp:view",
|
||||
"alert:view",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class UserService:
|
||||
"""用户服务"""
|
||||
|
||||
@staticmethod
|
||||
def init_default_permissions(db: Session) -> int:
|
||||
"""初始化默认权限项和角色权限映射(已存在则跳过)"""
|
||||
created = 0
|
||||
perm_map = {}
|
||||
for code, name, category in DEFAULT_PERMISSIONS:
|
||||
existing = db.query(Permission).filter(Permission.code == code).first()
|
||||
if existing:
|
||||
perm_map[code] = existing
|
||||
continue
|
||||
p = Permission(code=code, name=name, category=category)
|
||||
db.add(p)
|
||||
db.flush()
|
||||
perm_map[code] = p
|
||||
created += 1
|
||||
|
||||
# 角色 -> 权限关联
|
||||
for role, codes in DEFAULT_ROLE_PERMISSIONS.items():
|
||||
for code in codes:
|
||||
if code not in perm_map:
|
||||
continue
|
||||
pid = perm_map[code].id
|
||||
link_existing = db.query(RolePermission).filter(
|
||||
RolePermission.role == role,
|
||||
RolePermission.permission_id == pid
|
||||
).first()
|
||||
if link_existing:
|
||||
continue
|
||||
db.add(RolePermission(role=role, permission_id=pid))
|
||||
|
||||
db.commit()
|
||||
return created
|
||||
|
||||
@staticmethod
|
||||
def get_user_permissions(db: Session, user: User) -> set:
|
||||
"""获取用户拥有的全部权限代码集合"""
|
||||
if user.role == UserRole.SUPER_ADMIN:
|
||||
return {p.code for p in db.query(Permission).all()}
|
||||
# 其他角色查 role_permissions 关联表
|
||||
rows = db.query(Permission.code).join(
|
||||
RolePermission, RolePermission.permission_id == Permission.id
|
||||
).filter(RolePermission.role == user.role).all()
|
||||
return {r[0] for r in rows}
|
||||
|
||||
@staticmethod
|
||||
def init_default_admin(db: Session):
|
||||
"""初始化默认管理员账户"""
|
||||
admin_exists = db.query(User).filter(User.username == "admin").first()
|
||||
if not admin_exists:
|
||||
admin = User(
|
||||
username="admin",
|
||||
email="admin@ipam.local",
|
||||
real_name="系统管理员",
|
||||
password_hash=SecurityUtils.hash_password("admin123"),
|
||||
role=UserRole.SUPER_ADMIN,
|
||||
status=UserStatus.ACTIVE
|
||||
)
|
||||
db.add(admin)
|
||||
db.commit()
|
||||
db.refresh(admin)
|
||||
return admin
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def authenticate(db: Session, username: str, password: str) -> Optional[User]:
|
||||
"""用户认证"""
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
|
||||
if not user:
|
||||
return None
|
||||
|
||||
if not SecurityUtils.verify_password(password, user.password_hash):
|
||||
# 记录登录失败
|
||||
user.login_failed_count += 1
|
||||
# 登录失败5次锁定账号
|
||||
if user.login_failed_count >= 5:
|
||||
user.status = UserStatus.LOCKED
|
||||
db.commit()
|
||||
return None
|
||||
|
||||
# 登录成功,重置失败计数
|
||||
if user.login_failed_count > 0:
|
||||
user.login_failed_count = 0
|
||||
db.commit()
|
||||
|
||||
# 检查用户状态
|
||||
if user.status != UserStatus.ACTIVE:
|
||||
return None
|
||||
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def create_user(db: Session,
|
||||
username: str,
|
||||
password: str,
|
||||
email: Optional[str] = None,
|
||||
phone: Optional[str] = None,
|
||||
real_name: Optional[str] = None,
|
||||
role: UserRole = UserRole.VIEWER,
|
||||
created_by: Optional[int] = None) -> User:
|
||||
"""创建用户"""
|
||||
# 检查用户名是否存在
|
||||
existing = db.query(User).filter(User.username == username).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="用户名已存在"
|
||||
)
|
||||
|
||||
# 检查邮箱是否存在
|
||||
if email:
|
||||
existing_email = db.query(User).filter(User.email == email).first()
|
||||
if existing_email:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="邮箱已被使用"
|
||||
)
|
||||
|
||||
user = User(
|
||||
username=username,
|
||||
email=email,
|
||||
phone=phone,
|
||||
real_name=real_name,
|
||||
password_hash=SecurityUtils.hash_password(password),
|
||||
role=role,
|
||||
status=UserStatus.ACTIVE,
|
||||
created_by=created_by
|
||||
)
|
||||
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def update_login_info(db: Session, user_id: int, ip_address: str = None):
|
||||
"""更新登录信息"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if user:
|
||||
user.last_login_at = datetime.utcnow()
|
||||
user.last_login_ip = ip_address
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def get_user_by_id(db: Session, user_id: int) -> Optional[User]:
|
||||
"""根据ID获取用户"""
|
||||
return db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
@staticmethod
|
||||
def get_user_by_username(db: Session, username: str) -> Optional[User]:
|
||||
"""根据用户名获取用户"""
|
||||
return db.query(User).filter(User.username == username).first()
|
||||
|
||||
@staticmethod
|
||||
def get_users(db: Session, skip: int = 0, limit: int = 50, status: UserStatus = None, role: UserRole = None) -> tuple[int, List[User]]:
|
||||
"""获取用户列表"""
|
||||
query = db.query(User)
|
||||
|
||||
if status:
|
||||
query = query.filter(User.status == status)
|
||||
if role:
|
||||
query = query.filter(User.role == role)
|
||||
|
||||
total = query.count()
|
||||
items = query.order_by(User.id.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
return total, items
|
||||
|
||||
@staticmethod
|
||||
def change_password(db: Session, user_id: int, old_password: str, new_password: str) -> bool:
|
||||
"""修改密码"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return False
|
||||
|
||||
if not SecurityUtils.verify_password(old_password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="原密码不正确"
|
||||
)
|
||||
|
||||
user.password_hash = SecurityUtils.hash_password(new_password)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def reset_password(db: Session, user_id: int, new_password: str) -> bool:
|
||||
"""管理员重置密码"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return False
|
||||
|
||||
user.password_hash = SecurityUtils.hash_password(new_password)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def update_user_status(db: Session, user_id: int, status: UserStatus) -> Optional[User]:
|
||||
"""更新用户状态"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return None
|
||||
|
||||
user.status = status
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def delete_user(db: Session, user_id: int) -> bool:
|
||||
"""删除用户(软删除,实际标记为禁用)"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return False
|
||||
|
||||
user.status = UserStatus.INACTIVE
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def unlock_user(db: Session, user_id: int) -> Optional[User]:
|
||||
"""解锁被锁定的用户"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return None
|
||||
|
||||
if user.status == UserStatus.LOCKED:
|
||||
user.status = UserStatus.ACTIVE
|
||||
user.login_failed_count = 0
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class TokenService:
|
||||
"""令牌服务"""
|
||||
|
||||
@staticmethod
|
||||
def create_tokens(db: Session, user: User, ip_address: str = None, user_agent: str = None) -> dict:
|
||||
"""创建访问令牌和刷新令牌"""
|
||||
access_token_expires = timedelta(minutes=120) # 访问令牌2小时
|
||||
access_token = SecurityUtils.create_access_token(
|
||||
data={
|
||||
"sub": user.username,
|
||||
"user_id": user.id,
|
||||
"role": user.role.value
|
||||
},
|
||||
expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
refresh_token_expires = timedelta(days=7) # 刷新令牌7天
|
||||
refresh_token_value = SecurityUtils.create_refresh_token(
|
||||
data={
|
||||
"sub": user.username,
|
||||
"user_id": user.id
|
||||
},
|
||||
expires_delta=refresh_token_expires
|
||||
)
|
||||
|
||||
# 保存刷新令牌
|
||||
refresh_token = RefreshToken(
|
||||
user_id=user.id,
|
||||
token=refresh_token_value,
|
||||
expires_at=datetime.utcnow() + refresh_token_expires,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent
|
||||
)
|
||||
db.add(refresh_token)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token_value,
|
||||
"token_type": "bearer",
|
||||
"expires_in": int(access_token_expires.total_seconds())
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def refresh_access_token(db: Session, refresh_token: str) -> Optional[dict]:
|
||||
"""使用刷新令牌获取新的访问令牌"""
|
||||
try:
|
||||
payload = SecurityUtils.verify_token(refresh_token)
|
||||
username = payload.get("sub")
|
||||
user_id = payload.get("user_id")
|
||||
|
||||
# 检查刷新令牌是否有效且未被撤销
|
||||
token_record = db.query(RefreshToken).filter(
|
||||
RefreshToken.token == refresh_token,
|
||||
RefreshToken.user_id == user_id,
|
||||
RefreshToken.revoked == False,
|
||||
RefreshToken.expires_at > datetime.utcnow()
|
||||
).first()
|
||||
|
||||
if not token_record:
|
||||
return None
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user or user.status != UserStatus.ACTIVE:
|
||||
return None
|
||||
|
||||
# 创建新的访问令牌
|
||||
access_token_expires = timedelta(minutes=120)
|
||||
access_token = SecurityUtils.create_access_token(
|
||||
data={
|
||||
"sub": user.username,
|
||||
"user_id": user.id,
|
||||
"role": user.role.value
|
||||
},
|
||||
expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": int(access_token_expires.total_seconds())
|
||||
}
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def revoke_token(db: Session, refresh_token: str) -> bool:
|
||||
"""撤销刷新令牌"""
|
||||
token_record = db.query(RefreshToken).filter(RefreshToken.token == refresh_token).first()
|
||||
if token_record:
|
||||
token_record.revoked = True
|
||||
token_record.revoked_at = datetime.utcnow()
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def revoke_all_user_tokens(db: Session, user_id: int) -> int:
|
||||
"""撤销用户所有刷新令牌"""
|
||||
tokens = db.query(RefreshToken).filter(
|
||||
RefreshToken.user_id == user_id,
|
||||
RefreshToken.revoked == False
|
||||
).all()
|
||||
|
||||
for token in tokens:
|
||||
token.revoked = True
|
||||
token.revoked_at = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
return len(tokens)
|
||||
@@ -0,0 +1,4 @@
|
||||
# app/tasks/__init__.py
|
||||
from app.tasks.celery_app import celery_app
|
||||
|
||||
__all__ = ['celery_app']
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,55 @@
|
||||
from celery import Celery
|
||||
from celery.schedules import crontab
|
||||
from app.core.config import settings
|
||||
|
||||
# 创建 Celery 应用
|
||||
celery_app = Celery(
|
||||
'ipam_tasks',
|
||||
broker=settings.CELERY_BROKER_URL,
|
||||
backend=settings.CELERY_RESULT_BACKEND
|
||||
)
|
||||
|
||||
# 配置
|
||||
celery_app.conf.update(
|
||||
task_serializer='json',
|
||||
accept_content=['json'],
|
||||
result_serializer='json',
|
||||
timezone='Asia/Shanghai',
|
||||
enable_utc=True,
|
||||
task_track_started=True,
|
||||
task_time_limit=30 * 60, # 任务超时30分钟
|
||||
worker_prefetch_multiplier=1,
|
||||
worker_max_tasks_per_child=1000,
|
||||
)
|
||||
|
||||
# 必须在配置后导入并注册任务
|
||||
from app.tasks.scan_tasks import register_tasks
|
||||
_tasks = register_tasks(celery_app)
|
||||
|
||||
# 导出任务函数
|
||||
scan_network_task = _tasks['scan_network_task']
|
||||
scan_single_ip_task = _tasks['scan_single_ip_task']
|
||||
full_network_scan = _tasks['full_network_scan']
|
||||
update_all_statistics = _tasks['update_all_statistics']
|
||||
quick_status_update = _tasks['quick_status_update']
|
||||
|
||||
# 定时任务配置
|
||||
celery_app.conf.beat_schedule = {
|
||||
# 每小时执行一次全量扫描
|
||||
'full-scan-every-hour': {
|
||||
'task': 'app.tasks.scan_tasks.full_network_scan',
|
||||
'schedule': 3600.0, # 每小时
|
||||
'args': (True, True, True) # ping, arp, dns
|
||||
},
|
||||
# 每天凌晨2点执行一次完整扫描
|
||||
'full-scan-daily': {
|
||||
'task': 'app.tasks.scan_tasks.full_network_scan',
|
||||
'schedule': crontab(hour=2, minute=0),
|
||||
'args': (True, True, True)
|
||||
},
|
||||
# 每15分钟更新一次统计
|
||||
'update-stats-every-15min': {
|
||||
'task': 'app.tasks.scan_tasks.update_all_statistics',
|
||||
'schedule': 900.0, # 15分钟
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
from typing import List, Dict, Any, Optional
|
||||
from celery import Task
|
||||
import logging
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.services.network_service import NetworkService
|
||||
from app.services.enhanced_scan_service import EnhancedScanService
|
||||
from app.services.scan_service import ScanService
|
||||
from app.models.network import TaskStatus, ScanTask, IPAddress
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register_tasks(celery_app):
|
||||
"""
|
||||
注册任务到 Celery 应用
|
||||
"""
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def scan_network_task(self, network_id: int,
|
||||
enable_ping: bool = True,
|
||||
enable_arp: bool = True,
|
||||
enable_dns: bool = True,
|
||||
timeout: int = 2):
|
||||
"""
|
||||
Celery 异步任务:扫描指定网段
|
||||
"""
|
||||
task_id = self.request.id
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# 获取网段信息
|
||||
network = NetworkService.get_by_id(db, network_id)
|
||||
if not network:
|
||||
logger.error(f"网段 {network_id} 不存在")
|
||||
return {'status': 'failed', 'error': '网段不存在'}
|
||||
|
||||
logger.info(f"开始扫描网段: {network.cidr}")
|
||||
|
||||
# 更新任务状态为运行中
|
||||
# 查找已存在的任务记录
|
||||
db_task = db.query(ScanTask).filter(ScanTask.celery_task_id == task_id).first()
|
||||
if not db_task:
|
||||
db_task = ScanService.create_scan_task(db, 'ping', network_id)
|
||||
db_task.celery_task_id = task_id
|
||||
db.commit()
|
||||
db.refresh(db_task)
|
||||
|
||||
ScanService.update_task_status(db, 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)
|
||||
|
||||
# 更新任务状态
|
||||
ScanService.update_task_status(
|
||||
db,
|
||||
db_task.id,
|
||||
TaskStatus.COMPLETED,
|
||||
progress=100,
|
||||
total_count=stats['total_scanned'],
|
||||
success_count=stats['online_count']
|
||||
)
|
||||
|
||||
logger.info(f"网段 {network.cidr} 扫描完成: 在线 {stats['online_count']} 台, "
|
||||
f"发现MAC {stats['mac_found_count']} 个, 主机名 {stats['hostname_found_count']} 个")
|
||||
|
||||
return {
|
||||
'status': 'completed',
|
||||
'network_id': network_id,
|
||||
'cidr': network.cidr,
|
||||
'statistics': stats
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"扫描失败: {str(e)}", exc_info=True)
|
||||
if 'db_task' in locals() and db_task:
|
||||
ScanService.update_task_status(
|
||||
db,
|
||||
db_task.id,
|
||||
TaskStatus.FAILED,
|
||||
error_message=str(e)
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def scan_single_ip_task(self, ip_address: str,
|
||||
enable_ping: bool = True,
|
||||
enable_arp: bool = True,
|
||||
enable_dns: bool = True):
|
||||
"""
|
||||
Celery 异步任务:扫描单个IP
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
result = EnhancedScanService.comprehensive_scan(
|
||||
ip_address,
|
||||
enable_ping=enable_ping,
|
||||
enable_arp=enable_arp,
|
||||
enable_dns=enable_dns
|
||||
)
|
||||
|
||||
# 更新IP信息
|
||||
EnhancedScanService.update_ip_from_scan_result(db, result)
|
||||
db.commit()
|
||||
return result
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def full_network_scan(self, enable_ping: bool = True,
|
||||
enable_arp: bool = True,
|
||||
enable_dns: bool = True):
|
||||
"""
|
||||
Celery 定时任务:扫描所有网段
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
_, all_networks = NetworkService.get_list(db, limit=1000)
|
||||
|
||||
total_stats = {
|
||||
'total_networks': len(all_networks),
|
||||
'total_ips': 0,
|
||||
'online_count': 0,
|
||||
'mac_found_count': 0,
|
||||
'hostname_found_count': 0
|
||||
}
|
||||
|
||||
logger.info(f"开始全量扫描: {len(all_networks)} 个网段")
|
||||
|
||||
for network in all_networks:
|
||||
try:
|
||||
results = EnhancedScanService.scan_network(
|
||||
network.cidr,
|
||||
enable_ping=enable_ping,
|
||||
enable_arp=enable_arp,
|
||||
enable_dns=enable_dns
|
||||
)
|
||||
|
||||
stats = EnhancedScanService.bulk_update_ips_from_scan(db, results)
|
||||
|
||||
total_stats['total_ips'] += stats['total_scanned']
|
||||
total_stats['online_count'] += stats['online_count']
|
||||
total_stats['mac_found_count'] += stats['mac_found_count']
|
||||
total_stats['hostname_found_count'] += stats['hostname_found_count']
|
||||
|
||||
logger.info(f"网段 {network.cidr} 扫描完成: 在线 {stats['online_count']} 台")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"网段 {network.cidr} 扫描失败: {str(e)}")
|
||||
|
||||
logger.info(f"全量扫描完成: {total_stats}")
|
||||
|
||||
return {
|
||||
'status': 'completed',
|
||||
'statistics': total_stats
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def update_all_statistics(self):
|
||||
"""
|
||||
定时任务:更新所有网段统计信息
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
NetworkService.get_all_stats(db)
|
||||
logger.info("所有网段统计信息已更新")
|
||||
return {'status': 'completed'}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def quick_status_update(self, ip_addresses: List[str]):
|
||||
"""
|
||||
快速更新多个IP的状态(用于实时扫描)
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
results = []
|
||||
|
||||
for ip in ip_addresses:
|
||||
result = EnhancedScanService.comprehensive_scan(ip, enable_ping=True, enable_arp=True, enable_dns=False)
|
||||
EnhancedScanService.update_ip_from_scan_result(db, result)
|
||||
results.append(result)
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
'scanned_count': len(ip_addresses),
|
||||
'online_count': sum(1 for r in results if r.get('success')),
|
||||
'results': results
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# 返回任务函数供外部使用
|
||||
return {
|
||||
'scan_network_task': scan_network_task,
|
||||
'scan_single_ip_task': scan_single_ip_task,
|
||||
'full_network_scan': full_network_scan,
|
||||
'update_all_statistics': update_all_statistics,
|
||||
'quick_status_update': quick_status_update
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
fastapi==0.110.0
|
||||
uvicorn[standard]==0.27.1
|
||||
sqlalchemy==2.0.28
|
||||
pymysql==1.1.0
|
||||
cryptography==42.0.5
|
||||
pydantic==2.6.4
|
||||
pydantic-settings==2.2.1
|
||||
celery==5.3.6
|
||||
redis==5.0.3
|
||||
python-multipart==0.0.9
|
||||
alembic==1.13.1
|
||||
pysnmp==7.1.15
|
||||
scapy==2.5.0
|
||||
python-dotenv==1.0.1
|
||||
passlib[bcrypt]==1.7.4
|
||||
httpx==0.27.0
|
||||
Reference in New Issue
Block a user