2026-05-20 17:24:21 +08:00
|
|
|
#!/bin/bash
|
|
|
|
|
# ================================================================
|
2026-05-22 00:37:39 +08:00
|
|
|
# Nexus Agent Install Script
|
2026-05-22 08:19:56 +08:00
|
|
|
# Usage: curl -fsSL https://YOUR_NEXUS_DOMAIN/agent/install.sh | bash -s -- --url https://YOUR_NEXUS_DOMAIN --key API_KEY --id SERVER_ID
|
2026-05-24 16:26:40 +08:00
|
|
|
#
|
|
|
|
|
# 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
|
2026-05-20 17:24:21 +08:00
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
set -e
|
|
|
|
|
|
|
|
|
|
CENTRAL_URL=""
|
|
|
|
|
WEB_URL=""
|
|
|
|
|
API_KEY=""
|
2026-05-22 00:37:39 +08:00
|
|
|
SERVER_ID=""
|
2026-05-20 17:24:21 +08:00
|
|
|
AGENT_PORT="8601"
|
2026-05-22 00:37:39 +08:00
|
|
|
INSTALL_DIR="/opt/nexus-agent"
|
2026-05-27 14:33:19 +08:00
|
|
|
|
|
|
|
|
# --- 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
|
2026-05-20 17:24:21 +08:00
|
|
|
|
|
|
|
|
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 ;;
|
2026-05-22 00:37:39 +08:00
|
|
|
--id) SERVER_ID="$2"; shift 2 ;;
|
2026-05-20 17:24:21 +08:00
|
|
|
--port) AGENT_PORT="$2"; shift 2 ;;
|
|
|
|
|
*) shift ;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
[ -z "$CENTRAL_URL" ] && echo "ERROR: --url is required" && exit 1
|
2026-05-22 00:37:39 +08:00
|
|
|
[ -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
|
2026-05-20 17:24:21 +08:00
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "=========================================="
|
2026-05-22 00:37:39 +08:00
|
|
|
echo " Nexus Agent Install"
|
2026-05-20 17:24:21 +08:00
|
|
|
echo "=========================================="
|
2026-05-22 00:37:39 +08:00
|
|
|
echo " Server: $CENTRAL_URL"
|
|
|
|
|
echo " ID: $SERVER_ID"
|
2026-05-24 16:26:40 +08:00
|
|
|
echo " Port: ${AGENT_PORT} (localhost only)"
|
2026-05-20 17:24:21 +08:00
|
|
|
echo "=========================================="
|
|
|
|
|
echo ""
|
|
|
|
|
|
2026-05-24 16:26:40 +08:00
|
|
|
# 1. Resolve Python 3.10+ (prefer system 3.12 on BT panels; skip 3.7 pyenv)
|
|
|
|
|
echo "[1/5] 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
|
|
|
|
|
ver=$("$candidate" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true)
|
|
|
|
|
major=${ver%%.*}
|
|
|
|
|
minor=${ver#*.}
|
|
|
|
|
if [ -n "$major" ] && [ "$major" -eq 3 ] && [ "${minor:-0}" -ge 10 ] 2>/dev/null; then
|
|
|
|
|
PYTHON="$candidate"
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
if [ -z "$PYTHON" ]; then
|
|
|
|
|
echo "ERROR: Python 3.10+ required (found only older pyenv/BT Python?). Install system python3.12."
|
|
|
|
|
exit 1
|
2026-05-20 17:24:21 +08:00
|
|
|
fi
|
2026-05-24 16:26:40 +08:00
|
|
|
echo " Using: $PYTHON ($($PYTHON --version 2>&1))"
|
2026-05-20 17:24:21 +08:00
|
|
|
|
2026-05-29 23:45:01 +08:00
|
|
|
# 2. Ensure rsync (required for push/sync)
|
|
|
|
|
echo "[2/6] 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/6] Create venv..."
|
2026-05-27 14:33:19 +08:00
|
|
|
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
|
2026-05-27 00:53:00 +08:00
|
|
|
if [ ! -f "$VENV_DIR/bin/activate" ]; then
|
2026-05-27 14:33:19 +08:00
|
|
|
# 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
|
2026-05-27 00:53:00 +08:00
|
|
|
if [ ! -f "$VENV_DIR/bin/activate" ]; then
|
|
|
|
|
echo " venv incomplete — installing python3-venv..."
|
2026-05-27 14:33:19 +08:00
|
|
|
$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; }
|
2026-05-27 00:53:00 +08:00
|
|
|
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
|
2026-05-24 16:26:40 +08:00
|
|
|
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
|
2026-05-20 17:24:21 +08:00
|
|
|
|
2026-05-29 23:45:01 +08:00
|
|
|
# 4. Download agent.py
|
|
|
|
|
echo "[4/6] Download agent.py..."
|
2026-05-22 00:37:39 +08:00
|
|
|
[ -z "$WEB_URL" ] && WEB_URL="${CENTRAL_URL}"
|
2026-05-20 17:24:21 +08:00
|
|
|
curl -fsSL "${WEB_URL}/agent/agent.py" -o "$INSTALL_DIR/agent.py" || {
|
|
|
|
|
echo "ERROR: Cannot download from $WEB_URL/agent/agent.py"
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 23:45:01 +08:00
|
|
|
# 5. Config
|
|
|
|
|
echo "[5/6] Write config..."
|
2026-05-23 18:27:12 +08:00
|
|
|
CENTRAL_HOST=$(echo "${CENTRAL_URL}" | sed 's~https\?://~~' | cut -d'/' -f1 | cut -d':' -f1)
|
|
|
|
|
|
2026-05-20 17:24:21 +08:00
|
|
|
cat > "$INSTALL_DIR/config.json" << EOF
|
|
|
|
|
{
|
2026-05-22 00:37:39 +08:00
|
|
|
"server_id": ${SERVER_ID},
|
2026-05-20 17:24:21 +08:00
|
|
|
"central": { "url": "${CENTRAL_URL}", "api_key": "${API_KEY}" },
|
|
|
|
|
"api_key": "${API_KEY}",
|
2026-05-22 00:37:39 +08:00
|
|
|
"heartbeat_interval": 60,
|
2026-05-23 18:27:12 +08:00
|
|
|
"allowed_ips": ["${CENTRAL_HOST}"],
|
2026-05-22 00:37:39 +08:00
|
|
|
"log_file": "/var/log/nexus-agent.log",
|
|
|
|
|
"log_level": "INFO",
|
2026-05-23 15:26:56 +08:00
|
|
|
"server": { "host": "127.0.0.1", "port": ${AGENT_PORT} }
|
2026-05-20 17:24:21 +08:00
|
|
|
}
|
|
|
|
|
EOF
|
2026-05-24 16:26:40 +08:00
|
|
|
echo " IP allowlist (Agent HTTP): ${CENTRAL_HOST}"
|
|
|
|
|
echo " Note: inbound TCP ${AGENT_PORT} is NOT required (heartbeat uses HTTPS outbound)."
|
2026-05-20 17:24:21 +08:00
|
|
|
|
2026-05-29 23:45:01 +08:00
|
|
|
# 6. systemd + start (bind localhost only)
|
|
|
|
|
echo "[6/6] Setup systemd + start..."
|
2026-05-20 17:24:21 +08:00
|
|
|
|
2026-05-27 14:33:19 +08:00
|
|
|
$SUDO tee /etc/systemd/system/nexus-agent.service > /dev/null << EOF
|
2026-05-20 17:24:21 +08:00
|
|
|
[Unit]
|
2026-05-22 00:37:39 +08:00
|
|
|
Description=Nexus Agent
|
2026-05-20 17:24:21 +08:00
|
|
|
After=network.target
|
|
|
|
|
|
|
|
|
|
[Service]
|
|
|
|
|
Type=simple
|
|
|
|
|
User=root
|
|
|
|
|
WorkingDirectory=${INSTALL_DIR}
|
2026-05-24 16:26:40 +08:00
|
|
|
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
|
2026-05-20 17:24:21 +08:00
|
|
|
Restart=always
|
|
|
|
|
RestartSec=5
|
|
|
|
|
|
|
|
|
|
[Install]
|
|
|
|
|
WantedBy=multi-user.target
|
|
|
|
|
EOF
|
|
|
|
|
|
2026-05-27 14:33:19 +08:00
|
|
|
$SUDO systemctl daemon-reload
|
|
|
|
|
$SUDO systemctl enable nexus-agent
|
|
|
|
|
$SUDO systemctl start nexus-agent
|
2026-05-20 17:24:21 +08:00
|
|
|
sleep 2
|
|
|
|
|
|
|
|
|
|
STATUS="running"
|
2026-05-27 14:33:19 +08:00
|
|
|
$SUDO systemctl is-active --quiet nexus-agent || STATUS="FAILED"
|
2026-05-20 17:24:21 +08:00
|
|
|
|
|
|
|
|
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 ""
|
2026-05-24 16:26:40 +08:00
|
|
|
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}"
|
2026-05-20 17:24:21 +08:00
|
|
|
echo ""
|
|
|
|
|
echo " Commands:"
|
2026-05-22 00:37:39 +08:00
|
|
|
echo " systemctl status nexus-agent"
|
|
|
|
|
echo " journalctl -u nexus-agent -f"
|
|
|
|
|
echo " systemctl restart nexus-agent"
|
2026-05-20 17:24:21 +08:00
|
|
|
echo ""
|