#!/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