Files
Nexus/server/application/server_batch_common.py
T

191 lines
6.4 KiB
Python
Raw Normal View History

"""Shared helpers for server batch SSH operations (Agent install, path detect, etc.)."""
from __future__ import annotations
import logging
import shlex
from server.domain.models import Server
from server.utils.posix_paths import posix_dirname
logger = logging.getLogger("nexus.server_batch")
_INSTALL_ERROR_ZH: dict[str, str] = {
"Python 3.10+ required but not found and could not be installed automatically.":
"Python 版本低于 3.10,且无法自动安装",
"Python 3.10+ required. System install failed — install python3.12 manually and re-run.":
"Python 3.10+ 不可用且自动安装系统 python3.12 失败,请手动安装后重试",
"Python 3.10+ required (found only older pyenv/BT Python?). Install system python3.12 or use BT panel pyenv under /www/server/panel/pyenv/bin.":
"Python 版本低于 3.10;宝塔 pyenv 不可执行时将自动安装系统 python3.12",
"sudo requires a password. Configure passwordless sudo first:":
"sudo 需要密码,请先配置免密 sudo",
"Running as non-root and sudo is not available.":
"非 root 用户且无 sudo 权限",
"--url is required":
"缺少 --url 参数",
"--id is required (server ID from Nexus dashboard)":
"缺少 --id 参数(服务器 ID",
"--key is required (API key from Nexus settings)":
"缺少 --key 参数(API Key",
"venv creation failed even after installing python3-venv":
"虚拟环境创建失败(python3-venv 模块不可用)",
"Cannot download from":
"无法下载 agent.py",
"upgrade_ok":
"升级验证失败",
}
def sudo_wrap(cmd: str, ssh_user: str) -> str:
"""Wrap a command with sudo setup/teardown for non-root SSH users."""
if ssh_user == "root":
return cmd
sudoers_tag = "nexus-agent"
sudoers_line = (
f"{ssh_user} ALL=(ALL) NOPASSWD: "
"/usr/bin/systemctl, "
"/usr/bin/apt-get, /usr/bin/yum, /usr/bin/dnf, /usr/bin/apk, "
"/usr/bin/rm, /usr/bin/mkdir, /usr/bin/cp, /usr/bin/tee, "
"/usr/bin/curl, /usr/bin/fuser, /usr/bin/kill, /usr/bin/pkill, "
f"{shlex.quote('/opt/nexus-agent/.venv/bin/pip')}*"
)
setup = (
f"echo {shlex.quote(sudoers_line)} "
f"| sudo tee /etc/sudoers.d/{sudoers_tag} > /dev/null 2>&1; "
)
cleanup = f"; sudo rm -f /etc/sudoers.d/{sudoers_tag} 2>/dev/null"
return setup + cmd + cleanup
def install_error_msg(stdout: str, stderr: str, exit_code: int) -> str:
"""Extract concise error reason from install.sh / upgrade output."""
for line in reversed(stdout.split("\n")):
line = line.strip()
if line.startswith("ERROR:"):
en = line.replace("ERROR: ", "")
zh = _INSTALL_ERROR_ZH.get(en)
if not zh:
for prefix, translation in _INSTALL_ERROR_ZH.items():
if en.startswith(prefix):
zh = translation
break
if zh:
return f"{zh}{en[:200]}"
return en[:300]
for line in reversed(stdout.split("\n")):
line = line.strip()
if line:
return line[:300]
if stderr.strip():
return stderr.strip()[:300]
return f"exit {exit_code}"
def batch_server_display_name(server: Server | None) -> str:
if server is None:
return "未知服务器"
name = (server.name or "").strip()
if name:
return name
domain = (server.domain or "").strip()
if domain:
return domain
return "未知服务器"
async def prefetch_batch_context(service, server_ids: list[int]) -> tuple[dict[int, Server], dict[int, str]]:
rows = await service.server_repo.get_by_ids(server_ids)
server_map = {s.id: s for s in rows}
labels = {sid: batch_server_display_name(server_map.get(sid)) for sid in server_ids}
return server_map, labels
def generate_agent_api_key_value() -> str:
import secrets
return f"nxs-{secrets.token_urlsafe(32)}"
async def ensure_agent_api_key(server: Server) -> str:
"""Return per-server agent key, auto-generating and persisting when missing."""
key = (server.agent_api_key or "").strip()
if key:
return key
new_key = generate_agent_api_key_value()
from server.infrastructure.database.server_repo import ServerRepositoryImpl
from server.infrastructure.database.session import AsyncSessionLocal
async with AsyncSessionLocal() as session:
repo = ServerRepositoryImpl(session)
row = await repo.get_by_id(server.id)
if not row:
raise ValueError("服务器不存在")
existing = (row.agent_api_key or "").strip()
if existing:
server.agent_api_key = existing
return existing
row.agent_api_key = new_key
await session.commit()
server.agent_api_key = new_key
return new_key
async def update_server_target_path(sid: int, target_dir: str) -> None:
from server.infrastructure.database.session import AsyncSessionLocal
from server.infrastructure.database.server_repo import ServerRepositoryImpl
async with AsyncSessionLocal() as session:
repo = ServerRepositoryImpl(session)
server = await repo.get_by_id(sid)
if not server:
raise ValueError("服务器不存在")
server.target_path = target_dir
await session.commit()
async def mark_agent_uninstalled(sid: int) -> None:
from server.infrastructure.database.session import AsyncSessionLocal
from server.infrastructure.database.server_repo import ServerRepositoryImpl
async with AsyncSessionLocal() as session:
repo = ServerRepositoryImpl(session)
server = await repo.get_by_id(sid)
if not server:
return
server.is_online = False
server.agent_version = None
server.agent_api_key = None
await session.commit()
def result_item_dict(
*,
server_id: int,
server_name: str,
success: bool,
stdout: str = "",
error: str = "",
) -> dict:
return {
"server_id": server_id,
"server_name": server_name,
"success": success,
"stdout": stdout,
"error": error,
}
__all__ = [
"batch_server_display_name",
"ensure_agent_api_key",
"generate_agent_api_key_value",
"install_error_msg",
"mark_agent_uninstalled",
"posix_dirname",
"prefetch_batch_context",
"result_item_dict",
"sudo_wrap",
"update_server_target_path",
]