Files
pulse/install.sh
T
Pulse Monitor 8965e71ad2 feat: add quick and advanced modes for LXC container creation
Users now have two options when creating the LXC container:
1. Quick mode - Uses optimized defaults for production (2GB RAM, 16GB disk, etc)
2. Advanced mode - Full control over all settings like the community script

Advanced mode allows customization of:
- Memory, CPU cores, CPU limits, swap
- Static IP vs DHCP
- DNS servers
- Firewall settings
- Privileged vs unprivileged
- Auto-start on boot
- Startup order

This gives users the best of both worlds - quick setup with good defaults
or full control when needed.
2025-08-21 19:26:31 +00:00

730 lines
22 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
}
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
}
create_lxc_container() {
print_header
echo -e "${YELLOW}Proxmox VE host detected!${NC}"
echo
echo "Installing directly on a PVE host is not recommended."
echo "Pulse should run in a container for better isolation."
echo
echo -e "${GREEN}This script will:${NC}"
echo "1. Create a new LXC container"
echo "2. Install Pulse inside the container"
echo "3. Start the service"
echo
echo "Installation mode:"
echo "1) Quick (recommended defaults)"
echo "2) Advanced (customize all settings)"
echo "3) Cancel"
read -p "Select [1-3]: " -n 1 -r mode < /dev/tty
echo
case $mode in
3)
print_info "Installation cancelled"
exit 0
;;
2)
ADVANCED_MODE=true
;;
*)
ADVANCED_MODE=false
;;
esac
# Find next available container ID
local CTID=100
while pct status $CTID &>/dev/null; do
((CTID++))
done
print_info "Using container ID: $CTID"
if [[ "$ADVANCED_MODE" == "true" ]]; then
echo
echo -e "${BLUE}Advanced Mode - Customize all settings${NC}"
echo
# Container settings
read -p "Container hostname [pulse]: " hostname < /dev/tty
hostname=${hostname:-pulse}
read -p "Memory (MB) [2048]: " memory < /dev/tty
memory=${memory:-2048}
read -p "Disk size (GB) [16]: " disk < /dev/tty
disk=${disk:-16}
read -p "CPU cores [2]: " cores < /dev/tty
cores=${cores:-2}
read -p "CPU limit (0=unlimited) [2]: " cpulimit < /dev/tty
cpulimit=${cpulimit:-2}
read -p "Swap (MB) [512]: " swap < /dev/tty
swap=${swap:-512}
read -p "Start on boot? [Y/n]: " -n 1 -r onboot < /dev/tty
echo
if [[ "$onboot" =~ ^[Nn]$ ]]; then
onboot=0
else
onboot=1
fi
read -p "Enable firewall? [Y/n]: " -n 1 -r firewall < /dev/tty
echo
if [[ "$firewall" =~ ^[Nn]$ ]]; then
firewall=0
else
firewall=1
fi
read -p "Unprivileged container? [Y/n]: " -n 1 -r unprivileged < /dev/tty
echo
if [[ "$unprivileged" =~ ^[Nn]$ ]]; then
unprivileged=0
else
unprivileged=1
fi
else
echo
echo -e "${BLUE}Quick Mode - Using optimized defaults:${NC}"
echo " • Memory: 2GB"
echo " • CPU: 2 cores (with limit)"
echo " • Disk: 16GB"
echo " • Swap: 512MB"
echo " • Auto-start: Yes"
echo " • Firewall: Enabled"
echo " • Unprivileged: Yes (secure)"
echo
# Use optimized defaults
hostname="pulse"
memory=2048
disk=16
cores=2
cpulimit=2
swap=512
onboot=1
firewall=1
unprivileged=1
# Still ask for hostname in quick mode
read -p "Container hostname [pulse]: " custom_hostname < /dev/tty
hostname=${custom_hostname:-pulse}
fi
# Get network and storage settings
local DEFAULT_BRIDGE=$(ip route | grep default | head -1 | grep -oP 'dev \K\S+' | grep -E '^vmbr')
DEFAULT_BRIDGE=${DEFAULT_BRIDGE:-vmbr0}
local DEFAULT_STORAGE=$(pvesm status -content rootdir | grep -E "^local" | head -1 | awk '{print $1}')
DEFAULT_STORAGE=${DEFAULT_STORAGE:-local-lvm}
if [[ "$ADVANCED_MODE" == "true" ]]; then
read -p "Network bridge [$DEFAULT_BRIDGE]: " bridge < /dev/tty
bridge=${bridge:-$DEFAULT_BRIDGE}
read -p "Storage [$DEFAULT_STORAGE]: " storage < /dev/tty
storage=${storage:-$DEFAULT_STORAGE}
read -p "Static IP (leave empty for DHCP): " static_ip < /dev/tty
read -p "DNS servers (comma-separated, empty for host settings): " nameserver < /dev/tty
read -p "Startup order [99]: " startup < /dev/tty
startup=${startup:-99}
else
# Quick mode - use defaults
bridge=$DEFAULT_BRIDGE
storage=$DEFAULT_STORAGE
static_ip=""
nameserver=""
startup=99
fi
print_info "Creating LXC container..."
# Download Debian template if not exists
local TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
if [[ ! -f "$TEMPLATE" ]]; then
print_info "Downloading Debian 12 template..."
pveam download local debian-12-standard_12.7-1_amd64.tar.zst
fi
# 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
eval $CREATE_CMD
if [[ $? -ne 0 ]]; then
print_error "Failed to create container"
exit 1
fi
# Start container
print_info "Starting container..."
pct start $CTID
sleep 5
# Wait for network to be ready
print_info "Waiting for container network..."
for i in {1..30}; do
if pct exec $CTID -- ping -c 1 8.8.8.8 &>/dev/null; then
break
fi
sleep 1
done
# Install dependencies and optimize container
print_info "Installing dependencies in container..."
pct exec $CTID -- bash -c "
apt-get update && apt-get install -y curl wget ca-certificates
# Set timezone to UTC for consistent logging
ln -sf /usr/share/zoneinfo/UTC /etc/localtime
# 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
"
# Install Pulse inside container
print_info "Installing Pulse in container..."
# Copy this script to container and run it
pct push $CTID $0 /tmp/install.sh
pct exec $CTID -- bash /tmp/install.sh --in-container
if [[ $? -eq 0 ]]; then
# Get container IP
local IP=$(pct exec $CTID -- hostname -I | awk '{print $1}')
print_header
print_success "Pulse installed successfully in LXC container $CTID!"
echo
echo -e "${GREEN}Access Pulse at:${NC} http://${IP}:7655"
echo
echo -e "${YELLOW}Container management:${NC}"
echo " pct enter $CTID - Enter container console"
echo " pct stop $CTID - Stop container"
echo " pct start $CTID - Start container"
echo " pct destroy $CTID - Remove container"
echo
if [[ "$ADVANCED_MODE" == "true" ]]; then
echo -e "${BLUE}Container created with your custom settings${NC}"
else
echo -e "${BLUE}Container optimizations applied:${NC}"
echo " ✓ Unprivileged container (enhanced security)"
echo " ✓ Firewall enabled on network interface"
echo " ✓ Auto-start on boot with order 99"
echo " ✓ CPU limit set to prevent resource hogging"
echo " ✓ 512MB swap for memory flexibility"
echo " ✓ UTC timezone for consistent logging"
fi
echo
else
print_error "Installation failed"
exit 1
fi
exit 0
}
check_existing_installation() {
if systemctl is-active --quiet $SERVICE_NAME 2>/dev/null; then
print_info "Pulse is currently running"
return 0
elif [[ -f "$INSTALL_DIR/pulse" ]]; then
print_info "Pulse is installed but not running"
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
# Check if user has RC channel configured
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
# 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"
# 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}:7655"
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
}
# 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_pre_v4_installation
if check_existing_installation; then
print_info "Existing Pulse installation detected"
echo
echo "What would you like to do?"
echo "1) Update to latest version"
echo "2) Reinstall"
echo "3) Remove"
echo "4) Cancel"
read -p "Select option [1-4]: " choice < /dev/tty
case $choice in
1)
backup_existing
systemctl stop $SERVICE_NAME || true
create_user
download_pulse
start_pulse
print_completion
;;
2)
backup_existing
systemctl stop $SERVICE_NAME || true
create_user
download_pulse
setup_directories
install_systemd_service
start_pulse
print_completion
;;
3)
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"
;;
4)
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