Files
Pulse/scripts/install.sh
rcourtman 7346d48872 fix: add FreeBSD agent binaries to Docker build and fix pfSense boot (#1051)
Two fixes for FreeBSD agent support:

1. The Docker image never built or included FreeBSD agent binaries, causing
   404 errors when FreeBSD clients requested the download. Added FreeBSD
   amd64/arm64 cross-compilation for both host-agent and unified-agent,
   plus COPY statements to include them in the image. Also added bare
   FreeBSD binaries to GitHub release assets for the redirect fallback.

2. pfSense does not use the standard FreeBSD rc.d boot system — scripts
   in /usr/local/etc/rc.d/ must end in .sh to run at boot. The installer
   now detects pfSense and creates a .sh boot wrapper alongside the
   standard rc.d script. Also added -r flag to daemon for auto-restart.

Related to #1051
2026-02-04 10:55:55 +00:00

1689 lines
62 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Pulse Unified Agent Installer
# Supports: Linux (systemd, OpenRC, SysV init), macOS (launchd), FreeBSD (rc.d), Synology DSM (6.x/7+), Unraid
#
# Usage:
# curl -fsSL http://pulse/install.sh | bash -s -- --url http://pulse --token <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)
# --kubeconfig <path> Path to kubeconfig file (auto-detected if not specified)
# --disable-kubernetes Disable Kubernetes monitoring even if detected
# --kube-include-all-pods Include all non-succeeded pods (default: false)
# --kube-include-all-deployments Include all deployments (default: false)
# --enable-proxmox Force enable Proxmox integration (default: auto-detect)
# --disable-proxmox Disable Proxmox integration even if detected
# --interval <dur> Reporting interval (default: 30s)
# --agent-id <id> Custom agent identifier (default: auto-generated)
# --disk-exclude <pattern> Exclude mount points matching pattern (repeatable)
# --insecure Skip TLS certificate verification
# --enable-commands Enable AI command execution on agent (disabled by default)
# --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() {
# Use ${arr[@]+"${arr[@]}"} for bash 3.2 compatibility with set -u
for f in ${TMP_FILES[@]+"${TMP_FILES[@]}"}; do
rm -f "$f" 2>/dev/null || true
done
}
trap cleanup EXIT
# --- 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=""
HOSTNAME_OVERRIDE=""
ENABLE_COMMANDS="false"
KUBECONFIG_PATH="" # Path to kubeconfig file for Kubernetes monitoring
KUBE_INCLUDE_ALL_PODS="false"
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=() # Array for multiple --disk-exclude values
# 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
}
show_help() {
cat <<EOF
Pulse Unified Agent Installer
Usage:
install.sh [options]
Options:
--url <url> Pulse server URL (e.g. http://pulse:7655)
--token <token> Pulse API token
--interval <duration> Reporting interval (default: 30s)
--enable-host Enable host metrics (default: true)
--disable-host Disable host metrics
--enable-docker Force enable Docker monitoring
--enable-kubernetes Force enable Kubernetes monitoring
--kubeconfig <path> Path to kubeconfig file
--kube-include-all-pods Include all non-succeeded pods
--kube-include-all-deployments Include all deployments
--enable-proxmox Force enable Proxmox integration
--agent-id <id> Custom agent identifier
--hostname <name> Override hostname reported to Pulse
--disk-exclude <path> Exclude mount point (repeatable)
--insecure Skip TLS verification
--enable-commands Enable AI command execution
--uninstall Remove the agent
--help, -h Show this help
EOF
}
# --- SELinux Context Restoration ---
# On SELinux-enforcing systems (Fedora, RHEL, CentOS), binaries in non-standard
# locations need proper security contexts for systemd to execute them.
restore_selinux_contexts() {
# Check if SELinux is available and enforcing
if ! command -v getenforce >/dev/null 2>&1; then
return 0 # SELinux not installed
fi
if [[ "$(getenforce 2>/dev/null)" != "Enforcing" ]]; then
return 0 # SELinux not enforcing
fi
# restorecon is the proper way to fix SELinux contexts
if command -v restorecon >/dev/null 2>&1; then
log_info "Restoring SELinux contexts for installed binaries..."
restorecon -v "${INSTALL_DIR}/${BINARY_NAME}" >/dev/null 2>&1 || true
log_info "SELinux context restored"
else
# Fallback to chcon if restorecon isn't available
if command -v chcon >/dev/null 2>&1; then
log_info "Setting SELinux context for installed binary..."
chcon -t bin_t "${INSTALL_DIR}/${BINARY_NAME}" 2>/dev/null || true
fi
fi
}
# --- 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
else
log_warn "Docker binary found ($(command -v docker)) but 'docker info' failed. Is the daemon running?"
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
else
log_warn "Podman binary found but 'podman info' failed."
fi
fi
return 1
}
detect_kubernetes() {
# If user already specified a kubeconfig path, just verify it exists
if [[ -n "$KUBECONFIG_PATH" ]]; then
if [[ -f "$KUBECONFIG_PATH" ]]; then
return 0
else
log_warn "Specified kubeconfig not found: $KUBECONFIG_PATH"
return 1
fi
fi
# 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
# kubectl works, try to find the kubeconfig it's using
if [[ -n "${KUBECONFIG:-}" ]] && [[ -f "${KUBECONFIG:-}" ]]; then
KUBECONFIG_PATH="${KUBECONFIG}"
elif [[ -f "${HOME}/.kube/config" ]]; then
KUBECONFIG_PATH="${HOME}/.kube/config"
fi
return 0
fi
fi
# Search for kubeconfig in common locations
# Priority: /etc/kubernetes/admin.conf (standard k8s), then user home directories
local search_paths=(
"/etc/kubernetes/admin.conf"
"/root/.kube/config"
)
# Add all user home directories
for user_home in /home/*; do
if [[ -d "$user_home/.kube" ]]; then
search_paths+=("$user_home/.kube/config")
fi
done
for kconfig in "${search_paths[@]}"; do
if [[ -f "$kconfig" ]]; then
KUBECONFIG_PATH="$kconfig"
log_info "Found kubeconfig at: $KUBECONFIG_PATH"
return 0
fi
done
# Check if running inside a Kubernetes pod (in-cluster config)
if [[ -f "/var/run/secrets/kubernetes.io/serviceaccount/token" ]]; then
# In-cluster config doesn't need a kubeconfig file
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
# Pass explicit false when Docker was explicitly disabled (prevents auto-detection)
if [[ "$ENABLE_DOCKER" == "false" && "$DOCKER_EXPLICIT" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-docker=false"; fi
if [[ "$ENABLE_KUBERNETES" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-kubernetes"; fi
if [[ -n "$KUBECONFIG_PATH" ]]; then EXEC_ARGS="$EXEC_ARGS --kubeconfig ${KUBECONFIG_PATH}"; 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 [[ "$ENABLE_COMMANDS" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-commands"; fi
if [[ "$KUBE_INCLUDE_ALL_PODS" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --kube-include-all-pods"; fi
if [[ "$KUBE_INCLUDE_ALL_DEPLOYMENTS" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --kube-include-all-deployments"; fi
if [[ -n "$AGENT_ID" ]]; then EXEC_ARGS="$EXEC_ARGS --agent-id ${AGENT_ID}"; fi
if [[ -n "$HOSTNAME_OVERRIDE" ]]; then EXEC_ARGS="$EXEC_ARGS --hostname ${HOSTNAME_OVERRIDE}"; fi
# Add disk exclude patterns (use ${arr[@]+"${arr[@]}"} for bash 3.2 compatibility with set -u)
for pattern in ${DISK_EXCLUDES[@]+"${DISK_EXCLUDES[@]}"}; do
EXEC_ARGS="$EXEC_ARGS --disk-exclude '${pattern}'"
done
}
# 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
# Pass explicit false when Docker was explicitly disabled (prevents auto-detection)
if [[ "$ENABLE_DOCKER" == "false" && "$DOCKER_EXPLICIT" == "true" ]]; then EXEC_ARGS_ARRAY+=(--enable-docker=false); fi
if [[ "$ENABLE_KUBERNETES" == "true" ]]; then EXEC_ARGS_ARRAY+=(--enable-kubernetes); fi
if [[ -n "$KUBECONFIG_PATH" ]]; then EXEC_ARGS_ARRAY+=(--kubeconfig "$KUBECONFIG_PATH"); 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 [[ "$ENABLE_COMMANDS" == "true" ]]; then EXEC_ARGS_ARRAY+=(--enable-commands); fi
if [[ "$KUBE_INCLUDE_ALL_PODS" == "true" ]]; then EXEC_ARGS_ARRAY+=(--kube-include-all-pods); fi
if [[ "$KUBE_INCLUDE_ALL_DEPLOYMENTS" == "true" ]]; then EXEC_ARGS_ARRAY+=(--kube-include-all-deployments); fi
if [[ -n "$AGENT_ID" ]]; then EXEC_ARGS_ARRAY+=(--agent-id "$AGENT_ID"); fi
if [[ -n "$HOSTNAME_OVERRIDE" ]]; then EXEC_ARGS_ARRAY+=(--hostname "$HOSTNAME_OVERRIDE"); fi
# Add disk exclude patterns (use ${arr[@]+"${arr[@]}"} for bash 3.2 compatibility with set -u)
for pattern in ${DISK_EXCLUDES[@]+"${DISK_EXCLUDES[@]}"}; do
EXEC_ARGS_ARRAY+=(--disk-exclude "$pattern")
done
}
# --- Parse Arguments ---
while [[ $# -gt 0 ]]; do
case $1 in
--help|-h) show_help; exit 0 ;;
--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 ;;
--kubeconfig) KUBECONFIG_PATH="$2"; KUBERNETES_EXPLICIT="true"; ENABLE_KUBERNETES="true"; shift 2 ;;
--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 ;;
--enable-commands) ENABLE_COMMANDS="true"; shift ;;
--uninstall) UNINSTALL="true"; shift ;;
--agent-id) AGENT_ID="$2"; shift 2 ;;
--hostname) HOSTNAME_OVERRIDE="$2"; shift 2 ;;
--kube-include-all-pods) KUBE_INCLUDE_ALL_PODS="true"; shift ;;
--kube-include-all-deployments) KUBE_INCLUDE_ALL_DEPLOYMENTS="true"; shift ;;
--disk-exclude) DISK_EXCLUDES+=("$2"); shift 2 ;;
*) fail "Unknown argument: $1" ;;
esac
done
# --- Check Root ---
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root. Please use sudo."
exit 1
fi
# --- URL Normalization ---
# Strip trailing slashes from PULSE_URL to prevent double-slash URLs
# (e.g., http://host:7655//download/... which would match frontend routes)
if [[ -n "$PULSE_URL" ]]; then
PULSE_URL="${PULSE_URL%/}"
fi
# --- 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..."
# Try to notify the Pulse server about uninstallation if we have connection details
# This ensures the host record is removed and any linked PVE nodes are updated immediately.
if [[ -n "$PULSE_URL" && -n "$PULSE_TOKEN" ]]; then
# Try to recover agent ID if not provided
if [[ -z "$AGENT_ID" ]]; then
if [[ -f /var/lib/pulse-agent/agent-id ]]; then
AGENT_ID=$(cat /var/lib/pulse-agent/agent-id)
elif [[ -f "$TRUENAS_STATE_DIR/agent-id" ]]; then
AGENT_ID=$(cat "$TRUENAS_STATE_DIR/agent-id")
fi
fi
if [[ -n "$AGENT_ID" ]]; then
log_info "Notifying Pulse server to unregister agent ID: ${AGENT_ID}..."
CURL_ARGS=(-fsSL --connect-timeout 5 -X POST -H "Content-Type: application/json" -H "X-API-Token: ${PULSE_TOKEN}")
if [[ "$INSECURE" == "true" ]]; then CURL_ARGS+=(-k); fi
# Send unregistration request (ignore errors as we are uninstalling anyway)
curl "${CURL_ARGS[@]}" -d "{\"hostId\": \"${AGENT_ID}\"}" "${PULSE_URL}/api/agents/host/uninstall" >/dev/null 2>&1 || true
fi
fi
# 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
# SysV init (legacy systems like Asustor, older Debian/RHEL, etc.)
if [[ -f "/etc/init.d/${AGENT_NAME}" ]]; then
"/etc/init.d/${AGENT_NAME}" stop 2>/dev/null || true
# Remove using available tools
if command -v update-rc.d >/dev/null 2>&1; then
update-rc.d -f "${AGENT_NAME}" remove >/dev/null 2>&1 || true
elif command -v chkconfig >/dev/null 2>&1; then
chkconfig "${AGENT_NAME}" off >/dev/null 2>&1 || true
chkconfig --del "${AGENT_NAME}" >/dev/null 2>&1 || true
fi
# Remove rc.d symlinks manually (in case tools weren't available)
for RL in 0 1 2 3 4 5 6; do
rm -f "/etc/rc${RL}.d/S99${AGENT_NAME}" 2>/dev/null || true
rm -f "/etc/rc${RL}.d/K01${AGENT_NAME}" 2>/dev/null || true
done
rm -f "/etc/init.d/${AGENT_NAME}"
rm -f "/var/run/${AGENT_NAME}.pid"
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) - case-insensitive for http:// or https://
# Normalize to lowercase for the check
url_lower=$(echo "$PULSE_URL" | tr '[:upper:]' '[:lower:]')
if [[ ! "$url_lower" =~ ^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|amd64) 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 files to ensure fresh registration
# This allows re-installation to re-create the Proxmox API tokens
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
rm -f /var/lib/pulse-agent/proxmox-pve-registered 2>/dev/null || true
rm -f /var/lib/pulse-agent/proxmox-pbs-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=" <string>${INSTALL_DIR}/${BINARY_NAME}</string>
<string>--url</string>
<string>${PULSE_URL}</string>
<string>--token</string>
<string>${PULSE_TOKEN}</string>
<string>--interval</string>
<string>${INTERVAL}</string>"
# Always pass enable-host flag since agent defaults to true
if [[ "$ENABLE_HOST" == "true" ]]; then
PLIST_ARGS="${PLIST_ARGS}
<string>--enable-host</string>"
else
PLIST_ARGS="${PLIST_ARGS}
<string>--enable-host=false</string>"
fi
if [[ "$ENABLE_DOCKER" == "true" ]]; then
PLIST_ARGS="${PLIST_ARGS}
<string>--enable-docker</string>"
fi
if [[ "$ENABLE_KUBERNETES" == "true" ]]; then
PLIST_ARGS="${PLIST_ARGS}
<string>--enable-kubernetes</string>"
fi
if [[ -n "$KUBECONFIG_PATH" ]]; then
PLIST_ARGS="${PLIST_ARGS}
<string>--kubeconfig</string>
<string>${KUBECONFIG_PATH}</string>"
fi
if [[ "$INSECURE" == "true" ]]; then
PLIST_ARGS="${PLIST_ARGS}
<string>--insecure</string>"
fi
if [[ "$ENABLE_COMMANDS" == "true" ]]; then
PLIST_ARGS="${PLIST_ARGS}
<string>--enable-commands</string>"
fi
if [[ -n "$AGENT_ID" ]]; then
PLIST_ARGS="${PLIST_ARGS}
<string>--agent-id</string>
<string>${AGENT_ID}</string>"
fi
# Add disk exclude patterns (use ${arr[@]+"${arr[@]}"} for bash 3.2 compatibility with set -u)
for pattern in ${DISK_EXCLUDES[@]+"${DISK_EXCLUDES[@]}"}; do
PLIST_ARGS="${PLIST_ARGS}
<string>--disk-exclude</string>
<string>${pattern}</string>"
done
cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.pulse.agent</string>
<key>ProgramArguments</key>
<array>
${PLIST_ARGS}
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>${LOG_FILE}</string>
<key>StandardErrorPath</key>
<string>${LOG_FILE}</string>
</dict>
</plist>
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" <<EOF
[Unit]
Description=Pulse Unified Agent
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
ExecStart=${INSTALL_DIR}/${BINARY_NAME} ${EXEC_ARGS}
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "${AGENT_NAME}"
systemctl restart "${AGENT_NAME}"
else
# DSM 6.x uses upstart
CONF="/etc/init/${AGENT_NAME}.conf"
log_info "Configuring Upstart service at $CONF (DSM 6.x)..."
cat > "$CONF" <<EOF
description "Pulse Unified Agent"
author "Pulse"
start on syno.network.ready
stop on runlevel [06]
respawn
respawn limit 5 10
exec ${INSTALL_DIR}/${BINARY_NAME} ${EXEC_ARGS} >> ${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" <<EOF
#!/bin/bash
# Pulse Agent startup script for Unraid
# Auto-generated by Pulse installer
# Includes watchdog loop to restart agent on failure
# Kill any existing pulse-agent 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
# Copy binary from persistent storage to RAM disk (needed after reboot)
cp "${UNRAID_STORED_BINARY}" "${RUNTIME_BINARY}"
chmod +x "${RUNTIME_BINARY}"
# Watchdog loop: restart agent if it exits
# Uses exponential backoff to prevent rapid restart loops
RESTART_DELAY=5
MAX_RESTART_DELAY=60
while true; do
echo "\$(date '+%Y-%m-%d %H:%M:%S') [watchdog] Starting pulse-agent..." >> /var/log/${AGENT_NAME}.log
${RUNTIME_BINARY} ${EXEC_ARGS} >> /var/log/${AGENT_NAME}.log 2>&1
EXIT_CODE=\$?
echo "\$(date '+%Y-%m-%d %H:%M:%S') [watchdog] pulse-agent exited with code \$EXIT_CODE, restarting in \${RESTART_DELAY}s..." >> /var/log/${AGENT_NAME}.log
sleep \$RESTART_DELAY
# Exponential backoff (cap at MAX_RESTART_DELAY)
RESTART_DELAY=\$((RESTART_DELAY * 2))
if [ \$RESTART_DELAY -gt \$MAX_RESTART_DELAY ]; then
RESTART_DELAY=\$MAX_RESTART_DELAY
fi
done
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" <<EOF
${GO_MARKER}
bash ${WRAPPER_SCRIPT}
EOF
log_info "Added startup entry to ${GO_SCRIPT}..."
# Start the agent now using the wrapper script (includes watchdog)
# Use shell backgrounding instead of nohup for broader compatibility (QNAP, etc.)
log_info "Starting agent with watchdog..."
bash "${WRAPPER_SCRIPT}" >> "/var/log/${AGENT_NAME}.log" 2>&1 &
disown 2>/dev/null || true # Disown if available to prevent SIGHUP
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..."
# Stop any existing agent before we modify binaries
# The runtime binary may be in /root/bin or /var/tmp, not just INSTALL_DIR
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
# Kill any remaining pulse-agent processes (may be running from different paths)
pkill -9 -f "pulse-agent" 2>/dev/null || true
sleep 1
# Remove old runtime binaries that may be "text file busy"
rm -f /root/bin/pulse-agent 2>/dev/null || true
rm -f /var/tmp/pulse-agent 2>/dev/null || true
# 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" <<EOF
[Unit]
Description=Pulse Unified Agent
After=network-online.target docker.service
Wants=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
ExecStart=${TRUENAS_RUNTIME_BINARY} ${EXEC_ARGS}
Restart=always
RestartSec=5s
User=root
StandardOutput=append:${LOG_FILE}
StandardError=append:${LOG_FILE}
[Install]
WantedBy=multi-user.target
EOF
# Store environment/config for reference
cat > "$TRUENAS_ENV_FILE" <<EOF
# Pulse Agent configuration (for reference)
PULSE_URL=${PULSE_URL}
PULSE_TOKEN=${PULSE_TOKEN}
PULSE_INTERVAL=${INTERVAL}
PULSE_ENABLE_HOST=${ENABLE_HOST}
PULSE_ENABLE_DOCKER=${ENABLE_DOCKER}
PULSE_ENABLE_KUBERNETES=${ENABLE_KUBERNETES}
PULSE_KUBE_INCLUDE_ALL_PODS=${KUBE_INCLUDE_ALL_PODS}
PULSE_KUBE_INCLUDE_ALL_DEPLOYMENTS=${KUBE_INCLUDE_ALL_DEPLOYMENTS}
EOF
chmod 600 "$TRUENAS_ENV_FILE"
# Create bootstrap script that runs on boot
# This script handles the runtime binary location and recreates the systemd symlink
cat > "$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
# 5b. FreeBSD rc.d (OPNsense, pfSense, vanilla FreeBSD)
if [[ "$OS" == "freebsd" ]] || [[ -f /etc/rc.subr ]]; then
RCSCRIPT="/usr/local/etc/rc.d/${AGENT_NAME}"
log_info "Configuring FreeBSD rc.d service at $RCSCRIPT..."
# Build command line args
build_exec_args
# Create FreeBSD rc.d script following FreeBSD conventions
cat > "$RCSCRIPT" <<'RCEOF'
#!/bin/sh
# PROVIDE: pulse_agent
# REQUIRE: LOGIN NETWORKING
# KEYWORD: shutdown
. /etc/rc.subr
name="pulse_agent"
rcvar="pulse_agent_enable"
pidfile="/var/run/${name}.pid"
# These placeholders are replaced by sed below
command="INSTALL_DIR_PLACEHOLDER/BINARY_NAME_PLACEHOLDER"
command_args="EXEC_ARGS_PLACEHOLDER"
start_cmd="${name}_start"
stop_cmd="${name}_stop"
status_cmd="${name}_status"
pulse_agent_start()
{
if checkyesno ${rcvar}; then
echo "Starting ${name}."
/usr/sbin/daemon -r -p ${pidfile} -f ${command} ${command_args}
fi
}
pulse_agent_stop()
{
if [ -f ${pidfile} ]; then
echo "Stopping ${name}."
kill $(cat ${pidfile}) 2>/dev/null
rm -f ${pidfile}
else
echo "${name} is not running."
fi
}
pulse_agent_status()
{
if [ -f ${pidfile} ] && kill -0 $(cat ${pidfile}) 2>/dev/null; then
echo "${name} is running as pid $(cat ${pidfile})."
else
echo "${name} is not running."
return 1
fi
}
load_rc_config $name
run_rc_command "$1"
RCEOF
# Replace placeholders with actual values
sed -i '' "s|INSTALL_DIR_PLACEHOLDER|${INSTALL_DIR}|g" "$RCSCRIPT" 2>/dev/null || \
sed -i "s|INSTALL_DIR_PLACEHOLDER|${INSTALL_DIR}|g" "$RCSCRIPT"
sed -i '' "s|BINARY_NAME_PLACEHOLDER|${BINARY_NAME}|g" "$RCSCRIPT" 2>/dev/null || \
sed -i "s|BINARY_NAME_PLACEHOLDER|${BINARY_NAME}|g" "$RCSCRIPT"
sed -i '' "s|EXEC_ARGS_PLACEHOLDER|${EXEC_ARGS}|g" "$RCSCRIPT" 2>/dev/null || \
sed -i "s|EXEC_ARGS_PLACEHOLDER|${EXEC_ARGS}|g" "$RCSCRIPT"
chmod +x "$RCSCRIPT"
# Enable the service in rc.conf
if ! grep -q "pulse_agent_enable" /etc/rc.conf 2>/dev/null; then
echo 'pulse_agent_enable="YES"' >> /etc/rc.conf
else
sed -i '' 's/pulse_agent_enable=.*/pulse_agent_enable="YES"/' /etc/rc.conf 2>/dev/null || \
sed -i 's/pulse_agent_enable=.*/pulse_agent_enable="YES"/' /etc/rc.conf
fi
# pfSense does not use the standard FreeBSD rc.d boot system.
# Scripts in /usr/local/etc/rc.d/ must end in .sh to run at boot.
# Create a .sh wrapper that invokes the rc.d script on boot.
if [ -f /usr/local/sbin/pfSsh.php ] || ([ -f /etc/platform ] && grep -qi pfsense /etc/platform 2>/dev/null); then
BOOT_WRAPPER="/usr/local/etc/rc.d/pulse_agent.sh"
log_info "Detected pfSense — creating boot wrapper at $BOOT_WRAPPER..."
cat > "$BOOT_WRAPPER" <<'BOOTEOF'
#!/bin/sh
# pfSense boot wrapper for pulse-agent
# pfSense requires .sh extension for scripts to run at boot
/usr/local/etc/rc.d/pulse-agent start
BOOTEOF
chmod +x "$BOOT_WRAPPER"
fi
# Stop existing agent if running
"$RCSCRIPT" stop 2>/dev/null || true
sleep 1
# Start the agent
"$RCSCRIPT" start
if [[ "$UPGRADE_MODE" == "true" ]]; then
log_info "Upgrade complete! Agent restarted with new configuration."
else
log_info "Installation complete! Agent service started."
fi
log_info "To check status: $RCSCRIPT status"
log_info "To view logs: tail -f /var/log/messages"
exit 0
fi
# 5. Linux (Systemd)
if command -v systemctl >/dev/null 2>&1; then
UNIT="/etc/systemd/system/${AGENT_NAME}.service"
TOKEN_DIR="/var/lib/pulse-agent"
TOKEN_FILE="${TOKEN_DIR}/token"
log_info "Configuring Systemd service at $UNIT..."
# Write token to secure file (not visible in ps or service file)
mkdir -p "$TOKEN_DIR"
echo -n "$PULSE_TOKEN" > "$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
chown root:root "$TOKEN_FILE"
log_info "Token stored securely at $TOKEN_FILE (mode 600)"
# Build command line args WITHOUT the token (token is read from file)
EXEC_ARGS="--url ${PULSE_URL} --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
# Pass explicit false when Docker was explicitly disabled (prevents auto-detection)
if [[ "$ENABLE_DOCKER" == "false" && "$DOCKER_EXPLICIT" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-docker=false"; fi
if [[ "$ENABLE_KUBERNETES" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-kubernetes"; fi
if [[ -n "$KUBECONFIG_PATH" ]]; then EXEC_ARGS="$EXEC_ARGS --kubeconfig ${KUBECONFIG_PATH}"; 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 [[ "$ENABLE_COMMANDS" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-commands"; fi
if [[ -n "$AGENT_ID" ]]; then EXEC_ARGS="$EXEC_ARGS --agent-id ${AGENT_ID}"; fi
if [[ -n "$HOSTNAME_OVERRIDE" ]]; then EXEC_ARGS="$EXEC_ARGS --hostname ${HOSTNAME_OVERRIDE}"; fi
# Add disk exclude patterns (use ${arr[@]+"${arr[@]}"} for bash 3.2 compatibility with set -u)
for pattern in ${DISK_EXCLUDES[@]+"${DISK_EXCLUDES[@]}"}; do
EXEC_ARGS="$EXEC_ARGS --disk-exclude '${pattern}'"
done
cat > "$UNIT" <<EOF
[Unit]
Description=Pulse Unified Agent
After=network-online.target docker.service
Wants=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
ExecStart=${INSTALL_DIR}/${BINARY_NAME} ${EXEC_ARGS}
Restart=always
RestartSec=5s
User=root
[Install]
WantedBy=multi-user.target
EOF
# Restrict service file permissions (contains no secrets now, but good practice)
chmod 644 "$UNIT"
# Restore SELinux contexts (required for Fedora, RHEL, CentOS)
restore_selinux_contexts
systemctl daemon-reload
systemctl enable "${AGENT_NAME}"
systemctl restart "${AGENT_NAME}"
if [[ "$UPGRADE_MODE" == "true" ]]; then
log_info "Upgrade complete! Agent restarted with new configuration."
else
log_info "Installation complete! Agent service started."
log_info "Token file: $TOKEN_FILE (mode 600, root only)"
fi
exit 0
fi
# 6. SysV Init (legacy systems like Asustor, older Debian/RHEL, etc.)
# This is a fallback for systems that have /etc/init.d but no systemd/OpenRC
if [[ -d /etc/init.d ]] && [[ -w /etc/init.d ]]; then
INITSCRIPT="/etc/init.d/${AGENT_NAME}"
log_info "Configuring SysV init script at $INITSCRIPT..."
# Build command line args
build_exec_args
# Create SysV init script following LSB conventions
cat > "$INITSCRIPT" <<'INITEOF'
#!/bin/sh
### BEGIN INIT INFO
# Provides: pulse-agent
# Required-Start: $network $remote_fs
# Required-Stop: $network $remote_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Pulse Unified Agent
# Description: Pulse monitoring agent for host metrics, Docker, and Kubernetes
### END INIT INFO
# Pulse Unified Agent SysV init script
NAME="pulse-agent"
DAEMON="INSTALL_DIR_PLACEHOLDER/BINARY_NAME_PLACEHOLDER"
DAEMON_ARGS="EXEC_ARGS_PLACEHOLDER"
PIDFILE="/var/run/${NAME}.pid"
LOGFILE="/var/log/${NAME}.log"
# Exit if the binary is not installed
[ -x "$DAEMON" ] || exit 0
do_start() {
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "$NAME is already running."
return 1
fi
echo "Starting $NAME..."
# Start daemon in background, redirect output to log file
# Use shell backgrounding instead of nohup for broader compatibility (QNAP, etc.)
$DAEMON $DAEMON_ARGS >> "$LOGFILE" 2>&1 &
echo $! > "$PIDFILE"
sleep 1
if kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "$NAME started."
return 0
else
echo "Failed to start $NAME."
rm -f "$PIDFILE"
return 1
fi
}
do_stop() {
if [ ! -f "$PIDFILE" ]; then
echo "$NAME is not running (no PID file)."
return 0
fi
PID=$(cat "$PIDFILE")
if ! kill -0 "$PID" 2>/dev/null; then
echo "$NAME is not running (stale PID file)."
rm -f "$PIDFILE"
return 0
fi
echo "Stopping $NAME..."
kill "$PID"
# Wait for process to stop
for i in 1 2 3 4 5; do
if ! kill -0 "$PID" 2>/dev/null; then
break
fi
sleep 1
done
# Force kill if still running
if kill -0 "$PID" 2>/dev/null; then
echo "Force killing $NAME..."
kill -9 "$PID" 2>/dev/null || true
fi
rm -f "$PIDFILE"
echo "$NAME stopped."
return 0
}
do_status() {
if [ -f "$PIDFILE" ]; then
PID=$(cat "$PIDFILE")
if kill -0 "$PID" 2>/dev/null; then
echo "$NAME is running (PID $PID)."
return 0
else
echo "$NAME is not running (stale PID file)."
return 1
fi
else
echo "$NAME is not running."
return 3
fi
}
case "$1" in
start)
do_start
;;
stop)
do_stop
;;
restart|reload|force-reload)
do_stop
sleep 1
do_start
;;
status)
do_status
;;
*)
echo "Usage: $0 {start|stop|restart|status}" >&2
exit 3
;;
esac
exit $?
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"
# Try to enable on boot using available tools
if command -v update-rc.d >/dev/null 2>&1; then
# Debian-based systems
update-rc.d "${AGENT_NAME}" defaults >/dev/null 2>&1 || true
log_info "Enabled service with update-rc.d."
elif command -v chkconfig >/dev/null 2>&1; then
# RHEL-based systems
chkconfig --add "${AGENT_NAME}" >/dev/null 2>&1 || true
chkconfig "${AGENT_NAME}" on >/dev/null 2>&1 || true
log_info "Enabled service with chkconfig."
else
# Manual symlink creation for systems without tools
# Try to create rc.d symlinks manually
for RL in 2 3 4 5; do
if [[ -d "/etc/rc${RL}.d" ]]; then
ln -sf "$INITSCRIPT" "/etc/rc${RL}.d/S99${AGENT_NAME}" 2>/dev/null || true
fi
done
for RL in 0 1 6; do
if [[ -d "/etc/rc${RL}.d" ]]; then
ln -sf "$INITSCRIPT" "/etc/rc${RL}.d/K01${AGENT_NAME}" 2>/dev/null || true
fi
done
log_info "Created rc.d symlinks manually."
fi
# Stop existing agent if running
"$INITSCRIPT" stop 2>/dev/null || true
sleep 1
# Start the agent
"$INITSCRIPT" start
if [[ "$UPGRADE_MODE" == "true" ]]; then
log_info "Upgrade complete! Agent restarted with new configuration."
else
log_info "Installation complete! Agent service started."
fi
log_info "To check status: $INITSCRIPT status"
log_info "To view logs: tail -f /var/log/${AGENT_NAME}.log"
exit 0
fi
fail "Could not detect a supported service manager (systemd, OpenRC, FreeBSD rc.d, SysV init, launchd, or Unraid)."
}
# Call main function with all arguments
main "$@"