diff --git a/docs/HOST_AGENT.md b/docs/HOST_AGENT.md index c8f2f14b0..e1f543e37 100644 --- a/docs/HOST_AGENT.md +++ b/docs/HOST_AGENT.md @@ -74,6 +74,15 @@ curl -fsSL http://pulse.example.local:7655/install-host-agent.sh | \ bash -s -- --url http://pulse.example.local:7655 --token ``` +- **TrueNAS SCALE**: Use `--platform truenas` (or rely on auto-detect) so the installer writes everything to `/data/pulse-host-agent`, installs a systemd unit from that location, and registers a POSTINIT Init/Shutdown task that re-links and restarts the service on boot. + +```bash +curl -fsSL http://pulse.example.local:7655/install-host-agent.sh | \ + bash -s -- --platform truenas --url http://pulse.example.local:7655 --token +``` + +The TrueNAS flow stores the binary, service unit, and logs under `/data/pulse-host-agent` and creates a POSTINIT entry in **System Settings → Advanced → Init/Shutdown Scripts** pointing at `/data/pulse-host-agent/bootstrap-pulse-host-agent.sh`. Uninstall with `/uninstall-host-agent.sh`; the script removes the init task and the persistent directory. SATA HDD temperatures are not collected yet; CPU/NVMe/GPU sensors continue to report when lm-sensors is available. + - On systemd machines the script installs the binary, wires up `/etc/systemd/system/pulse-host-agent.service`, enables it, and tails the registration status. - On Unraid hosts it starts the agent under `nohup`, creates `/var/log/pulse`, and (optionally) inserts the auto-start line into `/boot/config/go`. - On minimalist distros without systemd (e.g. Alpine) it creates/updates `/etc/rc.local`, adds the background runner, and verifies it launches. diff --git a/frontend-modern/public/install-host-agent.sh b/frontend-modern/public/install-host-agent.sh index cdc59b034..75cfbbb56 100755 --- a/frontend-modern/public/install-host-agent.sh +++ b/frontend-modern/public/install-host-agent.sh @@ -67,6 +67,11 @@ INTERVAL="30s" UNINSTALL="false" PLATFORM="" FORCE=false +KEYCHAIN_ENABLED=true +KEYCHAIN_OPT_OUT=false +KEYCHAIN_OPT_OUT_REASON="" +USE_KEYCHAIN=false +AGENT_ID="${PULSE_AGENT_ID:-}" while [[ $# -gt 0 ]]; do case "$1" in @@ -82,6 +87,10 @@ while [[ $# -gt 0 ]]; do INTERVAL="$2" shift 2 ;; + --agent-id) + AGENT_ID="$2" + shift 2 + ;; --platform) PLATFORM="$2" shift 2 @@ -94,6 +103,12 @@ while [[ $# -gt 0 ]]; do FORCE=true shift ;; + --no-keychain) + KEYCHAIN_ENABLED=false + KEYCHAIN_OPT_OUT=true + KEYCHAIN_OPT_OUT_REASON="flag" + shift + ;; *) echo "Unknown option: $1" exit 1 @@ -119,6 +134,15 @@ if [[ -f "$UNRAID_GO_FILE" ]] || [[ -f /etc/unraid-version ]]; then UNRAID=true fi +TRUENAS=false +TRUENAS_STATE_DIR="/data/pulse-host-agent" +TRUENAS_LOG_DIR="$TRUENAS_STATE_DIR/logs" +TRUENAS_SERVICE_STORAGE="$TRUENAS_STATE_DIR/pulse-host-agent.service" +TRUENAS_BOOTSTRAP_SCRIPT="$TRUENAS_STATE_DIR/bootstrap-pulse-host-agent.sh" +TRUENAS_ENV_FILE="$TRUENAS_STATE_DIR/pulse-host-agent.env" +TRUENAS_INIT_COMMENT="Pulse host agent bootstrap" +TRUENAS_SYSTEMD_LINK="/etc/systemd/system/pulse-host-agent.service" + # Uninstall function if [[ "$UNINSTALL" == "true" ]]; then log_warn "The --uninstall flag is deprecated." @@ -136,6 +160,10 @@ fi print_header +if [[ "$FORCE" == true ]]; then + log_warn "--force enabled: skipping interactive confirmations and accepting secure defaults." +fi + # Interactive prompts if parameters not provided (unless --force is used) if [[ -z "$PULSE_URL" ]]; then if [[ "$FORCE" == false ]]; then @@ -148,7 +176,11 @@ fi if [[ -z "$PULSE_URL" ]]; then log_error "Pulse URL is required" - echo "Usage: $0 --url --token [--interval 30s] [--platform linux|darwin|windows] [--force]" + echo "Usage: $0 --url --token [--interval 30s] [--agent-id ] [--platform linux|darwin|windows|truenas] [--force] [--no-keychain]" + echo "" + echo " --force Skip interactive prompts and accept secure defaults (including Keychain storage)." + echo " --agent-id Override the identifier used to deduplicate hosts (defaults to machine-id)." + echo " --no-keychain Disable Keychain storage and embed the token in the launch agent plist instead." exit 1 fi @@ -165,6 +197,19 @@ if [[ -z "$PULSE_TOKEN" ]] && [[ "$FORCE" == false ]]; then fi fi +is_truenas_scale() { + if [[ -f /etc/truenas-version ]]; then + return 0 + fi + if [[ -f /etc/version ]] && grep -qi "truenas" /etc/version 2>/dev/null; then + return 0 + fi + if [[ -d /data/ix-applications ]] || [[ -d /etc/ix-apps.d ]]; then + return 0 + fi + return 1 +} + # Detect platform if not specified if [[ -z "$PLATFORM" ]]; then case "$(uname -s)" in @@ -183,6 +228,11 @@ if [[ -z "$PLATFORM" ]]; then ;; esac fi +PLATFORM=$(echo "$PLATFORM" | tr '[:upper:]' '[:lower:]') +if [[ "$PLATFORM" == "truenas" ]]; then + PLATFORM="linux" + TRUENAS=true +fi # Detect architecture ARCH="$(uname -m)" @@ -196,12 +246,31 @@ case "$ARCH" in armv7l|armhf) ARCH="armv7" ;; - *) - log_warn "Unknown architecture $ARCH, defaulting to amd64" - ARCH="amd64" + armv6l) + ARCH="armv6" ;; + i386|i686) + ARCH="386" + ;; + *) + log_warn "Unknown architecture $ARCH, defaulting to amd64" + ARCH="amd64" + ;; esac +if [[ "$PLATFORM" == "linux" && "$TRUENAS" == false ]]; then + if is_truenas_scale; then + TRUENAS=true + fi +fi + +if [[ "$TRUENAS" == true ]]; then + AGENT_PATH="$TRUENAS_STATE_DIR/pulse-host-agent" + SYSTEMD_SERVICE="$TRUENAS_SYSTEMD_LINK" + LINUX_LOG_DIR="$TRUENAS_LOG_DIR" + LINUX_LOG_FILE="$LINUX_LOG_DIR/host-agent.log" +fi + log_info "Configuration:" echo " Pulse URL: $PULSE_URL" if [[ -n "$PULSE_TOKEN" ]]; then @@ -211,8 +280,16 @@ if [[ -n "$PULSE_TOKEN" ]]; then else echo " Token: none" fi +if [[ -n "$AGENT_ID" ]]; then + echo " Agent ID: $AGENT_ID" +else + echo " Agent ID: machine-id (default)" +fi echo " Interval: $INTERVAL" echo " Platform: $PLATFORM/$ARCH" +if [[ "$TRUENAS" == true ]]; then + echo " TrueNAS SCALE mode: enabled (immutable root detected)" +fi echo "" log_info "Installing Pulse host agent for $PLATFORM/$ARCH..." @@ -363,8 +440,18 @@ else log_info "Checksum not available (server doesn't provide it yet)" fi -sudo mv "$TEMP_BINARY" "$AGENT_PATH" -sudo chmod +x "$AGENT_PATH" +# Use install command instead of mv to ensure correct SELinux context +# The install command creates a new file with the correct label for the target directory +sudo install -D -m 0755 "$TEMP_BINARY" "$AGENT_PATH" +rm -f "$TEMP_BINARY" + +# On SELinux systems, explicitly restore context to ensure policy compliance +if command -v selinuxenabled &> /dev/null && selinuxenabled 2>/dev/null; then + if command -v restorecon &> /dev/null; then + sudo restorecon -F "$AGENT_PATH" 2>/dev/null || true + fi +fi + log_success "Agent binary installed to $AGENT_PATH" # Build reusable agent command strings @@ -373,11 +460,156 @@ if [[ -n "$PULSE_TOKEN" ]]; then AGENT_CMD="$AGENT_CMD --token $PULSE_TOKEN" fi AGENT_CMD="$AGENT_CMD --interval $INTERVAL" +if [[ -n "$AGENT_ID" ]]; then + AGENT_CMD="$AGENT_CMD --agent-id $AGENT_ID" +fi MANUAL_START_CMD="$AGENT_CMD" MANUAL_START_WRAPPED="nohup $MANUAL_START_CMD >$LINUX_LOG_FILE 2>&1 &" +write_truenas_env_file() { + sudo install -d -m 0700 "$TRUENAS_STATE_DIR" "$TRUENAS_LOG_DIR" + local tmp_env + tmp_env=$(mktemp) + { + echo "PULSE_URL=$PULSE_URL" + echo "PULSE_INTERVAL=$INTERVAL" + echo "PULSE_LOG_FILE=$LINUX_LOG_FILE" + if [[ -n "$PULSE_TOKEN" ]]; then + echo "PULSE_TOKEN=$PULSE_TOKEN" + fi + if [[ -n "$AGENT_ID" ]]; then + echo "PULSE_AGENT_ID=$AGENT_ID" + fi + } > "$tmp_env" + sudo install -m 0600 "$tmp_env" "$TRUENAS_ENV_FILE" + rm -f "$tmp_env" +} + +write_truenas_service_unit() { + local exec_start="$AGENT_PATH --url \$PULSE_URL --interval \$PULSE_INTERVAL" + if [[ -n "$PULSE_TOKEN" ]]; then + exec_start="$exec_start --token \$PULSE_TOKEN" + fi + if [[ -n "$AGENT_ID" ]]; then + exec_start="$exec_start --agent-id \$PULSE_AGENT_ID" + fi + + sudo tee "$TRUENAS_SERVICE_STORAGE" > /dev/null < /dev/null </dev/null 2>&1; then + systemctl restart pulse-host-agent >/dev/null 2>&1 || true +else + systemctl enable --now pulse-host-agent >/dev/null 2>&1 || true +fi +EOF + sudo chmod 0755 "$TRUENAS_BOOTSTRAP_SCRIPT" +} + +register_truenas_init_task() { + if ! command -v midclt >/dev/null 2>&1; then + log_warn "midclt not found - add a POSTINIT task for $TRUENAS_BOOTSTRAP_SCRIPT manually in the TrueNAS UI." + return + fi + if ! command -v python3 >/dev/null 2>&1; then + log_warn "python3 not found - cannot parse init task state; add the POSTINIT task manually if needed." + return + fi + + local query existing_id payload + query='[["script","=","'"$TRUENAS_BOOTSTRAP_SCRIPT"'"]]' + local query_output + query_output=$(midclt call initshutdownscript.query "$query" 2>/dev/null || true) + existing_id=$(printf '%s' "$query_output" | python3 - <<'PY' +import json, sys +try: + data = json.load(sys.stdin) + print(data[0]["id"] if data else "") +except Exception: + print("") +PY +) + + payload=$(cat </dev/null 2>&1; then + log_info "Updated existing TrueNAS init task (id $existing_id)" + else + log_warn "Failed to update existing TrueNAS init task (id $existing_id)" + fi + else + if midclt call initshutdownscript.create "$payload" >/dev/null 2>&1; then + log_success "Registered TrueNAS init task to restore the service on boot" + else + log_warn "Failed to register TrueNAS init task; add it manually via System Settings → Advanced → Init/Shutdown Scripts." + fi + fi +} + +setup_truenas_service() { + log_info "Detected TrueNAS SCALE (immutable root). Storing agent under $TRUENAS_STATE_DIR" + write_truenas_env_file + write_truenas_service_unit + write_truenas_bootstrap_script + + sudo ln -sf "$TRUENAS_SERVICE_STORAGE" "$TRUENAS_SYSTEMD_LINK" + sudo systemctl daemon-reload + if sudo systemctl is-enabled pulse-host-agent >/dev/null 2>&1; then + sudo systemctl restart pulse-host-agent || true + else + sudo systemctl enable --now pulse-host-agent || true + fi + + register_truenas_init_task + SERVICE_MODE="truenas" + log_success "TrueNAS SCALE service installed and started" +} + # Set up service based on platform -if [[ "$PLATFORM" == "linux" ]] && command -v systemctl &> /dev/null; then +if [[ "$TRUENAS" == true ]]; then + setup_truenas_service +elif [[ "$PLATFORM" == "linux" ]] && command -v systemctl &> /dev/null; then log_info "Setting up systemd service..." # Create log directory @@ -416,8 +648,32 @@ elif [[ "$PLATFORM" == "darwin" ]] && command -v launchctl &> /dev/null; then mkdir -p "$MACOS_LOG_DIR" mkdir -p "$HOME/Library/LaunchAgents" + if [[ -n "$PULSE_TOKEN" && "$KEYCHAIN_ENABLED" == true && "$FORCE" == false ]]; then + echo "" + log_info "It is recommended to store the token in your Keychain so it never lands on disk." + KEYCHAIN_PROMPTED=false + if [[ -t 0 ]]; then + read -r -p "Store the token in the macOS Keychain? [Y/n]: " KEYCHAIN_RESPONSE + KEYCHAIN_PROMPTED=true + elif [[ -r /dev/tty ]]; then + read -r -p "Store the token in the macOS Keychain? [Y/n]: " KEYCHAIN_RESPONSE /dev/null; then KEYCHAIN_APPS=( "/usr/local/bin/pulse-host-agent" - "/usr/local/bin/pulse-host-agent-wrapper.sh" "/usr/bin/security" ) KEYCHAIN_ARGS=() for app in "${KEYCHAIN_APPS[@]}"; do - KEYCHAIN_ARGS+=(-T "$app") + if [[ -e "$app" ]]; then + KEYCHAIN_ARGS+=(-T "$app") + fi done if security add-generic-password \ @@ -456,10 +713,27 @@ elif [[ "$PLATFORM" == "darwin" ]] && command -v launchctl &> /dev/null; then log_info "You may need to grant Keychain access permissions" USE_KEYCHAIN=false fi + elif [[ -n "$PULSE_TOKEN" ]]; then + if [[ "$KEYCHAIN_OPT_OUT" == true ]]; then + if [[ "$KEYCHAIN_OPT_OUT_REASON" == "flag" ]]; then + log_warn "Keychain storage disabled via --no-keychain; token will be embedded in the launchd plist." + elif [[ "$KEYCHAIN_OPT_OUT_REASON" == "prompt" ]]; then + log_warn "Keychain storage skipped at user prompt; token will be embedded in the launchd plist." + fi + else + log_warn "Keychain storage disabled; token will be embedded in the launchd plist." + fi + USE_KEYCHAIN=false else USE_KEYCHAIN=false fi + LAUNCHD_AGENT_ID_ARGS="" + if [[ -n "$AGENT_ID" ]]; then + LAUNCHD_AGENT_ID_ARGS=" --agent-id + $AGENT_ID" + fi + # Create wrapper script if using Keychain if [[ "$USE_KEYCHAIN" == true ]]; then WRAPPER_SCRIPT="/usr/local/bin/pulse-host-agent-wrapper.sh" @@ -519,6 +793,7 @@ WRAPPER_EOF $PULSE_URL --interval $INTERVAL +$LAUNCHD_AGENT_ID_ARGS RunAtLoad @@ -550,6 +825,7 @@ EOF $PULSE_TOKEN --interval $INTERVAL +$LAUNCHD_AGENT_ID_ARGS RunAtLoad @@ -720,7 +996,7 @@ VALIDATION_SUCCESS=false SERVICE_RUNNING=false # Check if service is running -if [[ "$SERVICE_MODE" == "systemd" ]]; then +if [[ "$SERVICE_MODE" == "systemd" || "$SERVICE_MODE" == "truenas" ]]; then SERVICE_STATUS=$(systemctl is-active pulse-host-agent 2>/dev/null || echo "inactive") if [[ "$SERVICE_STATUS" == "active" ]]; then SERVICE_RUNNING=true @@ -811,10 +1087,13 @@ else echo "" log_info "Troubleshooting:" echo "" - if [[ "$SERVICE_MODE" == "systemd" ]]; then + if [[ "$SERVICE_MODE" == "systemd" || "$SERVICE_MODE" == "truenas" ]]; then echo " View logs: sudo journalctl -u pulse-host-agent -f" echo " Check status: sudo systemctl status pulse-host-agent" echo " Restart: sudo systemctl restart pulse-host-agent" + if [[ "$SERVICE_MODE" == "truenas" ]]; then + echo " Persist: Confirm the POSTINIT task named \"$TRUENAS_INIT_COMMENT\" exists in the TrueNAS UI" + fi elif [[ "$SERVICE_MODE" == "launchd" ]]; then echo " View logs: tail -f $MACOS_LOG_FILE" echo " Check status: launchctl list | grep pulse" @@ -839,12 +1118,15 @@ fi print_footer log_info "Service Management Commands:" -if [[ "$SERVICE_MODE" == "systemd" ]]; then +if [[ "$SERVICE_MODE" == "systemd" || "$SERVICE_MODE" == "truenas" ]]; then echo " Start: sudo systemctl start pulse-host-agent" echo " Stop: sudo systemctl stop pulse-host-agent" echo " Restart: sudo systemctl restart pulse-host-agent" echo " Status: sudo systemctl status pulse-host-agent" echo " Logs: sudo journalctl -u pulse-host-agent -f" + if [[ "$SERVICE_MODE" == "truenas" ]]; then + echo " Persist: TrueNAS Init/Shutdown task stores $TRUENAS_BOOTSTRAP_SCRIPT as POSTINIT" + fi elif [[ "$SERVICE_MODE" == "launchd" ]]; then echo " Start: launchctl load $LAUNCHD_PLIST" echo " Stop: launchctl unload $LAUNCHD_PLIST" diff --git a/frontend-modern/public/uninstall-host-agent.sh b/frontend-modern/public/uninstall-host-agent.sh index d394c2e93..bc9a80d95 100755 --- a/frontend-modern/public/uninstall-host-agent.sh +++ b/frontend-modern/public/uninstall-host-agent.sh @@ -66,6 +66,15 @@ SYSTEMD_SERVICE="/etc/systemd/system/pulse-host-agent.service" LAUNCHD_PLIST="$HOME/Library/LaunchAgents/com.pulse.host-agent.plist" MACOS_LOG_DIR="$HOME/Library/Logs/Pulse" LINUX_LOG_DIR="/var/log/pulse" +LINUX_LOG_FILE="$LINUX_LOG_DIR/host-agent.log" + +TRUENAS=false +TRUENAS_STATE_DIR="/data/pulse-host-agent" +TRUENAS_LOG_DIR="$TRUENAS_STATE_DIR/logs" +TRUENAS_SERVICE_STORAGE="$TRUENAS_STATE_DIR/pulse-host-agent.service" +TRUENAS_BOOTSTRAP_SCRIPT="$TRUENAS_STATE_DIR/bootstrap-pulse-host-agent.sh" +TRUENAS_ENV_FILE="$TRUENAS_STATE_DIR/pulse-host-agent.env" +TRUENAS_SYSTEMD_LINK="/etc/systemd/system/pulse-host-agent.service" print_header @@ -83,9 +92,63 @@ case "$(uname -s)" in ;; esac +is_truenas_scale() { + if [[ -f /etc/truenas-version ]]; then + return 0 + fi + if [[ -f /etc/version ]] && grep -qi "truenas" /etc/version 2>/dev/null; then + return 0 + fi + if [[ -d /data/ix-applications ]] || [[ -d /etc/ix-apps.d ]]; then + return 0 + fi + return 1 +} + +if [[ "$PLATFORM" == "linux" ]] && is_truenas_scale; then + TRUENAS=true + AGENT_PATH="$TRUENAS_STATE_DIR/pulse-host-agent" + SYSTEMD_SERVICE="$TRUENAS_SYSTEMD_LINK" + LINUX_LOG_DIR="$TRUENAS_LOG_DIR" + LINUX_LOG_FILE="$LINUX_LOG_DIR/host-agent.log" +fi + log_info "Detected platform: $PLATFORM" +if [[ "$TRUENAS" == true ]]; then + log_info "TrueNAS SCALE detected (immutable root). Using $TRUENAS_STATE_DIR for cleanup." +fi echo "" +remove_truenas_init_task() { + if [[ "$TRUENAS" != true ]]; then + return + fi + if ! command -v midclt >/dev/null 2>&1 || ! command -v python3 >/dev/null 2>&1; then + log_warn "midclt/python3 not available - remove the POSTINIT task for $TRUENAS_BOOTSTRAP_SCRIPT manually if it exists." + return + fi + + local query_output task_id + query_output=$(midclt call initshutdownscript.query '[["script","=","'"$TRUENAS_BOOTSTRAP_SCRIPT"'"]]' 2>/dev/null || true) + task_id=$(printf '%s' "$query_output" | python3 - <<'PY' +import json, sys +try: + data = json.load(sys.stdin) + print(data[0]["id"] if data else "") +except Exception: + print("") +PY +) + + if [[ -n "$task_id" ]]; then + if midclt call initshutdownscript.delete "$task_id" >/dev/null 2>&1; then + log_success "Removed TrueNAS Init/Shutdown task (id $task_id)" + else + log_warn "Failed to remove TrueNAS Init/Shutdown task id $task_id; remove it manually in the TrueNAS UI." + fi + fi +} + # Stop and remove systemd service (Linux) if [[ "$PLATFORM" == "linux" ]]; then if [[ -f "$SYSTEMD_SERVICE" ]] && command -v systemctl &> /dev/null; then @@ -115,12 +178,35 @@ if [[ "$PLATFORM" == "linux" ]]; then log_success "Processes terminated" fi + if [[ "$TRUENAS" == true ]]; then + remove_truenas_init_task + + if [[ -f "$TRUENAS_SERVICE_STORAGE" ]]; then + log_info "Removing stored TrueNAS service unit..." + sudo rm -f "$TRUENAS_SERVICE_STORAGE" + fi + if [[ -f "$TRUENAS_BOOTSTRAP_SCRIPT" ]]; then + log_info "Removing TrueNAS bootstrap script..." + sudo rm -f "$TRUENAS_BOOTSTRAP_SCRIPT" + fi + if [[ -f "$TRUENAS_ENV_FILE" ]]; then + log_info "Removing TrueNAS environment file..." + sudo rm -f "$TRUENAS_ENV_FILE" + fi + fi + # Remove log directory if [[ -d "$LINUX_LOG_DIR" ]]; then log_info "Removing log directory..." sudo rm -rf "$LINUX_LOG_DIR" log_success "Log directory removed: $LINUX_LOG_DIR" fi + + if [[ "$TRUENAS" == true ]] && [[ -d "$TRUENAS_STATE_DIR" ]]; then + log_info "Removing persistent state directory..." + sudo rm -rf "$TRUENAS_STATE_DIR" + log_success "Removed $TRUENAS_STATE_DIR" + fi fi # Stop and remove launchd service (macOS) diff --git a/scripts/install-host-agent.sh b/scripts/install-host-agent.sh index 0e9794a57..75cfbbb56 100755 --- a/scripts/install-host-agent.sh +++ b/scripts/install-host-agent.sh @@ -134,6 +134,15 @@ if [[ -f "$UNRAID_GO_FILE" ]] || [[ -f /etc/unraid-version ]]; then UNRAID=true fi +TRUENAS=false +TRUENAS_STATE_DIR="/data/pulse-host-agent" +TRUENAS_LOG_DIR="$TRUENAS_STATE_DIR/logs" +TRUENAS_SERVICE_STORAGE="$TRUENAS_STATE_DIR/pulse-host-agent.service" +TRUENAS_BOOTSTRAP_SCRIPT="$TRUENAS_STATE_DIR/bootstrap-pulse-host-agent.sh" +TRUENAS_ENV_FILE="$TRUENAS_STATE_DIR/pulse-host-agent.env" +TRUENAS_INIT_COMMENT="Pulse host agent bootstrap" +TRUENAS_SYSTEMD_LINK="/etc/systemd/system/pulse-host-agent.service" + # Uninstall function if [[ "$UNINSTALL" == "true" ]]; then log_warn "The --uninstall flag is deprecated." @@ -167,7 +176,7 @@ fi if [[ -z "$PULSE_URL" ]]; then log_error "Pulse URL is required" - echo "Usage: $0 --url --token [--interval 30s] [--agent-id ] [--platform linux|darwin|windows] [--force] [--no-keychain]" + echo "Usage: $0 --url --token [--interval 30s] [--agent-id ] [--platform linux|darwin|windows|truenas] [--force] [--no-keychain]" echo "" echo " --force Skip interactive prompts and accept secure defaults (including Keychain storage)." echo " --agent-id Override the identifier used to deduplicate hosts (defaults to machine-id)." @@ -188,6 +197,19 @@ if [[ -z "$PULSE_TOKEN" ]] && [[ "$FORCE" == false ]]; then fi fi +is_truenas_scale() { + if [[ -f /etc/truenas-version ]]; then + return 0 + fi + if [[ -f /etc/version ]] && grep -qi "truenas" /etc/version 2>/dev/null; then + return 0 + fi + if [[ -d /data/ix-applications ]] || [[ -d /etc/ix-apps.d ]]; then + return 0 + fi + return 1 +} + # Detect platform if not specified if [[ -z "$PLATFORM" ]]; then case "$(uname -s)" in @@ -206,6 +228,11 @@ if [[ -z "$PLATFORM" ]]; then ;; esac fi +PLATFORM=$(echo "$PLATFORM" | tr '[:upper:]' '[:lower:]') +if [[ "$PLATFORM" == "truenas" ]]; then + PLATFORM="linux" + TRUENAS=true +fi # Detect architecture ARCH="$(uname -m)" @@ -226,11 +253,24 @@ case "$ARCH" in ARCH="386" ;; *) - log_warn "Unknown architecture $ARCH, defaulting to amd64" - ARCH="amd64" - ;; + log_warn "Unknown architecture $ARCH, defaulting to amd64" + ARCH="amd64" + ;; esac +if [[ "$PLATFORM" == "linux" && "$TRUENAS" == false ]]; then + if is_truenas_scale; then + TRUENAS=true + fi +fi + +if [[ "$TRUENAS" == true ]]; then + AGENT_PATH="$TRUENAS_STATE_DIR/pulse-host-agent" + SYSTEMD_SERVICE="$TRUENAS_SYSTEMD_LINK" + LINUX_LOG_DIR="$TRUENAS_LOG_DIR" + LINUX_LOG_FILE="$LINUX_LOG_DIR/host-agent.log" +fi + log_info "Configuration:" echo " Pulse URL: $PULSE_URL" if [[ -n "$PULSE_TOKEN" ]]; then @@ -247,6 +287,9 @@ else fi echo " Interval: $INTERVAL" echo " Platform: $PLATFORM/$ARCH" +if [[ "$TRUENAS" == true ]]; then + echo " TrueNAS SCALE mode: enabled (immutable root detected)" +fi echo "" log_info "Installing Pulse host agent for $PLATFORM/$ARCH..." @@ -399,7 +442,7 @@ fi # Use install command instead of mv to ensure correct SELinux context # The install command creates a new file with the correct label for the target directory -sudo install -m 0755 "$TEMP_BINARY" "$AGENT_PATH" +sudo install -D -m 0755 "$TEMP_BINARY" "$AGENT_PATH" rm -f "$TEMP_BINARY" # On SELinux systems, explicitly restore context to ensure policy compliance @@ -423,8 +466,150 @@ fi MANUAL_START_CMD="$AGENT_CMD" MANUAL_START_WRAPPED="nohup $MANUAL_START_CMD >$LINUX_LOG_FILE 2>&1 &" +write_truenas_env_file() { + sudo install -d -m 0700 "$TRUENAS_STATE_DIR" "$TRUENAS_LOG_DIR" + local tmp_env + tmp_env=$(mktemp) + { + echo "PULSE_URL=$PULSE_URL" + echo "PULSE_INTERVAL=$INTERVAL" + echo "PULSE_LOG_FILE=$LINUX_LOG_FILE" + if [[ -n "$PULSE_TOKEN" ]]; then + echo "PULSE_TOKEN=$PULSE_TOKEN" + fi + if [[ -n "$AGENT_ID" ]]; then + echo "PULSE_AGENT_ID=$AGENT_ID" + fi + } > "$tmp_env" + sudo install -m 0600 "$tmp_env" "$TRUENAS_ENV_FILE" + rm -f "$tmp_env" +} + +write_truenas_service_unit() { + local exec_start="$AGENT_PATH --url \$PULSE_URL --interval \$PULSE_INTERVAL" + if [[ -n "$PULSE_TOKEN" ]]; then + exec_start="$exec_start --token \$PULSE_TOKEN" + fi + if [[ -n "$AGENT_ID" ]]; then + exec_start="$exec_start --agent-id \$PULSE_AGENT_ID" + fi + + sudo tee "$TRUENAS_SERVICE_STORAGE" > /dev/null < /dev/null </dev/null 2>&1; then + systemctl restart pulse-host-agent >/dev/null 2>&1 || true +else + systemctl enable --now pulse-host-agent >/dev/null 2>&1 || true +fi +EOF + sudo chmod 0755 "$TRUENAS_BOOTSTRAP_SCRIPT" +} + +register_truenas_init_task() { + if ! command -v midclt >/dev/null 2>&1; then + log_warn "midclt not found - add a POSTINIT task for $TRUENAS_BOOTSTRAP_SCRIPT manually in the TrueNAS UI." + return + fi + if ! command -v python3 >/dev/null 2>&1; then + log_warn "python3 not found - cannot parse init task state; add the POSTINIT task manually if needed." + return + fi + + local query existing_id payload + query='[["script","=","'"$TRUENAS_BOOTSTRAP_SCRIPT"'"]]' + local query_output + query_output=$(midclt call initshutdownscript.query "$query" 2>/dev/null || true) + existing_id=$(printf '%s' "$query_output" | python3 - <<'PY' +import json, sys +try: + data = json.load(sys.stdin) + print(data[0]["id"] if data else "") +except Exception: + print("") +PY +) + + payload=$(cat </dev/null 2>&1; then + log_info "Updated existing TrueNAS init task (id $existing_id)" + else + log_warn "Failed to update existing TrueNAS init task (id $existing_id)" + fi + else + if midclt call initshutdownscript.create "$payload" >/dev/null 2>&1; then + log_success "Registered TrueNAS init task to restore the service on boot" + else + log_warn "Failed to register TrueNAS init task; add it manually via System Settings → Advanced → Init/Shutdown Scripts." + fi + fi +} + +setup_truenas_service() { + log_info "Detected TrueNAS SCALE (immutable root). Storing agent under $TRUENAS_STATE_DIR" + write_truenas_env_file + write_truenas_service_unit + write_truenas_bootstrap_script + + sudo ln -sf "$TRUENAS_SERVICE_STORAGE" "$TRUENAS_SYSTEMD_LINK" + sudo systemctl daemon-reload + if sudo systemctl is-enabled pulse-host-agent >/dev/null 2>&1; then + sudo systemctl restart pulse-host-agent || true + else + sudo systemctl enable --now pulse-host-agent || true + fi + + register_truenas_init_task + SERVICE_MODE="truenas" + log_success "TrueNAS SCALE service installed and started" +} + # Set up service based on platform -if [[ "$PLATFORM" == "linux" ]] && command -v systemctl &> /dev/null; then +if [[ "$TRUENAS" == true ]]; then + setup_truenas_service +elif [[ "$PLATFORM" == "linux" ]] && command -v systemctl &> /dev/null; then log_info "Setting up systemd service..." # Create log directory @@ -811,7 +996,7 @@ VALIDATION_SUCCESS=false SERVICE_RUNNING=false # Check if service is running -if [[ "$SERVICE_MODE" == "systemd" ]]; then +if [[ "$SERVICE_MODE" == "systemd" || "$SERVICE_MODE" == "truenas" ]]; then SERVICE_STATUS=$(systemctl is-active pulse-host-agent 2>/dev/null || echo "inactive") if [[ "$SERVICE_STATUS" == "active" ]]; then SERVICE_RUNNING=true @@ -902,10 +1087,13 @@ else echo "" log_info "Troubleshooting:" echo "" - if [[ "$SERVICE_MODE" == "systemd" ]]; then + if [[ "$SERVICE_MODE" == "systemd" || "$SERVICE_MODE" == "truenas" ]]; then echo " View logs: sudo journalctl -u pulse-host-agent -f" echo " Check status: sudo systemctl status pulse-host-agent" echo " Restart: sudo systemctl restart pulse-host-agent" + if [[ "$SERVICE_MODE" == "truenas" ]]; then + echo " Persist: Confirm the POSTINIT task named \"$TRUENAS_INIT_COMMENT\" exists in the TrueNAS UI" + fi elif [[ "$SERVICE_MODE" == "launchd" ]]; then echo " View logs: tail -f $MACOS_LOG_FILE" echo " Check status: launchctl list | grep pulse" @@ -930,12 +1118,15 @@ fi print_footer log_info "Service Management Commands:" -if [[ "$SERVICE_MODE" == "systemd" ]]; then +if [[ "$SERVICE_MODE" == "systemd" || "$SERVICE_MODE" == "truenas" ]]; then echo " Start: sudo systemctl start pulse-host-agent" echo " Stop: sudo systemctl stop pulse-host-agent" echo " Restart: sudo systemctl restart pulse-host-agent" echo " Status: sudo systemctl status pulse-host-agent" echo " Logs: sudo journalctl -u pulse-host-agent -f" + if [[ "$SERVICE_MODE" == "truenas" ]]; then + echo " Persist: TrueNAS Init/Shutdown task stores $TRUENAS_BOOTSTRAP_SCRIPT as POSTINIT" + fi elif [[ "$SERVICE_MODE" == "launchd" ]]; then echo " Start: launchctl load $LAUNCHD_PLIST" echo " Stop: launchctl unload $LAUNCHD_PLIST" diff --git a/scripts/uninstall-host-agent.sh b/scripts/uninstall-host-agent.sh index d394c2e93..bc9a80d95 100755 --- a/scripts/uninstall-host-agent.sh +++ b/scripts/uninstall-host-agent.sh @@ -66,6 +66,15 @@ SYSTEMD_SERVICE="/etc/systemd/system/pulse-host-agent.service" LAUNCHD_PLIST="$HOME/Library/LaunchAgents/com.pulse.host-agent.plist" MACOS_LOG_DIR="$HOME/Library/Logs/Pulse" LINUX_LOG_DIR="/var/log/pulse" +LINUX_LOG_FILE="$LINUX_LOG_DIR/host-agent.log" + +TRUENAS=false +TRUENAS_STATE_DIR="/data/pulse-host-agent" +TRUENAS_LOG_DIR="$TRUENAS_STATE_DIR/logs" +TRUENAS_SERVICE_STORAGE="$TRUENAS_STATE_DIR/pulse-host-agent.service" +TRUENAS_BOOTSTRAP_SCRIPT="$TRUENAS_STATE_DIR/bootstrap-pulse-host-agent.sh" +TRUENAS_ENV_FILE="$TRUENAS_STATE_DIR/pulse-host-agent.env" +TRUENAS_SYSTEMD_LINK="/etc/systemd/system/pulse-host-agent.service" print_header @@ -83,9 +92,63 @@ case "$(uname -s)" in ;; esac +is_truenas_scale() { + if [[ -f /etc/truenas-version ]]; then + return 0 + fi + if [[ -f /etc/version ]] && grep -qi "truenas" /etc/version 2>/dev/null; then + return 0 + fi + if [[ -d /data/ix-applications ]] || [[ -d /etc/ix-apps.d ]]; then + return 0 + fi + return 1 +} + +if [[ "$PLATFORM" == "linux" ]] && is_truenas_scale; then + TRUENAS=true + AGENT_PATH="$TRUENAS_STATE_DIR/pulse-host-agent" + SYSTEMD_SERVICE="$TRUENAS_SYSTEMD_LINK" + LINUX_LOG_DIR="$TRUENAS_LOG_DIR" + LINUX_LOG_FILE="$LINUX_LOG_DIR/host-agent.log" +fi + log_info "Detected platform: $PLATFORM" +if [[ "$TRUENAS" == true ]]; then + log_info "TrueNAS SCALE detected (immutable root). Using $TRUENAS_STATE_DIR for cleanup." +fi echo "" +remove_truenas_init_task() { + if [[ "$TRUENAS" != true ]]; then + return + fi + if ! command -v midclt >/dev/null 2>&1 || ! command -v python3 >/dev/null 2>&1; then + log_warn "midclt/python3 not available - remove the POSTINIT task for $TRUENAS_BOOTSTRAP_SCRIPT manually if it exists." + return + fi + + local query_output task_id + query_output=$(midclt call initshutdownscript.query '[["script","=","'"$TRUENAS_BOOTSTRAP_SCRIPT"'"]]' 2>/dev/null || true) + task_id=$(printf '%s' "$query_output" | python3 - <<'PY' +import json, sys +try: + data = json.load(sys.stdin) + print(data[0]["id"] if data else "") +except Exception: + print("") +PY +) + + if [[ -n "$task_id" ]]; then + if midclt call initshutdownscript.delete "$task_id" >/dev/null 2>&1; then + log_success "Removed TrueNAS Init/Shutdown task (id $task_id)" + else + log_warn "Failed to remove TrueNAS Init/Shutdown task id $task_id; remove it manually in the TrueNAS UI." + fi + fi +} + # Stop and remove systemd service (Linux) if [[ "$PLATFORM" == "linux" ]]; then if [[ -f "$SYSTEMD_SERVICE" ]] && command -v systemctl &> /dev/null; then @@ -115,12 +178,35 @@ if [[ "$PLATFORM" == "linux" ]]; then log_success "Processes terminated" fi + if [[ "$TRUENAS" == true ]]; then + remove_truenas_init_task + + if [[ -f "$TRUENAS_SERVICE_STORAGE" ]]; then + log_info "Removing stored TrueNAS service unit..." + sudo rm -f "$TRUENAS_SERVICE_STORAGE" + fi + if [[ -f "$TRUENAS_BOOTSTRAP_SCRIPT" ]]; then + log_info "Removing TrueNAS bootstrap script..." + sudo rm -f "$TRUENAS_BOOTSTRAP_SCRIPT" + fi + if [[ -f "$TRUENAS_ENV_FILE" ]]; then + log_info "Removing TrueNAS environment file..." + sudo rm -f "$TRUENAS_ENV_FILE" + fi + fi + # Remove log directory if [[ -d "$LINUX_LOG_DIR" ]]; then log_info "Removing log directory..." sudo rm -rf "$LINUX_LOG_DIR" log_success "Log directory removed: $LINUX_LOG_DIR" fi + + if [[ "$TRUENAS" == true ]] && [[ -d "$TRUENAS_STATE_DIR" ]]; then + log_info "Removing persistent state directory..." + sudo rm -rf "$TRUENAS_STATE_DIR" + log_success "Removed $TRUENAS_STATE_DIR" + fi fi # Stop and remove launchd service (macOS)