#!/bin/bash # ================================================================ # Nexus Agent Install Script # Usage: curl -fsSL https://YOUR_NEXUS_DOMAIN/agent/install.sh | bash -s -- --url https://YOUR_NEXUS_DOMAIN --key API_KEY --id SERVER_ID # # Network model: # - Heartbeat: child → central HTTPS (443 outbound), no inbound 8601 required # - Scripts / push: central → child SSH (22) # - Agent HTTP listens on 127.0.0.1 only (local); not exposed on public 8601 # ================================================================ set -e CENTRAL_URL="" WEB_URL="" API_KEY="" 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="" else if command -v sudo >/dev/null 2>&1; then # Test passwordless sudo (-n = non-interactive) if sudo -n true 2>/dev/null; then SUDO="sudo" echo " Non-root user detected — using sudo for privileged operations." else echo "ERROR: sudo requires a password. Configure passwordless sudo first:" echo " echo \"$(whoami) ALL=(ALL) NOPASSWD:ALL\" | sudo tee /etc/sudoers.d/nexus-agent" exit 1 fi else echo "ERROR: Running as non-root and sudo is not available." echo " Either run as root or install sudo." exit 1 fi fi while [[ $# -gt 0 ]]; do case $1 in --url) CENTRAL_URL="$2"; shift 2 ;; --web-url) WEB_URL="$2"; shift 2 ;; --key) API_KEY="$2"; shift 2 ;; --id) SERVER_ID="$2"; shift 2 ;; --port) AGENT_PORT="$2"; shift 2 ;; *) shift ;; esac done [ -z "$CENTRAL_URL" ] && echo "ERROR: --url is required" && exit 1 [ -z "$SERVER_ID" ] && echo "ERROR: --id is required (server ID from Nexus dashboard)" && exit 1 [ -z "$API_KEY" ] && echo "ERROR: --key is required (API key from Nexus settings)" && exit 1 echo "" echo "==========================================" echo " Nexus Agent Install" echo "==========================================" echo " Server: $CENTRAL_URL" echo " ID: $SERVER_ID" 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+ (system → BT only if executable → auto-install system 3.12) echo "[1/7] Resolve Python 3.10+..." PYTHON="" _python_version_ok() { local ver="$1" major minor [ -n "$ver" ] || return 1 major=${ver%%.*} minor=${ver#*.} [ "$major" -eq 3 ] && [ "${minor:-0}" -ge 10 ] 2>/dev/null } _try_python_candidate() { local candidate="$1" ver [ -n "$candidate" ] || return 1 if [ "${candidate#/}" != "$candidate" ]; then [ -x "$candidate" ] || return 1 else command -v "$candidate" >/dev/null 2>&1 || return 1 fi ver=$("$candidate" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true) if _python_version_ok "$ver"; then PYTHON="$candidate" return 0 fi return 1 } _bt_executable_python_candidates() { local base p for base in /www/server/panel/pyenv/bin; do [ -d "$base" ] || continue for p in "$base"/python3.12 "$base"/python3.11 "$base"/python3.10; do [ -x "$p" ] && echo "$p" done if command -v find >/dev/null 2>&1; then find "$base" -maxdepth 1 -type f -name 'python3.*' -executable 2>/dev/null | sort -Vr else for p in "$base"/python3.*; do [ -x "$p" ] && echo "$p" done fi done for p in /www/server/python-manager/versions/*/bin/python3.* \ /www/server/python_manager/versions/*/bin/python3.*; do [ -x "$p" ] && echo "$p" done } _resolve_python_from() { local candidate for candidate in $1; do if _try_python_candidate "$candidate"; then return 0 fi done return 1 } _install_system_python312() { echo " No executable Python 3.10+ — installing system python3.12..." export DEBIAN_FRONTEND=noninteractive if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update -qq if ! $SUDO apt-get install -y -qq python3.12 python3.12-venv python3.12-dev >/dev/null 2>&1; then echo " Trying deadsnakes PPA (Ubuntu)..." $SUDO apt-get install -y -qq software-properties-common >/dev/null 2>&1 || true $SUDO add-apt-repository -y ppa:deadsnakes/ppa >/dev/null 2>&1 || true $SUDO apt-get update -qq $SUDO apt-get install -y -qq python3.12 python3.12-venv python3.12-dev >/dev/null 2>&1 \ || return 1 fi return 0 fi if command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y -q python3.12 python3.12-devel >/dev/null 2>&1 \ || $SUDO dnf install -y -q python3.11 python3.11-devel >/dev/null 2>&1 \ || $SUDO dnf install -y -q python3 python3-devel >/dev/null 2>&1 \ || return 1 return 0 fi if command -v yum >/dev/null 2>&1; then $SUDO yum install -y -q python3.12 python3.12-devel >/dev/null 2>&1 \ || $SUDO yum install -y -q python3 python3-devel >/dev/null 2>&1 \ || return 1 return 0 fi if command -v apk >/dev/null 2>&1; then $SUDO apk add --quiet python3 py3-pip >/dev/null 2>&1 || return 1 return 0 fi return 1 } SYSTEM_CANDIDATES="python3.12 python3.11 python3.10 /usr/bin/python3.12 /usr/bin/python3.11 /usr/bin/python3.10" BT_CANDIDATES="" while IFS= read -r p; do [ -n "$p" ] && BT_CANDIDATES="$BT_CANDIDATES $p" done < <(_bt_executable_python_candidates | awk '!seen[$0]++') if ! _resolve_python_from "$SYSTEM_CANDIDATES $BT_CANDIDATES"; then if _install_system_python312; then POST_INSTALL="python3.12 python3.11 python3.10 /usr/bin/python3.12 /usr/bin/python3.11 /usr/bin/python3.10 python3" _resolve_python_from "$POST_INSTALL" || true fi fi if [ -z "$PYTHON" ]; then echo "ERROR: Python 3.10+ required. System install failed — install python3.12 manually and re-run." exit 1 fi echo " Using: $PYTHON ($("$PYTHON" --version 2>&1))" # 2. Ensure rsync (required for push/sync) 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 $SUDO apt-get update -qq && $SUDO apt-get install -y -qq rsync elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y -q rsync elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y -q rsync elif command -v apk >/dev/null 2>&1; then $SUDO apk add --quiet rsync else echo "ERROR: Cannot detect package manager. Please install rsync manually." exit 1 fi echo " rsync installed OK" else echo " rsync already available" fi # 3. Create dir + 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 if [ -n "$SUDO" ]; then $SUDO chown -R "$(id -u):$(id -g)" "$INSTALL_DIR" fi if [ ! -f "$VENV_DIR/bin/activate" ]; then # Allow venv creation to fail (set -e won't abort) so we can install python3-venv "$PYTHON" -m venv "$VENV_DIR" 2>/dev/null || true # venv creation fails when python3-venv is missing — check if activate exists if [ ! -f "$VENV_DIR/bin/activate" ]; then echo " venv incomplete — installing python3-venv..." $SUDO apt-get update -qq && $SUDO apt-get install -y -qq "python$( "$PYTHON" -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")' )-venv" 2>/dev/null \ || $SUDO apt-get install -y -qq python3-venv 2>/dev/null \ || { echo "ERROR: Cannot install python3-venv. Run: $SUDO apt install python3-venv"; exit 1; } rm -rf "$VENV_DIR" "$PYTHON" -m venv "$VENV_DIR" if [ ! -f "$VENV_DIR/bin/activate" ]; then echo "ERROR: venv creation failed even after installing python3-venv" exit 1 fi fi fi # shellcheck disable=SC1091 source "$VENV_DIR/bin/activate" pip install -q --upgrade pip 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 + heartbeat_policy.py echo "[4/7] Download agent files..." [ -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" exit 1 } curl -fsSL "${WEB_URL}/agent/heartbeat_policy.py" -o "$INSTALL_DIR/heartbeat_policy.py" || { echo "ERROR: Cannot download from $WEB_URL/agent/heartbeat_policy.py" exit 1 } # 5. 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 { "server_id": ${SERVER_ID}, "central": { "url": "${CENTRAL_URL}", "api_key": "${API_KEY}" }, "api_key": "${API_KEY}", "heartbeat_interval": 60, "allowed_ips": ["${CENTRAL_HOST}"], "log_file": "/var/log/nexus-agent.log", "log_level": "INFO", "server": { "host": "127.0.0.1", "port": ${AGENT_PORT} } } EOF 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/7] Setup systemd + start..." $SUDO tee /etc/systemd/system/nexus-agent.service > /dev/null << EOF [Unit] Description=Nexus Agent After=network.target [Service] Type=simple User=root WorkingDirectory=${INSTALL_DIR} Environment=PATH=${VENV_DIR}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ExecStart=${VENV_DIR}/bin/python -m uvicorn agent:app --host 127.0.0.1 --port ${AGENT_PORT} --log-level info Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF $SUDO systemctl daemon-reload $SUDO systemctl enable nexus-agent $SUDO systemctl start nexus-agent 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") echo "" echo "==========================================" echo " Done! Status: ${STATUS}" echo "==========================================" echo "" echo " Heartbeat: ${CENTRAL_URL}/api/agent/heartbeat (HTTPS outbound)" echo " Agent HTTP: 127.0.0.1:${AGENT_PORT} (local only, no public firewall rule needed)" echo " Scripts: via SSH port 22 from Nexus center" echo " Server ID: ${SERVER_ID}" echo "" echo " Commands:" echo " systemctl status nexus-agent" echo " journalctl -u nexus-agent -f" echo " systemctl restart nexus-agent" echo ""