mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Refs #1515 The install command is generated under the Settings infrastructure installer, not a "Settings > Agents" tab. Correct the agent log, installer warning, and Machines tooltip to say "the Pulse UI" so the recovery step is accurate.
3495 lines
124 KiB
Bash
Executable file
3495 lines
124 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, QNAP QTS/QuTS hero, TrueNAS
|
|
#
|
|
# Usage:
|
|
# curl -fsSL http://pulse/install.sh | bash -s -- --url http://pulse --token <token> [options]
|
|
# curl -fsSL http://pulse/install.sh | bash -s -- --update --url http://pulse [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 Pulse command execution on agent (disabled by default; required for Patrol actions and Proxmox LXC Docker inventory)
|
|
# --health-addr <addr> Health/metrics listener address (default: 127.0.0.1:9191, use "" to disable)
|
|
# --update Update an existing agent using saved connection state
|
|
# --uninstall Remove the agent
|
|
#
|
|
# Auto-Detection:
|
|
# The installer automatically detects Docker, Kubernetes, and Proxmox on the
|
|
# target machine and enables monitoring for detected platforms. Proxmox auto
|
|
# mode keeps the runtime unpinned so the agent can register every detected
|
|
# PVE / PBS service on that host. 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_LOG_FILE="" # Set during TrueNAS detection
|
|
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=""
|
|
UPDATE_ONLY="false"
|
|
UNINSTALL="false"
|
|
INSECURE="false"
|
|
AGENT_ID=""
|
|
HOSTNAME_OVERRIDE=""
|
|
ENABLE_COMMANDS="false"
|
|
HEALTH_ADDR="${PULSE_HEALTH_ADDR:-}"
|
|
HEALTH_ADDR_SET="false"
|
|
if [[ -n "${PULSE_HEALTH_ADDR+x}" ]]; then
|
|
HEALTH_ADDR_SET="true"
|
|
fi
|
|
ENROLL="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
|
|
STATE_DIR="/var/lib/pulse-agent" # Persistent state directory (overridden per platform)
|
|
CURL_CA_BUNDLE="" # Path to CA bundle for curl and agent TLS (sets SSL_CERT_FILE)
|
|
NON_INTERACTIVE="false"
|
|
TOKEN_FILE_PATH="" # Path to file containing the token
|
|
RUNTIME_TOKEN_FILE="" # Secure token file passed to the installed service
|
|
OUTPUT_FORMAT="text" # "text" (default) or "json"
|
|
PREFLIGHT_ONLY="false"
|
|
INSTALL_SIGNATURE_NAMESPACE="pulse-install"
|
|
INSTALL_SIGNATURE_IDENTITY="pulse-installer"
|
|
PINNED_INSTALLER_SSH_PUBLIC_KEY="__PULSE_INSTALLER_SSH_PUBLIC_KEY__"
|
|
|
|
ROOTLESS_RUNTIME_KIND=""
|
|
ROOTLESS_RUNTIME_SOCKET_PATH=""
|
|
ROOTLESS_RUNTIME_SOCKET_URI=""
|
|
ROOTLESS_RUNTIME_XDG_DIR=""
|
|
|
|
SYSTEMD_ENV_LINES=""
|
|
SHELL_EXPORT_LINES=""
|
|
PLIST_ENV_ENTRIES=""
|
|
PLIST_ENV_BLOCK=""
|
|
UPSTART_ENV_LINES=""
|
|
SED_EXPORT_LINES=""
|
|
APPLIED_SERVICE_ENV_KEYS="|"
|
|
|
|
# Track if flags were explicitly set (to override auto-detection)
|
|
DOCKER_EXPLICIT="false"
|
|
KUBERNETES_EXPLICIT="false"
|
|
PROXMOX_EXPLICIT="false"
|
|
HOST_EXPLICIT="false"
|
|
INTERVAL_EXPLICIT="false"
|
|
|
|
# --- Helper Functions ---
|
|
log_info() {
|
|
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
printf "[INFO] %s\n" "$(redact_token "$1")"
|
|
else
|
|
printf "[INFO] %s\n" "$1"
|
|
fi
|
|
}
|
|
log_warn() {
|
|
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
printf "[WARN] %s\n" "$(redact_token "$1")"
|
|
else
|
|
printf "[WARN] %s\n" "$1"
|
|
fi
|
|
}
|
|
log_error() {
|
|
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
printf "[ERROR] %s\n" "$(redact_token "$1")"
|
|
else
|
|
printf "[ERROR] %s\n" "$1"
|
|
fi
|
|
}
|
|
url_encode() {
|
|
local input="$1"
|
|
local output=""
|
|
local i c encoded
|
|
local old_lc_all="${LC_ALL-}"
|
|
LC_ALL=C
|
|
for ((i=0; i<${#input}; i++)); do
|
|
c="${input:i:1}"
|
|
case "$c" in
|
|
[a-zA-Z0-9.~_-])
|
|
output+="$c"
|
|
;;
|
|
*)
|
|
printf -v encoded '%%%02X' "'$c"
|
|
output+="$encoded"
|
|
;;
|
|
esac
|
|
done
|
|
if [[ -n "${old_lc_all}" ]]; then
|
|
LC_ALL="$old_lc_all"
|
|
else
|
|
unset LC_ALL
|
|
fi
|
|
printf '%s' "$output"
|
|
}
|
|
fail() {
|
|
local code="${2:-1}"
|
|
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
|
printf '{"phase":"error","code":"install_failed","message":"%s","exitCode":%d}\n' \
|
|
"$(echo "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr -d '\n\r')" "$code"
|
|
else
|
|
log_error "$1"
|
|
fi
|
|
if [[ "$NON_INTERACTIVE" != "true" ]]; then
|
|
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
|
|
fi
|
|
exit "$code"
|
|
}
|
|
|
|
# Stable exit codes by failure class
|
|
EXIT_OK=0
|
|
EXIT_GENERAL=1
|
|
EXIT_UNSUPPORTED_ARCH=10
|
|
EXIT_DOWNLOAD_FAILED=11
|
|
EXIT_CHECKSUM_FAILED=12
|
|
EXIT_SERVICE_START_FAILED=13
|
|
EXIT_PREFLIGHT_FAILED=14
|
|
EXIT_ALREADY_INSTALLED=15 # Not a failure — used with --preflight-only
|
|
EXIT_MISSING_ARGS=16
|
|
EXIT_SIGNATURE_FAILED=17
|
|
|
|
json_event() {
|
|
# Usage: json_event <phase> <code> <message> [exitCode]
|
|
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
|
local exit_code="${4:-0}"
|
|
printf '{"phase":"%s","code":"%s","message":"%s","exitCode":%d}\n' \
|
|
"$1" "$2" "$(echo "$3" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr -d '\n\r')" "$exit_code"
|
|
fi
|
|
}
|
|
|
|
redact_token() {
|
|
# Replace token values with redacted placeholder in log output
|
|
local msg="$1"
|
|
if [[ -n "$PULSE_TOKEN" ]]; then
|
|
msg="${msg//$PULSE_TOKEN/[REDACTED]}"
|
|
fi
|
|
if [[ -n "$TOKEN_FILE_PATH" ]]; then
|
|
msg="${msg//$TOKEN_FILE_PATH/[token-file]}"
|
|
fi
|
|
echo "$msg"
|
|
}
|
|
|
|
has_pinned_installer_signature_key() {
|
|
[[ -n "$PINNED_INSTALLER_SSH_PUBLIC_KEY" && "$PINNED_INSTALLER_SSH_PUBLIC_KEY" != "__PULSE_INSTALLER_SSH_PUBLIC_KEY__" ]]
|
|
}
|
|
|
|
decode_base64_to_file() {
|
|
local encoded="$1"
|
|
local output="$2"
|
|
|
|
if command -v base64 >/dev/null 2>&1; then
|
|
if printf '%s' "$encoded" | base64 --decode > "$output" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
if printf '%s' "$encoded" | base64 -d > "$output" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
if printf '%s' "$encoded" | base64 -D > "$output" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
fail "Base64 decoder is required to verify signed Pulse downloads." "$EXIT_SIGNATURE_FAILED"
|
|
}
|
|
|
|
verify_download_signature() {
|
|
local target_path="$1"
|
|
local signature_header="$2"
|
|
|
|
if ! has_pinned_installer_signature_key; then
|
|
return 0
|
|
fi
|
|
if [[ -z "$signature_header" ]]; then
|
|
fail "Server did not provide SSH signature metadata; refusing signed install." "$EXIT_SIGNATURE_FAILED"
|
|
fi
|
|
if ! command -v ssh-keygen >/dev/null 2>&1; then
|
|
fail "ssh-keygen is required to verify signed Pulse downloads." "$EXIT_SIGNATURE_FAILED"
|
|
fi
|
|
|
|
local allowed_signers signature_file
|
|
allowed_signers=$(mktemp)
|
|
signature_file=$(mktemp)
|
|
TMP_FILES+=("$allowed_signers" "$signature_file")
|
|
|
|
printf '%s %s\n' "$INSTALL_SIGNATURE_IDENTITY" "$PINNED_INSTALLER_SSH_PUBLIC_KEY" > "$allowed_signers"
|
|
decode_base64_to_file "$signature_header" "$signature_file"
|
|
|
|
if ! ssh-keygen -Y verify \
|
|
-f "$allowed_signers" \
|
|
-I "$INSTALL_SIGNATURE_IDENTITY" \
|
|
-n "$INSTALL_SIGNATURE_NAMESPACE" \
|
|
-s "$signature_file" < "$target_path" >/dev/null 2>&1; then
|
|
fail "Cryptographic signature verification failed for the downloaded agent binary." "$EXIT_SIGNATURE_FAILED"
|
|
fi
|
|
|
|
json_event "download" "signature_ok" "Binary signature verified"
|
|
log_info "Binary signature verified"
|
|
}
|
|
|
|
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
|
|
--state-dir <path> Override persistent state directory
|
|
--disk-exclude <path> Exclude mount point (repeatable)
|
|
--insecure Skip TLS verification (auto-enabled for http:// URLs)
|
|
--cacert <path> Custom CA certificate for TLS (used by curl and agent)
|
|
--enable-commands Enable Pulse command execution (disabled by default; required for Patrol actions and Proxmox LXC Docker inventory)
|
|
--health-addr <addr> Health/metrics listener address (default: 127.0.0.1:9191; use "" to disable)
|
|
--enroll Exchange bootstrap token for runtime token (deploy wizard)
|
|
--update Update an existing agent using saved connection state
|
|
--uninstall Remove the agent
|
|
--non-interactive Skip TTY prompts (for automated/scripted installs)
|
|
--token-file <path> Read token from file (alternative to --token)
|
|
--pulse-url <url> Alias for --url
|
|
--preflight-only Run preflight checks and exit (no install)
|
|
--output <format> Output format: text (default) or json
|
|
--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
|
|
}
|
|
|
|
# --- Post-Start Health Verification ---
|
|
# After starting the agent service, poll its readiness endpoint to verify it
|
|
# actually started. The agent exposes /readyz on the configured health address
|
|
# once modules are initialized. The default is 127.0.0.1:9191.
|
|
# warn_agent_token_rejected surfaces the actionable recovery path when the
|
|
# server rejects the agent's token. Keeps the message in one place so both
|
|
# health-check paths report it identically.
|
|
warn_agent_token_rejected() {
|
|
log_warn "Pulse rejected this agent's API token (HTTP 401/403). The saved token is no longer valid on the server, which usually means Pulse was restored/reinstalled or upgraded (for example v5 -> v6) without carrying the token across."
|
|
log_warn "Re-run the full agent install command from the Pulse UI to mint a fresh token. The agent will keep reporting 401 until the token is replaced."
|
|
}
|
|
|
|
# verify_agent_server_registration returns:
|
|
# 0 - the server confirmed this agent's registration
|
|
# 1 - registration not confirmed yet (transient: agent not reported, network)
|
|
# 2 - the server rejected the token (HTTP 401/403 - actionable, permanent)
|
|
verify_agent_server_registration() {
|
|
local lookup_hostname="${HOSTNAME_OVERRIDE}"
|
|
local lookup_out=""
|
|
local lookup_status=""
|
|
local lookup_body=""
|
|
# No -f: we need the response body AND the HTTP status even on 4xx so a
|
|
# rejected token (401/403) can be told apart from "not reported yet". The
|
|
# -w format appends "\n<http_code>" after the body.
|
|
local lookup_args=(-sSL --connect-timeout 5 --max-time 10 -w $'\n%{http_code}')
|
|
|
|
if [[ -z "$PULSE_URL" ]]; then
|
|
return 1
|
|
fi
|
|
if [[ -z "$lookup_hostname" ]]; then
|
|
lookup_hostname=$(hostname 2>/dev/null || true)
|
|
fi
|
|
if [[ -z "$lookup_hostname" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
if [[ -n "$PULSE_TOKEN" ]]; then lookup_args+=(-H "X-API-Token: ${PULSE_TOKEN}"); fi
|
|
if [[ "$INSECURE" == "true" ]]; then lookup_args+=(-k); fi
|
|
if [[ -n "$CURL_CA_BUNDLE" ]]; then lookup_args+=(--cacert "$CURL_CA_BUNDLE"); fi
|
|
|
|
lookup_out=$(curl "${lookup_args[@]}" "${PULSE_URL}/api/agents/agent/lookup?hostname=$(url_encode "$lookup_hostname")" 2>/dev/null || true)
|
|
lookup_status="${lookup_out##*$'\n'}"
|
|
lookup_body="${lookup_out%$'\n'*}"
|
|
|
|
# A rejected token is a definitive, actionable failure distinct from "the
|
|
# agent has not reported yet"; signal it so the caller can tell the operator
|
|
# the token needs replacing instead of leaving a silent 401 loop behind.
|
|
case "$lookup_status" in
|
|
401|403) return 2 ;;
|
|
esac
|
|
|
|
if echo "$lookup_body" | grep -q '"agent"[[:space:]]*:' && echo "$lookup_body" | grep -q '"id"[[:space:]]*:'; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
resolve_agent_health_url() {
|
|
if [[ "$HEALTH_ADDR_SET" == "true" && -z "$HEALTH_ADDR" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
local addr="${HEALTH_ADDR:-127.0.0.1:9191}"
|
|
local ipv6_any_prefix="[::]:"
|
|
case "$addr" in
|
|
:*) addr="127.0.0.1${addr}" ;;
|
|
0.0.0.0:*) addr="127.0.0.1:${addr#0.0.0.0:}" ;;
|
|
esac
|
|
if [[ "$addr" == "$ipv6_any_prefix"* ]]; then
|
|
addr="[::1]:${addr#$ipv6_any_prefix}"
|
|
fi
|
|
|
|
printf 'http://%s/readyz\n' "$addr"
|
|
}
|
|
|
|
agent_process_running() {
|
|
if command -v pgrep >/dev/null 2>&1; then
|
|
# Use -x (exact match) if supported, otherwise fall back to -f.
|
|
pgrep -x "${BINARY_NAME}" >/dev/null 2>&1
|
|
local pgrep_rc=$?
|
|
if [ $pgrep_rc -eq 0 ]; then
|
|
return 0
|
|
elif [ $pgrep_rc -ge 2 ]; then
|
|
pgrep -f "${BINARY_NAME}" >/dev/null 2>&1 && return 0
|
|
fi
|
|
else
|
|
# shellcheck disable=SC2009
|
|
# Use bracket trick ([p]ulse-agent) to prevent grep from matching itself.
|
|
local grep_pattern="[${BINARY_NAME:0:1}]${BINARY_NAME:1}"
|
|
if ps -e -o comm= 2>/dev/null | grep -q "$grep_pattern" || ps aux 2>/dev/null | grep -q "$grep_pattern"; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
verify_agent_started() {
|
|
local health_url=""
|
|
local max_iterations=8
|
|
local interval=2
|
|
local iteration=0
|
|
local log_file="${TRUENAS_LOG_FILE:-$LOG_FILE}"
|
|
|
|
log_info "Verifying agent started successfully..."
|
|
|
|
# Brief pause to let the agent process spawn (especially for background starts like Unraid)
|
|
sleep 2
|
|
|
|
health_url="$(resolve_agent_health_url || true)"
|
|
if [[ -z "$health_url" ]]; then
|
|
while [ $iteration -lt $max_iterations ]; do
|
|
if agent_process_running; then
|
|
verify_agent_server_registration
|
|
local reg_rc=$?
|
|
if [[ $reg_rc -eq 0 ]]; then
|
|
log_info "Agent process is running and registered with Pulse."
|
|
elif [[ $reg_rc -eq 2 ]]; then
|
|
warn_agent_token_rejected
|
|
else
|
|
log_warn "Agent process is running, but server registration was not confirmed yet."
|
|
fi
|
|
return 0
|
|
fi
|
|
sleep $interval
|
|
iteration=$((iteration + 1))
|
|
done
|
|
|
|
log_warn "Agent process is not running!"
|
|
if [ -f "$log_file" ]; then
|
|
log_warn "Last log lines:"
|
|
tail -5 "$log_file" 2>/dev/null | while IFS= read -r line; do log_warn " $line"; done
|
|
fi
|
|
return 1
|
|
fi
|
|
|
|
while [ $iteration -lt $max_iterations ]; do
|
|
# Check the readiness endpoint first — this is the definitive signal
|
|
if curl -sf --max-time 2 "$health_url" >/dev/null 2>&1; then
|
|
verify_agent_server_registration
|
|
local reg_rc=$?
|
|
if [[ $reg_rc -eq 0 ]]; then
|
|
log_info "Agent is running, healthy, and registered with Pulse."
|
|
elif [[ $reg_rc -eq 2 ]]; then
|
|
warn_agent_token_rejected
|
|
else
|
|
log_warn "Agent local health is ready, but server registration was not confirmed yet."
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
# If curl failed, check whether the process is still alive.
|
|
# Use pgrep where available, fall back to ps + grep.
|
|
local agent_running=false
|
|
if agent_process_running; then
|
|
agent_running=true
|
|
fi
|
|
|
|
if [ "$agent_running" = "false" ] && [ $iteration -ge 3 ]; then
|
|
# Only treat missing process as failure after ~8s — on Unraid the wrapper
|
|
# script takes several seconds before the actual binary launches.
|
|
log_warn "Agent process is not running!"
|
|
# Show last few log lines for diagnostics
|
|
if [ -f "$log_file" ]; then
|
|
log_warn "Last log lines:"
|
|
tail -5 "$log_file" 2>/dev/null | while IFS= read -r line; do log_warn " $line"; done
|
|
fi
|
|
return 1
|
|
fi
|
|
|
|
sleep $interval
|
|
iteration=$((iteration + 1))
|
|
done
|
|
|
|
# Timed out — process alive but not ready
|
|
log_warn "Agent process is running but did not become ready within ~$((max_iterations * interval + 2))s."
|
|
log_warn "It may still be initializing. Check logs: tail -f $log_file"
|
|
return 1
|
|
}
|
|
|
|
stop_existing_agent_service() {
|
|
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
|
|
return 0
|
|
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
|
|
return 0
|
|
fi
|
|
elif command -v service >/dev/null 2>&1; then
|
|
if service "${AGENT_NAME}" status >/dev/null 2>&1; then
|
|
log_info "Stopping existing ${AGENT_NAME} service..."
|
|
service "${AGENT_NAME}" stop 2>/dev/null || true
|
|
sleep 2
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
restart_systemd_agent_service() {
|
|
systemctl daemon-reload
|
|
systemctl enable "${AGENT_NAME}" 2>/dev/null || true
|
|
systemctl restart "${AGENT_NAME}"
|
|
}
|
|
|
|
restart_openrc_agent_service() {
|
|
rc-service "${AGENT_NAME}" stop 2>/dev/null || true
|
|
rc-update add "${AGENT_NAME}" default 2>/dev/null || true
|
|
rc-service "${AGENT_NAME}" start
|
|
}
|
|
|
|
restart_service_command_agent() {
|
|
service "${AGENT_NAME}" stop 2>/dev/null || true
|
|
sleep 1
|
|
service "${AGENT_NAME}" start 2>/dev/null || true
|
|
}
|
|
|
|
restart_sysv_agent_service() {
|
|
local initscript="$1"
|
|
"$initscript" stop 2>/dev/null || true
|
|
sleep 1
|
|
"$initscript" start
|
|
}
|
|
|
|
teardown_systemd_agent_service() {
|
|
local unit_path="${1:-/etc/systemd/system/${AGENT_NAME}.service}"
|
|
systemctl stop "${AGENT_NAME}" 2>/dev/null || true
|
|
systemctl disable "${AGENT_NAME}" 2>/dev/null || true
|
|
rm -f "$unit_path"
|
|
systemctl daemon-reload 2>/dev/null || true
|
|
}
|
|
|
|
teardown_openrc_agent_service() {
|
|
local init_path="${1:-/etc/init.d/${AGENT_NAME}}"
|
|
rc-service "${AGENT_NAME}" stop 2>/dev/null || true
|
|
rc-update del "${AGENT_NAME}" default 2>/dev/null || true
|
|
rm -f "$init_path"
|
|
}
|
|
|
|
teardown_service_command_agent() {
|
|
local service_path="$1"
|
|
service "${AGENT_NAME}" stop 2>/dev/null || true
|
|
if [[ -n "$service_path" ]]; then
|
|
rm -f "$service_path"
|
|
fi
|
|
}
|
|
|
|
teardown_sysv_agent_service() {
|
|
local init_path="${1:-/etc/init.d/${AGENT_NAME}}"
|
|
"$init_path" stop 2>/dev/null || true
|
|
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
|
|
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 "$init_path"
|
|
rm -f "/var/run/${AGENT_NAME}.pid"
|
|
}
|
|
|
|
enable_sysv_agent_service() {
|
|
local init_path="${1:-/etc/init.d/${AGENT_NAME}}"
|
|
if command -v update-rc.d >/dev/null 2>&1; then
|
|
update-rc.d "${AGENT_NAME}" defaults >/dev/null 2>&1 || true
|
|
log_info "Enabled service with update-rc.d."
|
|
return 0
|
|
elif command -v chkconfig >/dev/null 2>&1; then
|
|
chkconfig --add "${AGENT_NAME}" >/dev/null 2>&1 || true
|
|
chkconfig "${AGENT_NAME}" on >/dev/null 2>&1 || true
|
|
log_info "Enabled service with chkconfig."
|
|
return 0
|
|
fi
|
|
|
|
for RL in 2 3 4 5; do
|
|
if [[ -d "/etc/rc${RL}.d" ]]; then
|
|
ln -sf "$init_path" "/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 "$init_path" "/etc/rc${RL}.d/K01${AGENT_NAME}" 2>/dev/null || true
|
|
fi
|
|
done
|
|
log_info "Created rc.d symlinks manually."
|
|
}
|
|
|
|
write_truenas_bootstrap_script() {
|
|
local platform="$1"
|
|
local service_link=""
|
|
local service_management_functions=""
|
|
|
|
if [[ "$platform" == "Linux" ]]; then
|
|
service_link="/etc/systemd/system/${AGENT_NAME}.service"
|
|
service_management_functions=$(cat <<'EOF'
|
|
start_agent_service() {
|
|
systemctl daemon-reload
|
|
systemctl enable "$SERVICE_NAME" 2>/dev/null || true
|
|
systemctl restart "$SERVICE_NAME"
|
|
}
|
|
EOF
|
|
)
|
|
else
|
|
service_link="/usr/local/etc/rc.d/${AGENT_NAME}"
|
|
service_management_functions="$(freebsd_enable_snippet)
|
|
|
|
start_agent_service() {
|
|
ensure_freebsd_agent_enabled
|
|
service \"\${SERVICE_NAME}\" stop 2>/dev/null || true
|
|
sleep 1
|
|
service \"\${SERVICE_NAME}\" start 2>/dev/null || true
|
|
}"
|
|
fi
|
|
|
|
cat > "$TRUENAS_BOOTSTRAP_SCRIPT" <<BOOTSTRAP
|
|
#!/bin/bash
|
|
# Pulse Agent Bootstrap for TrueNAS
|
|
# Called by TrueNAS Init/Shutdown task on boot.
|
|
|
|
set -e
|
|
|
|
SERVICE_NAME="${AGENT_NAME}"
|
|
STATE_DIR="${TRUENAS_STATE_DIR}"
|
|
STORED_BINARY="\${STATE_DIR}/pulse-agent"
|
|
RUNTIME_BINARY="${TRUENAS_RUNTIME_BINARY}"
|
|
SERVICE_STORAGE="\${STATE_DIR}/pulse-agent.service"
|
|
SERVICE_LINK="${service_link}"
|
|
|
|
require_bootstrap_file() {
|
|
local path="\$1"
|
|
local label="\$2"
|
|
if [[ ! -f "\$path" ]]; then
|
|
echo "ERROR: \$label not found at \$path"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
sync_runtime_binary() {
|
|
if [[ "\$RUNTIME_BINARY" == "\$STORED_BINARY" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
mkdir -p "\$(dirname "\$RUNTIME_BINARY")" 2>/dev/null || true
|
|
cp "\$STORED_BINARY" "\$RUNTIME_BINARY"
|
|
chmod +x "\$RUNTIME_BINARY"
|
|
}
|
|
|
|
link_service_artifact() {
|
|
ln -sf "\$SERVICE_STORAGE" "\$SERVICE_LINK"
|
|
}
|
|
|
|
${service_management_functions}
|
|
|
|
require_bootstrap_file "\$STORED_BINARY" "Binary"
|
|
require_bootstrap_file "\$SERVICE_STORAGE" "Service file"
|
|
sync_runtime_binary
|
|
link_service_artifact
|
|
start_agent_service
|
|
|
|
echo "Pulse agent started successfully"
|
|
BOOTSTRAP
|
|
|
|
chmod +x "$TRUENAS_BOOTSTRAP_SCRIPT"
|
|
}
|
|
|
|
freebsd_enable_snippet() {
|
|
cat <<'EOF'
|
|
apply_freebsd_agent_enablement() {
|
|
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
|
|
}
|
|
EOF
|
|
}
|
|
|
|
ensure_freebsd_agent_enabled() {
|
|
eval "$(freebsd_enable_snippet)"
|
|
apply_freebsd_agent_enablement
|
|
}
|
|
|
|
render_systemd_agent_unit() {
|
|
local unit_path="$1"
|
|
local exec_path="$2"
|
|
local exec_args="$3"
|
|
local after_targets="$4"
|
|
local wants_targets="$5"
|
|
local run_as_user="$6"
|
|
local log_target="$7"
|
|
local env_line=""
|
|
local wants_line=""
|
|
local user_line=""
|
|
local log_lines=""
|
|
local no_new_privileges="true"
|
|
local restrict_suidsgid="true"
|
|
|
|
env_line="$SYSTEMD_ENV_LINES"
|
|
if [[ -n "$wants_targets" ]]; then
|
|
wants_line=$'\n'"Wants=${wants_targets}"
|
|
fi
|
|
if [[ -n "$run_as_user" ]]; then
|
|
user_line=$'\n'"User=${run_as_user}"
|
|
fi
|
|
if [[ -n "$log_target" ]]; then
|
|
log_lines=$'\n'"StandardOutput=append:${log_target}"$'\n'"StandardError=append:${log_target}"
|
|
fi
|
|
if systemd_agent_requires_lxc_attach; then
|
|
no_new_privileges="false"
|
|
restrict_suidsgid="false"
|
|
fi
|
|
|
|
cat > "$unit_path" <<EOF
|
|
[Unit]
|
|
Description=Pulse Unified Agent
|
|
After=${after_targets}${wants_line}
|
|
StartLimitIntervalSec=0
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=${exec_path} ${exec_args}${env_line}
|
|
Restart=always
|
|
RestartSec=5s${user_line}${log_lines}
|
|
UMask=0077
|
|
NoNewPrivileges=${no_new_privileges}
|
|
PrivateTmp=true
|
|
ProtectKernelTunables=true
|
|
ProtectKernelModules=true
|
|
ProtectControlGroups=true
|
|
LockPersonality=true
|
|
RestrictSUIDSGID=${restrict_suidsgid}
|
|
SystemCallArchitectures=native
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
}
|
|
|
|
systemd_agent_requires_lxc_attach() {
|
|
if [[ "$ENABLE_COMMANDS" != "true" || "$ENABLE_PROXMOX" != "true" ]]; then
|
|
return 1
|
|
fi
|
|
case "${PROXMOX_TYPE:-}" in
|
|
""|pve|all)
|
|
return 0
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
render_freebsd_rc_agent_script() {
|
|
local script_path="$1"
|
|
local exec_path="$2"
|
|
local exec_args="$3"
|
|
local service_env_lines="$SHELL_EXPORT_LINES"
|
|
|
|
cat > "$script_path" <<EOF
|
|
#!/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"
|
|
child_pidfile="/var/run/\${name}.child.pid"
|
|
|
|
command="${exec_path}"
|
|
command_args="${exec_args}"
|
|
|
|
start_cmd="\${name}_start"
|
|
stop_cmd="\${name}_stop"
|
|
status_cmd="\${name}_status"
|
|
|
|
pulse_agent_pid_command()
|
|
{
|
|
ps -o command= -p "\$1" 2>/dev/null | sed 's/^[[:space:]]*//'
|
|
}
|
|
|
|
pulse_agent_supervisor_pid()
|
|
{
|
|
agent_pid="\$1"
|
|
agent_command=\$(pulse_agent_pid_command "\${agent_pid}")
|
|
case "\${agent_command}" in
|
|
daemon:*)
|
|
echo "\${agent_pid}"
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
parent_pid=\$(ps -o ppid= -p "\${agent_pid}" 2>/dev/null | tr -d '[:space:]')
|
|
if [ -z "\${parent_pid}" ] || [ "\${parent_pid}" = "1" ]; then
|
|
return 1
|
|
fi
|
|
|
|
parent_command=\$(pulse_agent_pid_command "\${parent_pid}")
|
|
case "\${parent_command}" in
|
|
daemon:*)
|
|
echo "\${parent_pid}"
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
return 1
|
|
}
|
|
|
|
pulse_agent_start()
|
|
{
|
|
if checkyesno \${rcvar}; then
|
|
if [ -f \${pidfile} ]; then
|
|
existing_pid=\$(cat \${pidfile} 2>/dev/null)
|
|
if [ -n "\${existing_pid}" ] && kill -0 "\${existing_pid}" 2>/dev/null; then
|
|
echo "\${name} is already running as pid \${existing_pid}."
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
rm -f \${pidfile} \${child_pidfile}
|
|
echo "Starting \${name}."
|
|
${service_env_lines}
|
|
/usr/sbin/daemon -r -P \${pidfile} -p \${child_pidfile} -f "\${command}" \${command_args}
|
|
fi
|
|
}
|
|
|
|
pulse_agent_stop()
|
|
{
|
|
supervisor_pid=""
|
|
child_pid=""
|
|
stopped=0
|
|
|
|
if [ -f \${child_pidfile} ]; then
|
|
child_pid=\$(cat \${child_pidfile} 2>/dev/null)
|
|
fi
|
|
|
|
if [ -f \${pidfile} ]; then
|
|
primary_pid=\$(cat \${pidfile} 2>/dev/null)
|
|
if [ -n "\${primary_pid}" ] && kill -0 "\${primary_pid}" 2>/dev/null; then
|
|
detected_supervisor=\$(pulse_agent_supervisor_pid "\${primary_pid}" 2>/dev/null || true)
|
|
if [ -n "\${detected_supervisor}" ]; then
|
|
supervisor_pid="\${detected_supervisor}"
|
|
if [ "\${detected_supervisor}" != "\${primary_pid}" ] && [ -z "\${child_pid}" ]; then
|
|
child_pid="\${primary_pid}"
|
|
fi
|
|
else
|
|
supervisor_pid="\${primary_pid}"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ -n "\${supervisor_pid}" ] && kill -0 "\${supervisor_pid}" 2>/dev/null; then
|
|
echo "Stopping \${name} supervisor."
|
|
kill "\${supervisor_pid}" 2>/dev/null || true
|
|
sleep 1
|
|
if kill -0 "\${supervisor_pid}" 2>/dev/null; then
|
|
kill -KILL "\${supervisor_pid}" 2>/dev/null || true
|
|
fi
|
|
stopped=1
|
|
fi
|
|
|
|
if [ -n "\${child_pid}" ] && kill -0 "\${child_pid}" 2>/dev/null; then
|
|
echo "Stopping \${name} child."
|
|
kill "\${child_pid}" 2>/dev/null || true
|
|
sleep 1
|
|
if kill -0 "\${child_pid}" 2>/dev/null; then
|
|
kill -KILL "\${child_pid}" 2>/dev/null || true
|
|
fi
|
|
stopped=1
|
|
fi
|
|
|
|
rm -f \${pidfile} \${child_pidfile}
|
|
|
|
if [ "\${stopped}" -eq 0 ]; then
|
|
echo "\${name} is not running."
|
|
fi
|
|
}
|
|
|
|
pulse_agent_status()
|
|
{
|
|
if [ -f \${pidfile} ]; then
|
|
primary_pid=\$(cat \${pidfile} 2>/dev/null)
|
|
if [ -n "\${primary_pid}" ] && kill -0 "\${primary_pid}" 2>/dev/null; then
|
|
child_status=""
|
|
if [ -f \${child_pidfile} ]; then
|
|
child_pid=\$(cat \${child_pidfile} 2>/dev/null)
|
|
if [ -n "\${child_pid}" ] && kill -0 "\${child_pid}" 2>/dev/null; then
|
|
child_status=" with child pid \${child_pid}"
|
|
fi
|
|
fi
|
|
echo "\${name} is running as supervisor pid \${primary_pid}\${child_status}."
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if [ -f \${child_pidfile} ]; then
|
|
child_pid=\$(cat \${child_pidfile} 2>/dev/null)
|
|
if [ -n "\${child_pid}" ] && kill -0 "\${child_pid}" 2>/dev/null; then
|
|
echo "\${name} is running as child pid \${child_pid}."
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if [ -f \${pidfile} ]; then
|
|
legacy_pid=\$(cat \${pidfile} 2>/dev/null)
|
|
legacy_supervisor=\$(pulse_agent_supervisor_pid "\${legacy_pid}" 2>/dev/null || true)
|
|
if [ -n "\${legacy_supervisor}" ] && kill -0 "\${legacy_supervisor}" 2>/dev/null; then
|
|
echo "\${name} is running as legacy child pid \${legacy_pid} supervised by pid \${legacy_supervisor}."
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
echo "\${name} is not running."
|
|
return 1
|
|
}
|
|
|
|
load_rc_config \$name
|
|
run_rc_command "\$1"
|
|
EOF
|
|
|
|
chmod +x "$script_path"
|
|
}
|
|
|
|
complete_installation_flow() {
|
|
local state_dir="$1"
|
|
local install_success_message="$2"
|
|
local upgrade_success_message="$3"
|
|
local unhealthy_log_hint="$4"
|
|
|
|
save_connection_info "$state_dir"
|
|
if verify_agent_started; then
|
|
if [[ "$UPGRADE_MODE" == "true" ]]; then
|
|
log_info "$upgrade_success_message"
|
|
json_event "complete" "updated" "Installation updated"
|
|
else
|
|
log_info "$install_success_message"
|
|
json_event "complete" "installed" "Installation installed"
|
|
fi
|
|
else
|
|
if [[ "$UPGRADE_MODE" == "true" ]]; then
|
|
log_warn "Upgrade complete, but the agent may not be running correctly."
|
|
json_event "complete" "updated_unhealthy" "Agent updated but not responding"
|
|
else
|
|
log_warn "Installation complete, but the agent may not be running correctly."
|
|
if [[ -n "$unhealthy_log_hint" ]]; then
|
|
log_warn "Check logs: $unhealthy_log_hint"
|
|
fi
|
|
json_event "complete" "installed_unhealthy" "Agent installed but not responding"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$SAVED_INSTALL_SCRIPT" ]]; then
|
|
log_info "To uninstall later: sudo bash ${SAVED_INSTALL_SCRIPT} --uninstall"
|
|
fi
|
|
}
|
|
|
|
portable_sed_in_place() {
|
|
local expr="$1"
|
|
local target="$2"
|
|
|
|
sed -i '' "$expr" "$target" 2>/dev/null || sed -i "$expr" "$target" 2>/dev/null || true
|
|
}
|
|
|
|
detect_qnap_data_volume() {
|
|
local qnap_vol=""
|
|
local candidate=""
|
|
|
|
if command -v getcfg >/dev/null 2>&1; then
|
|
qnap_vol=$(getcfg SHARE_DEF defVolMP -f /etc/config/def_share.info 2>/dev/null || echo "")
|
|
qnap_vol="${qnap_vol%/}"
|
|
if [[ -n "$qnap_vol" ]] && [[ -d "$qnap_vol" ]] && [[ -w "$qnap_vol" ]]; then
|
|
printf '%s\n' "$qnap_vol"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
for candidate in /share/CACHEDEV1_DATA /share/CACHEDEV2_DATA /share/MD0_DATA /share/HDA_DATA; do
|
|
if [[ -d "$candidate" ]] && [[ -w "$candidate" ]]; then
|
|
printf '%s\n' "$candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
find_qnap_state_dir() {
|
|
local candidate=""
|
|
|
|
if [[ -n "$STATE_DIR" ]] && [[ "$STATE_DIR" != "/var/lib/pulse-agent" ]]; then
|
|
if [[ -d "$STATE_DIR" ]] || [[ -f "$STATE_DIR/connection.env" ]] || [[ -f "$STATE_DIR/agent-id" ]]; then
|
|
printf '%s\n' "$STATE_DIR"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
candidate=$(detect_qnap_data_volume || true)
|
|
if [[ -n "$candidate" ]]; then
|
|
printf '%s\n' "${candidate}/.pulse-agent"
|
|
return 0
|
|
fi
|
|
|
|
for candidate in /share/CACHEDEV1_DATA/.pulse-agent /share/CACHEDEV2_DATA/.pulse-agent /share/MD0_DATA/.pulse-agent /share/HDA_DATA/.pulse-agent; do
|
|
if [[ -d "$candidate" ]] || [[ -f "$candidate/connection.env" ]] || [[ -f "$candidate/agent-id" ]]; then
|
|
printf '%s\n' "$candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
remove_qnap_autorun_block() {
|
|
local autorun_path="$1"
|
|
|
|
portable_sed_in_place '/^# Pulse Agent bootstrap begin$/,/^# Pulse Agent bootstrap end$/d' "$autorun_path"
|
|
portable_sed_in_place '/^# Pulse Agent$/d' "$autorun_path"
|
|
portable_sed_in_place '/start-pulse-agent\.sh/d' "$autorun_path"
|
|
}
|
|
|
|
write_qnap_wrapper_script() {
|
|
local wrapper_script="$1"
|
|
local runtime_binary="$2"
|
|
local stored_binary="$3"
|
|
local service_env_lines="$SHELL_EXPORT_LINES"
|
|
|
|
cat > "$wrapper_script" <<EOF
|
|
#!/bin/sh
|
|
# Pulse Agent startup script for QNAP
|
|
# Auto-generated by Pulse installer.
|
|
|
|
wait_for_file() {
|
|
path="\$1"
|
|
while [ ! -f "\$path" ]; do
|
|
sleep 5
|
|
done
|
|
}
|
|
|
|
wait_for_file "${stored_binary}"
|
|
|
|
# Kill any existing pulse-agent processes before refreshing the runtime copy.
|
|
pkill -x "pulse-agent" 2>/dev/null || true
|
|
sleep 2
|
|
|
|
mkdir -p "$(dirname "$runtime_binary")" 2>/dev/null || true
|
|
cp "${stored_binary}" "${runtime_binary}"
|
|
chmod +x "${runtime_binary}"${service_env_lines}
|
|
|
|
# Watchdog loop: restart agent if it exits.
|
|
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
|
|
|
|
RESTART_DELAY=\$((RESTART_DELAY * 2))
|
|
if [ \$RESTART_DELAY -gt \$MAX_RESTART_DELAY ]; then
|
|
RESTART_DELAY=\$MAX_RESTART_DELAY
|
|
fi
|
|
done
|
|
EOF
|
|
|
|
chmod +x "$wrapper_script"
|
|
}
|
|
|
|
append_qnap_autorun_block() {
|
|
local autorun_path="$1"
|
|
local wrapper_script="$2"
|
|
local state_dir="$3"
|
|
|
|
remove_qnap_autorun_block "$autorun_path"
|
|
if [[ ! -f "$autorun_path" ]]; then
|
|
echo "#!/bin/sh" > "$autorun_path"
|
|
fi
|
|
|
|
cat >> "$autorun_path" <<EOF
|
|
|
|
# Pulse Agent bootstrap begin
|
|
(
|
|
_pulse_wrapper="${wrapper_script}"
|
|
_pulse_log="/var/log/${AGENT_NAME}.log"
|
|
_pulse_waited=0
|
|
_pulse_wait_max=1800
|
|
while [ ! -x "\$_pulse_wrapper" ] && [ "\$_pulse_waited" -lt "\$_pulse_wait_max" ]; do
|
|
sleep 2
|
|
_pulse_waited=\$((_pulse_waited + 2))
|
|
done
|
|
if [ -x "\$_pulse_wrapper" ]; then
|
|
[ "\$_pulse_waited" -gt 0 ] && echo "\$(date '+%Y-%m-%d %H:%M:%S') [pulse-agent-autorun] data volume available after \${_pulse_waited}s" >> "\$_pulse_log"
|
|
"\$_pulse_wrapper" >> "\$_pulse_log" 2>&1
|
|
else
|
|
echo "\$(date '+%Y-%m-%d %H:%M:%S') [pulse-agent-autorun] timed out after \${_pulse_wait_max}s waiting for \$_pulse_wrapper" >> "\$_pulse_log"
|
|
fi
|
|
) >> /var/log/${AGENT_NAME}.log 2>&1 &
|
|
# Pulse Agent bootstrap end
|
|
|
|
EOF
|
|
|
|
chmod +x "$autorun_path"
|
|
}
|
|
|
|
# --- 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
|
|
if discover_rootless_container_runtime; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
discover_single_socket_match() {
|
|
local pattern="$1"
|
|
local matches=()
|
|
local candidate=""
|
|
|
|
for candidate in $pattern; do
|
|
[[ -S "$candidate" ]] || continue
|
|
matches+=("$candidate")
|
|
done
|
|
|
|
case "${#matches[@]}" in
|
|
0)
|
|
return 1
|
|
;;
|
|
1)
|
|
printf '%s\n' "${matches[0]}"
|
|
return 0
|
|
;;
|
|
*)
|
|
printf '%s\n' "__AMBIGUOUS__"
|
|
return 0
|
|
;;
|
|
esac
|
|
}
|
|
|
|
discover_rootless_container_runtime() {
|
|
local docker_match=""
|
|
local podman_match=""
|
|
|
|
ROOTLESS_RUNTIME_KIND=""
|
|
ROOTLESS_RUNTIME_SOCKET_PATH=""
|
|
ROOTLESS_RUNTIME_SOCKET_URI=""
|
|
ROOTLESS_RUNTIME_XDG_DIR=""
|
|
|
|
if [[ "$(uname -s)" != "Linux" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
docker_match=$(discover_single_socket_match "/run/user/*/docker.sock" || true)
|
|
podman_match=$(discover_single_socket_match "/run/user/*/podman/podman.sock" || true)
|
|
|
|
if [[ "$docker_match" == "__AMBIGUOUS__" ]]; then
|
|
log_warn "Multiple rootless Docker sockets found under /run/user; not auto-selecting one."
|
|
elif [[ -n "$docker_match" ]]; then
|
|
ROOTLESS_RUNTIME_KIND="docker"
|
|
ROOTLESS_RUNTIME_SOCKET_PATH="$docker_match"
|
|
ROOTLESS_RUNTIME_SOCKET_URI="unix://${docker_match}"
|
|
ROOTLESS_RUNTIME_XDG_DIR="$(dirname "$docker_match")"
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$podman_match" == "__AMBIGUOUS__" ]]; then
|
|
log_warn "Multiple rootless Podman sockets found under /run/user; not auto-selecting one."
|
|
elif [[ -n "$podman_match" ]]; then
|
|
ROOTLESS_RUNTIME_KIND="podman"
|
|
ROOTLESS_RUNTIME_SOCKET_PATH="$podman_match"
|
|
ROOTLESS_RUNTIME_SOCKET_URI="unix://${podman_match}"
|
|
ROOTLESS_RUNTIME_XDG_DIR="${podman_match%/podman/podman.sock}"
|
|
return 0
|
|
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
|
|
}
|
|
|
|
pulse_url_uses_plain_http() {
|
|
local url_lower
|
|
url_lower=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
|
|
[[ "$url_lower" =~ ^http:// ]]
|
|
}
|
|
|
|
auto_enable_insecure_for_plain_http_url() {
|
|
if [[ "$INSECURE" == "true" ]]; then
|
|
return 0
|
|
fi
|
|
if ! pulse_url_uses_plain_http "$PULSE_URL"; then
|
|
return 0
|
|
fi
|
|
INSECURE="true"
|
|
log_info "Plain HTTP Pulse URL detected; enabling insecure mode for installer downloads and persisted agent update checks."
|
|
}
|
|
|
|
build_exec_arg_items() {
|
|
local include_token="${1:-true}"
|
|
|
|
EXEC_ARG_ITEMS=(--url "$PULSE_URL" --interval "$INTERVAL")
|
|
if [[ "$include_token" == "true" && -n "$PULSE_TOKEN" ]]; then
|
|
if [[ -n "$RUNTIME_TOKEN_FILE" ]]; then
|
|
EXEC_ARG_ITEMS+=(--token-file "$RUNTIME_TOKEN_FILE")
|
|
else
|
|
fail "Internal installer error: runtime token file was not prepared before service rendering." "$EXIT_GENERAL"
|
|
fi
|
|
fi
|
|
# Always pass enable-host flag since agent defaults to true
|
|
if [[ "$ENABLE_HOST" == "true" ]]; then
|
|
EXEC_ARG_ITEMS+=(--enable-host)
|
|
else
|
|
EXEC_ARG_ITEMS+=(--enable-host=false)
|
|
fi
|
|
if [[ "$ENABLE_DOCKER" == "true" ]]; then EXEC_ARG_ITEMS+=(--enable-docker); fi
|
|
# Pass explicit false when Docker was explicitly disabled (prevents auto-detection)
|
|
if [[ "$ENABLE_DOCKER" == "false" && "$DOCKER_EXPLICIT" == "true" ]]; then EXEC_ARG_ITEMS+=(--enable-docker=false); fi
|
|
if [[ "$ENABLE_KUBERNETES" == "true" ]]; then EXEC_ARG_ITEMS+=(--enable-kubernetes); fi
|
|
if [[ -n "$KUBECONFIG_PATH" ]]; then EXEC_ARG_ITEMS+=(--kubeconfig "$KUBECONFIG_PATH"); fi
|
|
if [[ "$ENABLE_PROXMOX" == "true" ]]; then EXEC_ARG_ITEMS+=(--enable-proxmox); fi
|
|
if [[ -n "$PROXMOX_TYPE" ]]; then EXEC_ARG_ITEMS+=(--proxmox-type "$PROXMOX_TYPE"); fi
|
|
if [[ "$INSECURE" == "true" ]]; then EXEC_ARG_ITEMS+=(--insecure); fi
|
|
if [[ "$ENABLE_COMMANDS" == "true" ]]; then EXEC_ARG_ITEMS+=(--enable-commands); fi
|
|
if [[ "$HEALTH_ADDR_SET" == "true" ]]; then EXEC_ARG_ITEMS+=(--health-addr "$HEALTH_ADDR"); fi
|
|
if [[ "$ENROLL" == "true" ]]; then EXEC_ARG_ITEMS+=(--enroll); fi
|
|
if [[ "$KUBE_INCLUDE_ALL_PODS" == "true" ]]; then EXEC_ARG_ITEMS+=(--kube-include-all-pods); fi
|
|
if [[ "$KUBE_INCLUDE_ALL_DEPLOYMENTS" == "true" ]]; then EXEC_ARG_ITEMS+=(--kube-include-all-deployments); fi
|
|
if [[ -n "$AGENT_ID" ]]; then EXEC_ARG_ITEMS+=(--agent-id "$AGENT_ID"); fi
|
|
if [[ -n "$HOSTNAME_OVERRIDE" ]]; then EXEC_ARG_ITEMS+=(--hostname "$HOSTNAME_OVERRIDE"); fi
|
|
if [[ -n "$STATE_DIR" ]]; then EXEC_ARG_ITEMS+=(--state-dir "$STATE_DIR"); 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_ARG_ITEMS+=(--disk-exclude "$pattern")
|
|
done
|
|
}
|
|
|
|
join_exec_arg_items() {
|
|
local joined=""
|
|
local arg=""
|
|
local quoted=""
|
|
|
|
for arg in ${EXEC_ARG_ITEMS[@]+"${EXEC_ARG_ITEMS[@]}"}; do
|
|
printf -v quoted '%q' "$arg"
|
|
if [[ -n "$joined" ]]; then
|
|
joined="$joined "
|
|
fi
|
|
joined="${joined}${quoted}"
|
|
done
|
|
|
|
EXEC_ARGS="$joined"
|
|
}
|
|
|
|
# Build exec args string for use in service files
|
|
# Returns via EXEC_ARGS variable
|
|
build_exec_args() {
|
|
build_exec_arg_items "true"
|
|
join_exec_arg_items
|
|
}
|
|
|
|
build_exec_args_without_token() {
|
|
build_exec_arg_items "false"
|
|
join_exec_arg_items
|
|
}
|
|
|
|
# Build exec args as array for direct execution (proper quoting)
|
|
# Returns via EXEC_ARGS_ARRAY variable
|
|
build_exec_args_array() {
|
|
build_exec_arg_items "true"
|
|
EXEC_ARGS_ARRAY=("${EXEC_ARG_ITEMS[@]}")
|
|
}
|
|
|
|
xml_escape() {
|
|
local value="$1"
|
|
value="${value//&/&}"
|
|
value="${value//</<}"
|
|
value="${value//>/>}"
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
append_plist_arg() {
|
|
local arg="$1"
|
|
PLIST_ARGS="${PLIST_ARGS}
|
|
<string>$(xml_escape "$arg")</string>"
|
|
}
|
|
|
|
build_plist_program_arguments() {
|
|
local executable="$1"
|
|
PLIST_ARGS=""
|
|
append_plist_arg "$executable"
|
|
build_exec_arg_items "true"
|
|
|
|
local arg=""
|
|
for arg in ${EXEC_ARG_ITEMS[@]+"${EXEC_ARG_ITEMS[@]}"}; do
|
|
append_plist_arg "$arg"
|
|
done
|
|
}
|
|
|
|
ensure_runtime_token_file() {
|
|
local state_dir="${1:-$STATE_DIR}"
|
|
local token_file="${state_dir}/token"
|
|
|
|
RUNTIME_TOKEN_FILE=""
|
|
if [[ -z "$PULSE_TOKEN" ]]; then
|
|
rm -f "$token_file" 2>/dev/null || true
|
|
log_info "No API token provided; installer will configure token-optional agent runtime."
|
|
return 0
|
|
fi
|
|
|
|
mkdir -p "$state_dir"
|
|
local old_umask=""
|
|
old_umask=$(umask)
|
|
umask 077
|
|
if ! printf '%s' "$PULSE_TOKEN" > "$token_file"; then
|
|
umask "$old_umask"
|
|
fail "Failed to write runtime token file: $token_file" "$EXIT_GENERAL"
|
|
fi
|
|
umask "$old_umask"
|
|
chmod 600 "$token_file"
|
|
if [[ "$(id -u 2>/dev/null || echo 1)" == "0" ]]; then
|
|
chown root:root "$token_file" 2>/dev/null || true
|
|
fi
|
|
RUNTIME_TOKEN_FILE="$token_file"
|
|
log_info "Token stored securely at $token_file (mode 600)"
|
|
}
|
|
|
|
clear_proxmox_state_if_needed() {
|
|
if [[ "$ENABLE_PROXMOX" != "true" ]]; then
|
|
return 0
|
|
fi
|
|
log_info "Clearing Proxmox state for fresh registration..."
|
|
rm -f "${STATE_DIR}/proxmox-registered" 2>/dev/null || true
|
|
rm -f "${STATE_DIR}/proxmox-pve-registered" 2>/dev/null || true
|
|
rm -f "${STATE_DIR}/proxmox-pbs-registered" 2>/dev/null || true
|
|
}
|
|
|
|
write_connection_state_value() {
|
|
local file="$1"
|
|
local key="$2"
|
|
local value="$3"
|
|
|
|
if [[ -z "$value" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
printf "%s='%s'\n" "$key" "$value" >> "$file"
|
|
}
|
|
|
|
read_connection_state_value() {
|
|
local file="$1"
|
|
local key="$2"
|
|
|
|
if [[ ! -f "$file" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
awk -F= -v key="$key" '
|
|
$1 == key {
|
|
value = substr($0, index($0, "=") + 1)
|
|
sub(/^'\''/, "", value)
|
|
sub(/'\''$/, "", value)
|
|
print value
|
|
exit
|
|
}
|
|
' "$file" 2>/dev/null || true
|
|
}
|
|
|
|
recover_token_from_default_agent_token_file() {
|
|
local token_path=""
|
|
local recovered_token=""
|
|
|
|
if [[ -n "$PULSE_TOKEN" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# v5.1.x Linux services could omit --token and --token-file because the
|
|
# Go agent read this default file itself.
|
|
for token_path in "${STATE_DIR%/}/token" "/var/lib/pulse-agent/token" "$TRUENAS_STATE_DIR/token"; do
|
|
[[ -n "$token_path" && -f "$token_path" ]] || continue
|
|
recovered_token=$(cat "$token_path" 2>/dev/null || true)
|
|
if [[ -n "$recovered_token" ]]; then
|
|
PULSE_TOKEN="$recovered_token"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
recover_connection_state() {
|
|
local file="$1"
|
|
|
|
if [[ -z "$PULSE_URL" ]]; then
|
|
PULSE_URL=$(read_connection_state_value "$file" "PULSE_URL")
|
|
fi
|
|
if [[ -z "$PULSE_TOKEN" ]]; then
|
|
PULSE_TOKEN=$(read_connection_state_value "$file" "PULSE_TOKEN")
|
|
fi
|
|
if [[ -z "$PULSE_TOKEN" ]]; then
|
|
local saved_token_file=""
|
|
saved_token_file=$(read_connection_state_value "$file" "PULSE_TOKEN_FILE")
|
|
if [[ -n "$saved_token_file" && -f "$saved_token_file" ]]; then
|
|
PULSE_TOKEN=$(cat "$saved_token_file")
|
|
fi
|
|
fi
|
|
if [[ -z "$PULSE_TOKEN" && -n "$PULSE_URL" ]]; then
|
|
recover_token_from_default_agent_token_file || true
|
|
fi
|
|
if [[ -z "$AGENT_ID" ]]; then
|
|
AGENT_ID=$(read_connection_state_value "$file" "PULSE_AGENT_ID")
|
|
fi
|
|
if [[ -z "$HOSTNAME_OVERRIDE" ]]; then
|
|
HOSTNAME_OVERRIDE=$(read_connection_state_value "$file" "PULSE_HOSTNAME")
|
|
fi
|
|
if [[ "$INSECURE" != "true" ]]; then
|
|
local saved_insecure=""
|
|
saved_insecure=$(read_connection_state_value "$file" "PULSE_INSECURE_SKIP_VERIFY")
|
|
if [[ "$saved_insecure" == "true" ]]; then
|
|
INSECURE="true"
|
|
fi
|
|
fi
|
|
if [[ -z "$CURL_CA_BUNDLE" ]]; then
|
|
CURL_CA_BUNDLE=$(read_connection_state_value "$file" "PULSE_CACERT")
|
|
fi
|
|
}
|
|
|
|
strip_recovered_arg_quotes() {
|
|
local value="$1"
|
|
|
|
case "$value" in
|
|
\"*\") value="${value#\"}"; value="${value%\"}" ;;
|
|
\'*\') value="${value#\'}"; value="${value%\'}" ;;
|
|
esac
|
|
|
|
printf '%s\n' "$value"
|
|
}
|
|
|
|
normalize_recovered_agent_arg_key() {
|
|
local key="$1"
|
|
|
|
key="${key#--}"
|
|
key="${key#-}"
|
|
printf '%s\n' "$key"
|
|
}
|
|
|
|
apply_recovered_agent_arg_value() {
|
|
local key="$1"
|
|
local value="$2"
|
|
|
|
key=$(normalize_recovered_agent_arg_key "$key")
|
|
value=$(strip_recovered_arg_quotes "$value")
|
|
|
|
case "$key" in
|
|
url|pulse-url)
|
|
if [[ -z "$PULSE_URL" ]]; then PULSE_URL="$value"; fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
token)
|
|
if [[ -z "$PULSE_TOKEN" ]]; then PULSE_TOKEN="$value"; fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
token-file)
|
|
if [[ -z "$PULSE_TOKEN" && -n "$value" && -f "$value" ]]; then
|
|
PULSE_TOKEN=$(cat "$value")
|
|
fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
interval)
|
|
if [[ "$INTERVAL_EXPLICIT" != "true" && -n "$value" ]]; then INTERVAL="$value"; fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
agent-id)
|
|
if [[ -z "$AGENT_ID" ]]; then AGENT_ID="$value"; fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
hostname)
|
|
if [[ -z "$HOSTNAME_OVERRIDE" ]]; then HOSTNAME_OVERRIDE="$value"; fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
cacert)
|
|
if [[ -z "$CURL_CA_BUNDLE" ]]; then CURL_CA_BUNDLE="$value"; fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
health-addr)
|
|
if [[ "$HEALTH_ADDR_SET" != "true" ]]; then
|
|
HEALTH_ADDR="$value"
|
|
HEALTH_ADDR_SET="true"
|
|
fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
state-dir)
|
|
if [[ -n "$value" && "$STATE_DIR" == "/var/lib/pulse-agent" ]]; then STATE_DIR="$value"; fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
kubeconfig)
|
|
if [[ "$KUBERNETES_EXPLICIT" != "true" ]]; then
|
|
KUBECONFIG_PATH="$value"
|
|
KUBERNETES_EXPLICIT="true"
|
|
ENABLE_KUBERNETES="true"
|
|
fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
proxmox-type)
|
|
if [[ -z "$PROXMOX_TYPE" ]]; then PROXMOX_TYPE="$value"; fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
disk-exclude)
|
|
DISK_EXCLUDES+=("$value")
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
recovered_connection_state_ready() {
|
|
[[ -n "$PULSE_URL" && -n "$PULSE_TOKEN" ]]
|
|
}
|
|
|
|
update_connection_state_incomplete() {
|
|
[[ -z "$PULSE_URL" || -z "$PULSE_TOKEN" || -z "$AGENT_ID" || -z "$HOSTNAME_OVERRIDE" || -z "$CURL_CA_BUNDLE" || "$INSECURE" != "true" ]]
|
|
}
|
|
|
|
recover_connection_state_from_arg_stream() {
|
|
local arg=""
|
|
local pending_key=""
|
|
local key=""
|
|
local value=""
|
|
|
|
RECOVERED_AGENT_ARG_STATE="false"
|
|
|
|
while IFS= read -r arg; do
|
|
if [[ -n "$pending_key" ]]; then
|
|
apply_recovered_agent_arg_value "$pending_key" "$arg"
|
|
pending_key=""
|
|
continue
|
|
fi
|
|
|
|
case "$arg" in
|
|
--url|--pulse-url|--token|--token-file|--interval|--agent-id|--hostname|--cacert|--health-addr|--state-dir|--kubeconfig|--proxmox-type|--disk-exclude|-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-cacert|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude)
|
|
pending_key=$(normalize_recovered_agent_arg_key "$arg")
|
|
;;
|
|
--url=*|--pulse-url=*|--token=*|--token-file=*|--interval=*|--agent-id=*|--hostname=*|--cacert=*|--health-addr=*|--state-dir=*|--kubeconfig=*|--proxmox-type=*|--disk-exclude=*|-url=*|-pulse-url=*|-token=*|-token-file=*|-interval=*|-agent-id=*|-hostname=*|-cacert=*|-health-addr=*|-state-dir=*|-kubeconfig=*|-proxmox-type=*|-disk-exclude=*)
|
|
key="${arg%%=*}"
|
|
value="${arg#*=}"
|
|
apply_recovered_agent_arg_value "$key" "$value"
|
|
;;
|
|
--enable-host|-enable-host|--enable-host=true|-enable-host=true)
|
|
if [[ "$HOST_EXPLICIT" != "true" ]]; then ENABLE_HOST="true"; fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
--enable-host=false|-enable-host=false|--disable-host|-disable-host)
|
|
if [[ "$HOST_EXPLICIT" != "true" ]]; then ENABLE_HOST="false"; fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
--enable-docker|-enable-docker|--enable-docker=true|-enable-docker=true)
|
|
if [[ "$DOCKER_EXPLICIT" != "true" ]]; then
|
|
ENABLE_DOCKER="true"
|
|
DOCKER_EXPLICIT="true"
|
|
fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
--enable-docker=false|-enable-docker=false|--disable-docker|-disable-docker)
|
|
if [[ "$DOCKER_EXPLICIT" != "true" ]]; then
|
|
ENABLE_DOCKER="false"
|
|
DOCKER_EXPLICIT="true"
|
|
fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
--enable-kubernetes|-enable-kubernetes|--enable-kubernetes=true|-enable-kubernetes=true)
|
|
if [[ "$KUBERNETES_EXPLICIT" != "true" ]]; then
|
|
ENABLE_KUBERNETES="true"
|
|
KUBERNETES_EXPLICIT="true"
|
|
fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
--enable-kubernetes=false|-enable-kubernetes=false|--disable-kubernetes|-disable-kubernetes)
|
|
if [[ "$KUBERNETES_EXPLICIT" != "true" ]]; then
|
|
ENABLE_KUBERNETES="false"
|
|
KUBERNETES_EXPLICIT="true"
|
|
fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
--enable-proxmox|-enable-proxmox|--enable-proxmox=true|-enable-proxmox=true)
|
|
if [[ "$PROXMOX_EXPLICIT" != "true" ]]; then
|
|
ENABLE_PROXMOX="true"
|
|
PROXMOX_EXPLICIT="true"
|
|
fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
--enable-proxmox=false|-enable-proxmox=false|--disable-proxmox|-disable-proxmox)
|
|
if [[ "$PROXMOX_EXPLICIT" != "true" ]]; then
|
|
ENABLE_PROXMOX="false"
|
|
PROXMOX_EXPLICIT="true"
|
|
fi
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
--insecure|-insecure)
|
|
INSECURE="true"
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
--enable-commands|-enable-commands)
|
|
ENABLE_COMMANDS="true"
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
--enroll|-enroll)
|
|
ENROLL="true"
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
--kube-include-all-pods|-kube-include-all-pods)
|
|
KUBE_INCLUDE_ALL_PODS="true"
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
--kube-include-all-deployments|-kube-include-all-deployments)
|
|
KUBE_INCLUDE_ALL_DEPLOYMENTS="true"
|
|
RECOVERED_AGENT_ARG_STATE="true"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$RECOVERED_AGENT_ARG_STATE" == "true" && -z "$PULSE_TOKEN" && -n "$PULSE_URL" ]]; then
|
|
recover_token_from_default_agent_token_file || true
|
|
fi
|
|
|
|
[[ "$RECOVERED_AGENT_ARG_STATE" == "true" ]] && recovered_connection_state_ready
|
|
}
|
|
|
|
recover_connection_state_from_env_stream() {
|
|
local env_line=""
|
|
local value=""
|
|
|
|
RECOVERED_AGENT_ENV_STATE="false"
|
|
|
|
while IFS= read -r env_line; do
|
|
case "$env_line" in
|
|
PULSE_URL=*|PULSE_AGENT_URL=*|PULSE_AGENT_CONNECT_URL=*)
|
|
value="${env_line#*=}"
|
|
if [[ -z "$PULSE_URL" ]]; then PULSE_URL="$value"; fi
|
|
RECOVERED_AGENT_ENV_STATE="true"
|
|
;;
|
|
PULSE_TOKEN=*)
|
|
value="${env_line#*=}"
|
|
if [[ -z "$PULSE_TOKEN" ]]; then PULSE_TOKEN="$value"; fi
|
|
RECOVERED_AGENT_ENV_STATE="true"
|
|
;;
|
|
PULSE_TOKEN_FILE=*)
|
|
value="${env_line#*=}"
|
|
if [[ -z "$PULSE_TOKEN" && -n "$value" && -f "$value" ]]; then
|
|
PULSE_TOKEN=$(cat "$value")
|
|
fi
|
|
RECOVERED_AGENT_ENV_STATE="true"
|
|
;;
|
|
PULSE_AGENT_ID=*)
|
|
value="${env_line#*=}"
|
|
if [[ -z "$AGENT_ID" ]]; then AGENT_ID="$value"; fi
|
|
RECOVERED_AGENT_ENV_STATE="true"
|
|
;;
|
|
PULSE_HOSTNAME=*)
|
|
value="${env_line#*=}"
|
|
if [[ -z "$HOSTNAME_OVERRIDE" ]]; then HOSTNAME_OVERRIDE="$value"; fi
|
|
RECOVERED_AGENT_ENV_STATE="true"
|
|
;;
|
|
PULSE_INSECURE_SKIP_VERIFY=true)
|
|
INSECURE="true"
|
|
RECOVERED_AGENT_ENV_STATE="true"
|
|
;;
|
|
PULSE_CACERT=*)
|
|
value="${env_line#*=}"
|
|
if [[ -z "$CURL_CA_BUNDLE" ]]; then CURL_CA_BUNDLE="$value"; fi
|
|
RECOVERED_AGENT_ENV_STATE="true"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$RECOVERED_AGENT_ENV_STATE" == "true" && -z "$PULSE_TOKEN" && -n "$PULSE_URL" ]]; then
|
|
recover_token_from_default_agent_token_file || true
|
|
fi
|
|
|
|
[[ "$RECOVERED_AGENT_ENV_STATE" == "true" ]] && recovered_connection_state_ready
|
|
}
|
|
|
|
collect_running_agent_pids() {
|
|
local path=""
|
|
local pid=""
|
|
|
|
if command -v pgrep >/dev/null 2>&1; then
|
|
{
|
|
pgrep -x "$BINARY_NAME" 2>/dev/null || true
|
|
pgrep -f "/${BINARY_NAME}( |$)" 2>/dev/null || true
|
|
} | awk '!seen[$0]++'
|
|
return 0
|
|
fi
|
|
|
|
for path in /proc/[0-9]*/cmdline; do
|
|
[[ -r "$path" ]] || continue
|
|
if tr '\0' '\n' < "$path" 2>/dev/null | grep -qx ".*/${BINARY_NAME}"; then
|
|
pid="${path%/cmdline}"
|
|
printf '%s\n' "${pid##*/}"
|
|
fi
|
|
done
|
|
}
|
|
|
|
recover_connection_state_from_running_agent() {
|
|
local pid=""
|
|
local recovered="false"
|
|
|
|
[[ -d /proc ]] || return 1
|
|
|
|
while IFS= read -r pid; do
|
|
[[ -n "$pid" && "$pid" != "$$" ]] || continue
|
|
recovered="false"
|
|
|
|
if [[ -r "/proc/${pid}/cmdline" ]]; then
|
|
if recover_connection_state_from_arg_stream < <(tr '\0' '\n' < "/proc/${pid}/cmdline" 2>/dev/null); then
|
|
recovered="true"
|
|
fi
|
|
fi
|
|
if [[ -r "/proc/${pid}/environ" ]]; then
|
|
if recover_connection_state_from_env_stream < <(tr '\0' '\n' < "/proc/${pid}/environ" 2>/dev/null); then
|
|
recovered="true"
|
|
fi
|
|
fi
|
|
if [[ "$recovered" == "true" ]] && recovered_connection_state_ready; then
|
|
return 0
|
|
fi
|
|
done < <(collect_running_agent_pids)
|
|
|
|
return 1
|
|
}
|
|
|
|
recover_connection_state_from_systemd_unit() {
|
|
local unit_path=""
|
|
local candidate=""
|
|
local line=""
|
|
local payload=""
|
|
local recovered="false"
|
|
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
unit_path=$(systemctl show -p FragmentPath --value "$AGENT_NAME" 2>/dev/null || true)
|
|
fi
|
|
|
|
for candidate in "$unit_path" "/etc/systemd/system/${AGENT_NAME}.service" "/lib/systemd/system/${AGENT_NAME}.service" "/usr/lib/systemd/system/${AGENT_NAME}.service"; do
|
|
[[ -n "$candidate" && -f "$candidate" ]] || continue
|
|
recovered="false"
|
|
while IFS= read -r line; do
|
|
case "$line" in
|
|
ExecStart=*)
|
|
payload="${line#ExecStart=}"
|
|
if recover_connection_state_from_arg_stream < <(printf '%s\n' "$payload" | tr ' ' '\n'); then
|
|
recovered="true"
|
|
fi
|
|
;;
|
|
Environment=*)
|
|
payload="${line#Environment=}"
|
|
if recover_connection_state_from_env_stream < <(printf '%s\n' "$payload" | tr ' ' '\n' | sed 's/^"//; s/"$//'); then
|
|
recovered="true"
|
|
fi
|
|
;;
|
|
esac
|
|
done < "$candidate"
|
|
if [[ "$recovered" == "true" ]] && recovered_connection_state_ready; then
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
recover_connection_state_from_existing_agent() {
|
|
if recover_connection_state_from_running_agent; then
|
|
log_info "Recovered connection details from the running Pulse Agent process."
|
|
return 0
|
|
fi
|
|
if recover_connection_state_from_systemd_unit; then
|
|
log_info "Recovered connection details from the existing Pulse Agent service."
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
find_connection_state_file() {
|
|
local conn_env=""
|
|
local qnap_state_dir=""
|
|
|
|
for conn_env in /var/lib/pulse-agent/connection.env /boot/config/plugins/pulse-agent/connection.env "$TRUENAS_STATE_DIR/connection.env"; do
|
|
if [[ -f "$conn_env" ]]; then
|
|
printf '%s\n' "$conn_env"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
qnap_state_dir=$(find_qnap_state_dir || true)
|
|
if [[ -n "$qnap_state_dir" ]] && [[ -f "$qnap_state_dir/connection.env" ]]; then
|
|
printf '%s\n' "$qnap_state_dir/connection.env"
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
recover_agent_id_from_state_file() {
|
|
local aid_path=""
|
|
local qnap_state_dir=""
|
|
local aid_paths=(/var/lib/pulse-agent/agent-id /boot/config/plugins/pulse-agent/agent-id "$TRUENAS_STATE_DIR/agent-id")
|
|
|
|
qnap_state_dir=$(find_qnap_state_dir || true)
|
|
if [[ -n "$qnap_state_dir" ]]; then
|
|
aid_paths+=("$qnap_state_dir/agent-id")
|
|
fi
|
|
|
|
for aid_path in "${aid_paths[@]}"; do
|
|
if [[ -f "$aid_path" ]]; then
|
|
cat "$aid_path"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# Save install script and connection details for offline uninstall
|
|
save_connection_info() {
|
|
local state_dir="$1"
|
|
local conn_env="${state_dir}/connection.env"
|
|
mkdir -p "$state_dir"
|
|
# Save connection details so uninstall can deregister without --url/--token.
|
|
# Single-quote values to prevent shell interpretation on read-back.
|
|
# Legacy connection files may contain PULSE_TOKEN, but new installs persist
|
|
# only the protected token file path.
|
|
: > "$conn_env"
|
|
write_connection_state_value "$conn_env" "PULSE_URL" "$PULSE_URL"
|
|
write_connection_state_value "$conn_env" "PULSE_TOKEN_FILE" "$RUNTIME_TOKEN_FILE"
|
|
write_connection_state_value "$conn_env" "PULSE_AGENT_ID" "$AGENT_ID"
|
|
write_connection_state_value "$conn_env" "PULSE_HOSTNAME" "$HOSTNAME_OVERRIDE"
|
|
if [[ "$INSECURE" == "true" ]]; then
|
|
write_connection_state_value "$conn_env" "PULSE_INSECURE_SKIP_VERIFY" "true"
|
|
fi
|
|
write_connection_state_value "$conn_env" "PULSE_CACERT" "$CURL_CA_BUNDLE"
|
|
chmod 600 "$conn_env"
|
|
# Save a copy of this install script for offline uninstall.
|
|
# When run via "curl | bash", $0 is /dev/stdin — not a usable file.
|
|
# Try local copy first, then download a fresh copy from the server.
|
|
local saved=false
|
|
if [[ -f "$0" && "$0" != "/dev/stdin" && "$0" != "bash" && "$0" != "-bash" ]]; then
|
|
if cp "$0" "${state_dir}/install.sh" 2>/dev/null; then
|
|
saved=true
|
|
fi
|
|
fi
|
|
if [[ "$saved" != "true" ]]; then
|
|
# Download from the server (we know it's reachable — we just installed from it)
|
|
local dl_args=(-fsSL --connect-timeout 10 --max-time 30)
|
|
if [[ "$INSECURE" == "true" ]]; then dl_args+=(-k); fi
|
|
if [[ -n "$CURL_CA_BUNDLE" ]]; then dl_args+=(--cacert "$CURL_CA_BUNDLE"); fi
|
|
curl "${dl_args[@]}" -o "${state_dir}/install.sh" "${PULSE_URL}/install.sh" 2>/dev/null || true
|
|
fi
|
|
if [[ -f "${state_dir}/install.sh" ]]; then
|
|
chmod +x "${state_dir}/install.sh"
|
|
SAVED_INSTALL_SCRIPT="${state_dir}/install.sh"
|
|
else
|
|
SAVED_INSTALL_SCRIPT=""
|
|
fi
|
|
}
|
|
|
|
# --- 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"; INTERVAL_EXPLICIT="true"; shift 2 ;;
|
|
--enable-host) ENABLE_HOST="true"; HOST_EXPLICIT="true"; shift ;;
|
|
--enable-host=true) ENABLE_HOST="true"; HOST_EXPLICIT="true"; shift ;;
|
|
--enable-host=false) ENABLE_HOST="false"; HOST_EXPLICIT="true"; shift ;;
|
|
--disable-host) ENABLE_HOST="false"; HOST_EXPLICIT="true"; shift ;;
|
|
--enable-docker) ENABLE_DOCKER="true"; DOCKER_EXPLICIT="true"; shift ;;
|
|
--enable-docker=true) ENABLE_DOCKER="true"; DOCKER_EXPLICIT="true"; shift ;;
|
|
--enable-docker=false) ENABLE_DOCKER="false"; DOCKER_EXPLICIT="true"; shift ;;
|
|
--disable-docker) ENABLE_DOCKER="false"; DOCKER_EXPLICIT="true"; shift ;;
|
|
--enable-kubernetes) ENABLE_KUBERNETES="true"; KUBERNETES_EXPLICIT="true"; shift ;;
|
|
--enable-kubernetes=true) ENABLE_KUBERNETES="true"; KUBERNETES_EXPLICIT="true"; shift ;;
|
|
--enable-kubernetes=false) ENABLE_KUBERNETES="false"; 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 ;;
|
|
--enable-proxmox=true) ENABLE_PROXMOX="true"; PROXMOX_EXPLICIT="true"; shift ;;
|
|
--enable-proxmox=false) ENABLE_PROXMOX="false"; PROXMOX_EXPLICIT="true"; shift ;;
|
|
--disable-proxmox) ENABLE_PROXMOX="false"; PROXMOX_EXPLICIT="true"; shift ;;
|
|
--proxmox-type) PROXMOX_TYPE="$2"; shift 2 ;;
|
|
--insecure) INSECURE="true"; shift ;;
|
|
--cacert) CURL_CA_BUNDLE="$2"; shift 2 ;;
|
|
--enable-commands) ENABLE_COMMANDS="true"; shift ;;
|
|
--health-addr) HEALTH_ADDR="$2"; HEALTH_ADDR_SET="true"; shift 2 ;;
|
|
--enroll) ENROLL="true"; shift ;;
|
|
--update) UPDATE_ONLY="true"; shift ;;
|
|
--uninstall) UNINSTALL="true"; shift ;;
|
|
--agent-id) AGENT_ID="$2"; shift 2 ;;
|
|
--hostname) HOSTNAME_OVERRIDE="$2"; shift 2 ;;
|
|
--state-dir) STATE_DIR="$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 ;;
|
|
--non-interactive) NON_INTERACTIVE="true"; shift ;;
|
|
--token-file) TOKEN_FILE_PATH="$2"; shift 2 ;;
|
|
--pulse-url) PULSE_URL="$2"; shift 2 ;;
|
|
--output) OUTPUT_FORMAT="$2"; shift 2 ;;
|
|
--preflight-only) PREFLIGHT_ONLY="true"; shift ;;
|
|
*) fail "Unknown argument: $1" ;;
|
|
esac
|
|
done
|
|
|
|
# Read token from file if --token-file was provided
|
|
if [[ -n "$TOKEN_FILE_PATH" ]]; then
|
|
if [[ ! -f "$TOKEN_FILE_PATH" ]]; then
|
|
fail "Token file not found: ${TOKEN_FILE_PATH}" "$EXIT_MISSING_ARGS"
|
|
fi
|
|
PULSE_TOKEN=$(cat "$TOKEN_FILE_PATH")
|
|
if [[ -z "$PULSE_TOKEN" ]]; then
|
|
fail "Token file is empty: ${TOKEN_FILE_PATH}" "$EXIT_MISSING_ARGS"
|
|
fi
|
|
# Clean up token file after reading in non-interactive mode (deploy bootstrap tokens are one-time use)
|
|
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
rm -f "$TOKEN_FILE_PATH" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$PROXMOX_TYPE" && "$PROXMOX_TYPE" != "pve" && "$PROXMOX_TYPE" != "pbs" ]]; then
|
|
fail "Invalid --proxmox-type value: ${PROXMOX_TYPE} (expected 'pve' or 'pbs')"
|
|
fi
|
|
|
|
# --- Check Root ---
|
|
if [[ $EUID -ne 0 && "$PREFLIGHT_ONLY" != "true" ]]; 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
|
|
|
|
# --- Update State Recovery ---
|
|
# Update commands are intentionally tokenless for already-installed agents. The
|
|
# installer reuses the canonical saved connection state instead of asking the
|
|
# operator to mint a new install token for a host Pulse already knows about.
|
|
if [[ "$UPDATE_ONLY" == "true" ]]; then
|
|
if update_connection_state_incomplete; then
|
|
local update_conn_env=""
|
|
update_conn_env=$(find_connection_state_file || true)
|
|
if [[ -n "$update_conn_env" ]]; then
|
|
log_info "Recovering connection details from ${update_conn_env}..."
|
|
recover_connection_state "$update_conn_env"
|
|
fi
|
|
if update_connection_state_incomplete; then
|
|
recover_connection_state_from_existing_agent || true
|
|
fi
|
|
if [[ -n "$PULSE_URL" && -n "$PULSE_TOKEN" ]]; then
|
|
:
|
|
elif [[ -z "$PULSE_URL" || -z "$PULSE_TOKEN" ]]; then
|
|
fail "No existing Pulse Agent connection state found. Use the install command instead." "$EXIT_MISSING_ARGS"
|
|
fi
|
|
if [[ -z "$AGENT_ID" ]]; then
|
|
AGENT_ID=$(recover_agent_id_from_state_file || true)
|
|
if [[ -n "$AGENT_ID" ]]; then
|
|
log_info "Recovered agent ID from persisted agent-id state."
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$PULSE_URL" ]]; then
|
|
PULSE_URL="${PULSE_URL%/}"
|
|
fi
|
|
|
|
# --- CA Certificate Validation ---
|
|
# --cacert must point to a PEM file (matches curl --cacert behaviour).
|
|
# The same path is passed to the agent process via SSL_CERT_FILE so that
|
|
# Go's crypto/x509 trusts the custom CA at runtime.
|
|
SSL_CERT_ENV_NAME=""
|
|
SSL_CERT_ENV_VALUE=""
|
|
if [[ -n "$CURL_CA_BUNDLE" ]]; then
|
|
if [[ -f "$CURL_CA_BUNDLE" ]]; then
|
|
SSL_CERT_ENV_NAME="SSL_CERT_FILE"
|
|
SSL_CERT_ENV_VALUE="$CURL_CA_BUNDLE"
|
|
log_info "CA certificate: ${CURL_CA_BUNDLE} (will set SSL_CERT_FILE for agent)"
|
|
elif [[ -d "$CURL_CA_BUNDLE" ]]; then
|
|
fail "--cacert requires a PEM file, not a directory. Try: --cacert ${CURL_CA_BUNDLE}/<cert-name>.pem"
|
|
else
|
|
fail "--cacert path does not exist: ${CURL_CA_BUNDLE}"
|
|
fi
|
|
fi
|
|
|
|
service_env_has_key() {
|
|
local env_key="$1"
|
|
case "$APPLIED_SERVICE_ENV_KEYS" in
|
|
*"|${env_key}|"*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
shell_export_value() {
|
|
local value="$1"
|
|
value="${value//\\/\\\\}"
|
|
value="${value//\"/\\\"}"
|
|
value="${value//\$/\\$}"
|
|
value="${value//\`/\\\`}"
|
|
printf '"%s"' "$value"
|
|
}
|
|
|
|
append_service_env() {
|
|
local env_key="$1"
|
|
local env_val="$2"
|
|
local shell_value=""
|
|
local plist_key=""
|
|
local plist_value=""
|
|
|
|
if [[ -z "$env_key" ]] || service_env_has_key "$env_key"; then
|
|
return
|
|
fi
|
|
|
|
shell_value="$(shell_export_value "$env_val")"
|
|
SYSTEMD_ENV_LINES+=$'\n'"Environment=\"${env_key}=${env_val}\""
|
|
SHELL_EXPORT_LINES+=$'\n'"export ${env_key}=${shell_value}"
|
|
UPSTART_ENV_LINES+=$'\n'"env ${env_key}=${env_val}"
|
|
if [[ -n "$SED_EXPORT_LINES" ]]; then
|
|
SED_EXPORT_LINES+="; "
|
|
fi
|
|
SED_EXPORT_LINES+="export ${env_key}=${shell_value}"
|
|
|
|
plist_key="$(xml_escape "$env_key")"
|
|
plist_value="$(xml_escape "$env_val")"
|
|
PLIST_ENV_ENTRIES+="
|
|
<key>${plist_key}</key>
|
|
<string>${plist_value}</string>"
|
|
APPLIED_SERVICE_ENV_KEYS+="${env_key}|"
|
|
}
|
|
|
|
finalize_plist_env_block() {
|
|
PLIST_ENV_BLOCK=""
|
|
if [[ -n "$PLIST_ENV_ENTRIES" ]]; then
|
|
PLIST_ENV_BLOCK="
|
|
<key>EnvironmentVariables</key>
|
|
<dict>${PLIST_ENV_ENTRIES}
|
|
</dict>"
|
|
fi
|
|
}
|
|
|
|
if [[ -n "$SSL_CERT_ENV_NAME" ]]; then
|
|
append_service_env "$SSL_CERT_ENV_NAME" "$SSL_CERT_ENV_VALUE"
|
|
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 " Agent metrics: $ENABLE_HOST"
|
|
log_info " Docker/Podman: $ENABLE_DOCKER"
|
|
log_info " Kubernetes: $ENABLE_KUBERNETES"
|
|
log_info " Proxmox: $ENABLE_PROXMOX"
|
|
log_info " Pulse command execution: $ENABLE_COMMANDS"
|
|
if [[ "$ENABLE_PROXMOX" == "true" ]]; then
|
|
if [[ -n "$PROXMOX_TYPE" ]]; then
|
|
log_info " Proxmox type: $PROXMOX_TYPE"
|
|
else
|
|
log_info " Proxmox type: auto-detect all installed services"
|
|
fi
|
|
fi
|
|
if [[ "$ENABLE_COMMANDS" == "true" ]]; then
|
|
log_info " Accepts Pulse-scoped command requests on this agent."
|
|
log_info " On Proxmox nodes this is required for opted-in LXC Docker inventory via pct exec."
|
|
log_info " The Pulse server must also be started with PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true."
|
|
else
|
|
log_info " Command execution is off; enable only when Patrol actions or Proxmox LXC Docker inventory are needed."
|
|
fi
|
|
|
|
if [[ "$ENABLE_DOCKER" == "true" ]] && discover_rootless_container_runtime; then
|
|
if [[ "$ROOTLESS_RUNTIME_KIND" == "docker" ]]; then
|
|
if ! service_env_has_key "DOCKER_HOST"; then
|
|
append_service_env "DOCKER_HOST" "$ROOTLESS_RUNTIME_SOCKET_URI"
|
|
if ! service_env_has_key "XDG_RUNTIME_DIR"; then
|
|
append_service_env "XDG_RUNTIME_DIR" "$ROOTLESS_RUNTIME_XDG_DIR"
|
|
fi
|
|
log_info "Using rootless Docker socket for agent service: ${ROOTLESS_RUNTIME_SOCKET_PATH}"
|
|
fi
|
|
elif [[ "$ROOTLESS_RUNTIME_KIND" == "podman" ]]; then
|
|
if ! service_env_has_key "CONTAINER_HOST" && ! service_env_has_key "PODMAN_HOST"; then
|
|
append_service_env "PULSE_DOCKER_RUNTIME" "podman"
|
|
append_service_env "CONTAINER_HOST" "$ROOTLESS_RUNTIME_SOCKET_URI"
|
|
append_service_env "PODMAN_HOST" "$ROOTLESS_RUNTIME_SOCKET_URI"
|
|
if ! service_env_has_key "XDG_RUNTIME_DIR"; then
|
|
append_service_env "XDG_RUNTIME_DIR" "$ROOTLESS_RUNTIME_XDG_DIR"
|
|
fi
|
|
log_info "Using rootless Podman socket for agent service: ${ROOTLESS_RUNTIME_SOCKET_PATH}"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
finalize_plist_env_block
|
|
|
|
# --- Uninstall Logic ---
|
|
if [[ "$UNINSTALL" == "true" ]]; then
|
|
log_info "Uninstalling ${AGENT_NAME} and cleaning up legacy agents..."
|
|
local qnap_state_dir=""
|
|
|
|
# Recover connection details from the canonical installer-owned state artifact
|
|
# if command line input is only partial. Read keys explicitly instead of
|
|
# sourcing the file so uninstall cannot execute persisted shell content.
|
|
if [[ -z "$PULSE_URL" || -z "$PULSE_TOKEN" || -z "$AGENT_ID" || -z "$HOSTNAME_OVERRIDE" || -z "$CURL_CA_BUNDLE" || "$INSECURE" != "true" ]]; then
|
|
local conn_env=""
|
|
conn_env=$(find_connection_state_file || true)
|
|
if [[ -n "$conn_env" ]]; then
|
|
log_info "Recovering connection details from ${conn_env}..."
|
|
recover_connection_state "$conn_env"
|
|
fi
|
|
fi
|
|
|
|
# Try to notify the Pulse server about uninstallation if we have connection details
|
|
# This ensures the agent record is removed and any linked PVE nodes are updated immediately.
|
|
if [[ -n "$PULSE_URL" ]]; then
|
|
# Try to recover agent ID if not provided.
|
|
# Priority: agent-id file (canonical) > hostname API lookup (fallback)
|
|
if [[ -z "$AGENT_ID" ]]; then
|
|
local aid_path=""
|
|
local aid_paths=(/var/lib/pulse-agent/agent-id /boot/config/plugins/pulse-agent/agent-id "$TRUENAS_STATE_DIR/agent-id")
|
|
qnap_state_dir=$(find_qnap_state_dir || true)
|
|
if [[ -n "$qnap_state_dir" ]]; then
|
|
aid_paths+=("$qnap_state_dir/agent-id")
|
|
fi
|
|
|
|
# Primary: canonical agent-id file
|
|
for aid_path in "${aid_paths[@]}"; do
|
|
if [[ -f "$aid_path" ]]; then
|
|
AGENT_ID=$(cat "$aid_path")
|
|
log_info "Recovered agent ID from ${aid_path}"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
if [[ -z "$AGENT_ID" ]]; then
|
|
# API fallback: prefer explicit hostname continuity from the caller,
|
|
# otherwise fall back to the local hostname.
|
|
LOOKUP_HOSTNAME="$HOSTNAME_OVERRIDE"
|
|
if [[ -z "$LOOKUP_HOSTNAME" ]]; then
|
|
LOOKUP_HOSTNAME=$(hostname 2>/dev/null || true)
|
|
fi
|
|
if [[ -n "$LOOKUP_HOSTNAME" ]]; then
|
|
LOOKUP_ARGS=(-fsSL --connect-timeout 5)
|
|
if [[ -n "$PULSE_TOKEN" ]]; then LOOKUP_ARGS+=(-H "X-API-Token: ${PULSE_TOKEN}"); fi
|
|
if [[ "$INSECURE" == "true" ]]; then LOOKUP_ARGS+=(-k); fi
|
|
if [[ -n "$CURL_CA_BUNDLE" ]]; then LOOKUP_ARGS+=(--cacert "$CURL_CA_BUNDLE"); fi
|
|
LOOKUP_HOSTNAME_ESCAPED=$(url_encode "$LOOKUP_HOSTNAME")
|
|
LOOKUP_RESP=$(curl "${LOOKUP_ARGS[@]}" "${PULSE_URL}/api/agents/agent/lookup?hostname=${LOOKUP_HOSTNAME_ESCAPED}" 2>/dev/null || true)
|
|
if [[ -n "$LOOKUP_RESP" ]]; then
|
|
# Extract .agent.id from JSON (portable, no jq dependency)
|
|
AGENT_ID=$(echo "$LOOKUP_RESP" | grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"id"[[:space:]]*:[[:space:]]*"//; s/"$//' || true)
|
|
if [[ -n "$AGENT_ID" ]]; then
|
|
log_info "Recovered agent ID via server lookup: ${AGENT_ID}"
|
|
fi
|
|
fi
|
|
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")
|
|
if [[ -n "$PULSE_TOKEN" ]]; then CURL_ARGS+=(-H "X-API-Token: ${PULSE_TOKEN}"); fi
|
|
if [[ "$INSECURE" == "true" ]]; then CURL_ARGS+=(-k); fi
|
|
if [[ -n "$CURL_CA_BUNDLE" ]]; then CURL_ARGS+=(--cacert "$CURL_CA_BUNDLE"); fi
|
|
|
|
# Send unregistration request (ignore errors as we are uninstalling anyway)
|
|
curl "${CURL_ARGS[@]}" -d "{\"agentId\": \"${AGENT_ID}\"}" "${PULSE_URL}/api/agents/agent/uninstall" >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
|
|
# Kill any running agent processes first.
|
|
# Use -x (exact process name match) to avoid killing THIS uninstall script,
|
|
# whose command line path contains "pulse-agent" (e.g. /boot/config/plugins/pulse-agent/install.sh).
|
|
pkill -x "pulse-agent" 2>/dev/null || true
|
|
# Kill Unraid wrapper scripts — both current (start-pulse-agent.sh) and
|
|
# legacy naming conventions.
|
|
pkill -f "start-pulse-agent.sh" 2>/dev/null || true
|
|
sleep 1
|
|
|
|
# Systemd - unified agent
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
teardown_systemd_agent_service
|
|
fi
|
|
|
|
# Remove legacy binaries
|
|
|
|
# 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
|
|
|
|
# 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"
|
|
|
|
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
|
|
teardown_systemd_agent_service
|
|
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..."
|
|
# Stop running agents and their wrapper scripts.
|
|
pkill -x "pulse-agent" 2>/dev/null || true
|
|
pkill -f "start-pulse-agent.sh" 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 (line-by-line, not range-based,
|
|
# to avoid consuming adjacent non-pulse entries when no trailing
|
|
# blank line separates them).
|
|
sed -i '/^# Pulse Agent$/d' "$GO_SCRIPT" 2>/dev/null || true
|
|
sed -i '/pulse-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}"
|
|
|
|
# Remove log directory
|
|
rm -rf /var/log/pulse
|
|
fi
|
|
|
|
# QNAP QTS/QuTS hero
|
|
qnap_state_dir=$(find_qnap_state_dir || true)
|
|
if [[ -n "$qnap_state_dir" ]] || [[ -f /sbin/getcfg ]] || [[ -f /etc/config/qpkg.conf ]]; then
|
|
log_info "Removing QNAP installation..."
|
|
if [[ -x /etc/init.d/init_disk.sh ]]; then
|
|
if /etc/init.d/init_disk.sh mount_flash_config 2>/dev/null && [[ -d /tmp/nasconfig_tmp ]]; then
|
|
AUTORUN_PATH="/tmp/nasconfig_tmp/autorun.sh"
|
|
if [[ -f "$AUTORUN_PATH" ]]; then
|
|
remove_qnap_autorun_block "$AUTORUN_PATH"
|
|
fi
|
|
/etc/init.d/init_disk.sh umount_flash_config 2>/dev/null || true
|
|
else
|
|
/etc/init.d/init_disk.sh umount_flash_config 2>/dev/null || true
|
|
log_warn "Could not mount QNAP flash config to remove autorun.sh entry."
|
|
fi
|
|
fi
|
|
if [[ -n "$qnap_state_dir" ]]; then
|
|
rm -rf "$qnap_state_dir"
|
|
fi
|
|
fi
|
|
|
|
# TrueNAS SCALE/CORE
|
|
if [[ -d "$TRUENAS_STATE_DIR" ]] || [[ -f /etc/truenas-version ]] || [[ -f /etc/version ]]; then
|
|
if [[ "$(uname -s)" == "Linux" ]]; then
|
|
log_info "Removing TrueNAS SCALE installation..."
|
|
teardown_systemd_agent_service
|
|
elif [[ "$(uname -s)" == "FreeBSD" ]]; then
|
|
log_info "Removing TrueNAS CORE installation..."
|
|
teardown_service_command_agent "/usr/local/etc/rc.d/${AGENT_NAME}"
|
|
fi
|
|
# 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
|
|
teardown_openrc_agent_service
|
|
fi
|
|
|
|
# SysV init (legacy systems like Asustor, older Debian/RHEL, etc.)
|
|
if [[ -f "/etc/init.d/${AGENT_NAME}" ]]; then
|
|
teardown_sysv_agent_service "/etc/init.d/${AGENT_NAME}"
|
|
fi
|
|
|
|
rm -f "${INSTALL_DIR}/${BINARY_NAME}"
|
|
log_info "Uninstallation complete."
|
|
exit 0
|
|
fi
|
|
|
|
# --- Validation ---
|
|
if [[ -z "$PULSE_URL" ]]; then
|
|
fail "Missing required argument: --url (or --pulse-url)" "$EXIT_MISSING_ARGS"
|
|
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
|
|
|
|
auto_enable_insecure_for_plain_http_url
|
|
|
|
# Validate token format when present (should be hex string, typically 64 chars)
|
|
if [[ -n "$PULSE_TOKEN" && ! "$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/CORE Detection ---
|
|
# TrueNAS SCALE/CORE often have immutable root filesystems; /usr/local/bin may be read-only.
|
|
# We store everything in /data which persists across reboots and upgrades.
|
|
is_truenas() {
|
|
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 ]] || [[ -d /etc/ix.rc.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; then
|
|
TRUENAS=true
|
|
INSTALL_DIR="$TRUENAS_STATE_DIR"
|
|
TRUENAS_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
|
|
TRUENAS=true
|
|
INSTALL_DIR="$TRUENAS_STATE_DIR"
|
|
TRUENAS_LOG_FILE="$TRUENAS_LOG_DIR/${AGENT_NAME}.log"
|
|
log_info "Immutable filesystem detected (read-only /usr/local/bin). Using $TRUENAS_STATE_DIR for installation."
|
|
elif [[ "$(uname -s)" == "FreeBSD" ]] && is_truenas; then
|
|
TRUENAS=true
|
|
INSTALL_DIR="$TRUENAS_STATE_DIR"
|
|
log_info "TrueNAS CORE detected (immutable root). Using $TRUENAS_STATE_DIR for installation."
|
|
elif [[ "$(uname -s)" == "FreeBSD" ]] && [[ -d /data ]] && ! is_install_dir_writable; then
|
|
TRUENAS=true
|
|
INSTALL_DIR="$TRUENAS_STATE_DIR"
|
|
log_info "Immutable filesystem detected (read-only /usr/local/bin). Using $TRUENAS_STATE_DIR for installation."
|
|
fi
|
|
|
|
# --- Preflight-Only Mode ---
|
|
if [[ "$PREFLIGHT_ONLY" == "true" ]]; then
|
|
json_event "preflight" "checking" "Running preflight checks"
|
|
|
|
# Check 1: Architecture
|
|
PF_OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
|
PF_ARCH=$(uname -m)
|
|
case "$PF_ARCH" in
|
|
x86_64|amd64) PF_ARCH="amd64" ;;
|
|
aarch64|arm64) PF_ARCH="arm64" ;;
|
|
armv7l|armhf) PF_ARCH="armv7" ;;
|
|
armv6l) PF_ARCH="armv6" ;;
|
|
i386|i686) PF_ARCH="386" ;;
|
|
*) fail "Unsupported architecture: $PF_ARCH" "$EXIT_UNSUPPORTED_ARCH" ;;
|
|
esac
|
|
PF_ARCH_PARAM="${PF_OS}-${PF_ARCH}"
|
|
json_event "preflight" "arch_ok" "Architecture: ${PF_ARCH_PARAM}"
|
|
|
|
# Check 2: Existing agent
|
|
AGENT_STATUS="not_installed"
|
|
if [[ -x "${INSTALL_DIR}/${BINARY_NAME}" ]]; then
|
|
AGENT_STATUS="already_installed"
|
|
elif command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet "${AGENT_NAME}" 2>/dev/null; then
|
|
AGENT_STATUS="already_installed"
|
|
fi
|
|
json_event "preflight" "$AGENT_STATUS" "Agent status: ${AGENT_STATUS}"
|
|
|
|
# Check 3: Pulse URL reachability and agent binary availability
|
|
PREFLIGHT_EXIT="$EXIT_OK"
|
|
if [[ -n "$PULSE_URL" ]]; then
|
|
CURL_TEST_ARGS=(-sf --connect-timeout 5 -o /dev/null)
|
|
if [[ "$INSECURE" == "true" ]]; then CURL_TEST_ARGS+=(-k); fi
|
|
if [[ -n "$CURL_CA_BUNDLE" ]]; then CURL_TEST_ARGS+=(--cacert "$CURL_CA_BUNDLE"); fi
|
|
if curl "${CURL_TEST_ARGS[@]}" "${PULSE_URL}/api/health"; then
|
|
json_event "preflight" "pulse_reachable" "Pulse URL reachable"
|
|
else
|
|
json_event "preflight" "pulse_unreachable" "Pulse URL not reachable" "$EXIT_PREFLIGHT_FAILED"
|
|
PREFLIGHT_EXIT="$EXIT_PREFLIGHT_FAILED"
|
|
fi
|
|
|
|
PREFLIGHT_HEADERS=$(mktemp)
|
|
TMP_FILES+=("$PREFLIGHT_HEADERS")
|
|
CURL_DOWNLOAD_CHECK_ARGS=(-fsSI --connect-timeout 5 --max-time 30 -D "$PREFLIGHT_HEADERS" -o /dev/null)
|
|
if [[ "$INSECURE" == "true" ]]; then CURL_DOWNLOAD_CHECK_ARGS+=(-k); fi
|
|
if [[ -n "$CURL_CA_BUNDLE" ]]; then CURL_DOWNLOAD_CHECK_ARGS+=(--cacert "$CURL_CA_BUNDLE"); fi
|
|
|
|
DOWNLOAD_CHECK_URL="${PULSE_URL}/download/${BINARY_NAME}?arch=${PF_ARCH_PARAM}"
|
|
if curl "${CURL_DOWNLOAD_CHECK_ARGS[@]}" "$DOWNLOAD_CHECK_URL"; then
|
|
PREFLIGHT_EXPECTED_SHA=$(grep -i '^X-Checksum-Sha256:' "$PREFLIGHT_HEADERS" 2>/dev/null | tr -d '\r' | awk '{print $2}' || true)
|
|
if [[ -n "$PREFLIGHT_EXPECTED_SHA" ]]; then
|
|
json_event "preflight" "agent_download_available" "Agent binary available for ${PF_ARCH_PARAM}"
|
|
else
|
|
json_event "preflight" "agent_download_checksum_missing" "Agent binary download did not include checksum header" "$EXIT_CHECKSUM_FAILED"
|
|
PREFLIGHT_EXIT="$EXIT_CHECKSUM_FAILED"
|
|
fi
|
|
else
|
|
json_event "preflight" "agent_download_unavailable" "Agent binary unavailable for ${PF_ARCH_PARAM}" "$EXIT_DOWNLOAD_FAILED"
|
|
PREFLIGHT_EXIT="$EXIT_DOWNLOAD_FAILED"
|
|
fi
|
|
fi
|
|
|
|
# Output summary
|
|
if [[ "$PREFLIGHT_EXIT" -eq 0 ]]; then
|
|
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
|
printf '{"phase":"preflight_complete","code":"ok","message":"Preflight checks passed","exitCode":0,"data":{"arch":"%s-%s","agent_status":"%s"}}\n' \
|
|
"$PF_OS" "$PF_ARCH" "$AGENT_STATUS"
|
|
else
|
|
log_info "Preflight checks passed (arch: ${PF_ARCH_PARAM}, agent: ${AGENT_STATUS})"
|
|
fi
|
|
else
|
|
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
|
printf '{"phase":"preflight_complete","code":"failed","message":"Preflight checks failed","exitCode":%d,"data":{"arch":"%s-%s","agent_status":"%s"}}\n' \
|
|
"$PREFLIGHT_EXIT" "$PF_OS" "$PF_ARCH" "$AGENT_STATUS"
|
|
else
|
|
log_error "Preflight checks failed (arch: ${PF_ARCH_PARAM}, agent: ${AGENT_STATUS})"
|
|
fi
|
|
fi
|
|
exit "$PREFLIGHT_EXIT"
|
|
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" "$EXIT_UNSUPPORTED_ARCH" ;;
|
|
esac
|
|
|
|
# Construct arch param in format expected by download endpoint (e.g., linux-amd64)
|
|
ARCH_PARAM="${OS}-${ARCH}"
|
|
|
|
# Create temp file and register for cleanup
|
|
TMP_BIN=$(mktemp)
|
|
TMP_FILES+=("$TMP_BIN")
|
|
TMP_HEADERS=$(mktemp)
|
|
TMP_FILES+=("$TMP_HEADERS")
|
|
|
|
# Build curl arguments as array for proper quoting
|
|
CURL_ARGS=(-fsSL --connect-timeout 30 --max-time 300 -D "$TMP_HEADERS" -o "$TMP_BIN")
|
|
if [[ "$INSECURE" == "true" ]]; then CURL_ARGS+=(-k); fi
|
|
if [[ -n "$CURL_CA_BUNDLE" ]]; then CURL_ARGS+=(--cacert "$CURL_CA_BUNDLE"); fi
|
|
|
|
SERVER_VERSION=""
|
|
VERSION_CURL_ARGS=(-fsSL --connect-timeout 10 --max-time 30)
|
|
if [[ "$INSECURE" == "true" ]]; then VERSION_CURL_ARGS+=(-k); fi
|
|
if [[ -n "$CURL_CA_BUNDLE" ]]; then VERSION_CURL_ARGS+=(--cacert "$CURL_CA_BUNDLE"); fi
|
|
if server_version_json="$(curl "${VERSION_CURL_ARGS[@]}" "${PULSE_URL}/api/version" 2>/dev/null)"; then
|
|
SERVER_VERSION="$(printf '%s' "$server_version_json" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
|
|
fi
|
|
|
|
DOWNLOAD_QUERY="arch=${ARCH_PARAM}"
|
|
if [[ -n "$SERVER_VERSION" ]]; then
|
|
DOWNLOAD_QUERY="${DOWNLOAD_QUERY}&serverVersion=${SERVER_VERSION}"
|
|
log_info "Pulse server version: ${SERVER_VERSION}"
|
|
fi
|
|
|
|
DOWNLOAD_URL="${PULSE_URL}/download/${BINARY_NAME}?${DOWNLOAD_QUERY}"
|
|
log_info "Downloading agent from ${DOWNLOAD_URL}..."
|
|
|
|
if ! curl "${CURL_ARGS[@]}" "$DOWNLOAD_URL"; then
|
|
fail "Download failed. Check URL and connectivity." "$EXIT_DOWNLOAD_FAILED"
|
|
fi
|
|
|
|
# Verify downloaded binary
|
|
if [[ ! -s "$TMP_BIN" ]]; then
|
|
fail "Downloaded file is empty." "$EXIT_DOWNLOAD_FAILED"
|
|
fi
|
|
|
|
# Check if it's a valid executable (ELF for Linux, Mach-O for macOS)
|
|
if [[ "$OS" == "linux" ]]; then
|
|
MAGIC=$(od -An -tx1 -N4 "$TMP_BIN" 2>/dev/null | tr -d ' \n' || true)
|
|
if [[ "$MAGIC" != "7f454c46" ]]; then
|
|
fail "Downloaded file is not a valid Linux executable." "$EXIT_DOWNLOAD_FAILED"
|
|
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." "$EXIT_DOWNLOAD_FAILED"
|
|
fi
|
|
fi
|
|
|
|
# Release metadata verification
|
|
EXPECTED_SHA=""
|
|
SSH_SIGNATURE_HEADER=""
|
|
EXPECTED_SHA=$(grep -i '^X-Checksum-Sha256:' "$TMP_HEADERS" 2>/dev/null | tr -d '\r' | awk '{print $2}' || true)
|
|
SSH_SIGNATURE_HEADER=$(grep -i '^X-Signature-SSHSIG:' "$TMP_HEADERS" 2>/dev/null | tr -d '\r' | sed 's/^[^:]*:[[:space:]]*//' || true)
|
|
|
|
if [[ -z "$EXPECTED_SHA" ]]; then
|
|
fail "Server did not provide checksum header; refusing install." "$EXIT_CHECKSUM_FAILED"
|
|
fi
|
|
|
|
if has_pinned_installer_signature_key && [[ -z "$SSH_SIGNATURE_HEADER" ]]; then
|
|
fail "Server did not provide SSH signature header; refusing signed install." "$EXIT_SIGNATURE_FAILED"
|
|
fi
|
|
|
|
ACTUAL_SHA=$(sha256sum "$TMP_BIN" 2>/dev/null | awk '{print $1}' || shasum -a 256 "$TMP_BIN" 2>/dev/null | awk '{print $1}')
|
|
if [[ -z "$ACTUAL_SHA" ]]; then
|
|
fail "Could not compute binary checksum." "$EXIT_CHECKSUM_FAILED"
|
|
fi
|
|
if [[ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]]; then
|
|
fail "Checksum verification failed (expected: ${EXPECTED_SHA:0:16}..., got: ${ACTUAL_SHA:0:16}...)" "$EXIT_CHECKSUM_FAILED"
|
|
fi
|
|
json_event "download" "checksum_ok" "Binary checksum verified"
|
|
log_info "Binary checksum verified"
|
|
|
|
verify_download_signature "$TMP_BIN" "$SSH_SIGNATURE_HEADER"
|
|
|
|
chmod +x "$TMP_BIN"
|
|
NEW_VERSION=$("$TMP_BIN" --version 2>/dev/null | head -1 || echo "unknown")
|
|
|
|
if [[ -n "$SERVER_VERSION" && -n "$NEW_VERSION" && "$NEW_VERSION" != "unknown" && "$NEW_VERSION" != "$SERVER_VERSION" ]]; then
|
|
log_warn "Downloaded agent version (${NEW_VERSION}) does not match Pulse server version (${SERVER_VERSION}). Check that Pulse is upgraded and that any reverse proxy is not serving a stale cached binary."
|
|
fi
|
|
|
|
# --- 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")
|
|
|
|
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 through the installer-owned helper.
|
|
stop_existing_agent_service || true
|
|
|
|
# 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
|
|
if [[ "$UPDATE_ONLY" == "true" ]]; then
|
|
fail "No existing Pulse Agent binary found to update. Use the install command instead." "$EXIT_MISSING_ARGS"
|
|
fi
|
|
log_info "Agent service exists but binary is missing. Reinstalling..."
|
|
systemctl stop "${AGENT_NAME}" 2>/dev/null || true
|
|
fi
|
|
|
|
if [[ "$UPDATE_ONLY" == "true" && "$UPGRADE_MODE" != "true" ]]; then
|
|
fail "No existing Pulse Agent installation found to update. Use the install command instead." "$EXIT_MISSING_ARGS"
|
|
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
|
|
|
|
# --- Service Installation ---
|
|
|
|
# 1. macOS (Launchd)
|
|
if [[ "$OS" == "darwin" ]]; then
|
|
PLIST="/Library/LaunchDaemons/com.pulse.agent.plist"
|
|
log_info "Configuring Launchd service at $PLIST..."
|
|
ensure_runtime_token_file "$STATE_DIR"
|
|
clear_proxmox_state_if_needed
|
|
|
|
build_plist_program_arguments "${INSTALL_DIR}/${BINARY_NAME}"
|
|
|
|
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>${PLIST_ENV_BLOCK}
|
|
<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"
|
|
complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f $LOG_FILE"
|
|
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
|
|
ensure_runtime_token_file "$STATE_DIR"
|
|
clear_proxmox_state_if_needed
|
|
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+)..."
|
|
|
|
render_systemd_agent_unit "$UNIT" "${INSTALL_DIR}/${BINARY_NAME}" "${EXEC_ARGS}" "network.target" "" "" ""
|
|
restart_systemd_agent_service
|
|
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${UPSTART_ENV_LINES}
|
|
|
|
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
|
|
|
|
complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f $LOG_FILE"
|
|
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"
|
|
|
|
STATE_DIR="$UNRAID_STORAGE_DIR"
|
|
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)
|
|
ensure_runtime_token_file "$STATE_DIR"
|
|
clear_proxmox_state_if_needed
|
|
build_exec_args
|
|
build_exec_args_array
|
|
|
|
# Kill any existing pulse agents.
|
|
log_info "Stopping any existing pulse agents..."
|
|
# Use process name matching to avoid killing unrelated processes
|
|
pkill -f "^${RUNTIME_BINARY}" 2>/dev/null || true
|
|
sleep 2
|
|
|
|
# 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
|
|
EXPORT_SERVICE_ENV="$SHELL_EXPORT_LINES"
|
|
|
|
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
|
|
sleep 2
|
|
|
|
# Copy binary from persistent storage to RAM disk (needed after reboot)
|
|
cp "${UNRAID_STORED_BINARY}" "${RUNTIME_BINARY}"
|
|
chmod +x "${RUNTIME_BINARY}"${EXPORT_SERVICE_ENV}
|
|
|
|
# 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 (line-by-line, not range-based)
|
|
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
|
|
|
|
complete_installation_flow "$UNRAID_STORAGE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent is running." "tail -f /var/log/${AGENT_NAME}.log"
|
|
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
|
|
|
|
# 3b. QNAP QTS/QuTS hero (ephemeral boot config; autorun.sh executes before the
|
|
# encrypted data volume is always ready, so boot persistence must wait for the
|
|
# canonical persistent wrapper on the data volume).
|
|
if [[ -f /sbin/getcfg ]] || [[ -f /etc/config/qpkg.conf ]]; then
|
|
log_info "Detected QNAP QTS/QuTS hero system..."
|
|
|
|
QNAP_VOL=$(detect_qnap_data_volume || true)
|
|
if [[ -z "$QNAP_VOL" ]]; then
|
|
fail "Could not find a writable QNAP data volume. Is a storage volume configured?"
|
|
fi
|
|
|
|
STATE_DIR="${QNAP_VOL}/.pulse-agent"
|
|
QNAP_STORED_BINARY="${STATE_DIR}/${BINARY_NAME}"
|
|
RUNTIME_BINARY="${INSTALL_DIR}/${BINARY_NAME}"
|
|
WRAPPER_SCRIPT="${STATE_DIR}/start-pulse-agent.sh"
|
|
|
|
mkdir -p "$STATE_DIR"
|
|
|
|
# Copy binary to persistent storage and keep the runtime copy executable.
|
|
cp "${RUNTIME_BINARY}" "$QNAP_STORED_BINARY"
|
|
chmod +x "$QNAP_STORED_BINARY"
|
|
chmod +x "$RUNTIME_BINARY"
|
|
|
|
log_info "Installed binary to ${QNAP_STORED_BINARY} (persistent) and ${RUNTIME_BINARY} (runtime)..."
|
|
|
|
ensure_runtime_token_file "$STATE_DIR"
|
|
clear_proxmox_state_if_needed
|
|
build_exec_args
|
|
|
|
log_info "Stopping any existing pulse agents..."
|
|
pkill -x "pulse-agent" 2>/dev/null || true
|
|
pkill -f "start-pulse-agent.sh" 2>/dev/null || true
|
|
sleep 2
|
|
|
|
write_qnap_wrapper_script "$WRAPPER_SCRIPT" "$RUNTIME_BINARY" "$QNAP_STORED_BINARY"
|
|
|
|
AUTORUN_CONFIGURED=false
|
|
if [[ -x /etc/init.d/init_disk.sh ]]; then
|
|
if /etc/init.d/init_disk.sh mount_flash_config 2>/dev/null && [[ -d /tmp/nasconfig_tmp ]]; then
|
|
AUTORUN_PATH="/tmp/nasconfig_tmp/autorun.sh"
|
|
append_qnap_autorun_block "$AUTORUN_PATH" "$WRAPPER_SCRIPT" "$STATE_DIR"
|
|
/etc/init.d/init_disk.sh umount_flash_config 2>/dev/null || true
|
|
AUTORUN_CONFIGURED=true
|
|
log_info "Configured autorun.sh with a deferred Pulse Agent bootstrap."
|
|
else
|
|
/etc/init.d/init_disk.sh umount_flash_config 2>/dev/null || true
|
|
fi
|
|
fi
|
|
|
|
if [[ "$AUTORUN_CONFIGURED" != true ]]; then
|
|
log_warn "Could not configure autorun.sh automatically."
|
|
log_warn "To persist across reboots, add a block to autorun.sh that waits for ${WRAPPER_SCRIPT} and then launches it."
|
|
log_warn "See: https://wiki.qnap.com/wiki/Running_Your_Own_Application_at_Startup"
|
|
fi
|
|
|
|
log_info "Starting agent with QNAP watchdog..."
|
|
sh "${WRAPPER_SCRIPT}" >> "/var/log/${AGENT_NAME}.log" 2>&1 &
|
|
disown 2>/dev/null || true
|
|
|
|
complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent is running." "tail -f /var/log/${AGENT_NAME}.log"
|
|
log_info "Persistent state: $STATE_DIR"
|
|
if [[ "$AUTORUN_CONFIGURED" == true ]]; then
|
|
log_info "The agent will start automatically after the QNAP data volume becomes available."
|
|
log_info "IMPORTANT: Ensure 'Run user defined startup processes (autorun.sh)' is enabled"
|
|
log_info " in QNAP Control Panel > Hardware > General."
|
|
fi
|
|
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/CORE (immutable root, uses systemd on SCALE and rc.d on CORE)
|
|
# TrueNAS can wipe service registration files 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. We try multiple runtime locations.
|
|
if [[ "$TRUENAS" == true ]]; then
|
|
log_info "Configuring TrueNAS SCALE/CORE installation..."
|
|
STATE_DIR="$TRUENAS_STATE_DIR"
|
|
|
|
# Stop any existing agent before we modify binaries
|
|
# The runtime binary may be in /root/bin or /var/tmp, not just INSTALL_DIR
|
|
if [[ "$(uname -s)" == "Linux" ]]; 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 [[ "$(uname -s)" == "FreeBSD" ]]; then
|
|
if service "${AGENT_NAME}" status >/dev/null 2>&1; then
|
|
log_info "Stopping existing ${AGENT_NAME} service..."
|
|
service "${AGENT_NAME}" stop 2>/dev/null || true
|
|
sleep 2
|
|
fi
|
|
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
|
|
ensure_runtime_token_file "$STATE_DIR"
|
|
clear_proxmox_state_if_needed
|
|
build_exec_args
|
|
|
|
# Store service file in /data (persists across upgrades)
|
|
TRUENAS_SERVICE_STORAGE="$TRUENAS_STATE_DIR/${AGENT_NAME}.service"
|
|
|
|
if [[ "$(uname -s)" == "Linux" ]]; then
|
|
TRUENAS_LOG_TARGET="$LOG_FILE"
|
|
if [[ -n "$TRUENAS_LOG_FILE" ]]; then
|
|
TRUENAS_LOG_TARGET="$TRUENAS_LOG_FILE"
|
|
fi
|
|
|
|
render_systemd_agent_unit "$TRUENAS_SERVICE_STORAGE" "${TRUENAS_RUNTIME_BINARY}" "${EXEC_ARGS}" "network-online.target docker.service" "network-online.target" "root" "${TRUENAS_LOG_TARGET}"
|
|
elif [[ "$(uname -s)" == "FreeBSD" ]]; then
|
|
render_freebsd_rc_agent_script "$TRUENAS_SERVICE_STORAGE" "${TRUENAS_RUNTIME_BINARY}" "${EXEC_ARGS}"
|
|
fi
|
|
|
|
# Store environment/config for reference
|
|
cat > "$TRUENAS_ENV_FILE" <<EOF
|
|
# Pulse Agent configuration (for reference)
|
|
PULSE_URL=${PULSE_URL}
|
|
PULSE_TOKEN_FILE=${RUNTIME_TOKEN_FILE}
|
|
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 service symlink.
|
|
write_truenas_bootstrap_script "$(uname -s)"
|
|
|
|
# Create systemd/rc.d symlink now
|
|
if [[ "$(uname -s)" == "Linux" ]]; then
|
|
SYSTEMD_LINK="/etc/systemd/system/${AGENT_NAME}.service"
|
|
ln -sf "$TRUENAS_SERVICE_STORAGE" "$SYSTEMD_LINK"
|
|
elif [[ "$(uname -s)" == "FreeBSD" ]]; then
|
|
RCSCRIPT_LINK="/usr/local/etc/rc.d/${AGENT_NAME}"
|
|
ln -sf "$TRUENAS_SERVICE_STORAGE" "$RCSCRIPT_LINK"
|
|
fi
|
|
|
|
# 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
|
|
if [[ "$(uname -s)" == "Linux" ]]; then
|
|
restart_systemd_agent_service
|
|
elif [[ "$(uname -s)" == "FreeBSD" ]]; then
|
|
ensure_freebsd_agent_enabled
|
|
restart_service_command_agent
|
|
fi
|
|
|
|
complete_installation_flow "$TRUENAS_STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent is running." ""
|
|
log_info "Binary: $TRUENAS_STORED_BINARY (persistent)"
|
|
log_info "Runtime: $TRUENAS_RUNTIME_BINARY (for execution)"
|
|
if [[ "$(uname -s)" == "Linux" ]]; then
|
|
log_info "Service: $TRUENAS_SERVICE_STORAGE (symlinked to systemd)"
|
|
log_info "Logs: tail -f ${TRUENAS_LOG_FILE}"
|
|
elif [[ "$(uname -s)" == "FreeBSD" ]]; then
|
|
log_info "Service: $TRUENAS_SERVICE_STORAGE (symlinked to rc.d)"
|
|
log_info "Logs: tail -f /var/log/messages"
|
|
fi
|
|
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
|
|
ensure_runtime_token_file "$STATE_DIR"
|
|
clear_proxmox_state_if_needed
|
|
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"
|
|
SSL_CERT_FILE_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"
|
|
sed -i "s|SSL_CERT_FILE_PLACEHOLDER|${SED_EXPORT_LINES}|g" "$INITSCRIPT"
|
|
|
|
chmod +x "$INITSCRIPT"
|
|
restart_openrc_agent_service
|
|
complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f $LOG_FILE"
|
|
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
|
|
ensure_runtime_token_file "$STATE_DIR"
|
|
clear_proxmox_state_if_needed
|
|
build_exec_args
|
|
|
|
render_freebsd_rc_agent_script "$RCSCRIPT" "${INSTALL_DIR}/${BINARY_NAME}" "${EXEC_ARGS}"
|
|
|
|
# Enable the service in rc.conf
|
|
ensure_freebsd_agent_enabled
|
|
|
|
# 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
|
|
restart_sysv_agent_service "$RCSCRIPT"
|
|
complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f /var/log/messages"
|
|
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="$STATE_DIR"
|
|
TOKEN_FILE="${TOKEN_DIR}/token"
|
|
log_info "Configuring Systemd service at $UNIT..."
|
|
|
|
ensure_runtime_token_file "$STATE_DIR"
|
|
clear_proxmox_state_if_needed
|
|
|
|
# Build command line args with --token-file instead of the raw token.
|
|
build_exec_args
|
|
|
|
render_systemd_agent_unit "$UNIT" "${INSTALL_DIR}/${BINARY_NAME}" "${EXEC_ARGS}" "network-online.target docker.service" "network-online.target" "root" ""
|
|
# 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
|
|
|
|
restart_systemd_agent_service
|
|
complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "journalctl -u ${AGENT_NAME} --no-pager -n 20"
|
|
if [[ "$UPGRADE_MODE" != "true" && -n "$PULSE_TOKEN" ]]; then
|
|
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
|
|
ensure_runtime_token_file "$STATE_DIR"
|
|
clear_proxmox_state_if_needed
|
|
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"
|
|
SSL_CERT_FILE_PLACEHOLDER
|
|
|
|
# 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"
|
|
sed -i "s|SSL_CERT_FILE_PLACEHOLDER|${SED_EXPORT_LINES}|g" "$INITSCRIPT"
|
|
|
|
chmod +x "$INITSCRIPT"
|
|
|
|
enable_sysv_agent_service "$INITSCRIPT"
|
|
|
|
# Stop existing agent if running
|
|
"$INITSCRIPT" stop 2>/dev/null || true
|
|
sleep 1
|
|
|
|
# Start the agent
|
|
"$INITSCRIPT" start
|
|
complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f /var/log/${AGENT_NAME}.log"
|
|
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 "$@"
|