feat: 综合平台services多服务支持+SW网络优先缓存修复+适配层/定时任务完善

This commit is contained in:
gouki
2026-08-05 11:57:17 +00:00
parent 7f1268f508
commit 1b7d4823c3
47 changed files with 2895 additions and 1189 deletions
+43 -4
View File
@@ -1,17 +1,53 @@
"""Agent 上报接收路由(数据写入 metrics.db)"""
from datetime import datetime
import threading
import time
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.core.config import settings
from app.core.security import require_agent_key
from app.database import get_metrics_session
from app.core.timeutils import utcnow
from app.database import assets_engine, get_metrics_session
from app.models.asset import Asset
from app.models.monitor import EventLog, MetricPoint, SecurityCheck, ServerInfo
from app.schemas.agent import AgentReport
router = APIRouter(prefix="/api/agent", tags=["agent"])
# 内存级频率限制:{asset_id: 上次上报的 monotonic 时间戳}
# 同步路由运行在线程池,多线程并发读写需加锁保证“读-判断-写”原子性
_last_report_ts: dict = {}
_rate_limit_lock = threading.Lock()
def _check_rate_limit(asset_id: int) -> None:
"""限制同一资产的上报频率,防止配置错误的 Agent 高频写入填满数据库"""
interval = settings.AGENT_REPORT_MIN_INTERVAL
if interval <= 0:
return
now = time.monotonic()
with _rate_limit_lock:
last = _last_report_ts.get(asset_id)
if last is not None and (now - last) < interval:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail=f"上报过于频繁,同一资产最小间隔 {interval}",
)
_last_report_ts[asset_id] = now
def _validate_asset_id(asset_id: int) -> None:
"""校验上报的 asset_id 在资产库中真实存在,防止脏数据写入"""
with Session(assets_engine) as session:
asset = session.get(Asset, asset_id)
if not asset:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"asset_id={asset_id} 不存在,请先在资产列表中登记该服务器",
)
@router.post(
"/report",
@@ -19,6 +55,9 @@ router = APIRouter(prefix="/api/agent", tags=["agent"])
dependencies=[Depends(require_agent_key)],
)
def report(data: AgentReport, session: Session = Depends(get_metrics_session)) -> dict:
_validate_asset_id(data.asset_id)
_check_rate_limit(data.asset_id)
# 资源监控时序
if data.metrics:
session.add(MetricPoint(asset_id=data.asset_id, **data.metrics.model_dump()))
@@ -26,7 +65,7 @@ def report(data: AgentReport, session: Session = Depends(get_metrics_session)) -
# 服务器信息快照(每资产一条,覆盖更新)
if data.server_info:
info = data.server_info.model_dump()
info["last_seen"] = datetime.utcnow()
info["last_seen"] = utcnow()
existing = session.exec(
select(ServerInfo).where(ServerInfo.asset_id == data.asset_id)
).first()
+1 -1
View File
@@ -32,7 +32,7 @@ def list_assets(
)
@router.get("/export", summary="导出所有资产(数据迁移/备份)")
@router.get("/export", summary="导出所有资产(数据迁移/备份)", dependencies=[Depends(require_api_key)])
def export_assets(session: Session = Depends(get_session)) -> dict:
return asset_service.export_assets(session)
+15 -1
View File
@@ -5,9 +5,10 @@ from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlmodel import Session, select
from app.core.security import require_api_key
from app.database import get_metrics_session
from app.models.monitor import MetricPoint, SecurityCheck, ServerInfo
from app.services import security_service
from app.services import cleanup_service, security_service
router = APIRouter(prefix="/api/monitor", tags=["monitor"])
@@ -17,6 +18,19 @@ def security_overview(session: Session = Depends(get_metrics_session)):
return security_service.get_security_overview(session)
@router.post(
"/cleanup",
summary="手动触发监控数据清理(按保留周期删除过期数据)",
dependencies=[Depends(require_api_key)],
)
def cleanup(
metrics_days: Optional[int] = Query(default=None, description="时序数据保留天数(默认取配置)"),
security_days: Optional[int] = Query(default=None, description="安全检查保留天数(默认取配置)"),
event_log_days: Optional[int] = Query(default=None, description="事件日志保留天数(默认取配置)"),
):
return cleanup_service.cleanup_metrics(metrics_days, security_days, event_log_days)
@router.get("/{asset_id}/metrics", summary="资源监控时序(倒序,最新在前)")
def metrics(
asset_id: int,
+148
View File
@@ -0,0 +1,148 @@
"""SSL 监控路由:子域名管理与站点证书监控
- /api/subdomains:域名资产下的子域名 CRUD
- /api/site-certshttps 站点证书的探测与监控
写操作(POST/PUT/DELETE)受 API Key 保护。
"""
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session
from app.core.security import require_api_key
from app.database import get_session
from app.schemas.ssl import SiteCertCreate, SubdomainCreate, SubdomainUpdate
from app.services import ssl_service
router = APIRouter(prefix="/api", tags=["ssl"])
def _guard(exc: ValueError) -> HTTPException:
"""service 抛出的 ValueError 统一转 404"""
return HTTPException(status_code=404, detail=str(exc))
# ---------------- 子域名 ----------------
@router.get("/subdomains", summary="子域名列表(可按域名资产筛选)")
def list_subdomains(
asset_id: Optional[int] = Query(default=None, description="按域名资产 ID 筛选"),
session: Session = Depends(get_session),
) -> list:
return ssl_service.list_subdomains(session, asset_id)
@router.post(
"/subdomains",
status_code=201,
summary="创建子域名",
dependencies=[Depends(require_api_key)],
)
def create_subdomain(data: SubdomainCreate, session: Session = Depends(get_session)) -> dict:
sub = ssl_service.create_subdomain(session, data.model_dump())
return {
"id": sub.id,
"asset_id": sub.asset_id,
"host": sub.host,
"record_type": sub.record_type,
"record_value": sub.record_value,
"is_active": sub.is_active,
"note": sub.note,
}
@router.put(
"/subdomains/{sub_id}",
summary="更新子域名",
dependencies=[Depends(require_api_key)],
)
def update_subdomain(sub_id: int, data: SubdomainUpdate, session: Session = Depends(get_session)) -> dict:
try:
sub = ssl_service.update_subdomain(session, sub_id, data.model_dump(exclude_unset=True))
except ValueError as e:
raise _guard(e) from e
return {
"id": sub.id,
"asset_id": sub.asset_id,
"host": sub.host,
"record_type": sub.record_type,
"record_value": sub.record_value,
"is_active": sub.is_active,
"note": sub.note,
}
@router.delete(
"/subdomains/{sub_id}",
status_code=204,
summary="删除子域名",
dependencies=[Depends(require_api_key)],
)
def delete_subdomain(sub_id: int, session: Session = Depends(get_session)):
try:
ssl_service.delete_subdomain(session, sub_id)
except ValueError as e:
raise _guard(e) from e
# ---------------- 站点证书监控 ----------------
@router.get("/site-certs", summary="站点证书监控列表(可按状态/资产筛选)")
def list_site_certs(
status: Optional[str] = Query(default=None, description="按状态筛选"),
asset_id: Optional[int] = Query(default=None, description="按关联资产筛选"),
session: Session = Depends(get_session),
) -> list:
return ssl_service.list_site_certs(session, status, asset_id)
@router.post(
"/site-certs",
status_code=201,
summary="创建证书探测目标(创建后立即探测一次)",
dependencies=[Depends(require_api_key)],
)
def create_site_cert(data: SiteCertCreate, session: Session = Depends(get_session)) -> dict:
try:
cert = ssl_service.create_site_cert(
session, data.hostname, data.port, data.asset_id
)
except ValueError as e:
raise _guard(e) from e
return ssl_service._to_dict(cert)
@router.post(
"/site-certs/check-all",
summary="全量重新探测所有站点证书",
dependencies=[Depends(require_api_key)],
)
def check_all_site_certs(session: Session = Depends(get_session)) -> dict:
return ssl_service.check_all_site_certs(session)
@router.post(
"/site-certs/{cert_id}/check",
summary="重新探测单条站点证书",
dependencies=[Depends(require_api_key)],
)
def check_one_site_cert(cert_id: int, session: Session = Depends(get_session)) -> dict:
cert = session.get(ssl_service.SiteCert, cert_id)
if not cert:
raise HTTPException(status_code=404, detail=f"证书监控记录不存在(id={cert_id}")
ssl_service.check_one(session, cert)
return ssl_service._to_dict(cert)
@router.delete(
"/site-certs/{cert_id}",
status_code=204,
summary="删除证书探测目标",
dependencies=[Depends(require_api_key)],
)
def delete_site_cert(cert_id: int, session: Session = Depends(get_session)):
try:
ssl_service.delete_site_cert(session, cert_id)
except ValueError as e:
raise _guard(e) from e