feat: 综合平台services多服务支持+SW网络优先缓存修复+适配层/定时任务完善
This commit is contained in:
+43
-4
@@ -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()
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
"""SSL 监控路由:子域名管理与站点证书监控
|
||||
|
||||
- /api/subdomains:域名资产下的子域名 CRUD
|
||||
- /api/site-certs:https 站点证书的探测与监控
|
||||
写操作(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
|
||||
Reference in New Issue
Block a user