Pulse/install.sh
Pulse Monitor 3b0699f294 feat: auto-select update in non-interactive container mode
- Detect non-TTY environments when running with --in-container
- Automatically select appropriate update based on configured channel
- Prevents hanging on menu prompt in ProxmoxVE deployments
2025-08-22 08:55:12 +00:00

1074 lines
No EOL
36 KiB
Bash
Executable file

#!/usr/bin/env bash
# Pulse Installer Script
# Supports: Ubuntu 20.04+, Debian 11+, Proxmox VE 7+
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
INSTALL_DIR="/opt/pulse"
CONFIG_DIR="/etc/pulse" # All config and data goes here for manual installs
SERVICE_NAME="pulse"
GITHUB_REPO="rcourtman/Pulse"
# Detect existing service name (pulse or pulse-backend)
detect_service_name() {
if systemctl list-unit-files --no-legend | grep -q "^pulse-backend.service"; then
echo "pulse-backend"
elif systemctl list-unit-files --no-legend | grep -q "^pulse.service"; then
echo "pulse"
else
echo "pulse" # Default for new installations
fi
}
# Functions
print_header() {
echo -e "${BLUE}=================================================${NC}"
echo -e "${BLUE} Pulse Installation Script${NC}"
echo -e "${BLUE}=================================================${NC}"
echo
}
# Safe read function that works with or without TTY
safe_read() {
local prompt="$1"
local var_name="$2"
shift 2
local read_args="$@"
# When piped, we need to both prompt and read from /dev/tty if available
if test -e /dev/tty; then
# Send prompt to /dev/tty and read from /dev/tty
echo -n "$prompt" > /dev/tty
read $read_args $var_name < /dev/tty 2>/dev/null || {
# If /dev/tty fails, fallback to normal read
read -p "$prompt" $read_args $var_name
}
else
# No /dev/tty available, use stdin
read -p "$prompt" $read_args $var_name
fi
}
print_error() {
echo -e "${RED}[ERROR] $1${NC}" >&2
}
print_success() {
echo -e "${GREEN}[SUCCESS] $1${NC}"
}
print_info() {
echo -e "${YELLOW}[INFO] $1${NC}"
}
check_root() {
if [[ $EUID -ne 0 ]]; then
print_error "This script must be run as root"
exit 1
fi
}
check_pre_v4_installation() {
# Check for Node.js version indicators (pre-v4 used Node.js)
# Note: pulse-backend.service is NOT a reliable indicator as v4 can use it too
if [[ -f "$INSTALL_DIR/.env" ]] || \
[[ -d "$INSTALL_DIR/node_modules" ]] || \
[[ -f "$INSTALL_DIR/package.json" ]] || \
[[ -f "$INSTALL_DIR/server.js" ]] || \
[[ -d "$INSTALL_DIR/backend" ]] || \
[[ -d "$INSTALL_DIR/frontend" ]]; then
echo
print_error "Pre-v4 Pulse Installation Detected"
echo
echo -e "${BLUE}=========================================================${NC}"
echo -e "${YELLOW}Pulse v4 is a complete rewrite that requires migration${NC}"
echo -e "${BLUE}=========================================================${NC}"
echo
echo "Your current installation appears to be a pre-v4 version."
echo "Due to fundamental architecture changes, automatic upgrade is not supported."
echo
echo -e "${GREEN}Recommended approach:${NC}"
echo "1. Create a fresh LXC container or VM"
echo "2. Install Pulse v4 in the new environment"
echo "3. Configure your nodes through the new web UI"
echo "4. Reference your old .env file for credentials if needed"
echo
echo -e "${YELLOW}Your existing data is preserved at: $INSTALL_DIR${NC}"
echo
echo "For more information, see:"
echo "https://github.com/rcourtman/Pulse/releases/v4.0.0"
echo -e "${BLUE}=========================================================${NC}"
echo
exit 1
fi
}
detect_os() {
if [[ -f /etc/os-release ]]; then
. /etc/os-release
OS=$ID
VER=$VERSION_ID
else
print_error "Cannot detect OS"
exit 1
fi
}
check_proxmox_host() {
# Check if this is a Proxmox VE host
if command -v pvesh &> /dev/null && [[ -d /etc/pve ]]; then
return 0
fi
return 1
}
check_docker_environment() {
# Detect if we're running inside Docker
if [[ -f /.dockerenv ]] || grep -q docker /proc/1/cgroup 2>/dev/null; then
print_error "Docker environment detected"
echo "Please use the Docker image directly: docker run -d -p 7655:7655 rcourtman/pulse:latest"
echo "See: https://github.com/rcourtman/Pulse/blob/main/docs/DOCKER.md"
exit 1
fi
}
create_lxc_container() {
# Set up trap to cleanup on interrupt (CTID will be set later)
trap 'echo ""; print_error "Installation cancelled"; exit 1' INT
print_header
echo "Proxmox VE detected. Installing Pulse in a container."
echo
# Check if we can interact with the user
# Try to read from /dev/tty to test if we have terminal access
if test -e /dev/tty && (echo -n "" > /dev/tty) 2>/dev/null; then
# We have terminal access, show menu
echo "Installation mode:"
echo " 1) Quick (recommended)"
echo " 2) Advanced"
echo " 3) Cancel"
safe_read "Select [1-3]: " mode -n 1 -r
echo
else
# No terminal access - truly non-interactive
echo "Non-interactive mode detected. Using Quick installation."
mode="1"
fi
case $mode in
3)
print_info "Installation cancelled"
exit 0
;;
2)
ADVANCED_MODE=true
;;
*)
ADVANCED_MODE=false
;;
esac
# Get next available container ID from Proxmox
local CTID=$(pvesh get /cluster/nextid 2>/dev/null || echo "100")
# If pvesh failed, fallback to manual search
if [[ "$CTID" == "100" ]]; then
while pct status $CTID &>/dev/null 2>&1 || qm status $CTID &>/dev/null 2>&1; do
((CTID++))
done
fi
if [[ "$ADVANCED_MODE" == "true" ]]; then
echo
# Ask for port configuration
safe_read "Frontend port [7655]: " frontend_port
frontend_port=${frontend_port:-7655}
if [[ ! "$frontend_port" =~ ^[0-9]+$ ]] || [[ "$frontend_port" -lt 1 ]] || [[ "$frontend_port" -gt 65535 ]]; then
print_error "Invalid port number. Using default port 7655."
frontend_port=7655
fi
echo
# Try to get cluster-wide IDs, fall back to local
local USED_IDS=""
if command -v pvesh &>/dev/null; then
# Parse JSON output using grep and sed (works without jq)
USED_IDS=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | \
grep -o '"vmid":[0-9]*' | \
sed 's/"vmid"://' | \
sort -n | \
paste -sd',' -)
fi
if [[ -z "$USED_IDS" ]]; then
# Fallback: get local containers and VMs
local LOCAL_CTS=$(pct list 2>/dev/null | tail -n +2 | awk '{print $1}' | sort -n)
local LOCAL_VMS=$(qm list 2>/dev/null | tail -n +2 | awk '{print $1}' | sort -n)
USED_IDS=$(echo -e "$LOCAL_CTS\n$LOCAL_VMS" | grep -v '^$' | sort -n | paste -sd',' -)
fi
echo "Container/VM IDs in use: ${USED_IDS:-none}"
safe_read "Container ID [$CTID]: " custom_ctid
if [[ -n "$custom_ctid" ]] && [[ "$custom_ctid" =~ ^[0-9]+$ ]]; then
# Check if ID is in use
if pct status $custom_ctid &>/dev/null 2>&1 || qm status $custom_ctid &>/dev/null 2>&1; then
print_error "Container/VM ID $custom_ctid is already in use"
exit 1
fi
# Also check cluster if possible
if command -v pvesh &>/dev/null; then
if pvesh get /cluster/resources --type vm 2>/dev/null | jq -e ".[] | select(.vmid == $custom_ctid)" &>/dev/null; then
print_error "Container/VM ID $custom_ctid is already in use in the cluster"
exit 1
fi
fi
CTID=$custom_ctid
fi
fi
print_info "Using container ID: $CTID"
if [[ "$ADVANCED_MODE" == "true" ]]; then
echo
echo -e "${BLUE}Advanced Mode - Customize all settings${NC}"
echo -e "${YELLOW}Defaults shown are suitable for monitoring 10-20 nodes${NC}"
echo
# Container settings
safe_read "Container hostname [pulse]: " hostname
hostname=${hostname:-pulse}
safe_read "Memory (MB) [1024]: " memory
memory=${memory:-1024}
safe_read "Disk size (GB) [4]: " disk
disk=${disk:-4}
safe_read "CPU cores [2]: " cores
cores=${cores:-2}
safe_read "CPU limit (0=unlimited) [2]: " cpulimit
cpulimit=${cpulimit:-2}
safe_read "Swap (MB) [256]: " swap
swap=${swap:-256}
safe_read "Start on boot? [Y/n]: " onboot -n 1 -r
echo
if [[ "$onboot" =~ ^[Nn]$ ]]; then
onboot=0
else
onboot=1
fi
safe_read "Enable firewall? [Y/n]: " firewall -n 1 -r
echo
if [[ "$firewall" =~ ^[Nn]$ ]]; then
firewall=0
else
firewall=1
fi
safe_read "Unprivileged container? [Y/n]: " unprivileged -n 1 -r
echo
if [[ "$unprivileged" =~ ^[Nn]$ ]]; then
unprivileged=0
else
unprivileged=1
fi
else
# Quick mode - just use defaults silently
# Use optimized defaults
hostname="pulse"
memory=1024
disk=4
cores=2
cpulimit=2
swap=256
onboot=1
firewall=1
unprivileged=1
frontend_port=7655
fi
# Get available network bridges
echo
print_info "Detecting available resources..."
# Get available bridges
local BRIDGES=$(ip link show type bridge | grep -E '^[0-9]+:' | cut -d: -f2 | tr -d ' ' | paste -sd',' -)
local DEFAULT_BRIDGE=$(ip route | grep default | head -1 | grep -oP 'dev \K\S+' | grep -E '^vmbr')
DEFAULT_BRIDGE=${DEFAULT_BRIDGE:-vmbr0}
# Get available storage with usage info
local STORAGE_INFO=$(pvesm status -content rootdir 2>/dev/null | tail -n +2)
local DEFAULT_STORAGE=$(echo "$STORAGE_INFO" | awk '{print $1}' | head -1)
DEFAULT_STORAGE=${DEFAULT_STORAGE:-local-lvm}
if [[ "$ADVANCED_MODE" == "true" ]]; then
# Show available bridges
echo
echo "Available network bridges: ${BRIDGES:-none detected}"
safe_read "Network bridge [$DEFAULT_BRIDGE]: " bridge
bridge=${bridge:-$DEFAULT_BRIDGE}
# Show available storage with usage details
echo
echo "Available storage pools:"
echo "$STORAGE_INFO" | awk '{printf " %-15s %-8s %5s used\n", $1, $2, $6}' || echo " No storage pools found"
safe_read "Storage [$DEFAULT_STORAGE]: " storage
storage=${storage:-$DEFAULT_STORAGE}
safe_read "Static IP (leave empty for DHCP): " static_ip
safe_read "DNS servers (comma-separated, empty for host settings): " nameserver
safe_read "Startup order [99]: " startup
startup=${startup:-99}
else
# Quick mode - use defaults
bridge=$DEFAULT_BRIDGE
storage=$DEFAULT_STORAGE
static_ip=""
nameserver=""
startup=99
fi
# Handle OS template selection
echo
if [[ "$ADVANCED_MODE" == "true" ]]; then
echo "Available OS templates:"
# Use pveam to list templates properly
local TEMPLATES=$(pveam list local 2>/dev/null | tail -n +2 | awk '{print $1}' | sed 's/local:vztmpl\///' | nl -w2 -s') ')
if [[ -n "$TEMPLATES" ]]; then
echo "$TEMPLATES"
echo
echo "Or download a new template:"
echo " d) Download Debian 12 (recommended)"
echo " u) Download Ubuntu 22.04 LTS"
echo " a) Download Alpine Linux (minimal)"
echo
safe_read "Select template number or option [Enter for Debian 12]: " template_choice
if [[ -n "$template_choice" ]]; then
case "$template_choice" in
d|D)
print_info "Downloading Debian 12..."
pveam download local debian-12-standard_12.7-1_amd64.tar.zst
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
;;
u|U)
print_info "Downloading Ubuntu 22.04..."
pveam download local ubuntu-22.04-standard_22.04-1_amd64.tar.zst
TEMPLATE="/var/lib/vz/template/cache/ubuntu-22.04-standard_22.04-1_amd64.tar.zst"
;;
a|A)
print_info "Downloading Alpine Linux..."
pveam download local alpine-3.18-default_20230607_amd64.tar.xz
TEMPLATE="/var/lib/vz/template/cache/alpine-3.18-default_20230607_amd64.tar.xz"
;;
[0-9]*)
TEMPLATE_NAME=$(pveam list local 2>/dev/null | tail -n +2 | awk '{print $1}' | sed 's/local:vztmpl\///' | sed -n "${template_choice}p")
if [[ -n "$TEMPLATE_NAME" ]]; then
TEMPLATE="/var/lib/vz/template/cache/$TEMPLATE_NAME"
print_info "Using template: $TEMPLATE_NAME"
else
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
print_info "Invalid selection, using Debian 12"
fi
;;
*)
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
;;
esac
else
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
fi
else
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
fi
else
# Quick mode - use Debian 12
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
fi
# Download template if it doesn't exist
if [[ ! -f "$TEMPLATE" ]]; then
print_info "Template not found, downloading Debian 12..."
if ! pveam download local debian-12-standard_12.7-1_amd64.tar.zst; then
print_error "Failed to download template. Please check your internet connection and try again."
print_info "You can manually download with: pveam download local debian-12-standard_12.7-1_amd64.tar.zst"
exit 1
fi
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
if [[ ! -f "$TEMPLATE" ]]; then
print_error "Template download succeeded but file not found at expected location"
exit 1
fi
fi
print_info "Creating container..."
# Build network configuration
if [[ -n "$static_ip" ]]; then
NET_CONFIG="name=eth0,bridge=${bridge},ip=${static_ip},firewall=${firewall}"
else
NET_CONFIG="name=eth0,bridge=${bridge},ip=dhcp,firewall=${firewall}"
fi
# Build container create command
local CREATE_CMD="pct create $CTID $TEMPLATE"
CREATE_CMD="$CREATE_CMD --hostname $hostname"
CREATE_CMD="$CREATE_CMD --memory $memory"
CREATE_CMD="$CREATE_CMD --cores $cores"
if [[ "$cpulimit" != "0" ]]; then
CREATE_CMD="$CREATE_CMD --cpulimit $cpulimit"
fi
CREATE_CMD="$CREATE_CMD --rootfs ${storage}:${disk}"
CREATE_CMD="$CREATE_CMD --net0 $NET_CONFIG"
CREATE_CMD="$CREATE_CMD --unprivileged $unprivileged"
CREATE_CMD="$CREATE_CMD --features nesting=1"
CREATE_CMD="$CREATE_CMD --onboot $onboot"
CREATE_CMD="$CREATE_CMD --startup order=$startup"
CREATE_CMD="$CREATE_CMD --protection 0"
CREATE_CMD="$CREATE_CMD --swap $swap"
if [[ -n "$nameserver" ]]; then
CREATE_CMD="$CREATE_CMD --nameserver $nameserver"
fi
# Execute container creation (suppress verbose output)
if ! eval $CREATE_CMD >/dev/null 2>&1; then
print_error "Failed to create container"
exit 1
fi
# From this point on, cleanup container if we fail
cleanup_on_error() {
print_error "Installation failed, cleaning up container $CTID..."
pct stop $CTID 2>/dev/null || true
sleep 2
pct destroy $CTID 2>/dev/null || true
exit 1
}
# Start container
print_info "Starting container..."
if ! pct start $CTID >/dev/null 2>&1; then
print_error "Failed to start container"
cleanup_on_error
fi
sleep 3
# Wait for network to be ready
print_info "Waiting for network..."
local network_ready=false
for i in {1..60}; do
if pct exec $CTID -- ping -c 1 8.8.8.8 &>/dev/null 2>&1; then
network_ready=true
break
fi
sleep 1
done
if [[ "$network_ready" != "true" ]]; then
print_error "Container network failed to come up after 60 seconds"
cleanup_on_error
fi
# Install dependencies and optimize container
print_info "Installing dependencies..."
if ! pct exec $CTID -- bash -c "
apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq curl wget ca-certificates >/dev/null 2>&1
# Set timezone to UTC for consistent logging
ln -sf /usr/share/zoneinfo/UTC /etc/localtime 2>/dev/null
# Optimize sysctl for monitoring workload
echo 'net.core.somaxconn=1024' >> /etc/sysctl.conf
echo 'net.ipv4.tcp_keepalive_time=60' >> /etc/sysctl.conf
sysctl -p >/dev/null 2>&1
# Create convenient update script
cat > /usr/local/bin/update << 'EOF'
#!/bin/bash
echo 'Updating Pulse...'
curl -sSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash
EOF
chmod +x /usr/local/bin/update
"; then
print_error "Failed to install dependencies in container"
cleanup_on_error
fi
# Install Pulse inside container
print_info "Installing Pulse..."
# When piped through curl, $0 is "bash" not the script. Download fresh copy.
local script_source="/tmp/pulse_install_$$.sh"
if [[ "$0" == "bash" ]] || [[ ! -f "$0" ]]; then
# We're being piped, download the script
if ! curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh > "$script_source" 2>/dev/null; then
print_error "Failed to download install script"
cleanup_on_error
fi
else
# We have a local script file
script_source="$0"
fi
# Copy this script to container and run it
if ! pct push $CTID "$script_source" /tmp/install.sh >/dev/null 2>&1; then
print_error "Failed to copy install script to container"
cleanup_on_error
fi
# Clean up temp file if we created one
if [[ "$script_source" == "/tmp/pulse_install_"* ]]; then
rm -f "$script_source"
fi
# Run installation quietly (suppress verbose output) with port configuration
local install_cmd="bash /tmp/install.sh --in-container"
if [[ "$frontend_port" != "7655" ]]; then
install_cmd="FRONTEND_PORT=$frontend_port $install_cmd"
fi
local install_output=$(pct exec $CTID -- bash -c "$install_cmd" 2>&1)
local install_status=$?
if [[ $install_status -ne 0 ]]; then
print_error "Failed to install Pulse inside container"
cleanup_on_error
fi
# Get container IP
local IP=$(pct exec $CTID -- hostname -I | awk '{print $1}')
# Clean final output
echo
print_success "Pulse installation complete!"
echo
echo " Web UI: http://${IP}:${frontend_port}"
echo " Container: $CTID"
echo
echo " Commands:"
echo " pct enter $CTID # Enter container"
echo " pct exec $CTID -- update # Update Pulse"
echo
exit 0
}
check_existing_installation() {
local CURRENT_VERSION=""
local BINARY_PATH=""
# Check for the binary in expected locations
if [[ -f "$INSTALL_DIR/bin/pulse" ]]; then
BINARY_PATH="$INSTALL_DIR/bin/pulse"
elif [[ -f "$INSTALL_DIR/pulse" ]]; then
BINARY_PATH="$INSTALL_DIR/pulse"
fi
# Try to get version if binary exists
if [[ -n "$BINARY_PATH" ]]; then
CURRENT_VERSION=$($BINARY_PATH --version 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
fi
if systemctl is-active --quiet $SERVICE_NAME 2>/dev/null; then
if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "unknown" ]]; then
print_info "Pulse $CURRENT_VERSION is currently running"
else
print_info "Pulse is currently running"
fi
return 0
elif [[ -n "$BINARY_PATH" ]]; then
if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "unknown" ]]; then
print_info "Pulse $CURRENT_VERSION is installed but not running"
else
print_info "Pulse is installed but not running"
fi
return 0
else
return 1
fi
}
install_dependencies() {
print_info "Installing dependencies..."
apt-get update -qq
apt-get install -y -qq curl wget
}
create_user() {
if ! id -u pulse &>/dev/null; then
print_info "Creating pulse user..."
useradd --system --home-dir $INSTALL_DIR --shell /bin/false pulse
fi
}
backup_existing() {
if [[ -d "$CONFIG_DIR" ]]; then
print_info "Backing up existing configuration..."
cp -a "$CONFIG_DIR" "${CONFIG_DIR}.backup.$(date +%Y%m%d-%H%M%S)"
fi
}
download_pulse() {
print_info "Downloading Pulse..."
# Check for forced version first
if [[ -n "${FORCE_VERSION}" ]]; then
LATEST_RELEASE="${FORCE_VERSION}"
print_info "Installing specific version: $LATEST_RELEASE"
# Verify the version exists
if ! curl -fsS "https://api.github.com/repos/$GITHUB_REPO/releases/tags/$LATEST_RELEASE" > /dev/null 2>&1; then
print_error "Version $LATEST_RELEASE not found"
exit 1
fi
else
# UPDATE_CHANNEL should already be set by main(), but set default if not
if [[ -z "${UPDATE_CHANNEL:-}" ]]; then
UPDATE_CHANNEL="stable"
# Allow override via command line
if [[ -n "${FORCE_CHANNEL}" ]]; then
UPDATE_CHANNEL="${FORCE_CHANNEL}"
print_info "Using $UPDATE_CHANNEL channel from command line"
elif [[ -f "$CONFIG_DIR/system.json" ]]; then
CONFIGURED_CHANNEL=$(cat "$CONFIG_DIR/system.json" 2>/dev/null | grep -o '"updateChannel"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')
if [[ "$CONFIGURED_CHANNEL" == "rc" ]]; then
UPDATE_CHANNEL="rc"
print_info "RC channel detected in configuration"
fi
fi
fi
# Get appropriate release based on channel
if [[ "$UPDATE_CHANNEL" == "rc" ]]; then
# Get all releases and find the latest (including pre-releases)
LATEST_RELEASE=$(curl -s https://api.github.com/repos/$GITHUB_REPO/releases | grep '"tag_name":' | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
else
# Get latest stable release only
LATEST_RELEASE=$(curl -s https://api.github.com/repos/$GITHUB_REPO/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
fi
if [[ -z "$LATEST_RELEASE" ]]; then
print_error "Could not determine latest release"
exit 1
fi
print_info "Latest version: $LATEST_RELEASE"
fi
# Detect architecture
ARCH=$(uname -m)
case $ARCH in
x86_64)
PULSE_ARCH="amd64"
;;
aarch64)
PULSE_ARCH="arm64"
;;
armv7l)
PULSE_ARCH="armv7"
;;
*)
print_error "Unsupported architecture: $ARCH"
exit 1
;;
esac
print_info "Detected architecture: $ARCH ($PULSE_ARCH)"
# Download architecture-specific release
DOWNLOAD_URL="https://github.com/$GITHUB_REPO/releases/download/$LATEST_RELEASE/pulse-${LATEST_RELEASE}-linux-${PULSE_ARCH}.tar.gz"
print_info "Downloading from: $DOWNLOAD_URL"
# Detect and stop existing service BEFORE downloading (to free the binary)
EXISTING_SERVICE=$(detect_service_name)
if systemctl is-active --quiet $EXISTING_SERVICE; then
print_info "Stopping existing Pulse service ($EXISTING_SERVICE)..."
systemctl stop $EXISTING_SERVICE
sleep 2 # Give the process time to fully stop and release the binary
fi
cd /tmp
if ! wget -q -O pulse.tar.gz "$DOWNLOAD_URL"; then
print_error "Failed to download Pulse release"
exit 1
fi
# Extract to temporary directory first
TEMP_EXTRACT="/tmp/pulse-extract-$$"
mkdir -p "$TEMP_EXTRACT"
tar -xzf pulse.tar.gz -C "$TEMP_EXTRACT"
# Ensure install directory and bin subdirectory exist
mkdir -p "$INSTALL_DIR/bin"
# Copy Pulse binary to the correct location (/opt/pulse/bin/pulse)
if [[ -f "$TEMP_EXTRACT/bin/pulse" ]]; then
cp "$TEMP_EXTRACT/bin/pulse" "$INSTALL_DIR/bin/pulse"
elif [[ -f "$TEMP_EXTRACT/pulse" ]]; then
# Fallback for old archives (pre-v4.3.1)
cp "$TEMP_EXTRACT/pulse" "$INSTALL_DIR/bin/pulse"
else
print_error "Pulse binary not found in archive"
exit 1
fi
chmod +x "$INSTALL_DIR/bin/pulse"
chown -R pulse:pulse "$INSTALL_DIR"
# Create symlink in /usr/local/bin for PATH convenience
ln -sf "$INSTALL_DIR/bin/pulse" /usr/local/bin/pulse
print_success "Pulse binary installed to $INSTALL_DIR/bin/pulse"
print_success "Symlink created at /usr/local/bin/pulse"
# Copy VERSION file if present
if [[ -f "$TEMP_EXTRACT/VERSION" ]]; then
cp "$TEMP_EXTRACT/VERSION" "$INSTALL_DIR/VERSION"
chown pulse:pulse "$INSTALL_DIR/VERSION"
fi
# Cleanup
rm -rf "$TEMP_EXTRACT" pulse.tar.gz
}
setup_directories() {
print_info "Setting up directories..."
# Create directories
mkdir -p "$CONFIG_DIR"
mkdir -p "$INSTALL_DIR"
# Set permissions
chown -R pulse:pulse "$CONFIG_DIR" "$INSTALL_DIR"
chmod 700 "$CONFIG_DIR"
}
install_systemd_service() {
print_info "Installing systemd service..."
# Use existing service name if found, otherwise use default
EXISTING_SERVICE=$(detect_service_name)
if [[ "$EXISTING_SERVICE" == "pulse-backend" ]] && [[ -f "/etc/systemd/system/pulse-backend.service" ]]; then
# Keep using pulse-backend for compatibility (ProxmoxVE)
SERVICE_NAME="pulse-backend"
print_info "Using existing service name: pulse-backend"
fi
cat > /etc/systemd/system/$SERVICE_NAME.service << EOF
[Unit]
Description=Pulse Monitoring Server
After=network.target
[Service]
Type=simple
User=pulse
Group=pulse
WorkingDirectory=$INSTALL_DIR
ExecStart=$INSTALL_DIR/bin/pulse
Restart=always
RestartSec=3
StandardOutput=journal
StandardError=journal
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Environment="PULSE_DATA_DIR=$CONFIG_DIR"
EOF
# Add port configuration if not default
if [[ "$FRONTEND_PORT" != "7655" ]]; then
cat >> /etc/systemd/system/$SERVICE_NAME.service << EOF
Environment="FRONTEND_PORT=$FRONTEND_PORT"
EOF
fi
cat >> /etc/systemd/system/$SERVICE_NAME.service << EOF
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=$INSTALL_DIR $CONFIG_DIR
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
}
start_pulse() {
print_info "Starting Pulse..."
systemctl enable $SERVICE_NAME
systemctl start $SERVICE_NAME
# Wait for service to start
sleep 3
if systemctl is-active --quiet $SERVICE_NAME; then
print_success "Pulse started successfully"
else
print_error "Failed to start Pulse"
journalctl -u $SERVICE_NAME -n 20
exit 1
fi
}
print_completion() {
local IP=$(hostname -I | awk '{print $1}')
echo
print_header
print_success "Pulse installation completed!"
echo
echo -e "${GREEN}Access Pulse at:${NC} http://${IP}:${FRONTEND_PORT}"
echo
echo -e "${YELLOW}Useful commands:${NC}"
echo " systemctl status $SERVICE_NAME - Check service status"
echo " systemctl restart $SERVICE_NAME - Restart Pulse"
echo " journalctl -u $SERVICE_NAME -f - View logs"
echo " systemctl edit $SERVICE_NAME - Configure custom port"
echo
echo -e "${YELLOW}Updating Pulse:${NC}"
echo " curl -sSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash"
echo " Or: wget -qO- https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash"
echo
}
# Main installation flow
main() {
# Skip Proxmox host check if we're already inside a container
if [[ "$IN_CONTAINER" != "true" ]] && check_proxmox_host; then
create_lxc_container
exit 0
fi
print_header
check_root
detect_os
check_docker_environment
check_pre_v4_installation
# Ask for port configuration (non-container installs)
local FRONTEND_PORT=${FRONTEND_PORT:-}
if [[ -z "$FRONTEND_PORT" ]]; then
if [[ "$IN_CONTAINER" == "true" ]]; then
# In container mode, use default port without prompting
FRONTEND_PORT=7655
else
echo
safe_read "Frontend port [7655]: " FRONTEND_PORT
FRONTEND_PORT=${FRONTEND_PORT:-7655}
if [[ ! "$FRONTEND_PORT" =~ ^[0-9]+$ ]] || [[ "$FRONTEND_PORT" -lt 1 ]] || [[ "$FRONTEND_PORT" -gt 65535 ]]; then
print_error "Invalid port number. Using default port 7655."
FRONTEND_PORT=7655
fi
fi
fi
if check_existing_installation; then
# Get both stable and RC versions
local STABLE_VERSION=$(curl -s https://api.github.com/repos/$GITHUB_REPO/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' 2>/dev/null)
local RC_VERSION=$(curl -s https://api.github.com/repos/$GITHUB_REPO/releases | grep '"tag_name":' | head -1 | sed -E 's/.*"([^"]+)".*/\1/' 2>/dev/null)
# Determine default update channel
UPDATE_CHANNEL="stable"
# Allow override via command line
if [[ -n "${FORCE_CHANNEL}" ]]; then
UPDATE_CHANNEL="${FORCE_CHANNEL}"
elif [[ -f "$CONFIG_DIR/system.json" ]]; then
CONFIGURED_CHANNEL=$(cat "$CONFIG_DIR/system.json" 2>/dev/null | grep -o '"updateChannel"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')
if [[ "$CONFIGURED_CHANNEL" == "rc" ]]; then
UPDATE_CHANNEL="rc"
fi
fi
echo
echo "What would you like to do?"
# Show update options based on available versions
local menu_option=1
if [[ -n "$STABLE_VERSION" ]]; then
echo "${menu_option}) Update to $STABLE_VERSION (stable)"
((menu_option++))
fi
if [[ -n "$RC_VERSION" ]] && [[ "$RC_VERSION" != "$STABLE_VERSION" ]]; then
echo "${menu_option}) Update to $RC_VERSION (release candidate)"
((menu_option++))
fi
echo "${menu_option}) Reinstall current version"
((menu_option++))
echo "${menu_option}) Remove Pulse"
((menu_option++))
echo "${menu_option}) Cancel"
local max_option=$menu_option
# In non-interactive container mode, auto-select update
if [[ "$IN_CONTAINER" == "true" ]] && ! test -t 0; then
print_info "Non-interactive mode detected. Auto-selecting update option."
# Select stable update by default, or RC if configured
if [[ "$UPDATE_CHANNEL" == "rc" ]] && [[ -n "$RC_VERSION" ]] && [[ "$RC_VERSION" != "$STABLE_VERSION" ]]; then
choice=2 # RC version
else
choice=1 # Stable version
fi
else
safe_read "Select option [1-${max_option}]: " choice
fi
# Determine what action to take based on the dynamic menu
local action=""
local target_version=""
local current_choice=1
# Check if user selected stable update
if [[ -n "$STABLE_VERSION" ]]; then
if [[ "$choice" == "$current_choice" ]]; then
action="update"
target_version="$STABLE_VERSION"
UPDATE_CHANNEL="stable"
fi
((current_choice++))
fi
# Check if user selected RC update
if [[ -n "$RC_VERSION" ]] && [[ "$RC_VERSION" != "$STABLE_VERSION" ]]; then
if [[ "$choice" == "$current_choice" ]]; then
action="update"
target_version="$RC_VERSION"
UPDATE_CHANNEL="rc"
fi
((current_choice++))
fi
# Check if user selected reinstall
if [[ "$choice" == "$current_choice" ]]; then
action="reinstall"
fi
((current_choice++))
# Check if user selected remove
if [[ "$choice" == "$current_choice" ]]; then
action="remove"
fi
((current_choice++))
# Check if user selected cancel
if [[ "$choice" == "$current_choice" ]]; then
action="cancel"
fi
case $action in
update)
print_info "Updating to $target_version..."
LATEST_RELEASE="$target_version"
backup_existing
systemctl stop $SERVICE_NAME || true
create_user
download_pulse
start_pulse
print_completion
;;
reinstall)
backup_existing
systemctl stop $SERVICE_NAME || true
create_user
download_pulse
setup_directories
install_systemd_service
start_pulse
print_completion
;;
remove)
systemctl stop $SERVICE_NAME || true
systemctl disable $SERVICE_NAME || true
rm -f /etc/systemd/system/$SERVICE_NAME.service
rm -rf "$INSTALL_DIR"
print_success "Pulse removed successfully"
;;
cancel)
print_info "Installation cancelled"
exit 0
;;
*)
print_error "Invalid option"
exit 1
;;
esac
else
# Fresh installation
install_dependencies
create_user
setup_directories
download_pulse
install_systemd_service
start_pulse
print_completion
fi
}
# Parse command line arguments
FORCE_VERSION=""
FORCE_CHANNEL=""
IN_CONTAINER=false
while [[ $# -gt 0 ]]; do
case $1 in
--rc|--pre|--prerelease)
FORCE_CHANNEL="rc"
shift
;;
--stable)
FORCE_CHANNEL="stable"
shift
;;
--version)
FORCE_VERSION="$2"
shift 2
;;
--in-container)
IN_CONTAINER=true
shift
;;
-h|--help)
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " --rc, --pre Install latest RC/pre-release version"
echo " --stable Install latest stable version (default)"
echo " --version VERSION Install specific version (e.g., v4.4.0-rc.1)"
echo " -h, --help Show this help message"
exit 0
;;
*)
print_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Export for use in download_pulse function
export FORCE_VERSION FORCE_CHANNEL
# Run main function
main