feat: Agent 安装/卸载/升级脚本全面迭代

install.sh:
- 新增 Step 0:停旧服务 + 杀端口占用(fuser/ss/netstat 兜底)
- 修正步骤编号 [1/5] → [0/7]
- pip 依赖版本提取为脚本顶部变量,方便统一维护

uninstall.sh:
- 加 pkill 杀残留 Agent 进程
- 加 fuser 杀端口占用
- 清理 /etc/sudoers.d/nexus-agent 残留
- root 用户跳过 sudo 前缀

_sudo_wrap:
- NOPASSWD 命令白名单替代 ALL=(ALL)(仅 systemctl/apt/yum/rm 等)
- 降低 SSH 超时后 sudoers 残留的风险

升级 API:
- restart 前加 fuser -k 杀端口占用(单台+批量)
- install_agent_remote 检查 stdout 中的 FAILED 标记

卸载后清理:
- 清空 agent_api_key(之前只清 agent_version)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-05-30 16:11:09 +08:00
parent dc48f71b25
commit 3f50a40d25
3 changed files with 82 additions and 24 deletions
+26 -5
View File
@@ -33,14 +33,24 @@ import shlex
def _sudo_wrap(cmd: str, ssh_user: str) -> str:
"""Wrap a command with sudo setup/teardown for non-root SSH users.
For non-root users, pre-configures NOPASSWD sudo before the command
and cleans up the sudoers entry afterwards. For root, returns cmd as-is.
For non-root users, pre-configures NOPASSWD sudo (command whitelist only)
before the command and cleans up the sudoers entry afterwards.
For root, returns cmd as-is (no sudoers manipulation needed).
"""
if ssh_user == "root":
return cmd
sudoers_tag = "nexus-agent"
# Whitelist only the commands needed by install/upgrade/uninstall — not ALL=(ALL)
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(ssh_user + ' ALL=(ALL) NOPASSWD:ALL')} "
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"
@@ -656,6 +666,7 @@ async def batch_upgrade_agent(
venv_python = f"{install_dir}/.venv/bin/python"
ssh_user = (server.username or "root").strip()
batch_systemctl_prefix = "sudo " if ssh_user != "root" else ""
batch_kill_port = "fuser -k 8601/tcp 2>/dev/null || true; "
upgrade_cmd = _sudo_wrap(
f"(command -v rsync >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq rsync) || (yum install -y -q rsync) || (dnf install -y -q rsync)) "
@@ -663,7 +674,7 @@ async def batch_upgrade_agent(
f"&& {venv_python} -m pip install -q fastapi==0.115.6 uvicorn==0.34.0 httpx==0.28.1 psutil python-multipart==0.0.19 "
f"&& cp {install_dir}/agent.py {install_dir}/agent.py.bak "
f"&& curl -fsSL {shlex.quote(agent_url)} -o {install_dir}/agent.py "
f"&& {batch_systemctl_prefix}systemctl restart nexus-agent "
f"&& {batch_kill_port}{batch_systemctl_prefix}systemctl restart nexus-agent "
f"&& sleep 2 "
f"&& {batch_systemctl_prefix}systemctl is-active nexus-agent "
f"&& echo 'upgrade_ok'",
@@ -833,6 +844,7 @@ async def batch_uninstall_agent(
pass
server.is_online = False
server.agent_version = None
server.agent_api_key = None
await db.commit()
return BatchAgentResultItem(
@@ -1188,6 +1200,13 @@ async def install_agent_remote(
detail=f"安装失败: {_install_error_msg(result.get('stdout', ''), result.get('stderr', ''), result['exit_code'])}",
)
# Check for "Status: FAILED" in stdout (install.sh exits 0 but service may not start)
if "FAILED" in result.get("stdout", ""):
raise HTTPException(
status_code=400,
detail=f"安装完成但 Agent 启动失败,请检查子服务器日志: journalctl -u nexus-agent",
)
# Audit
ip_address = request.client.host if request.client else ""
audit_repo = AuditLogRepositoryImpl(db)
@@ -1265,6 +1284,7 @@ async def uninstall_agent_remote(
logger.warning(f"Failed to clear Redis heartbeat for server {id}", exc_info=True)
server.is_online = False
server.agent_version = None
server.agent_api_key = None
await db.commit()
# Audit
@@ -1430,11 +1450,12 @@ async def upgrade_agent(
# Step 4: pip install + backup + download new agent.py → restart service
# For non-root users, systemctl needs sudo (even after _sudo_wrap sets up NOPASSWD)
systemctl_prefix = "sudo " if ssh_user != "root" else ""
kill_port = f"fuser -k {agent_port}/tcp 2>/dev/null || true; "
upgrade_cmd = _sudo_wrap(
f"{pip_cmd} "
f"&& {backup_cmd} "
f"&& curl -fsSL {shlex.quote(agent_url)} -o {install_dir}/agent.py "
f"&& {systemctl_prefix}systemctl restart nexus-agent "
f"&& {kill_port}{systemctl_prefix}systemctl restart nexus-agent "
f"&& sleep 2 "
f"&& {systemctl_prefix}systemctl is-active nexus-agent "
f"&& echo 'upgrade_ok'",
+33 -7
View File
@@ -18,6 +18,13 @@ SERVER_ID=""
AGENT_PORT="8601"
INSTALL_DIR="/opt/nexus-agent"
# --- Python dependency versions (single source of truth) ---
FASTAPI_VER="0.115.6"
UVICORN_VER="0.34.0"
HTTPX_VER="0.28.1"
PSUTIL_VER="" # latest
MULTIPART_VER="0.0.19"
# --- Privilege detection ---
if [ "$(id -u)" -eq 0 ]; then
SUDO=""
@@ -64,8 +71,23 @@ echo " Port: ${AGENT_PORT} (localhost only)"
echo "=========================================="
echo ""
# 0. Stop existing service + kill stale processes on the port
echo "[0/7] Clean up old installation..."
$SUDO systemctl stop nexus-agent 2>/dev/null || true
# Kill any process occupying the agent port (stale process from manual start, etc.)
if command -v fuser >/dev/null 2>&1; then
$SUDO fuser -k "${AGENT_PORT}"/tcp 2>/dev/null || true
elif command -v ss >/dev/null 2>&1; then
PID=$(ss -tlnp 2>/dev/null | grep ":${AGENT_PORT}" | grep -oP 'pid=\K[0-9]+' | head -1)
[ -n "$PID" ] && kill "$PID" 2>/dev/null || true
elif command -v netstat >/dev/null 2>&1; then
PID=$(netstat -tlnp 2>/dev/null | grep ":${AGENT_PORT}" | awk '{print $7}' | cut -d'/' -f1 | head -1)
[ -n "$PID" ] && kill "$PID" 2>/dev/null || true
fi
echo " Old service stopped, port cleared"
# 1. Resolve Python 3.10+ (prefer system 3.12 on BT panels; skip 3.7 pyenv)
echo "[1/5] Resolve Python 3.10+..."
echo "[1/7] Resolve Python 3.10+..."
PYTHON=""
for candidate in python3.12 python3.11 python3.10 /usr/bin/python3.12 /usr/bin/python3.11 /usr/bin/python3.10 python3; do
if command -v "$candidate" >/dev/null 2>&1; then
@@ -85,7 +107,7 @@ fi
echo " Using: $PYTHON ($($PYTHON --version 2>&1))"
# 2. Ensure rsync (required for push/sync)
echo "[2/6] Check rsync..."
echo "[2/7] Check rsync..."
if ! command -v rsync >/dev/null 2>&1; then
echo " rsync not found — installing..."
if command -v apt-get >/dev/null 2>&1; then
@@ -106,7 +128,7 @@ else
fi
# 3. Create dir + venv
echo "[3/6] Create venv..."
echo "[3/7] Create venv..."
VENV_DIR="${INSTALL_DIR}/.venv"
$SUDO mkdir -p "$INSTALL_DIR"
# If using sudo, give ownership to current user so venv/pip work without sudo
@@ -133,10 +155,12 @@ fi
# shellcheck disable=SC1091
source "$VENV_DIR/bin/activate"
pip install -q --upgrade pip
pip install -q fastapi==0.115.6 uvicorn==0.34.0 httpx==0.28.1 psutil python-multipart==0.0.19
PIP_DEPS="fastapi==${FASTAPI_VER} uvicorn==${UVICORN_VER} httpx==${HTTPX_VER} python-multipart==${MULTIPART_VER}"
[ -n "$PSUTIL_VER" ] && PIP_DEPS="${PIP_DEPS} psutil==${PSUTIL_VER}" || PIP_DEPS="${PIP_DEPS} psutil"
pip install -q $PIP_DEPS
# 4. Download agent.py
echo "[4/6] Download agent.py..."
echo "[4/7] Download agent.py..."
[ -z "$WEB_URL" ] && WEB_URL="${CENTRAL_URL}"
curl -fsSL "${WEB_URL}/agent/agent.py" -o "$INSTALL_DIR/agent.py" || {
echo "ERROR: Cannot download from $WEB_URL/agent/agent.py"
@@ -144,7 +168,7 @@ curl -fsSL "${WEB_URL}/agent/agent.py" -o "$INSTALL_DIR/agent.py" || {
}
# 5. Config
echo "[5/6] Write config..."
echo "[5/7] Write config..."
CENTRAL_HOST=$(echo "${CENTRAL_URL}" | sed 's~https\?://~~' | cut -d'/' -f1 | cut -d':' -f1)
cat > "$INSTALL_DIR/config.json" << EOF
@@ -163,7 +187,7 @@ echo " IP allowlist (Agent HTTP): ${CENTRAL_HOST}"
echo " Note: inbound TCP ${AGENT_PORT} is NOT required (heartbeat uses HTTPS outbound)."
# 6. systemd + start (bind localhost only)
echo "[6/6] Setup systemd + start..."
echo "[6/7] Setup systemd + start..."
$SUDO tee /etc/systemd/system/nexus-agent.service > /dev/null << EOF
[Unit]
@@ -191,6 +215,8 @@ sleep 2
STATUS="running"
$SUDO systemctl is-active --quiet nexus-agent || STATUS="FAILED"
# 7. Verify
echo "[7/7] Verify..."
HOSTNAME=$(hostname 2>/dev/null || echo "unknown")
IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "unknown")
+23 -12
View File
@@ -20,28 +20,30 @@ echo "=========================================="
echo ""
# --- Privilege detection ---
if [ "$(id -u)" -ne 0 ]; then
if [ "$(id -u)" -eq 0 ]; then
SUDO=""
else
if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
SUDO="sudo"
else
echo "ERROR: Must run as root or have passwordless sudo."
exit 1
fi
else
SUDO=""
fi
# 1. Stop service if running
echo "[1/4] Stop service..."
if $SUDO systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
$SUDO systemctl stop "$SERVICE_NAME" || true
echo " Service stopped"
else
echo " Service not running"
echo "[1/5] Stop service..."
$SUDO systemctl stop "$SERVICE_NAME" 2>/dev/null || true
# Kill any stale agent processes (e.g. manually started outside systemd)
pkill -f "uvicorn agent:app" 2>/dev/null || true
# Kill any process on the agent port
if command -v fuser >/dev/null 2>&1; then
$SUDO fuser -k 8601/tcp 2>/dev/null || true
fi
echo " Service stopped, stale processes killed"
# 2. Disable and remove systemd unit
echo "[2/4] Remove systemd unit..."
echo "[2/5] Remove systemd unit..."
if [ -f "$SERVICE_FILE" ]; then
$SUDO systemctl disable "$SERVICE_NAME" 2>/dev/null || true
$SUDO rm -f "$SERVICE_FILE"
@@ -52,7 +54,7 @@ else
fi
# 3. Remove install directory
echo "[3/4] Remove install directory..."
echo "[3/5] Remove install directory..."
if [ -d "$INSTALL_DIR" ]; then
$SUDO rm -rf "$INSTALL_DIR"
echo " ${INSTALL_DIR} removed"
@@ -61,7 +63,7 @@ else
fi
# 4. Remove log file
echo "[4/4] Remove log file..."
echo "[4/5] Remove log file..."
if [ -f "/var/log/nexus-agent.log" ]; then
$SUDO rm -f /var/log/nexus-agent.log
echo " /var/log/nexus-agent.log removed"
@@ -69,6 +71,15 @@ else
echo " No log file found"
fi
# 5. Clean up sudoers residual (left by install/upgrade _sudo_wrap)
echo "[5/5] Clean up sudoers..."
if [ -f "/etc/sudoers.d/nexus-agent" ]; then
$SUDO rm -f /etc/sudoers.d/nexus-agent
echo " /etc/sudoers.d/nexus-agent removed"
else
echo " No sudoers residual found"
fi
echo ""
echo "=========================================="
echo " Uninstall complete!"