From 6e404b24eb0ef9c462d76c1d4faf0fd564999612 Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Sun, 23 Nov 2025 12:27:11 +0000 Subject: [PATCH] Fix install-host-agent.sh function order, remove duplicate, and improve dev serving --- frontend-modern/public/install-host-agent.sh | 1203 ------------------ internal/api/router.go | 15 +- scripts/install-host-agent.sh | 62 +- 3 files changed, 46 insertions(+), 1234 deletions(-) delete mode 100755 frontend-modern/public/install-host-agent.sh diff --git a/frontend-modern/public/install-host-agent.sh b/frontend-modern/public/install-host-agent.sh deleted file mode 100755 index 05212d3..0000000 --- a/frontend-modern/public/install-host-agent.sh +++ /dev/null @@ -1,1203 +0,0 @@ -#!/bin/bash -set -e - -# Pulse Host Agent Installer -# Downloads and installs the Pulse host agent for Linux, macOS, or Windows (WSL) - -# ANSI color codes -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -RESET='\033[0m' - -# Check if colors are supported -if [ -t 1 ] && command -v tput &> /dev/null && [ "$(tput colors)" -ge 8 ]; then - USE_COLOR=true -else - USE_COLOR=false -fi - -print_color() { - local color="$1" - local message="$2" - if [ "$USE_COLOR" = true ]; then - printf "${color}%s${RESET}\n" "$message" - else - printf "%s\n" "$message" - fi -} - -log_success() { - print_color "$GREEN" "✓ $1" -} - -log_error() { - print_color "$RED" "✗ $1" >&2 -} - -log_info() { - print_color "$BLUE" "ℹ $1" -} - -log_warn() { - print_color "$YELLOW" "⚠ $1" -} - -print_header() { - echo "" - print_color "$BLUE" "═══════════════════════════════════════════════════════════" - print_color "$BLUE" " Pulse Host Agent - Installation" - print_color "$BLUE" "═══════════════════════════════════════════════════════════" - echo "" -} - -print_footer() { - echo "" - print_color "$GREEN" "═══════════════════════════════════════════════════════════" - log_success "Installation complete!" - print_color "$GREEN" "═══════════════════════════════════════════════════════════" - echo "" -} - -# Parse arguments -PULSE_URL="" -PULSE_TOKEN="" -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 - --url) - PULSE_URL="$2" - shift 2 - ;; - --token) - PULSE_TOKEN="$2" - shift 2 - ;; - --interval) - INTERVAL="$2" - shift 2 - ;; - --agent-id) - AGENT_ID="$2" - shift 2 - ;; - --platform) - PLATFORM="$2" - shift 2 - ;; - --uninstall) - UNINSTALL="true" - shift - ;; - --force|-f) - FORCE=true - shift - ;; - --no-keychain) - KEYCHAIN_ENABLED=false - KEYCHAIN_OPT_OUT=true - KEYCHAIN_OPT_OUT_REASON="flag" - shift - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac -done - -AGENT_PATH="/usr/local/bin/pulse-host-agent" -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" -MACOS_LOG_FILE="$MACOS_LOG_DIR/host-agent.log" -LINUX_LOG_DIR="/var/log/pulse" -LINUX_LOG_FILE="$LINUX_LOG_DIR/host-agent.log" - -SERVICE_MODE="manual" -MANUAL_START_CMD="" -MANUAL_START_WRAPPED="" -LAUNCH_IDENTIFIER="" -UNRAID=false -UNRAID_GO_FILE="/boot/config/go" -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." - log_info "Please use the dedicated uninstall script instead:" - echo "" - echo " curl -fsSL \$PULSE_URL/uninstall-host-agent.sh | bash" - echo "" - log_info "Or download and run manually:" - echo " wget \$PULSE_URL/uninstall-host-agent.sh" - echo " chmod +x uninstall-host-agent.sh" - echo " ./uninstall-host-agent.sh" - echo "" - exit 1 -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 - log_info "Interactive Installation Mode" - echo "" - read -p "Enter Pulse server URL (e.g., http://pulse.example.com:7656): " PULSE_URL - PULSE_URL=$(echo "$PULSE_URL" | sed 's:/*$::') # Remove trailing slashes - fi -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|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 - -if [[ -z "$PULSE_TOKEN" ]] && [[ "$FORCE" == false ]]; then - log_warn "No API token provided - agent will attempt to connect without authentication" - read -p "Enter API token (or press Enter to skip): " PULSE_TOKEN - - if [[ -z "$PULSE_TOKEN" ]]; then - read -p "Continue without token? (y/N): " CONTINUE_WITHOUT_TOKEN - if [[ "$CONTINUE_WITHOUT_TOKEN" != "y" ]] && [[ "$CONTINUE_WITHOUT_TOKEN" != "Y" ]]; then - log_error "Installation cancelled" - exit 1 - fi - 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 - Linux*) - PLATFORM="linux" - ;; - Darwin*) - PLATFORM="darwin" - ;; - MINGW*|MSYS*|CYGWIN*) - PLATFORM="windows" - ;; - *) - log_error "Unsupported platform: $(uname -s)" - exit 1 - ;; - esac -fi -PLATFORM=$(echo "$PLATFORM" | tr '[:upper:]' '[:lower:]') -if [[ "$PLATFORM" == "truenas" ]]; then - PLATFORM="linux" - TRUENAS=true -fi - -# Detect architecture -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" - ;; - *) - 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 - # Mask token, showing only last 4 characters - TOKEN_MASKED="***${PULSE_TOKEN: -4}" - echo " Token: $TOKEN_MASKED" -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..." - -# Check for existing installation and version -if [[ -f "$AGENT_PATH" ]]; then - log_info "Existing installation detected" - CURRENT_VERSION=$("$AGENT_PATH" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "unknown") - if [[ "$CURRENT_VERSION" != "unknown" ]]; then - echo " Current version: $CURRENT_VERSION" - fi - - # Try to get latest version from server - if command -v curl &> /dev/null; then - LATEST_VERSION=$(curl -fsSL "$PULSE_URL/api/version" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "") - elif command -v wget &> /dev/null; then - LATEST_VERSION=$(wget -qO- "$PULSE_URL/api/version" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "") - fi - - if [[ -n "$LATEST_VERSION" ]] && [[ "$CURRENT_VERSION" != "unknown" ]]; then - echo " Latest version: $LATEST_VERSION" - if [[ "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then - log_success "Already running latest version" - elif [[ "$CURRENT_VERSION" < "$LATEST_VERSION" ]]; then - log_info "Update available: $CURRENT_VERSION → $LATEST_VERSION" - fi - fi - - if [[ "$FORCE" == false ]]; then - read -p "Reinstall/update agent? (Y/n): " REINSTALL - if [[ "$REINSTALL" == "n" ]] || [[ "$REINSTALL" == "N" ]]; then - log_info "Installation cancelled" - exit 0 - fi - echo "" - else - log_info "Force mode: automatically reinstalling/updating agent" - echo "" - fi -fi - -# Download agent binary from Pulse server -DOWNLOAD_URL="$PULSE_URL/download/pulse-host-agent?platform=$PLATFORM&arch=$ARCH" -CHECKSUM_URL="$PULSE_URL/download/pulse-host-agent.sha256?platform=$PLATFORM&arch=$ARCH" -TEMP_BINARY="/tmp/pulse-host-agent-$$.tmp" - -log_info "Downloading agent binary from $PULSE_URL..." - -DOWNLOAD_SUCCESS=false - -if command -v curl &> /dev/null; then - if curl -fL --progress-bar -o "$TEMP_BINARY" "$DOWNLOAD_URL" 2>&1; then - DOWNLOAD_SUCCESS=true - fi - # Try to download checksum (optional, server may not provide it yet) - EXPECTED_CHECKSUM=$(curl -fsSL "$CHECKSUM_URL" 2>/dev/null || echo "") -elif command -v wget &> /dev/null; then - if wget -q --show-progress -O "$TEMP_BINARY" "$DOWNLOAD_URL" 2>&1; then - DOWNLOAD_SUCCESS=true - fi - # Try to download checksum (optional) - EXPECTED_CHECKSUM=$(wget -qO- "$CHECKSUM_URL" 2>/dev/null || echo "") -else - log_error "Neither curl nor wget found" - echo "" - log_info "Please install curl or wget to continue:" - if [[ "$PLATFORM" == "darwin" ]]; then - echo " brew install curl" - elif [[ "$PLATFORM" == "linux" ]]; then - echo " # Debian/Ubuntu:" - echo " sudo apt-get install curl" - echo "" - echo " # RHEL/CentOS/Fedora:" - echo " sudo yum install curl" - fi - echo "" - exit 1 -fi - -if [[ "$DOWNLOAD_SUCCESS" == false ]]; then - log_error "Failed to download agent binary from $DOWNLOAD_URL" - echo "" - log_info "Troubleshooting steps:" - echo "" - echo "1. Verify the Pulse server is running:" - echo " curl $PULSE_URL/health" - echo "" - echo "2. Check if the download endpoint is accessible:" - echo " curl -I $DOWNLOAD_URL" - echo "" - echo "3. Build from source as a fallback:" - echo " git clone https://github.com/rcourtman/Pulse.git" - echo " cd Pulse" - echo " go build -o pulse-host-agent ./cmd/pulse-host-agent" - echo " sudo mv pulse-host-agent /usr/local/bin/" - echo " # Then run this script again with --url and --token" - echo "" - echo "4. Check firewall/network settings blocking the connection" - echo "" - rm -f "$TEMP_BINARY" - exit 1 -fi - -log_success "Downloaded agent binary" - -# Verify checksum if available -if [[ -n "$EXPECTED_CHECKSUM" ]]; then - log_info "Verifying checksum..." - - if command -v sha256sum &> /dev/null; then - ACTUAL_CHECKSUM=$(sha256sum "$TEMP_BINARY" | awk '{print $1}') - elif command -v shasum &> /dev/null; then - ACTUAL_CHECKSUM=$(shasum -a 256 "$TEMP_BINARY" | awk '{print $1}') - else - log_warn "No checksum tool found (sha256sum/shasum), skipping verification" - ACTUAL_CHECKSUM="" - fi - - if [[ -n "$ACTUAL_CHECKSUM" ]]; then - # Clean up checksums (remove whitespace, convert to lowercase) - EXPECTED_CHECKSUM=$(echo "$EXPECTED_CHECKSUM" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]') - ACTUAL_CHECKSUM=$(echo "$ACTUAL_CHECKSUM" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]') - - if [[ "$EXPECTED_CHECKSUM" == "$ACTUAL_CHECKSUM" ]]; then - log_success "Checksum verified (SHA256: ${ACTUAL_CHECKSUM:0:16}...)" - else - log_error "Checksum mismatch!" - echo " Expected: $EXPECTED_CHECKSUM" - echo " Got: $ACTUAL_CHECKSUM" - echo "" - log_warn "The downloaded binary may be corrupted or tampered with." - - if [[ "$FORCE" == false ]]; then - read -p "Continue anyway? (y/N): " CONTINUE_ANYWAY - if [[ "$CONTINUE_ANYWAY" != "y" ]] && [[ "$CONTINUE_ANYWAY" != "Y" ]]; then - rm -f "$TEMP_BINARY" - log_error "Installation cancelled" - exit 1 - fi - else - log_error "Force mode: aborting due to checksum mismatch (security risk)" - rm -f "$TEMP_BINARY" - exit 1 - fi - fi - fi -else - log_info "Checksum not available (server doesn't provide it yet)" -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 -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" -validate_agent_binary_executable "$AGENT_PATH" - -# Build reusable agent command strings -AGENT_CMD="$AGENT_PATH --url $PULSE_URL" -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 &" - -validate_agent_binary_executable() { - local path="$1" - local mount_opts="" - - if command -v findmnt >/dev/null 2>&1; then - mount_opts=$(findmnt -no OPTIONS --target "$path" 2>/dev/null || true) - if echo "$mount_opts" | grep -Eq '(^|,)noexec(,|$)'; then - log_error "Install target $path is on a filesystem mounted noexec (options: $mount_opts)" - echo "" - log_info "Enable exec on that dataset or choose an exec-capable path, then rerun the installer." - exit 1 - fi - fi - - if command -v file >/dev/null 2>&1; then - local file_type="" - file_type=$(file -b "$path" 2>/dev/null || true) - if [[ -n "$file_type" ]] && ! echo "$file_type" | grep -qiE 'ELF|Mach-O|PE32'; then - log_error "Downloaded file at $path is not a recognizable executable (detected: $file_type)" - exit 1 - fi - fi - - if ! "$path" --version >/dev/null 2>&1; then - log_error "Installed agent at $path failed to execute; check for noexec mounts or download issues." - if [[ -n "$mount_opts" ]]; then - log_info "Filesystem options: $mount_opts" - fi - exit 1 - fi -} - -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 [[ "$TRUENAS" == true ]]; then - setup_truenas_service -elif [[ "$PLATFORM" == "linux" ]] && command -v systemctl &> /dev/null; then - log_info "Setting up systemd service..." - - # Create log directory - sudo mkdir -p "$LINUX_LOG_DIR" - - sudo tee "$SYSTEMD_SERVICE" > /dev/null < /dev/null; then - log_info "Setting up launchd service..." - - # Create log directory - 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 || true - - # Add token to Keychain - KEYCHAIN_SERVICE="pulse-host-agent" - KEYCHAIN_ACCOUNT="$USER" - - KEYCHAIN_APPS=( - "/usr/local/bin/pulse-host-agent" - "/usr/bin/security" - ) - KEYCHAIN_ARGS=() - for app in "${KEYCHAIN_APPS[@]}"; do - if [[ -e "$app" ]]; then - KEYCHAIN_ARGS+=(-T "$app") - fi - done - - if security add-generic-password \ - -s "$KEYCHAIN_SERVICE" \ - -a "$KEYCHAIN_ACCOUNT" \ - -w "$PULSE_TOKEN" \ - -U \ - "${KEYCHAIN_ARGS[@]}" 2>/dev/null; then - if security find-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" -w >/dev/null 2>&1; then - log_success "Token stored securely in macOS Keychain" - USE_KEYCHAIN=true - else - log_warn "Token saved but Keychain denied non-interactive read access" - log_info "Will fall back to embedding token in the launchd plist" - USE_KEYCHAIN=false - fi - else - log_warn "Failed to store token in Keychain, will use plist instead" - 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" - TMP_WRAPPER=$(mktemp) - - cat > "$TMP_WRAPPER" <<'WRAPPER_EOF' -#!/bin/bash -# Pulse Host Agent Wrapper - Reads token from Keychain -set -u - -LOG_FILE="$HOME/Library/Logs/Pulse/host-agent-wrapper.log" -mkdir -p "$(dirname "$LOG_FILE")" - -# Read token from Keychain -if ! PULSE_TOKEN=$(security find-generic-password -s "pulse-host-agent" -a "$USER" -w 2>/dev/null); then - echo "$(date -Is) pulse-host-agent-wrapper: failed to read token from Keychain" >>"$LOG_FILE" - PULSE_TOKEN="" -fi - -# Export for agent to use -export PULSE_TOKEN - -# Run the actual agent with all arguments -exec /usr/local/bin/pulse-host-agent "$@" -WRAPPER_EOF - - if ! sudo mv "$TMP_WRAPPER" "$WRAPPER_SCRIPT"; then - if ! mv "$TMP_WRAPPER" "$WRAPPER_SCRIPT" 2>/dev/null; then - rm -f "$TMP_WRAPPER" - log_error "Failed to write Keychain wrapper to $WRAPPER_SCRIPT. Try re-running with sudo." - exit 1 - fi - fi - - if ! sudo chmod 755 "$WRAPPER_SCRIPT" 2>/dev/null && ! chmod 755 "$WRAPPER_SCRIPT" 2>/dev/null; then - log_error "Failed to set execute permissions on $WRAPPER_SCRIPT." - exit 1 - fi - - if command -v chown &>/dev/null; then - sudo chown root:wheel "$WRAPPER_SCRIPT" 2>/dev/null || sudo chown root:root "$WRAPPER_SCRIPT" 2>/dev/null || true - fi - log_success "Created Keychain wrapper script" - - # Create plist using wrapper (token not in plist!) - cat > "$LAUNCHD_PLIST" < - - - - Label - com.pulse.host-agent - ProgramArguments - - $WRAPPER_SCRIPT - --url - $PULSE_URL - --interval - $INTERVAL -$LAUNCHD_AGENT_ID_ARGS - - RunAtLoad - - KeepAlive - - StandardOutPath - $MACOS_LOG_FILE - StandardErrorPath - $MACOS_LOG_FILE - - -EOF - log_success "Created launchd service configuration (using Keychain)" - else - # Create plist with token directly (fallback) - cat > "$LAUNCHD_PLIST" < - - - - Label - com.pulse.host-agent - ProgramArguments - - $AGENT_PATH - --url - $PULSE_URL - --token - $PULSE_TOKEN - --interval - $INTERVAL -$LAUNCHD_AGENT_ID_ARGS - - RunAtLoad - - KeepAlive - - StandardOutPath - $MACOS_LOG_FILE - StandardErrorPath - $MACOS_LOG_FILE - - -EOF - log_success "Created launchd service configuration" - fi - - # Set restrictive permissions on plist - chmod 600 "$LAUNCHD_PLIST" - - LAUNCH_TARGET="gui/$(id -u)" - LAUNCH_IDENTIFIER="$LAUNCH_TARGET/com.pulse.host-agent" - - # Attempt to unload any existing service instance - if launchctl bootout "$LAUNCH_TARGET" "$LAUNCHD_PLIST" 2>/dev/null; then - log_info "Replaced existing launchd service definition" - fi - - if launchctl bootstrap "$LAUNCH_TARGET" "$LAUNCHD_PLIST"; then - launchctl enable "$LAUNCH_TARGET/com.pulse.host-agent" 2>/dev/null || true - launchctl kickstart -k "$LAUNCH_TARGET/com.pulse.host-agent" 2>/dev/null || true - log_success "Launchd service enabled and started" - else - log_error "Failed to load launchd service. Try running:" - echo " launchctl bootstrap $LAUNCH_TARGET $LAUNCHD_PLIST" - echo " launchctl kickstart -k $LAUNCH_TARGET/com.pulse.host-agent" - exit 1 - fi - SERVICE_MODE="launchd" -else - sudo mkdir -p "$LINUX_LOG_DIR" - if [[ "$UNRAID" == true ]]; then - log_info "Detected Unraid (no systemd). Configuring persistent background service..." - - if pgrep -f "$AGENT_PATH" >/dev/null 2>&1; then - log_warn "Existing pulse-host-agent process detected; restarting with new binary" - sudo pkill -f "$AGENT_PATH" 2>/dev/null || true - sleep 1 - fi - - log_info "Starting host agent with nohup (logs: $LINUX_LOG_FILE)" - if sudo bash -c "$MANUAL_START_WRAPPED"; then - log_success "Agent started in the background" - else - log_error "Failed to start agent automatically. Run manually:" - log_info " $MANUAL_START_WRAPPED" - fi - - if [[ -f "$UNRAID_GO_FILE" ]]; then - if sudo grep -qF -- "$MANUAL_START_WRAPPED" "$UNRAID_GO_FILE"; then - log_info "Auto-start entry already present in $UNRAID_GO_FILE" - else - APPEND_STARTUP=true - if [[ "$FORCE" == false ]]; then - read -p "Add agent auto-start to $UNRAID_GO_FILE? (Y/n): " ADD_STARTUP_CHOICE - if [[ "$ADD_STARTUP_CHOICE" == "n" || "$ADD_STARTUP_CHOICE" == "N" ]]; then - APPEND_STARTUP=false - fi - fi - - if [[ "$APPEND_STARTUP" == true ]]; then - if sudo grep -qF "# Pulse Host Agent auto-start" "$UNRAID_GO_FILE"; then - log_info "Updating existing auto-start entry in $UNRAID_GO_FILE" - sudo sed -i '/# Pulse Host Agent auto-start/,+1d' "$UNRAID_GO_FILE" 2>/dev/null || true - fi - sudo tee -a "$UNRAID_GO_FILE" > /dev/null < /dev/null <<'EOF' -#!/bin/sh -# /etc/rc.local - generated by Pulse host agent installer -# This script is executed at the end of each multi-user runlevel. -exit 0 -EOF - fi - - if [[ -f "$RC_LOCAL_PATH" ]]; then - RC_COMMENT="# Pulse Host Agent auto-start" - if sudo grep -qF "$MANUAL_START_WRAPPED" "$RC_LOCAL_PATH"; then - log_info "Auto-start entry already present in $RC_LOCAL_PATH" - else - APPEND_RC_LOCAL=true - if [[ "$FORCE" == false ]]; then - read -p "Add agent auto-start to $RC_LOCAL_PATH? (Y/n): " ADD_RC_CHOICE - if [[ "$ADD_RC_CHOICE" == "n" || "$ADD_RC_CHOICE" == "N" ]]; then - APPEND_RC_LOCAL=false - fi - fi - - if [[ "$APPEND_RC_LOCAL" == true ]]; then - sudo RC_APPEND_CMD="$MANUAL_START_WRAPPED" RC_COMMENT="$RC_COMMENT" RC_LOCAL_PATH="$RC_LOCAL_PATH" sh -c ' - tmpfile=$(mktemp) - cp "$RC_LOCAL_PATH" "$tmpfile" 2>/dev/null || touch "$tmpfile" - sed -i "/$RC_COMMENT/,+1d" "$tmpfile" 2>/dev/null || true - sed -i "/^exit 0$/d" "$tmpfile" 2>/dev/null || true - printf "\n%s\n%s\n" "$RC_COMMENT" "$RC_APPEND_CMD" >>"$tmpfile" - echo "exit 0" >>"$tmpfile" - mv "$tmpfile" "$RC_LOCAL_PATH" - chmod +x "$RC_LOCAL_PATH" - ' - log_success "Added auto-start command to $RC_LOCAL_PATH" - else - log_info "Skipped modifying $RC_LOCAL_PATH" - fi - fi - else - log_warn "Could not access $RC_LOCAL_PATH; skipping persistence step." - fi - - log_info "Starting host agent with nohup (logs: $LINUX_LOG_FILE)" - if sudo bash -c "$MANUAL_START_WRAPPED"; then - log_success "Agent started in the background" - else - log_error "Failed to start agent automatically. Run manually:" - log_info " $MANUAL_START_WRAPPED" - fi - - log_info "To manage manually, edit $RC_LOCAL_PATH or run:" - log_info " $MANUAL_START_CMD" - SERVICE_MODE="rc_local" - fi -fi - -# Validate installation -log_info "Waiting 10 seconds to validate agent reporting..." -sleep 10 - -VALIDATION_SUCCESS=false -SERVICE_RUNNING=false - -# Check if service is running -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 - log_success "Service is running successfully!" - else - log_warn "Service status: $SERVICE_STATUS" - log_info "Check logs with: sudo journalctl -u pulse-host-agent -n 50" - fi -elif [[ "$SERVICE_MODE" == "launchd" ]]; then - IDENTIFIER=${LAUNCH_IDENTIFIER:-"gui/$(id -u)/com.pulse.host-agent"} - for _ in 1 2 3 4 5; do - if launchctl print "$IDENTIFIER" >/dev/null 2>&1; then - SERVICE_RUNNING=true - break - fi - sleep 2 - done - if [[ "$SERVICE_RUNNING" == true ]]; then - log_success "Service is running successfully!" - elif launchctl list | grep -q "com.pulse.host-agent"; then - SERVICE_RUNNING=true - log_success "Service is running successfully!" - else - log_warn "Service may not be running properly" - log_info "Check logs with: tail -20 $MACOS_LOG_FILE" - fi -elif [[ "$SERVICE_MODE" == "unraid" ]]; then - if pgrep -f "$AGENT_PATH" >/dev/null 2>&1; then - SERVICE_RUNNING=true - log_success "Agent process is running (nohup background task)" - else - log_warn "Agent process not detected; check $LINUX_LOG_FILE for errors" - fi -elif [[ "$SERVICE_MODE" == "rc_local" ]]; then - if pgrep -f "$AGENT_PATH" >/dev/null 2>&1; then - SERVICE_RUNNING=true - log_success "Agent process is running (rc.local background task)" - else - log_warn "Agent process not detected; check $LINUX_LOG_FILE for errors" - fi -else - log_info "Skipping automated service validation – start the agent manually using the commands above." -fi - -if [[ "$SERVICE_RUNNING" == true ]]; then - VALIDATION_SUCCESS=true -fi - -# Try to verify with API endpoint that agent is reporting -if [[ "$SERVICE_MODE" != "manual" && "$SERVICE_RUNNING" == true ]]; then - HOSTNAME=$(hostname) - - if [[ -z "$PULSE_TOKEN" ]]; then - log_info "Registration check skipped (no API token available for lookup)." - elif command -v curl &> /dev/null; then - log_info "Verifying agent registration with Pulse server..." - LOOKUP_RESPONSE=$(curl -fsSL \ - -H "Authorization: Bearer $PULSE_TOKEN" \ - --get \ - --data-urlencode "hostname=$HOSTNAME" \ - "$PULSE_URL/api/agents/host/lookup" 2>/dev/null || true) - - if [[ "$LOOKUP_RESPONSE" == *'"success":true'* ]]; then - host_status=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p') - last_seen=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"lastSeen":"\([^"]*\)".*/\1/p') - log_success "Agent successfully registered with Pulse server!" - if [[ -n "$host_status" ]]; then - log_info "Pulse reports status: $host_status (last seen $last_seen)" - fi - else - log_warn "Agent lookup did not confirm registration yet (response: ${LOOKUP_RESPONSE:-no data})." - log_info "Service is running; metrics should appear shortly." - fi - else - log_info "Registration check skipped (curl is required for API validation)." - fi -fi - -if [[ "$SERVICE_MODE" == "manual" ]]; then - log_warn "Service validation requires starting the agent manually." - log_info "Run the following to launch the agent in the background:" - log_info " $MANUAL_START_WRAPPED" - log_info "Add the same line to /etc/rc.local (or equivalent) to auto-start on boot." -elif [[ "$VALIDATION_SUCCESS" == true ]]; then - log_info "Check your Pulse dashboard at: $PULSE_URL" -else - log_error "Service validation failed" - echo "" - log_info "Troubleshooting:" - echo "" - 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" - echo " Restart: launchctl unload $LAUNCHD_PLIST && launchctl load $LAUNCHD_PLIST" - elif [[ "$SERVICE_MODE" == "unraid" ]]; then - echo " Logs: tail -f $LINUX_LOG_FILE" - echo " Restart: sudo pkill -f $AGENT_PATH && $MANUAL_START_WRAPPED" - echo " Persist: Ensure the startup line exists in $UNRAID_GO_FILE" - elif [[ "$SERVICE_MODE" == "rc_local" ]]; then - echo " Logs: tail -f $LINUX_LOG_FILE" - echo " Restart: sudo pkill -f $AGENT_PATH && $MANUAL_START_WRAPPED" - echo " Persist: Ensure the startup block exists in $RC_LOCAL_PATH" - else - echo " Start agent: $MANUAL_START_WRAPPED" - echo " Persist: Add the wrapped command to /etc/rc.local (or equivalent)" - fi - echo "" - echo " Manual run: $MANUAL_START_CMD" - echo "" -fi - -print_footer - -log_info "Service Management Commands:" -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" - echo " Restart: launchctl unload $LAUNCHD_PLIST && launchctl load $LAUNCHD_PLIST" - echo " Status: launchctl list | grep pulse" - echo " Logs: tail -f $MACOS_LOG_FILE" -elif [[ "$SERVICE_MODE" == "unraid" ]]; then - echo " Start: $MANUAL_START_WRAPPED" - echo " Stop: sudo pkill -f $AGENT_PATH" - echo " Restart: sudo pkill -f $AGENT_PATH && $MANUAL_START_WRAPPED" - echo " Logs: tail -f $LINUX_LOG_FILE" - echo " Persist: Stored in $UNRAID_GO_FILE" -elif [[ "$SERVICE_MODE" == "rc_local" ]]; then - echo " Start: $MANUAL_START_WRAPPED" - echo " Stop: sudo pkill -f $AGENT_PATH" - echo " Restart: sudo pkill -f $AGENT_PATH && $MANUAL_START_WRAPPED" - echo " Logs: tail -f $LINUX_LOG_FILE" - echo " Persist: Stored in $RC_LOCAL_PATH" -else - echo " Start: $MANUAL_START_WRAPPED" - echo " Persist: Add the wrapped command to /etc/rc.local (or similar) to start on boot" -fi -echo "" - -log_info "Files installed:" -echo " Binary: $AGENT_PATH" -if [[ "$PLATFORM" == "linux" ]]; then - echo " Service: $SYSTEMD_SERVICE" - echo " Logs: $LINUX_LOG_FILE" -elif [[ "$PLATFORM" == "darwin" ]]; then - echo " Service: $LAUNCHD_PLIST" - echo " Logs: $MACOS_LOG_FILE" -fi -echo "" - -log_info "The agent is now reporting to: $PULSE_URL" -echo "" - -log_info "To uninstall, run:" -echo " curl -fsSL $PULSE_URL/uninstall-host-agent.sh | bash" -echo "" diff --git a/internal/api/router.go b/internal/api/router.go index 77ea86b..8d58654 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -2,6 +2,7 @@ package api import ( "bufio" + "bytes" "context" "crypto/sha256" "encoding/base64" @@ -3389,7 +3390,19 @@ func (r *Router) handleDownloadHostAgentInstallScript(w http.ResponseWriter, req w.Header().Set("Expires", "0") scriptPath := "/opt/pulse/scripts/install-host-agent.sh" - http.ServeFile(w, req, scriptPath) + content, err := os.ReadFile(scriptPath) + if err != nil { + // Fallback to project root (dev environment) + scriptPath = filepath.Join(r.projectRoot, "scripts", "install-host-agent.sh") + content, err = os.ReadFile(scriptPath) + if err != nil { + log.Error().Err(err).Str("path", scriptPath).Msg("Failed to read host agent installer script") + http.Error(w, "Failed to read installer script", http.StatusInternalServerError) + return + } + } + + http.ServeContent(w, req, "install-host-agent.sh", time.Now(), bytes.NewReader(content)) } // handleDownloadHostAgentInstallScriptPS serves the PowerShell installation script for Windows diff --git a/scripts/install-host-agent.sh b/scripts/install-host-agent.sh index 05212d3..7a401b6 100755 --- a/scripts/install-host-agent.sh +++ b/scripts/install-host-agent.sh @@ -60,6 +60,38 @@ print_footer() { echo "" } +validate_agent_binary_executable() { + local path="$1" + local mount_opts="" + + if command -v findmnt >/dev/null 2>&1; then + mount_opts=$(findmnt -no OPTIONS --target "$path" 2>/dev/null || true) + if echo "$mount_opts" | grep -Eq '(^|,)noexec(,|$)'; then + log_error "Install target $path is on a filesystem mounted noexec (options: $mount_opts)" + echo "" + log_info "Enable exec on that dataset or choose an exec-capable path, then rerun the installer." + exit 1 + fi + fi + + if command -v file >/dev/null 2>&1; then + local file_type="" + file_type=$(file -b "$path" 2>/dev/null || true) + if [[ -n "$file_type" ]] && ! echo "$file_type" | grep -qiE 'ELF|Mach-O|PE32'; then + log_error "Downloaded file at $path is not a recognizable executable (detected: $file_type)" + exit 1 + fi + fi + + if ! "$path" --version >/dev/null 2>&1; then + log_error "Installed agent at $path failed to execute; check for noexec mounts or download issues." + if [[ -n "$mount_opts" ]]; then + log_info "Filesystem options: $mount_opts" + fi + exit 1 + fi +} + # Parse arguments PULSE_URL="" PULSE_TOKEN="" @@ -467,37 +499,7 @@ fi MANUAL_START_CMD="$AGENT_CMD" MANUAL_START_WRAPPED="nohup $MANUAL_START_CMD >$LINUX_LOG_FILE 2>&1 &" -validate_agent_binary_executable() { - local path="$1" - local mount_opts="" - if command -v findmnt >/dev/null 2>&1; then - mount_opts=$(findmnt -no OPTIONS --target "$path" 2>/dev/null || true) - if echo "$mount_opts" | grep -Eq '(^|,)noexec(,|$)'; then - log_error "Install target $path is on a filesystem mounted noexec (options: $mount_opts)" - echo "" - log_info "Enable exec on that dataset or choose an exec-capable path, then rerun the installer." - exit 1 - fi - fi - - if command -v file >/dev/null 2>&1; then - local file_type="" - file_type=$(file -b "$path" 2>/dev/null || true) - if [[ -n "$file_type" ]] && ! echo "$file_type" | grep -qiE 'ELF|Mach-O|PE32'; then - log_error "Downloaded file at $path is not a recognizable executable (detected: $file_type)" - exit 1 - fi - fi - - if ! "$path" --version >/dev/null 2>&1; then - log_error "Installed agent at $path failed to execute; check for noexec mounts or download issues." - if [[ -n "$mount_opts" ]]; then - log_info "Filesystem options: $mount_opts" - fi - exit 1 - fi -} write_truenas_env_file() { sudo install -d -m 0700 "$TRUENAS_STATE_DIR" "$TRUENAS_LOG_DIR"