9c9a51263c
汇总未提交工作区:Servers 列表 site_url 链接、终端页与选机器 UI、SSH 连通性与 Telegram 通知、agent/offline/unset_path 后台调度与 gate_ai_review 门控;更新 AGENTS 与 agent-exploration 规则(仅本机 commit);同步 web/app 构建产物与相关文档。
111 lines
3.7 KiB
Python
111 lines
3.7 KiB
Python
"""Scheduled nightly install-agent for servers without Agent reporting."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from server.application.services.unset_path_detect_schedule import (
|
|
SCHEDULED_OPERATOR,
|
|
server_ssh_configured,
|
|
)
|
|
from server.config import settings
|
|
from server.infrastructure.database.server_repo import ServerRepositoryImpl
|
|
from server.infrastructure.database.session import AsyncSessionLocal
|
|
from server.infrastructure.redis import server_batch_store as sbs
|
|
from server.infrastructure.redis.client import get_redis
|
|
from server.utils.display_time import to_beijing
|
|
|
|
logger = logging.getLogger("nexus.agent_install_schedule")
|
|
|
|
REDIS_LAST_RUN_KEY = "nexus:agent_install_schedule:last_run_date"
|
|
|
|
|
|
def _enabled() -> bool:
|
|
return str(getattr(settings, "AGENT_INSTALL_SCHEDULE_ENABLED", "true")).lower() == "true"
|
|
|
|
|
|
def _cron_expr() -> str:
|
|
expr = (getattr(settings, "AGENT_INSTALL_SCHEDULE_CRON", None) or "40 23 * * *").strip()
|
|
return expr or "40 23 * * *"
|
|
|
|
|
|
async def list_no_agent_server_ids(*, ssh_only: bool = True) -> list[int]:
|
|
"""Servers with empty DB agent_version (not yet installed / reporting)."""
|
|
async with AsyncSessionLocal() as session:
|
|
repo = ServerRepositoryImpl(session)
|
|
servers, _total = await repo.get_filtered(agent_not_installed=True, max_rows=5000)
|
|
ids: list[int] = []
|
|
for server in servers:
|
|
if ssh_only and not server_ssh_configured(server):
|
|
continue
|
|
ids.append(server.id)
|
|
return ids
|
|
|
|
|
|
async def _install_agent_batch_running() -> bool:
|
|
for live in await sbs.list_running_live_jobs():
|
|
if live.get("op") == "install-agent":
|
|
return True
|
|
return False
|
|
|
|
|
|
async def _mark_ran_today() -> None:
|
|
today = to_beijing(datetime.now(timezone.utc)).strftime("%Y-%m-%d")
|
|
redis = get_redis()
|
|
await redis.set(REDIS_LAST_RUN_KEY, today, ex=86400 * 2)
|
|
|
|
|
|
async def _already_ran_today() -> bool:
|
|
today = to_beijing(datetime.now(timezone.utc)).strftime("%Y-%m-%d")
|
|
redis = get_redis()
|
|
last = await redis.get(REDIS_LAST_RUN_KEY)
|
|
if last is None:
|
|
return False
|
|
if isinstance(last, bytes):
|
|
last = last.decode()
|
|
return str(last).strip() == today
|
|
|
|
|
|
async def run_scheduled_agent_install(*, force: bool = False) -> dict[str, Any]:
|
|
"""Start batch install-agent for servers without Agent; once per Beijing day."""
|
|
if not _enabled() and not force:
|
|
return {"action": "skipped", "reason": "disabled"}
|
|
|
|
if not force and await _already_ran_today():
|
|
return {"action": "skipped", "reason": "already_ran_today"}
|
|
|
|
base_url = (settings.API_BASE_URL or "").strip()
|
|
if not base_url:
|
|
return {"action": "skipped", "reason": "api_base_url_unconfigured"}
|
|
|
|
if await _install_agent_batch_running():
|
|
return {"action": "skipped", "reason": "install_agent_batch_running"}
|
|
|
|
server_ids = await list_no_agent_server_ids()
|
|
if not server_ids:
|
|
await _mark_ran_today()
|
|
return {"action": "skipped", "reason": "no_servers_without_agent", "server_count": 0}
|
|
|
|
from server.application.services.server_batch_service import start_batch_job
|
|
|
|
started = await start_batch_job(
|
|
op="install-agent",
|
|
server_ids=server_ids,
|
|
operator=SCHEDULED_OPERATOR,
|
|
params={"scheduled": True, "cron": _cron_expr()},
|
|
)
|
|
await _mark_ran_today()
|
|
logger.info(
|
|
"Scheduled agent install started job_id=%s servers=%s",
|
|
started.get("job_id"),
|
|
len(server_ids),
|
|
)
|
|
return {
|
|
"action": "started",
|
|
"job_id": started.get("job_id"),
|
|
"server_count": len(server_ids),
|
|
"label": started.get("label"),
|
|
}
|