82426b1941
新增侧栏「宝塔面板」菜单、代理 API 与连接配置页;通过 SSH 读写子机 api.json 自动写入凭据,5 分钟后台重试,含审计修复、检测缓存与并发锁。 Co-authored-by: Cursor <cursoragent@cursor.com>
129 lines
4.4 KiB
Python
129 lines
4.4 KiB
Python
"""Fire-and-forget and background tick for Baota SSH API bootstrap."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
|
|
from server.config import settings
|
|
from server.infrastructure.btpanel.bootstrap_state import (
|
|
is_eligible_for_background_bootstrap,
|
|
mark_bootstrap_pending,
|
|
_parse_iso,
|
|
)
|
|
from server.infrastructure.btpanel.credentials import bt_panel_configured
|
|
from server.infrastructure.database.server_repo import ServerRepositoryImpl
|
|
from server.infrastructure.database.session import AsyncSessionLocal
|
|
|
|
logger = logging.getLogger("nexus.btpanel.bootstrap_schedule")
|
|
|
|
_BG_TASKS: set[asyncio.Task] = set()
|
|
|
|
|
|
def is_auto_bootstrap_enabled() -> bool:
|
|
val = (getattr(settings, "BT_PANEL_AUTO_BOOTSTRAP_ENABLED", None) or "true").strip().lower()
|
|
return val not in ("false", "0", "no", "off")
|
|
|
|
|
|
def bootstrap_batch_size() -> int:
|
|
raw = getattr(settings, "BT_PANEL_BOOTSTRAP_BATCH", 20)
|
|
try:
|
|
n = int(raw)
|
|
except (TypeError, ValueError):
|
|
n = 20
|
|
return max(1, min(n, 100))
|
|
|
|
|
|
async def mark_servers_bootstrap_pending(server_ids: list[int]) -> None:
|
|
ids = [int(x) for x in server_ids if x]
|
|
if not ids:
|
|
return
|
|
async with AsyncSessionLocal() as session:
|
|
repo = ServerRepositoryImpl(session)
|
|
for sid in ids:
|
|
server = await repo.get_by_id(sid)
|
|
if not server or bt_panel_configured(server):
|
|
continue
|
|
server.extra_attrs = mark_bootstrap_pending(
|
|
server.extra_attrs if isinstance(server.extra_attrs, dict) else {},
|
|
)
|
|
await repo.update(server)
|
|
|
|
|
|
async def schedule_immediate_bootstrap(server_ids: list[int], *, source: str = "immediate") -> None:
|
|
"""Mark pending and try once without waiting for the 5-minute loop."""
|
|
if not is_auto_bootstrap_enabled():
|
|
return
|
|
ids = [int(x) for x in server_ids if x]
|
|
if not ids:
|
|
return
|
|
await mark_servers_bootstrap_pending(ids)
|
|
for sid in ids:
|
|
task = asyncio.create_task(_bootstrap_one_background(sid, source=source))
|
|
_BG_TASKS.add(task)
|
|
task.add_done_callback(_BG_TASKS.discard)
|
|
|
|
|
|
async def _bootstrap_one_background(server_id: int, *, source: str) -> None:
|
|
from server.application.services.btpanel_service import BtPanelService
|
|
|
|
try:
|
|
async with AsyncSessionLocal() as session:
|
|
svc = BtPanelService(session)
|
|
await svc.bootstrap_server(
|
|
server_id,
|
|
source=source,
|
|
admin_username="system",
|
|
ip_address=None,
|
|
)
|
|
except Exception as exc:
|
|
logger.warning("bt panel bootstrap task failed server=%s source=%s: %s", server_id, source, exc)
|
|
|
|
|
|
async def run_bootstrap_tick() -> int:
|
|
"""Pick due servers and bootstrap up to batch limit; returns attempt count."""
|
|
if not is_auto_bootstrap_enabled():
|
|
return 0
|
|
|
|
from server.application.services.btpanel_service import BtPanelService
|
|
|
|
now = datetime.now(timezone.utc)
|
|
limit = bootstrap_batch_size()
|
|
due_ids: list[int] = []
|
|
|
|
async with AsyncSessionLocal() as session:
|
|
repo = ServerRepositoryImpl(session)
|
|
servers = await repo.get_all()
|
|
candidates: list[tuple[datetime, int]] = []
|
|
for server in servers:
|
|
if is_eligible_for_background_bootstrap(server, now=now):
|
|
from server.infrastructure.btpanel.credentials import get_bt_panel_block
|
|
|
|
block = get_bt_panel_block(server)
|
|
next_raw = block.get("bootstrap_next_attempt_at") if block else None
|
|
next_at = _parse_iso(next_raw) or now
|
|
candidates.append((next_at, server.id))
|
|
candidates.sort(key=lambda x: x[0])
|
|
due_ids = [sid for _, sid in candidates[:limit]]
|
|
|
|
if not due_ids:
|
|
return 0
|
|
|
|
succeeded = 0
|
|
for sid in due_ids:
|
|
try:
|
|
async with AsyncSessionLocal() as session:
|
|
svc = BtPanelService(session)
|
|
result = await svc.bootstrap_server(
|
|
sid,
|
|
source="background",
|
|
admin_username="system",
|
|
ip_address=None,
|
|
)
|
|
if result.get("ok"):
|
|
succeeded += 1
|
|
except Exception as exc:
|
|
logger.warning("bt panel background bootstrap failed server=%s: %s", sid, exc)
|
|
return succeeded
|