#!/usr/bin/env bash # # Pulse Unified Agent Installer # Supports: Linux (systemd, OpenRC), macOS (launchd), Synology DSM (6.x/7+), Unraid # # Usage: # curl -fsSL http://pulse/install.sh | bash -s -- --url http://pulse --token [options] # # Options: # --enable-host Enable host metrics (default: true) # --enable-docker Force enable Docker monitoring (default: auto-detect) # --disable-docker Disable Docker monitoring even if detected # --enable-kubernetes Force enable Kubernetes monitoring (default: auto-detect) # --disable-kubernetes Disable Kubernetes monitoring even if detected # --enable-proxmox Force enable Proxmox integration (default: auto-detect) # --disable-proxmox Disable Proxmox integration even if detected # --interval Reporting interval (default: 30s) # --agent-id Custom agent identifier (default: auto-generated) # --insecure Skip TLS certificate verification # --uninstall Remove the agent # # Auto-Detection: # The installer automatically detects Docker, Kubernetes, and Proxmox on the # target machine and enables monitoring for detected platforms. Use --disable-* # flags to skip specific platforms, or --enable-* to force enable even if not # detected. set -euo pipefail # Wrap entire script in a function to protect against partial download # See: https://www.kicksecure.com/wiki/Dev/curl_bash_pipe main() { # --- Cleanup trap --- TMP_FILES=() # shellcheck disable=SC2317 # Invoked by trap, not directly cleanup() { for f in "${TMP_FILES[@]}"; do rm -f "$f" 2>/dev/null || true done } trap cleanup EXIT # --- Check Root --- if [[ $EUID -ne 0 ]]; then echo "This script must be run as root. Please use sudo." exit 1 fi # --- Configuration --- AGENT_NAME="pulse-agent" BINARY_NAME="pulse-agent" INSTALL_DIR="/usr/local/bin" LOG_FILE="/var/log/${AGENT_NAME}.log" # TrueNAS SCALE configuration (immutable root filesystem) TRUENAS=false TRUENAS_STATE_DIR="/data/pulse-agent" TRUENAS_LOG_DIR="$TRUENAS_STATE_DIR/logs" TRUENAS_BOOTSTRAP_SCRIPT="$TRUENAS_STATE_DIR/bootstrap-pulse-agent.sh" TRUENAS_ENV_FILE="$TRUENAS_STATE_DIR/pulse-agent.env" # Defaults PULSE_URL="" PULSE_TOKEN="" INTERVAL="30s" ENABLE_HOST="true" ENABLE_DOCKER="" # Empty means "auto-detect" ENABLE_KUBERNETES="" # Empty means "auto-detect" ENABLE_PROXMOX="" # Empty means "auto-detect" PROXMOX_TYPE="" UNINSTALL="false" INSECURE="false" AGENT_ID="" # Track if flags were explicitly set (to override auto-detection) DOCKER_EXPLICIT="false" KUBERNETES_EXPLICIT="false" PROXMOX_EXPLICIT="false" # --- Helper Functions --- log_info() { printf "[INFO] %s\n" "$1"; } log_warn() { printf "[WARN] %s\n" "$1"; } log_error() { printf "[ERROR] %s\n" "$1"; } fail() { log_error "$1" if [[ -t 0 ]]; then read -r -p "Press Enter to exit..." elif [[ -e /dev/tty ]]; then read -r -p "Press Enter to exit..." < /dev/tty fi exit 1 } # --- Auto-Detection Functions --- detect_docker() { # Check if Docker is available and accessible if command -v docker &>/dev/null; then # Try to connect to Docker daemon if docker info &>/dev/null 2>&1; then return 0 fi fi # Also check for Podman (Docker-compatible) if command -v podman &>/dev/null; then if podman info &>/dev/null 2>&1; then return 0 fi fi return 1 } detect_kubernetes() { # Check for kubectl and cluster access if command -v kubectl &>/dev/null; then # Try to connect to cluster (quick timeout) if timeout 3 kubectl cluster-info &>/dev/null 2>&1; then return 0 fi fi # Check for kubeconfig file if [[ -f "${HOME}/.kube/config" ]] || [[ -f "/etc/kubernetes/admin.conf" ]]; then return 0 fi # Check if running inside a Kubernetes pod if [[ -f "/var/run/secrets/kubernetes.io/serviceaccount/token" ]]; then return 0 fi return 1 } detect_proxmox() { # Check for Proxmox VE if [[ -d "/etc/pve" ]]; then return 0 fi # Check for Proxmox Backup Server if [[ -d "/etc/proxmox-backup" ]]; then return 0 fi # Check for pveversion command if command -v pveversion &>/dev/null; then return 0 fi # Check for proxmox-backup-manager command if command -v proxmox-backup-manager &>/dev/null; then return 0 fi return 1 } # Build exec args string for use in service files # Returns via EXEC_ARGS variable build_exec_args() { EXEC_ARGS="--url ${PULSE_URL} --token ${PULSE_TOKEN} --interval ${INTERVAL}" # Always pass enable-host flag since agent defaults to true if [[ "$ENABLE_HOST" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-host" else EXEC_ARGS="$EXEC_ARGS --enable-host=false" fi if [[ "$ENABLE_DOCKER" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-docker"; fi if [[ "$ENABLE_KUBERNETES" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-kubernetes"; fi if [[ "$ENABLE_PROXMOX" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-proxmox"; fi if [[ -n "$PROXMOX_TYPE" ]]; then EXEC_ARGS="$EXEC_ARGS --proxmox-type ${PROXMOX_TYPE}"; fi if [[ "$INSECURE" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --insecure"; fi if [[ -n "$AGENT_ID" ]]; then EXEC_ARGS="$EXEC_ARGS --agent-id ${AGENT_ID}"; fi } # Build exec args as array for direct execution (proper quoting) # Returns via EXEC_ARGS_ARRAY variable build_exec_args_array() { EXEC_ARGS_ARRAY=(--url "$PULSE_URL" --token "$PULSE_TOKEN" --interval "$INTERVAL") # Always pass enable-host flag since agent defaults to true if [[ "$ENABLE_HOST" == "true" ]]; then EXEC_ARGS_ARRAY+=(--enable-host) else EXEC_ARGS_ARRAY+=(--enable-host=false) fi if [[ "$ENABLE_DOCKER" == "true" ]]; then EXEC_ARGS_ARRAY+=(--enable-docker); fi if [[ "$ENABLE_KUBERNETES" == "true" ]]; then EXEC_ARGS_ARRAY+=(--enable-kubernetes); fi if [[ "$ENABLE_PROXMOX" == "true" ]]; then EXEC_ARGS_ARRAY+=(--enable-proxmox); fi if [[ -n "$PROXMOX_TYPE" ]]; then EXEC_ARGS_ARRAY+=(--proxmox-type "$PROXMOX_TYPE"); fi if [[ "$INSECURE" == "true" ]]; then EXEC_ARGS_ARRAY+=(--insecure); fi if [[ -n "$AGENT_ID" ]]; then EXEC_ARGS_ARRAY+=(--agent-id "$AGENT_ID"); fi } # --- Parse Arguments --- while [[ $# -gt 0 ]]; do case $1 in --url) PULSE_URL="$2"; shift 2 ;; --token) PULSE_TOKEN="$2"; shift 2 ;; --interval) INTERVAL="$2"; shift 2 ;; --enable-host) ENABLE_HOST="true"; shift ;; --disable-host) ENABLE_HOST="false"; shift ;; --enable-docker) ENABLE_DOCKER="true"; DOCKER_EXPLICIT="true"; shift ;; --disable-docker) ENABLE_DOCKER="false"; DOCKER_EXPLICIT="true"; shift ;; --enable-kubernetes) ENABLE_KUBERNETES="true"; KUBERNETES_EXPLICIT="true"; shift ;; --disable-kubernetes) ENABLE_KUBERNETES="false"; KUBERNETES_EXPLICIT="true"; shift ;; --enable-proxmox) ENABLE_PROXMOX="true"; PROXMOX_EXPLICIT="true"; shift ;; --disable-proxmox) ENABLE_PROXMOX="false"; PROXMOX_EXPLICIT="true"; shift ;; --proxmox-type) PROXMOX_TYPE="$2"; shift 2 ;; --insecure) INSECURE="true"; shift ;; --uninstall) UNINSTALL="true"; shift ;; --agent-id) AGENT_ID="$2"; shift 2 ;; *) fail "Unknown argument: $1" ;; esac done # --- Platform Auto-Detection --- # Only auto-detect if flags weren't explicitly set log_info "Detecting available platforms..." if [[ "$DOCKER_EXPLICIT" != "true" ]]; then if detect_docker; then log_info "Docker/Podman detected - enabling container monitoring" log_info " (use --disable-docker to skip)" ENABLE_DOCKER="true" else ENABLE_DOCKER="false" fi fi if [[ "$KUBERNETES_EXPLICIT" != "true" ]]; then if detect_kubernetes; then log_info "Kubernetes detected - enabling cluster monitoring" log_info " (use --disable-kubernetes to skip)" ENABLE_KUBERNETES="true" else ENABLE_KUBERNETES="false" fi fi if [[ "$PROXMOX_EXPLICIT" != "true" ]]; then if detect_proxmox; then log_info "Proxmox detected - enabling Proxmox integration" log_info " (use --disable-proxmox to skip)" ENABLE_PROXMOX="true" else ENABLE_PROXMOX="false" fi fi # Summary of what will be monitored log_info "Monitoring configuration:" log_info " Host metrics: $ENABLE_HOST" log_info " Docker/Podman: $ENABLE_DOCKER" log_info " Kubernetes: $ENABLE_KUBERNETES" log_info " Proxmox: $ENABLE_PROXMOX" # --- Uninstall Logic --- if [[ "$UNINSTALL" == "true" ]]; then log_info "Uninstalling ${AGENT_NAME} and cleaning up legacy agents..." # Kill any running agent processes first pkill -f "pulse-agent" 2>/dev/null || true pkill -f "pulse-host-agent" 2>/dev/null || true pkill -f "pulse-docker-agent" 2>/dev/null || true sleep 1 # Systemd - unified agent if command -v systemctl >/dev/null 2>&1; then systemctl stop "${AGENT_NAME}" 2>/dev/null || true systemctl disable "${AGENT_NAME}" 2>/dev/null || true rm -f "/etc/systemd/system/${AGENT_NAME}.service" # Legacy agents cleanup systemctl stop pulse-host-agent 2>/dev/null || true systemctl disable pulse-host-agent 2>/dev/null || true rm -f /etc/systemd/system/pulse-host-agent.service systemctl stop pulse-docker-agent 2>/dev/null || true systemctl disable pulse-docker-agent 2>/dev/null || true rm -f /etc/systemd/system/pulse-docker-agent.service systemctl daemon-reload 2>/dev/null || true fi # Remove legacy binaries rm -f /usr/local/bin/pulse-host-agent rm -f /usr/local/bin/pulse-docker-agent # Remove agent state directory (contains agent ID, proxmox registration state, etc.) rm -rf /var/lib/pulse-agent # Remove log files rm -f /var/log/pulse-agent.log rm -f /var/log/pulse-host-agent.log rm -f /var/log/pulse-docker-agent.log # Launchd (macOS) if [[ "$(uname -s)" == "Darwin" ]]; then # Unified agent PLIST="/Library/LaunchDaemons/com.pulse.agent.plist" launchctl unload "$PLIST" 2>/dev/null || true rm -f "$PLIST" # Legacy agents launchctl unload /Library/LaunchDaemons/com.pulse.host-agent.plist 2>/dev/null || true rm -f /Library/LaunchDaemons/com.pulse.host-agent.plist launchctl unload /Library/LaunchDaemons/com.pulse.docker-agent.plist 2>/dev/null || true rm -f /Library/LaunchDaemons/com.pulse.docker-agent.plist fi # Synology DSM (handles both DSM 7+ systemd and DSM 6.x upstart) if [[ -d /usr/syno ]]; then # DSM 7+ uses systemd if [[ -f "/etc/systemd/system/${AGENT_NAME}.service" ]]; then systemctl stop "${AGENT_NAME}" 2>/dev/null || true systemctl disable "${AGENT_NAME}" 2>/dev/null || true rm -f "/etc/systemd/system/${AGENT_NAME}.service" systemctl daemon-reload 2>/dev/null || true fi # DSM 6.x uses upstart if [[ -f "/etc/init/${AGENT_NAME}.conf" ]]; then initctl stop "${AGENT_NAME}" 2>/dev/null || true rm -f "/etc/init/${AGENT_NAME}.conf" fi fi # Unraid if [[ -f /etc/unraid-version ]] || [[ -d /boot/config/plugins/pulse-agent ]]; then log_info "Removing Unraid installation (including legacy agents)..." # Stop running agents (unified + legacy) pkill -f "pulse-agent" 2>/dev/null || true pkill -f "pulse-host-agent" 2>/dev/null || true pkill -f "pulse-docker-agent" 2>/dev/null || true sleep 1 # Remove from /boot/config/go - all pulse-related entries GO_SCRIPT="/boot/config/go" if [[ -f "$GO_SCRIPT" ]]; then # Remove unified agent entries sed -i '/# Pulse Agent/,/^$/d' "$GO_SCRIPT" 2>/dev/null || true sed -i '/pulse-agent/d' "$GO_SCRIPT" 2>/dev/null || true # Remove legacy agent entries sed -i '/# Pulse Host Agent/,/^$/d' "$GO_SCRIPT" 2>/dev/null || true sed -i '/# Pulse Docker Agent/,/^$/d' "$GO_SCRIPT" 2>/dev/null || true sed -i '/pulse-host-agent/d' "$GO_SCRIPT" 2>/dev/null || true sed -i '/pulse-docker-agent/d' "$GO_SCRIPT" 2>/dev/null || true fi # Remove installation directories rm -rf /boot/config/plugins/pulse-agent rm -rf /boot/config/pulse # Legacy pulse directory # Remove binaries from RAM disk rm -f "${INSTALL_DIR}/${BINARY_NAME}" rm -f /usr/local/bin/pulse-host-agent rm -f /usr/local/bin/pulse-docker-agent # Remove log directory rm -rf /var/log/pulse fi # TrueNAS SCALE if [[ -d "$TRUENAS_STATE_DIR" ]] || [[ -f /etc/truenas-version ]]; then log_info "Removing TrueNAS SCALE installation..." # Stop and disable service systemctl stop "${AGENT_NAME}" 2>/dev/null || true systemctl disable "${AGENT_NAME}" 2>/dev/null || true # Remove systemd symlink rm -f "/etc/systemd/system/${AGENT_NAME}.service" systemctl daemon-reload 2>/dev/null || true # Remove Init/Shutdown task if command -v midclt >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then TASK_ID=$(midclt call initshutdownscript.query '[["script","=","'"$TRUENAS_BOOTSTRAP_SCRIPT"'"]]' 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d[0]['id'] if d else '')" 2>/dev/null || echo "") if [[ -n "$TASK_ID" ]]; then midclt call initshutdownscript.delete "$TASK_ID" >/dev/null 2>&1 || log_warn "Failed to remove Init/Shutdown task (id $TASK_ID)" fi fi # Remove state directory rm -rf "$TRUENAS_STATE_DIR" fi # OpenRC (Alpine, Gentoo, Artix, etc.) if command -v rc-service >/dev/null 2>&1; then rc-service "${AGENT_NAME}" stop 2>/dev/null || true rc-update del "${AGENT_NAME}" default 2>/dev/null || true rm -f "/etc/init.d/${AGENT_NAME}" fi rm -f "${INSTALL_DIR}/${BINARY_NAME}" log_info "Uninstallation complete." exit 0 fi # --- Validation --- if [[ -z "$PULSE_URL" || -z "$PULSE_TOKEN" ]]; then fail "Missing required arguments: --url and --token" fi # Validate URL format (basic check) if [[ ! "$PULSE_URL" =~ ^https?:// ]]; then fail "Invalid URL format. Must start with http:// or https://" fi # Validate token format (should be hex string, typically 64 chars) if [[ ! "$PULSE_TOKEN" =~ ^[a-fA-F0-9]+$ ]]; then fail "Invalid token format. Token should be a hexadecimal string." fi # Validate interval format if [[ ! "$INTERVAL" =~ ^[0-9]+[smh]?$ ]]; then fail "Invalid interval format. Use format like '30s', '5m', or '1h'." fi # --- TrueNAS SCALE Detection --- # TrueNAS SCALE has an immutable root filesystem; /usr/local/bin is read-only. # We store everything in /data which persists across reboots and upgrades. is_truenas_scale() { if [[ -f /etc/truenas-version ]]; then return 0 fi if [[ -f /etc/version ]] && grep -qi "truenas" /etc/version 2>/dev/null; then return 0 fi if [[ -d /data/ix-applications ]] || [[ -d /etc/ix-apps.d ]]; then return 0 fi # Fallback: check if hostname contains "truenas" (common default hostname) if hostname 2>/dev/null | grep -qi "truenas"; then return 0 fi return 1 } # Check if we can write to /usr/local/bin (catches immutable filesystems like TrueNAS) is_install_dir_writable() { local test_file="${INSTALL_DIR}/.pulse-write-test-$$" if touch "$test_file" 2>/dev/null; then rm -f "$test_file" 2>/dev/null return 0 fi return 1 } if [[ "$(uname -s)" == "Linux" ]] && is_truenas_scale; then TRUENAS=true INSTALL_DIR="$TRUENAS_STATE_DIR" LOG_FILE="$TRUENAS_LOG_DIR/${AGENT_NAME}.log" log_info "TrueNAS SCALE detected (immutable root). Using $TRUENAS_STATE_DIR for installation." elif [[ "$(uname -s)" == "Linux" ]] && [[ -d /data ]] && ! is_install_dir_writable; then # /usr/local/bin is read-only but /data exists - likely TrueNAS or similar immutable system TRUENAS=true INSTALL_DIR="$TRUENAS_STATE_DIR" LOG_FILE="$TRUENAS_LOG_DIR/${AGENT_NAME}.log" log_info "Immutable filesystem detected (read-only /usr/local/bin). Using $TRUENAS_STATE_DIR for installation." fi # --- Download --- OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) case "$ARCH" in x86_64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; armv7l|armhf) ARCH="armv7" ;; armv6l) ARCH="armv6" ;; i386|i686) ARCH="386" ;; *) fail "Unsupported architecture: $ARCH" ;; esac # Construct arch param in format expected by download endpoint (e.g., linux-amd64) ARCH_PARAM="${OS}-${ARCH}" DOWNLOAD_URL="${PULSE_URL}/download/${BINARY_NAME}?arch=${ARCH_PARAM}" log_info "Downloading agent from ${DOWNLOAD_URL}..." # Create temp file and register for cleanup TMP_BIN=$(mktemp) TMP_FILES+=("$TMP_BIN") # Build curl arguments as array for proper quoting CURL_ARGS=(-fsSL --connect-timeout 30 --max-time 300) if [[ "$INSECURE" == "true" ]]; then CURL_ARGS+=(-k); fi if ! curl "${CURL_ARGS[@]}" -o "$TMP_BIN" "$DOWNLOAD_URL"; then fail "Download failed. Check URL and connectivity." fi # Verify downloaded binary if [[ ! -s "$TMP_BIN" ]]; then fail "Downloaded file is empty." fi # Check if it's a valid executable (ELF for Linux, Mach-O for macOS) if [[ "$OS" == "linux" ]]; then if ! head -c 4 "$TMP_BIN" | grep -q "ELF"; then fail "Downloaded file is not a valid Linux executable." fi elif [[ "$OS" == "darwin" ]]; then # Mach-O magic: feedface (32-bit) or feedfacf (64-bit) or cafebabe (universal) MAGIC=$(xxd -p -l 4 "$TMP_BIN" 2>/dev/null || head -c 4 "$TMP_BIN" | od -A n -t x1 | tr -d ' ') if [[ ! "$MAGIC" =~ ^(cffaedfe|cefaedfe|cafebabe|feedface|feedfacf) ]]; then fail "Downloaded file is not a valid macOS executable." fi fi chmod +x "$TMP_BIN" # --- Upgrade Detection --- # Check if pulse-agent is already installed and handle upgrade gracefully EXISTING_VERSION="" UPGRADE_MODE=false if [[ -x "${INSTALL_DIR}/${BINARY_NAME}" ]]; then EXISTING_VERSION=$("${INSTALL_DIR}/${BINARY_NAME}" --version 2>/dev/null | head -1 || echo "unknown") NEW_VERSION=$("$TMP_BIN" --version 2>/dev/null | head -1 || echo "unknown") if [[ -n "$EXISTING_VERSION" && "$EXISTING_VERSION" != "unknown" ]]; then UPGRADE_MODE=true log_info "Existing installation detected: $EXISTING_VERSION" log_info "Upgrading to: $NEW_VERSION" # Stop the existing agent service gracefully if command -v systemctl >/dev/null 2>&1; then if systemctl is-active --quiet "${AGENT_NAME}" 2>/dev/null; then log_info "Stopping existing ${AGENT_NAME} service..." systemctl stop "${AGENT_NAME}" 2>/dev/null || true sleep 2 fi elif command -v rc-service >/dev/null 2>&1; then if rc-service "${AGENT_NAME}" status >/dev/null 2>&1; then log_info "Stopping existing ${AGENT_NAME} service..." rc-service "${AGENT_NAME}" stop 2>/dev/null || true sleep 2 fi fi # Also kill any running process in case it was started manually pkill -f "^${INSTALL_DIR}/${BINARY_NAME}" 2>/dev/null || true sleep 1 fi elif command -v systemctl >/dev/null 2>&1 && systemctl is-enabled --quiet "${AGENT_NAME}" 2>/dev/null; then # Service exists but binary is missing - reinstall scenario log_info "Agent service exists but binary is missing. Reinstalling..." systemctl stop "${AGENT_NAME}" 2>/dev/null || true fi # Install Binary log_info "Installing binary to ${INSTALL_DIR}/${BINARY_NAME}..." mkdir -p "$INSTALL_DIR" mv "$TMP_BIN" "${INSTALL_DIR}/${BINARY_NAME}" chmod +x "${INSTALL_DIR}/${BINARY_NAME}" if [[ "$UPGRADE_MODE" == "true" ]]; then log_info "Binary upgraded successfully. Updating service configuration..." fi # --- Legacy Cleanup --- # Remove old agents if they exist to prevent conflicts # This is critical because legacy agents use the same system ID and will cause # connection conflicts (rapid connect/disconnect cycles) with the unified agent. log_info "Checking for legacy agents..." # Kill any running legacy agent processes first (even if started manually) # This prevents WebSocket connection conflicts during installation pkill -f "pulse-host-agent" 2>/dev/null || true pkill -f "pulse-docker-agent" 2>/dev/null || true sleep 1 # Legacy Host Agent - systemd cleanup if command -v systemctl >/dev/null 2>&1; then if systemctl is-active --quiet pulse-host-agent 2>/dev/null || systemctl is-enabled --quiet pulse-host-agent 2>/dev/null || [[ -f /etc/systemd/system/pulse-host-agent.service ]]; then log_warn "Removing legacy pulse-host-agent..." systemctl stop pulse-host-agent 2>/dev/null || true systemctl disable pulse-host-agent 2>/dev/null || true rm -f /etc/systemd/system/pulse-host-agent.service rm -f /usr/local/bin/pulse-host-agent fi if systemctl is-active --quiet pulse-docker-agent 2>/dev/null || systemctl is-enabled --quiet pulse-docker-agent 2>/dev/null || [[ -f /etc/systemd/system/pulse-docker-agent.service ]]; then log_warn "Removing legacy pulse-docker-agent..." systemctl stop pulse-docker-agent 2>/dev/null || true systemctl disable pulse-docker-agent 2>/dev/null || true rm -f /etc/systemd/system/pulse-docker-agent.service rm -f /usr/local/bin/pulse-docker-agent fi systemctl daemon-reload 2>/dev/null || true fi # Legacy macOS if [[ "$OS" == "darwin" ]]; then if launchctl list | grep -q "com.pulse.host-agent"; then log_warn "Removing legacy com.pulse.host-agent..." launchctl unload /Library/LaunchDaemons/com.pulse.host-agent.plist 2>/dev/null || true rm -f /Library/LaunchDaemons/com.pulse.host-agent.plist rm -f /usr/local/bin/pulse-host-agent fi if launchctl list | grep -q "com.pulse.docker-agent"; then log_warn "Removing legacy com.pulse.docker-agent..." launchctl unload /Library/LaunchDaemons/com.pulse.docker-agent.plist 2>/dev/null || true rm -f /Library/LaunchDaemons/com.pulse.docker-agent.plist rm -f /usr/local/bin/pulse-docker-agent fi fi # --- Service Installation --- # If Proxmox mode is enabled, clear the state file to ensure fresh registration # This allows re-installation to re-create the Proxmox API token if [[ "$ENABLE_PROXMOX" == "true" ]]; then log_info "Clearing Proxmox state for fresh registration..." rm -f /var/lib/pulse-agent/proxmox-registered 2>/dev/null || true fi # 1. macOS (Launchd) if [[ "$OS" == "darwin" ]]; then PLIST="/Library/LaunchDaemons/com.pulse.agent.plist" log_info "Configuring Launchd service at $PLIST..." # Build program arguments array PLIST_ARGS=" ${INSTALL_DIR}/${BINARY_NAME} --url ${PULSE_URL} --token ${PULSE_TOKEN} --interval ${INTERVAL}" # Always pass enable-host flag since agent defaults to true if [[ "$ENABLE_HOST" == "true" ]]; then PLIST_ARGS="${PLIST_ARGS} --enable-host" else PLIST_ARGS="${PLIST_ARGS} --enable-host=false" fi if [[ "$ENABLE_DOCKER" == "true" ]]; then PLIST_ARGS="${PLIST_ARGS} --enable-docker" fi if [[ "$ENABLE_KUBERNETES" == "true" ]]; then PLIST_ARGS="${PLIST_ARGS} --enable-kubernetes" fi if [[ "$INSECURE" == "true" ]]; then PLIST_ARGS="${PLIST_ARGS} --insecure" fi if [[ -n "$AGENT_ID" ]]; then PLIST_ARGS="${PLIST_ARGS} --agent-id ${AGENT_ID}" fi cat > "$PLIST" < Label com.pulse.agent ProgramArguments ${PLIST_ARGS} RunAtLoad KeepAlive StandardOutPath ${LOG_FILE} StandardErrorPath ${LOG_FILE} EOF chmod 644 "$PLIST" launchctl unload "$PLIST" 2>/dev/null || true launchctl load -w "$PLIST" if [[ "$UPGRADE_MODE" == "true" ]]; then log_info "Upgrade complete! Agent restarted with new configuration." else log_info "Installation complete! Agent service started." fi exit 0 fi # 2. Synology DSM # DSM 7+ uses systemd, DSM 6.x uses upstart if [[ -d /usr/syno ]] && [[ -f /etc/VERSION ]]; then # Extract major version from /etc/VERSION DSM_MAJOR=$(grep 'majorversion=' /etc/VERSION | cut -d'"' -f2) log_info "Detected Synology DSM ${DSM_MAJOR}..." # Build command line args build_exec_args if [[ "$DSM_MAJOR" -ge 7 ]]; then # DSM 7+ uses systemd UNIT="/etc/systemd/system/${AGENT_NAME}.service" log_info "Configuring systemd service at $UNIT (DSM 7+)..." cat > "$UNIT" < "$CONF" <> ${LOG_FILE} 2>&1 EOF initctl stop "${AGENT_NAME}" 2>/dev/null || true initctl start "${AGENT_NAME}" fi if [[ "$UPGRADE_MODE" == "true" ]]; then log_info "Upgrade complete! Agent restarted with new configuration." else log_info "Installation complete! Agent service started." fi exit 0 fi # 3. Unraid (no init system - use /boot/config/go script) # Detect Unraid by /etc/unraid-version (preferred) or /boot/config/go with unraid markers if [[ -f /etc/unraid-version ]]; then log_info "Detected Unraid system..." # Unraid's /boot is FAT32 (no execute permission), so we store the binary there # for persistence but copy it to RAM disk (/usr/local/bin) for execution UNRAID_STORAGE_DIR="/boot/config/plugins/pulse-agent" UNRAID_STORED_BINARY="${UNRAID_STORAGE_DIR}/${BINARY_NAME}" RUNTIME_BINARY="${INSTALL_DIR}/${BINARY_NAME}" GO_SCRIPT="/boot/config/go" mkdir -p "$UNRAID_STORAGE_DIR" # Copy binary to persistent storage (for survival across reboots) cp "${RUNTIME_BINARY}" "$UNRAID_STORED_BINARY" # Keep binary in /usr/local/bin (RAM disk) with execute permission for runtime chmod +x "${RUNTIME_BINARY}" log_info "Installed binary to ${UNRAID_STORED_BINARY} (persistent) and ${RUNTIME_BINARY} (runtime)..." # Build command line args (string for wrapper script, array for direct execution) build_exec_args build_exec_args_array # Kill any existing pulse agents (legacy or current) log_info "Stopping any existing pulse agents..." # Use process name matching to avoid killing unrelated processes pkill -f "^${RUNTIME_BINARY}" 2>/dev/null || true pkill -f "^/usr/local/bin/pulse-host-agent" 2>/dev/null || true pkill -f "^/usr/local/bin/pulse-docker-agent" 2>/dev/null || true sleep 2 # Clean up legacy binaries from RAM disk rm -f /usr/local/bin/pulse-host-agent 2>/dev/null || true rm -f /usr/local/bin/pulse-docker-agent 2>/dev/null || true # Create a wrapper script that will be called from /boot/config/go # This script copies from persistent storage to RAM disk on boot, then starts the agent WRAPPER_SCRIPT="${UNRAID_STORAGE_DIR}/start-pulse-agent.sh" cat > "$WRAPPER_SCRIPT" </dev/null || true pkill -f "^/usr/local/bin/pulse-host-agent" 2>/dev/null || true pkill -f "^/usr/local/bin/pulse-docker-agent" 2>/dev/null || true sleep 2 # Copy binary from persistent storage to RAM disk (needed after reboot) cp "${UNRAID_STORED_BINARY}" "${RUNTIME_BINARY}" chmod +x "${RUNTIME_BINARY}" # Start the agent in the background nohup ${RUNTIME_BINARY} ${EXEC_ARGS} >> /var/log/${AGENT_NAME}.log 2>&1 & EOF # Add to /boot/config/go if not already present GO_MARKER="# Pulse Agent" if [[ -f "$GO_SCRIPT" ]]; then # Remove any existing Pulse agent entries sed -i "/${GO_MARKER}/,/^$/d" "$GO_SCRIPT" 2>/dev/null || true sed -i '/pulse-agent/d' "$GO_SCRIPT" 2>/dev/null || true else # Create go script if it doesn't exist echo "#!/bin/bash" > "$GO_SCRIPT" chmod +x "$GO_SCRIPT" fi # Append startup entry (use bash explicitly since /boot is FAT32 and doesn't support execute bits) cat >> "$GO_SCRIPT" <> "/var/log/${AGENT_NAME}.log" 2>&1 & log_info "Installation complete!" log_info "The agent will start automatically on boot." log_info "To check status: pgrep -a pulse-agent" log_info "To view logs: tail -f /var/log/${AGENT_NAME}.log" exit 0 fi # 4. TrueNAS SCALE (immutable root, uses systemd but needs special persistence) # TrueNAS SCALE wipes /etc/systemd/system on upgrades, so we store the service # in /data and create an Init/Shutdown task to recreate the symlink on boot. # Note: /data may have exec=off on some TrueNAS systems. On TrueNAS SCALE 24.04+, # /usr/local/bin is also read-only. We try multiple runtime locations. if [[ "$TRUENAS" == true ]]; then log_info "Configuring TrueNAS SCALE installation..." # Create directories mkdir -p "$TRUENAS_STATE_DIR" mkdir -p "$TRUENAS_LOG_DIR" TRUENAS_STORED_BINARY="$TRUENAS_STATE_DIR/${BINARY_NAME}" # Move binary to persistent storage location if [[ -f "${INSTALL_DIR}/${BINARY_NAME}" ]] && [[ "$INSTALL_DIR" == "$TRUENAS_STATE_DIR" ]]; then # Binary already in the right place from earlier mv : else mv "${INSTALL_DIR}/${BINARY_NAME}" "$TRUENAS_STORED_BINARY" fi chmod +x "$TRUENAS_STORED_BINARY" # Determine runtime binary location - try executing from /data first # TrueNAS SCALE 24.04+ has read-only /usr/local/bin, so we need alternatives TRUENAS_RUNTIME_BINARY="" # Test if /data allows execution (no noexec mount option) if "$TRUENAS_STORED_BINARY" --version >/dev/null 2>&1; then log_info "Binary can execute from /data - using direct execution." TRUENAS_RUNTIME_BINARY="$TRUENAS_STORED_BINARY" else # /data has noexec, need to copy to an executable location # Try locations in order of preference for RUNTIME_DIR in "/usr/local/bin" "/root/bin" "/var/tmp"; do if [[ "$RUNTIME_DIR" == "/root/bin" ]]; then mkdir -p "$RUNTIME_DIR" 2>/dev/null || continue fi # Test if we can write and execute from this location TEST_FILE="${RUNTIME_DIR}/.pulse-exec-test-$$" if cp "$TRUENAS_STORED_BINARY" "$TEST_FILE" 2>/dev/null && \ chmod +x "$TEST_FILE" 2>/dev/null && \ "$TEST_FILE" --version >/dev/null 2>&1; then rm -f "$TEST_FILE" TRUENAS_RUNTIME_BINARY="${RUNTIME_DIR}/${BINARY_NAME}" log_info "Using ${RUNTIME_DIR} for binary execution." break fi rm -f "$TEST_FILE" 2>/dev/null done fi if [[ -z "$TRUENAS_RUNTIME_BINARY" ]]; then log_error "Could not find a writable location that allows execution." log_error "Tried: /data (noexec), /usr/local/bin (read-only), /root/bin, /var/tmp" exit 1 fi # Copy to runtime location if different from storage location if [[ "$TRUENAS_RUNTIME_BINARY" != "$TRUENAS_STORED_BINARY" ]]; then cp "$TRUENAS_STORED_BINARY" "$TRUENAS_RUNTIME_BINARY" chmod +x "$TRUENAS_RUNTIME_BINARY" fi # Build command line args build_exec_args # Store service file in /data (persists across upgrades) # Service uses /usr/local/bin path (runtime location) TRUENAS_SERVICE_STORAGE="$TRUENAS_STATE_DIR/${AGENT_NAME}.service" cat > "$TRUENAS_SERVICE_STORAGE" < "$TRUENAS_ENV_FILE" < "$TRUENAS_BOOTSTRAP_SCRIPT" <<'BOOTSTRAP' #!/bin/bash # Pulse Agent Bootstrap for TrueNAS SCALE # This script is called by TrueNAS Init/Shutdown task on boot. # It ensures the binary is in an executable location and recreates the # systemd symlink (which is wiped on TrueNAS upgrades). set -e SERVICE_NAME="pulse-agent" STATE_DIR="STATE_DIR_PLACEHOLDER" STORED_BINARY="${STATE_DIR}/pulse-agent" RUNTIME_BINARY="RUNTIME_BINARY_PLACEHOLDER" SERVICE_STORAGE="${STATE_DIR}/pulse-agent.service" SYSTEMD_LINK="/etc/systemd/system/${SERVICE_NAME}.service" if [[ ! -f "$STORED_BINARY" ]]; then echo "ERROR: Binary not found at $STORED_BINARY" exit 1 fi if [[ ! -f "$SERVICE_STORAGE" ]]; then echo "ERROR: Service file not found at $SERVICE_STORAGE" exit 1 fi # If runtime binary is different from stored binary, copy it if [[ "$RUNTIME_BINARY" != "$STORED_BINARY" ]]; then # Ensure parent directory exists (e.g., /root/bin) mkdir -p "$(dirname "$RUNTIME_BINARY")" 2>/dev/null || true cp "$STORED_BINARY" "$RUNTIME_BINARY" chmod +x "$RUNTIME_BINARY" fi # Create symlink (or update if exists) ln -sf "$SERVICE_STORAGE" "$SYSTEMD_LINK" # Reload and start systemctl daemon-reload systemctl enable "$SERVICE_NAME" 2>/dev/null || true systemctl restart "$SERVICE_NAME" echo "Pulse agent started successfully" BOOTSTRAP # Replace placeholders sed -i "s|STATE_DIR_PLACEHOLDER|${TRUENAS_STATE_DIR}|g" "$TRUENAS_BOOTSTRAP_SCRIPT" sed -i "s|RUNTIME_BINARY_PLACEHOLDER|${TRUENAS_RUNTIME_BINARY}|g" "$TRUENAS_BOOTSTRAP_SCRIPT" chmod +x "$TRUENAS_BOOTSTRAP_SCRIPT" # Create systemd symlink now SYSTEMD_LINK="/etc/systemd/system/${AGENT_NAME}.service" ln -sf "$TRUENAS_SERVICE_STORAGE" "$SYSTEMD_LINK" # Register Init/Shutdown task using midclt if command -v midclt >/dev/null 2>&1; then log_info "Registering TrueNAS Init/Shutdown task..." # Check if task already exists EXISTING_TASK=$(midclt call initshutdownscript.query '[["script","=","'"$TRUENAS_BOOTSTRAP_SCRIPT"'"]]' 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d[0]['id'] if d else '')" 2>/dev/null || echo "") if [[ -n "$EXISTING_TASK" ]]; then log_info "Init/Shutdown task already exists (id $EXISTING_TASK), updating..." midclt call initshutdownscript.update "$EXISTING_TASK" '{"type":"SCRIPT","script":"'"$TRUENAS_BOOTSTRAP_SCRIPT"'","when":"POSTINIT","enabled":true,"timeout":30,"comment":"Pulse Agent Bootstrap"}' >/dev/null 2>&1 || true else midclt call initshutdownscript.create '{"type":"SCRIPT","script":"'"$TRUENAS_BOOTSTRAP_SCRIPT"'","when":"POSTINIT","enabled":true,"timeout":30,"comment":"Pulse Agent Bootstrap"}' >/dev/null 2>&1 || log_warn "Failed to create Init/Shutdown task. Please add it manually in TrueNAS UI." fi else log_warn "midclt not available. Please create an Init/Shutdown task manually in TrueNAS UI:" log_warn " Type: Script" log_warn " Script: $TRUENAS_BOOTSTRAP_SCRIPT" log_warn " When: Post Init" fi # Enable and start service systemctl daemon-reload systemctl enable "${AGENT_NAME}" 2>/dev/null || true systemctl restart "${AGENT_NAME}" log_info "Installation complete!" log_info "Binary: $TRUENAS_STORED_BINARY (persistent)" log_info "Runtime: $TRUENAS_RUNTIME_BINARY (for execution)" log_info "Service: $TRUENAS_SERVICE_STORAGE (symlinked to systemd)" log_info "Logs: tail -f ${LOG_FILE}" log_info "" log_info "The Init/Shutdown task ensures the agent survives TrueNAS upgrades." exit 0 fi # 5. OpenRC (Alpine, Gentoo, Artix, etc.) # Check for rc-service but make sure we're not on a systemd system that happens to have it if command -v rc-service >/dev/null 2>&1 && [[ -d /etc/init.d ]] && ! command -v systemctl >/dev/null 2>&1; then INITSCRIPT="/etc/init.d/${AGENT_NAME}" log_info "Configuring OpenRC service at $INITSCRIPT..." # Build command line args build_exec_args # Create OpenRC init script following Alpine best practices # Using command_background=yes with pidfile for proper daemon management cat > "$INITSCRIPT" <<'INITEOF' #!/sbin/openrc-run # Pulse Unified Agent OpenRC init script name="pulse-agent" description="Pulse Unified Agent" command="INSTALL_DIR_PLACEHOLDER/BINARY_NAME_PLACEHOLDER" command_args="EXEC_ARGS_PLACEHOLDER" command_background="yes" command_user="root" pidfile="/run/${RC_SVCNAME}.pid" output_log="/var/log/pulse-agent.log" error_log="/var/log/pulse-agent.log" # Ensure log file exists start_pre() { touch "$output_log" } depend() { need net use docker } INITEOF # Replace placeholders with actual values sed -i "s|INSTALL_DIR_PLACEHOLDER|${INSTALL_DIR}|g" "$INITSCRIPT" sed -i "s|BINARY_NAME_PLACEHOLDER|${BINARY_NAME}|g" "$INITSCRIPT" sed -i "s|EXEC_ARGS_PLACEHOLDER|${EXEC_ARGS}|g" "$INITSCRIPT" chmod +x "$INITSCRIPT" rc-service "${AGENT_NAME}" stop 2>/dev/null || true rc-update add "${AGENT_NAME}" default 2>/dev/null || true rc-service "${AGENT_NAME}" start if [[ "$UPGRADE_MODE" == "true" ]]; then log_info "Upgrade complete! Agent restarted with new configuration." else log_info "Installation complete! Agent service started." fi exit 0 fi # 5. Linux (Systemd) if command -v systemctl >/dev/null 2>&1; then UNIT="/etc/systemd/system/${AGENT_NAME}.service" log_info "Configuring Systemd service at $UNIT..." # Build command line args build_exec_args cat > "$UNIT" <