Pulse/scripts/install-host-agent.sh
rcourtman 8fb9ef2e8f fix: correct checksum URL construction in install script
The checksum URL was incorrectly constructed by appending .sha256
to the entire download URL including query parameters, resulting in:
  /download/pulse-host-agent?platform=linux&arch=amd64.sha256

This caused .sha256 to be part of the arch parameter, which prevented
the checksum endpoint from being reached correctly.

Fixed to construct checksum URL with .sha256 as part of the path:
  /download/pulse-host-agent.sha256?platform=linux&arch=amd64

Tested on Proxmox VE host (delly):
- Installation: ✓ Binary downloaded and installed successfully
- Service: ✓ systemd service created, enabled, and started
- Validation: ✓ Service running and attempting to report
- Logs: ✓ JSON logs writing to /var/log/pulse/host-agent.log
- Uninstallation: ✓ Complete cleanup (binary, service, logs)
- Colors: ✓ ANSI colored output working properly

Note: Checksum validation gracefully handled when endpoint
unavailable (server doesn't provide checksums yet)
2025-10-23 22:34:07 +00:00

624 lines
20 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
while [[ $# -gt 0 ]]; do
case "$1" in
--url)
PULSE_URL="$2"
shift 2
;;
--token)
PULSE_TOKEN="$2"
shift 2
;;
--interval)
INTERVAL="$2"
shift 2
;;
--platform)
PLATFORM="$2"
shift 2
;;
--uninstall)
UNINSTALL="true"
shift
;;
--force|-f)
FORCE=true
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"
# 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
# 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 <pulse-url> --token <api-token> [--interval 30s] [--platform linux|darwin|windows] [--force]"
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
# 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
# Detect architecture
ARCH="$(uname -m)"
case "$ARCH" in
x86_64|amd64)
ARCH="amd64"
;;
aarch64|arm64)
ARCH="arm64"
;;
armv7l|armhf)
ARCH="armv7"
;;
*)
log_warn "Unknown architecture $ARCH, defaulting to amd64"
ARCH="amd64"
;;
esac
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
echo " Interval: $INTERVAL"
echo " Platform: $PLATFORM/$ARCH"
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
sudo mv "$TEMP_BINARY" "$AGENT_PATH"
sudo chmod +x "$AGENT_PATH"
log_success "Agent binary installed to $AGENT_PATH"
# Set up service based on platform
if [[ "$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 <<EOF
[Unit]
Description=Pulse Host Agent
After=network.target
[Service]
Type=simple
ExecStart=$AGENT_PATH --url $PULSE_URL --token $PULSE_TOKEN --interval $INTERVAL
Restart=always
RestartSec=5s
User=root
StandardOutput=append:$LINUX_LOG_FILE
StandardError=append:$LINUX_LOG_FILE
[Install]
WantedBy=multi-user.target
EOF
log_success "Created systemd service configuration"
sudo systemctl daemon-reload
sudo systemctl enable pulse-host-agent
sudo systemctl start pulse-host-agent
log_success "Systemd service enabled and started"
elif [[ "$PLATFORM" == "darwin" ]] && command -v launchctl &> /dev/null; then
log_info "Setting up launchd service..."
# Create log directory
mkdir -p "$MACOS_LOG_DIR"
mkdir -p "$HOME/Library/LaunchAgents"
# Store token in macOS Keychain for better security
if [[ -n "$PULSE_TOKEN" ]]; then
log_info "Storing token in macOS Keychain..."
# Delete existing keychain entry if it exists
security delete-generic-password -s "pulse-host-agent" -a "$USER" 2>/dev/null || true
# Add token to Keychain
if security add-generic-password -s "pulse-host-agent" -a "$USER" -w "$PULSE_TOKEN" -U 2>/dev/null; then
log_success "Token stored securely in macOS Keychain"
USE_KEYCHAIN=true
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
else
USE_KEYCHAIN=false
fi
# Create wrapper script if using Keychain
if [[ "$USE_KEYCHAIN" == true ]]; then
WRAPPER_SCRIPT="/usr/local/bin/pulse-host-agent-wrapper.sh"
cat > "$WRAPPER_SCRIPT" <<'WRAPPER_EOF'
#!/bin/bash
# Pulse Host Agent Wrapper - Reads token from Keychain
set -e
# Read token from Keychain
PULSE_TOKEN=$(security find-generic-password -s "pulse-host-agent" -w 2>/dev/null || echo "")
# Export for agent to use
export PULSE_TOKEN
# Run the actual agent with all arguments
exec /usr/local/bin/pulse-host-agent "$@"
WRAPPER_EOF
chmod +x "$WRAPPER_SCRIPT"
log_success "Created Keychain wrapper script"
# Create plist using wrapper (token not in plist!)
cat > "$LAUNCHD_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.host-agent</string>
<key>ProgramArguments</key>
<array>
<string>$WRAPPER_SCRIPT</string>
<string>--url</string>
<string>$PULSE_URL</string>
<string>--interval</string>
<string>$INTERVAL</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>$MACOS_LOG_FILE</string>
<key>StandardErrorPath</key>
<string>$MACOS_LOG_FILE</string>
</dict>
</plist>
EOF
log_success "Created launchd service configuration (using Keychain)"
else
# Create plist with token directly (fallback)
cat > "$LAUNCHD_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.host-agent</string>
<key>ProgramArguments</key>
<array>
<string>$AGENT_PATH</string>
<string>--url</string>
<string>$PULSE_URL</string>
<string>--token</string>
<string>$PULSE_TOKEN</string>
<string>--interval</string>
<string>$INTERVAL</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>$MACOS_LOG_FILE</string>
<key>StandardErrorPath</key>
<string>$MACOS_LOG_FILE</string>
</dict>
</plist>
EOF
log_success "Created launchd service configuration"
fi
# Set restrictive permissions on plist
chmod 600 "$LAUNCHD_PLIST"
launchctl load "$LAUNCHD_PLIST"
log_success "Launchd service enabled and started"
else
log_warn "Automatic service setup not available for this platform"
log_info "To run the agent manually:"
log_info " $AGENT_PATH --url $PULSE_URL --token $PULSE_TOKEN --interval $INTERVAL"
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 [[ "$PLATFORM" == "linux" ]] && command -v systemctl &> /dev/null; 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 [[ "$PLATFORM" == "darwin" ]] && command -v launchctl &> /dev/null; then
if 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
fi
# Try to verify with API endpoint that agent is reporting
if [[ "$SERVICE_RUNNING" == true ]]; then
log_info "Verifying agent registration with Pulse server..."
# Get hostname for verification
HOSTNAME=$(hostname)
# Try to query the API for this host
if command -v curl &> /dev/null; then
API_CHECK=$(curl -fsSL "$PULSE_URL/api/hosts" 2>/dev/null || echo "")
elif command -v wget &> /dev/null; then
API_CHECK=$(wget -qO- "$PULSE_URL/api/hosts" 2>/dev/null || echo "")
fi
if [[ -n "$API_CHECK" ]] && echo "$API_CHECK" | grep -q "$HOSTNAME"; then
VALIDATION_SUCCESS=true
log_success "Agent successfully registered with Pulse server!"
log_success "Your host '$HOSTNAME' is now reporting metrics!"
elif [[ -n "$API_CHECK" ]]; then
log_warn "Agent is running but host not found in API yet (may take a few moments)"
log_info "Service appears healthy, metrics should appear shortly."
VALIDATION_SUCCESS=true # Service is running, so count as success
else
log_warn "Could not verify registration via API (endpoint may not be accessible)"
log_info "Service is running, check your Pulse dashboard manually."
VALIDATION_SUCCESS=true # Service is running, so count as success
fi
fi
if [[ "$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 [[ "$PLATFORM" == "linux" ]]; 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"
elif [[ "$PLATFORM" == "darwin" ]]; 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"
fi
echo ""
echo " Manual run: $AGENT_PATH --url $PULSE_URL $(if [[ -n "$PULSE_TOKEN" ]]; then echo "--token ***"; fi) --interval $INTERVAL"
echo ""
fi
print_footer
log_info "Service Management Commands:"
if [[ "$PLATFORM" == "linux" ]]; 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"
elif [[ "$PLATFORM" == "darwin" ]]; 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"
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 ""