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:
+26
-5
@@ -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
@@ -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
@@ -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!"
|
||||
|
||||
Reference in New Issue
Block a user