提交 65a88a6d authored 作者: 陈泽健's avatar 陈泽健

feat(service-monitor): 定时自动巡检功能(APScheduler + cron 表达式)

- 新增 schedule_service.py:定时任务 CRUD + APScheduler BackgroundScheduler 集成
- 支持 cron 表达式校验(croniter)+ 下次执行时间计算
- 新增 runner_service.run_inspection_sync() 同步执行函数
- 新增 schedule.html 定时任务管理页:表格展示 + 新增/编辑弹窗 + Cron 预设
- 新增 API 路由:schedules CRUD + toggle 启停
- server.py 初始化 APScheduler
- upload_to_server.py 增加 apscheduler 依赖
- PRD 需求文档 + 计划执行文档
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 f3893286
......@@ -67,8 +67,8 @@ _EXCLUDE_SUFFIXES = ('.pyc', '.pyo')
# 需要自动转换 CRLF→LF 的后缀(bash 脚本在 Linux 服务器执行,不能有 \r)
_UNIX_LINE_END_SUFFIXES = ('.sh', '.template')
# 服务监测模块需要的 Python 依赖(SSH 远程执行)
_REQUIRED_PACKAGES = ['paramiko', 'cryptography']
# 服务监测模块需要的 Python 依赖(SSH 远程执行 + 定时调度
_REQUIRED_PACKAGES = ['paramiko', 'cryptography', 'apscheduler']
def _check_and_install_deps(ssh):
......
......@@ -100,6 +100,10 @@ def create_app():
app.register_blueprint(export_bp)
app.register_blueprint(submit_bp)
# 初始化服务监测定时调度
from service_monitor.services import schedule_service
schedule_service.init_scheduler(app)
return app
......
......@@ -3,3 +3,4 @@
from . import target_service
from . import report_service
from . import runner_service
from . import schedule_service
......@@ -193,3 +193,95 @@ def run_inspection(target_id: str, suite: str):
logger.warning("执行器清理失败: %s", e)
# 延迟移除运行记录,给客户端最后取状态的机会
time.sleep(0)
def run_inspection_sync(target_id: str, suite: str) -> dict:
"""同步执行巡检(定时任务调用),返回结果摘要。
与 run_inspection() 生成器共享核心逻辑,但不 yield SSE 事件。
返回: {"success": bool, "report_id": str, "summary": dict, "finished_at": str}
"""
from datetime import datetime as _dt
run_id = _dt.now().strftime("%Y%m%d_%H%M%S_") + "sched"
target = target_service.get_target(target_id)
if not target:
return {"success": False, "report_id": "", "summary": {}, "finished_at": _now()}
modules = get_suite(suite)
if not modules:
return {"success": False, "report_id": "", "summary": {}, "finished_at": _now()}
_register_run(run_id, target_id, suite, len(modules))
started_at = _now()
# 准备 config(含解密后的凭据)
target_for_render = dict(target)
target_for_render["credential_overrides"] = target_service.resolve_credentials(target)
config_text = render_config(target_for_render)
executor = None
modules_result = []
try:
executor = target_service.make_executor(target, run_id)
# 连通性检查(远程)
if not executor.is_local:
if not executor.test_connection():
_update_run(run_id, finished=True, error="SSH 连接失败")
return {"success": False, "report_id": "", "summary": {}, "finished_at": _now()}
executor.setup()
executor.upload_assets()
for idx, module in enumerate(modules):
# 检查取消
run = _pop_run(run_id)
if run and run["cancel"].is_set():
_update_run(run_id, finished=True)
return {"success": False, "report_id": "", "summary": {}, "finished_at": _now()}
_update_run(run_id, current=module.name, done=idx)
raw = executor.run_module(module, config_text, timeout=90)
items = parse_module_output(raw, module.id, module.category)
summary = summarize(items)
module_result = {
"id": module.id,
"name": module.name,
"category": module.category,
"items": [{"key": it.key, "name": it.name, "value": it.value,
"threshold": it.threshold, "status": it.status}
for it in items],
"summary": summary,
}
modules_result.append(module_result)
_update_run(run_id, done=idx + 1)
report_id = report_service.save(target, suite, modules_result, started_at, _now())
_update_run(run_id, finished=True)
# 汇总
total_summary = {k: 0 for k in ("正常", "警告", "严重", "total")}
for m in modules_result:
ms = m["summary"]
for k in total_summary:
total_summary[k] += ms.get(k, 0)
return {
"success": True,
"report_id": report_id,
"summary": total_summary,
"finished_at": _now(),
}
except Exception as e:
logger.error("同步巡检执行异常: %s\n%s", e, traceback.format_exc())
_update_run(run_id, finished=True, error=str(e))
return {"success": False, "report_id": "", "summary": {}, "finished_at": _now()}
finally:
if executor:
try:
executor.cleanup()
except Exception as e:
logger.warning("执行器清理失败: %s", e)
......@@ -22,6 +22,7 @@ CONFIG_TEMPLATE = ASSETS_DIR / "config.sh.template"
DATA_DIR = MODULE_DIR / "data"
REPORTS_DIR = DATA_DIR / "reports"
TARGETS_FILE = DATA_DIR / "targets.json"
SCHEDULES_FILE = DATA_DIR / "schedules.json"
def ensure_dirs() -> None:
......
此差异已折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论