Files
pulse/scripts/install-pulse.sh
T
2025-04-29 10:59:13 +01:00

886 lines
35 KiB
Bash

#!/bin/bash
# Pulse for Proxmox VE LXC Installation Script
# This script automates the installation and setup of Pulse within a Proxmox LXC container.
# --- Configuration ---
NODE_MAJOR_VERSION=20 # Specify the desired Node.js major version (e.g., 18, 20)
PULSE_DIR="/opt/pulse-proxmox"
PULSE_USER="pulse" # Dedicated user to run Pulse
SERVICE_NAME="pulse-monitor.service"
SCRIPT_NAME="install-pulse.sh" # Used for cron job identification
LOG_FILE="/var/log/pulse_update.log" # Log file for cron updates
SCRIPT_ABS_PATH="" # Store absolute path of the script here
# --- Flags ---
MODE_UPDATE=false # Flag to run in non-interactive update mode
INSTALL_MODE="" # Stores the determined action: install, update, remove, cancel, error
# --- Argument Parsing ---
while [[ "$#" -gt 0 ]]; do
case $1 in
--update) MODE_UPDATE=true ;;
*) echo "Unknown parameter passed: $1"; exit 1 ;;
esac
shift
done
# --- Helper Functions ---
print_info() {
echo -e "\033[1;34m[INFO]\033[0m $1"
}
print_success() {
echo -e "\033[1;32m[SUCCESS]\033[0m $1"
}
print_warning() {
echo -e "\033[1;33m[WARNING]\033[0m $1"
}
print_error() {
echo -e "\033[1;31m[ERROR]\033[0m $1" >&2
}
check_root() {
if [ "$(id -u)" -ne 0 ]; then
print_error "This script must be run as root. Please use sudo."
exit 1
fi
}
# --- Installation Status Check & Action Determination ---
check_installation_status_and_determine_action() {
# Check if running in non-interactive update mode FIRST
if [ "$MODE_UPDATE" = true ]; then
print_info "Running in non-interactive update mode..."
INSTALL_MODE="update"
return 0 # Continue to main update logic
fi
# If not in update mode, proceed with interactive checks
print_info "Checking Pulse installation at $PULSE_DIR..."
if [ -d "$PULSE_DIR" ]; then
# Directory exists
if [ -d "$PULSE_DIR/.git" ]; then
# It's a git repository - Offer Update/Remove/Cancel
print_warning "Pulse seems to be already installed."
echo "Choose an action:"
echo " 1) Update Pulse to the latest version"
echo " 2) Remove Pulse"
echo " 3) Cancel"
read -p "Enter your choice (1-3): " user_choice
case $user_choice in
1) INSTALL_MODE="update" ;;
2) INSTALL_MODE="remove" ;;
3) INSTALL_MODE="cancel" ;;
*) print_error "Invalid choice."; INSTALL_MODE="error" ;;
esac
else
# Directory exists but isn't a git repository
print_error "Directory $PULSE_DIR exists but does not appear to be a valid Pulse git repository."
print_error "Please remove this directory manually ($PULSE_DIR) or choose a different installation path and re-run the script."
INSTALL_MODE="error"
fi
else
# Directory doesn't exist - Offer Install/Cancel
print_info "Pulse is not currently installed."
echo "Choose an action:"
echo " 1) Install Pulse"
echo " 2) Cancel"
read -p "Enter your choice (1-2): " user_choice
case $user_choice in
1) INSTALL_MODE="install" ;;
2) INSTALL_MODE="cancel" ;;
*) print_error "Invalid choice."; INSTALL_MODE="error" ;;
esac
fi
# We don't return here, INSTALL_MODE is now set globally for the main logic
}
# --- System Setup Functions --- (apt_update_upgrade, install_dependencies, setup_node, create_pulse_user)
apt_update_upgrade() {
print_info "Updating package lists and upgrading packages..."
if apt-get update > /dev/null && apt-get upgrade -y > /dev/null; then
print_success "System packages updated and upgraded."
else
print_error "Failed to update/upgrade system packages."
exit 1
fi
}
install_dependencies() {
print_info "Installing necessary dependencies (git, curl, sudo, gpg)..."
if apt-get install -y git curl sudo gpg > /dev/null; then
print_success "Dependencies installed."
else
print_error "Failed to install dependencies."
exit 1
fi
}
setup_node() {
print_info "Setting up Node.js repository (NodeSource)..."
# Check if Node.js is already installed and meets version requirement (optional, for robustness)
# node_version=$(node -v 2>/dev/null | sed 's/v//' | cut -d. -f1)
# if [[ "$node_version" -ge "$NODE_MAJOR_VERSION" ]]; then
# print_info "Node.js version ${node_version} already installed and meets requirement (>=${NODE_MAJOR_VERSION}). Skipping setup."
# return 0
# fi
if ! command -v curl &> /dev/null; then
print_error "curl is required but not found. Please install it first."
exit 1
fi
# Add NodeSource repository GPG key and setup script
KEYRING_DIR="/usr/share/keyrings"
KEYRING_FILE="$KEYRING_DIR/nodesource.gpg"
if [ ! -d "$KEYRING_DIR" ]; then
mkdir -p "$KEYRING_DIR" || { print_error "Failed to create $KEYRING_DIR"; exit 1; }
fi
print_info "Downloading/refreshing NodeSource GPG key..."
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --yes --dearmor -o "$KEYRING_FILE"
if [ $? -ne 0 ]; then
print_error "Failed to download or process NodeSource GPG key."
rm -f "$KEYRING_FILE" # Clean up partial file
exit 1
fi
# Add the repository configuration
echo "deb [signed-by=$KEYRING_FILE] https://deb.nodesource.com/node_$NODE_MAJOR_VERSION.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null
if [ $? -ne 0 ]; then
print_error "Failed to add NodeSource repository to sources list."
exit 1
fi
print_info "Updating package list after adding NodeSource repository..."
if ! apt-get update > /dev/null; then
print_error "Failed to update package list after adding NodeSource repository."
# Attempt cleanup
rm -f /etc/apt/sources.list.d/nodesource.list "$KEYRING_FILE"
exit 1
fi
print_info "Installing Node.js ${NODE_MAJOR_VERSION}.x..."
if apt-get install nodejs -y > /dev/null; then
print_success "Node.js ${NODE_MAJOR_VERSION}.x installed successfully."
print_info "Node version: $(node -v)"
print_info "npm version: $(npm -v)"
else
print_error "Failed to install Node.js."
# Attempt cleanup
rm -f /etc/apt/sources.list.d/nodesource.list "$KEYRING_FILE"
exit 1
fi
}
create_pulse_user() {
print_info "Creating dedicated user '$PULSE_USER'..."
if id "$PULSE_USER" &>/dev/null; then
print_warning "User '$PULSE_USER' already exists. Skipping creation."
else
# Create a system user with no login shell and no home directory (or specify one if needed)
useradd -r -s /bin/false "$PULSE_USER"
if [ $? -eq 0 ]; then
print_success "User '$PULSE_USER' created successfully."
else
print_error "Failed to create user '$PULSE_USER'."
# Decide if this is fatal. Maybe allow script to continue if user exists?
# For now, exiting as it might indicate a problem.
exit 1
fi
fi
}
# --- Core Action Functions --- (perform_update, perform_remove)
perform_update() {
print_info "Attempting to update Pulse..."
cd "$PULSE_DIR" || { print_error "Failed to change directory to $PULSE_DIR"; return 1; }
# Add safe directory config for root user, just in case
git config --global --add safe.directory "$PULSE_DIR" > /dev/null 2>&1 || print_warning "Could not configure safe.directory for root user."
print_info "Fetching latest changes from git (running as user $PULSE_USER)..."
# Add --force to allow overwriting local tags if they conflict with remote after amends/force-pushes
if ! sudo -u "$PULSE_USER" git fetch origin --tags --force; then # Ensure tags are fetched
print_error "Failed to fetch latest changes from git."
cd ..
return 1
fi
print_info "Resetting local repository to match remote 'main' branch..."
# Reset local main to exactly match the fetched origin/main, discarding local changes/commits
if ! sudo -u "$PULSE_USER" git reset --hard origin/main; then
print_error "Failed to reset local repository to origin/main."
cd ..
return 1
fi
# Get the latest tag pointing to the current HEAD
local latest_tag
latest_tag=$(sudo -u "$PULSE_USER" git describe --tags --abbrev=0 HEAD 2>/dev/null)
print_info "Cleaning repository (removing untracked files)..."
# Remove untracked files and directories to ensure a clean state
# Use -f for files, -d for directories. Do NOT use -x which removes ignored files like .env
if ! sudo -u "$PULSE_USER" git clean -fd; then
print_warning "Failed to clean untracked files from the repository."
# Continue anyway, as the core update (reset) succeeded
fi
# Ensure the script itself remains executable after update
if [ -n "$SCRIPT_ABS_PATH" ] && [ -f "$SCRIPT_ABS_PATH" ]; then
print_info "Ensuring install script ($SCRIPT_ABS_PATH) is executable..."
if chmod +x "$SCRIPT_ABS_PATH"; then
print_success "Install script executable permission set."
else
print_warning "Failed to set executable permission on install script."
# Not necessarily fatal, continue update
fi
else
print_warning "Could not find script path ($SCRIPT_ABS_PATH) to ensure executable permission."
fi
print_info "Re-installing npm dependencies (root)..."
# Keep --omit=dev here, we'll install cli separately
if ! npm install --unsafe-perm > /dev/null 2>&1; then
print_warning "Failed to install root npm dependencies during update. Continuing..."
else
print_success "Root dependencies updated."
fi
print_info "Re-installing server dependencies..."
cd server || { print_error "Failed to change directory to $PULSE_DIR/server"; cd ..; return 1; }
if npm install --unsafe-perm > /dev/null 2>&1; then
print_success "Server dependencies updated."
else
print_error "Failed to install server npm dependencies."
exit 1 # Exit if server deps fail
fi
cd .. # Back to PULSE_DIR
# Build CSS after dependencies
print_info "Building CSS assets..."
if ! npm run build:css > /dev/null 2>&1; then
print_error "Failed to build CSS assets during update."
# Potentially return 1 here if CSS build failure is critical
# For now, just warn and continue
print_warning "Continuing update despite CSS build failure."
else
print_success "CSS assets built."
fi
set_permissions # Ensure permissions are correct after update and build
# Ensure the systemd service is configured correctly before restarting
print_info "Ensuring systemd service ($SERVICE_NAME) is configured..."
if ! setup_systemd_service true; then # Pass true to indicate update mode (skip start/enable)
print_error "Failed to configure systemd service during update."
# Decide if this should be fatal, likely yes as restart will fail
return 1
fi
print_info "===> Attempting to restart Pulse service ($SERVICE_NAME)..."
systemctl restart "$SERVICE_NAME"
local restart_exit_code=$?
if [ $restart_exit_code -eq 0 ]; then
print_success "Pulse service restart command finished successfully (Exit code: $restart_exit_code)."
else
print_error "Pulse service restart command failed (Exit code: $restart_exit_code)."
print_warning "Please check the service status manually: sudo systemctl status $SERVICE_NAME"
print_warning "And check logs: sudo journalctl -u $SERVICE_NAME"
return 1
fi
# Use the detected tag in the success message
if [ -n "$latest_tag" ]; then
print_success "Pulse updated successfully to version $latest_tag!"
else
print_success "Pulse updated successfully!" # Fallback if tag detection failed
fi
return 0
}
perform_remove() {
print_warning "This will stop and disable the Pulse service(s) and remove the installation directory ($PULSE_DIR)."
# Allow non-interactive removal if called directly with logic before
# For now, keep interactive confirmation here
local remove_confirm=""
if [ -t 0 ]; then # Check if stdin is a terminal for interactive prompt
read -p "Are you sure you want to remove Pulse? (y/N): " remove_confirm
if [[ ! "$remove_confirm" =~ ^[Yy]$ ]]; then
print_info "Removal cancelled."
return 1 # Indicate cancellation
fi
else
print_warning "Running non-interactively. Proceeding with removal..."
fi
# List of potential service names to remove
local potential_services=("pulse-monitor.service" "pulse-proxmox.service")
local service_removed=false
for service_name in "${potential_services[@]}"; do
local service_file_path="/etc/systemd/system/$service_name"
if systemctl list-units --full -all | grep -q "$service_name"; then
print_info "Stopping service ($service_name)..."
systemctl stop "$service_name" > /dev/null 2>&1 # Ignore errors if already stopped
print_info "Disabling service ($service_name)..."
systemctl disable "$service_name" > /dev/null 2>&1 # Ignore errors if already disabled
if [ -f "$service_file_path" ]; then
print_info "Removing systemd service file ($service_file_path)..."
rm -f "$service_file_path"
if [ $? -eq 0 ]; then
print_success "Service file $service_file_path removed."
service_removed=true
else
print_warning "Failed to remove service file $service_file_path. Please remove it manually."
fi
else
print_info "Service file $service_file_path not found, skipping removal."
fi
else
print_info "Service $service_name not found, skipping stop/disable."
# Also check if the file exists even if service isn't loaded
if [ -f "$service_file_path" ]; then
print_info "Removing orphaned systemd service file ($service_file_path)..."
rm -f "$service_file_path"
if [ $? -eq 0 ]; then
print_success "Orphaned service file $service_file_path removed."
service_removed=true
else
print_warning "Failed to remove orphaned service file $service_file_path. Please remove it manually."
fi
fi
fi
done
# Reload systemd daemon if any service file was removed
if [ "$service_removed" = true ]; then
print_info "Reloading systemd daemon..."
systemctl daemon-reload
fi
print_info "Removing Pulse installation directory ($PULSE_DIR)..."
if rm -rf "$PULSE_DIR"; then
print_success "Installation directory removed."
else
print_error "Failed to remove installation directory $PULSE_DIR. Please remove it manually."
return 1 # Indicate error
fi
print_success "Pulse removed successfully."
return 0
}
# --- Installation Step Functions --- (install_npm_deps, set_permissions, configure_environment, setup_systemd_service)
install_npm_deps() {
print_info "Installing npm dependencies..."
if [ ! -d "$PULSE_DIR" ]; then
print_error "Pulse directory $PULSE_DIR not found. Cannot install dependencies."
# This shouldn't happen if called in the right flow, but check anyway
return 1
fi
cd "$PULSE_DIR" || { print_error "Failed to change directory to $PULSE_DIR"; return 1; }
print_info "Installing root dependencies (including dev)..."
# Use --unsafe-perm if running npm install as root, which might be necessary for some packages
# REMOVED --omit=dev to ensure build tools like postcss/autoprefixer are present
if npm install --unsafe-perm > /dev/null 2>&1; then
print_success "Root dependencies installed."
else
print_error "Failed to install root npm dependencies."
return 1
fi
print_info "Installing server dependencies (including dev)..."
cd server || { print_error "Failed to change directory to $PULSE_DIR/server"; cd ..; return 1; }
# REMOVED --omit=dev
if npm install --unsafe-perm > /dev/null 2>&1; then
print_success "Server dependencies installed."
else
print_error "Failed to install server npm dependencies."
cd .. # Go back before returning error
return 1
fi
# Return to script execution directory or root, if needed
cd ..
return 0
}
set_permissions() {
print_info "Setting permissions for $PULSE_DIR..."
if chown -R "$PULSE_USER":"$PULSE_USER" "$PULSE_DIR"; then
print_success "Permissions set correctly."
return 0
else
print_error "Failed to set permissions for $PULSE_DIR."
# This might not be fatal, but could cause runtime issues. Log warning?
print_warning "Check ownership and permissions for $PULSE_DIR."
return 1 # Indicate potential problem
fi
}
configure_environment() {
print_info "Configuring Pulse environment..."
local env_example_path="$PULSE_DIR/.env.example"
local env_path="$PULSE_DIR/.env"
if [ ! -f "$env_example_path" ]; then
print_error "Environment example file not found at $env_example_path. Cannot configure."
return 1
fi
# Check if .env already exists
if [ -f "$env_path" ]; then
print_warning "Configuration file $env_path already exists."
# Only prompt if interactive
if [ -t 0 ]; then
read -p "Overwrite existing configuration? (y/N): " overwrite_confirm
if [[ ! "$overwrite_confirm" =~ ^[Yy]$ ]]; then
print_info "Skipping environment configuration."
return 0 # Exit the function successfully without configuring
fi
print_info "Proceeding to overwrite existing configuration..."
else
print_info "Running non-interactively, skipping environment configuration as file exists."
return 0
fi
fi
# --- Gather Proxmox Details (Only if interactive) ---
local proxmox_host=""
local proxmox_token_id=""
local proxmox_token_secret=""
local allow_self_signed="y"
local pulse_port=""
if [ -t 0 ]; then
echo "Please provide your Proxmox connection details:"
read -p " -> Proxmox Host URL (e.g., https://192.168.1.100:8006): " proxmox_host
while [ -z "$proxmox_host" ]; do
print_warning "Proxmox Host URL cannot be empty."
read -p " -> Proxmox Host URL (e.g., https://192.168.1.100:8006): " proxmox_host
done
# Validate and potentially prepend https://
if [[ ! "$proxmox_host" =~ ^https?:// ]]; then
print_warning "URL does not start with http:// or https://. Prepending https://."
proxmox_host="https://$proxmox_host"
print_info "Using Proxmox Host URL: $proxmox_host"
fi
# --- Display Token Generation Info --- (Remains the same)
# ... (token info display code) ...
echo ""
print_info "You need a Proxmox API Token. You can create one via the Proxmox Web UI,"
print_info "or run the following commands on your Proxmox host shell:"
echo "----------------------------------------------------------------------"
echo ' # 1. Create user 'pulse-monitor' (enter password when prompted):'
echo " pveum useradd pulse-monitor@pam -comment "API user for Pulse monitoring""
echo ' '
echo ' # 2. Create API token 'pulse' for user (COPY THE SECRET VALUE!):'
echo " pveum user token add pulse-monitor@pam pulse --privsep=1"
echo ' '
echo ' # 3. Assign PVEAuditor role to user:'
echo " pveum acl modify / -user pulse-monitor@pam -role PVEAuditor"
echo "----------------------------------------------------------------------"
echo "After running the 'token add' command, copy the Token ID and Secret Value"
echo "and paste them below."
echo ""
read -p " -> Proxmox API Token ID (e.g., user@pam!tokenid): " proxmox_token_id
while [ -z "$proxmox_token_id" ]; do
print_warning "Proxmox Token ID cannot be empty."
read -p " -> Proxmox API Token ID (e.g., user@pam!tokenid): " proxmox_token_id
done
read -sp " -> Proxmox API Token Secret: " proxmox_token_secret
echo
while [ -z "$proxmox_token_secret" ]; do
print_warning "Proxmox Token Secret cannot be empty."
read -sp " -> Proxmox API Token Secret: " proxmox_token_secret
echo
done
# --- Optional Settings --- (Remains the same)
read -p "Allow self-signed certificates for Proxmox? (Y/n): " allow_self_signed
read -p "Port for Pulse server (leave blank for default 7655): " pulse_port
else
print_warning "Running non-interactively. Cannot prompt for environment details."
print_warning "Ensure $env_path is configured manually or exists from a previous run."
# Use defaults or skip creation if running non-interactively?
# For now, just skip the interactive part and proceed to copy/sed if file doesn't exist
# or if overwrite was forced (though non-interactive won't force)
if [ -f "$env_path" ]; then return 0; fi # Skip if file exists and non-interactive
fi
# Determine values (use defaults if not set interactively)
local self_signed_value="true"
if [[ "$allow_self_signed" =~ ^[Nn]$ ]]; then self_signed_value="false"; fi
local port_value="7655"
if [ -n "$pulse_port" ]; then
if [[ "$pulse_port" =~ ^[0-9]+$ ]] && [ "$pulse_port" -ge 1 ] && [ "$pulse_port" -le 65535 ]; then
port_value="$pulse_port"
else
print_warning "Invalid port number entered. Using default 7655."
fi
fi
# --- Create .env file --- (Handle case where variables might be empty if non-interactive)
print_info "Creating $env_path from example..."
if cp "$env_example_path" "$env_path"; then
# Only run sed if variables were set (i.e., interactive mode ran)
if [ -n "$proxmox_host" ]; then
sed -i "s|^PROXMOX_HOST=.*|PROXMOX_HOST=$proxmox_host|" "$env_path"
sed -i "s|^PROXMOX_TOKEN_ID=.*|PROXMOX_TOKEN_ID=$proxmox_token_id|" "$env_path"
sed -i "s|^PROXMOX_TOKEN_SECRET=.*|PROXMOX_TOKEN_SECRET=$proxmox_token_secret|" "$env_path"
sed -i "s|^PROXMOX_ALLOW_SELF_SIGNED_CERTS=.*|PROXMOX_ALLOW_SELF_SIGNED_CERTS=$self_signed_value|" "$env_path"
sed -i "s|^PORT=.*|PORT=$port_value|" "$env_path"
else
print_warning "Skipping variable substitution in .env as no values were provided (non-interactive?)."
print_warning "Please edit $env_path manually."
fi
# Set ownership & permissions regardless
chown "$PULSE_USER":"$PULSE_USER" "$env_path"
chmod 600 "$env_path"
print_success "Environment file created/updated in $env_path."
return 0
else
print_error "Failed to copy $env_example_path to $env_path."
return 1
fi
}
setup_systemd_service() {
# Add optional argument to skip start/enable during updates
local update_mode=${1:-false} # Default to false if no argument passed
print_info "Setting up systemd service ($SERVICE_NAME)..."
local service_file="/etc/systemd/system/$SERVICE_NAME"
# Find Node path (needed for systemd ExecStart)
local node_path
node_path=$(command -v node)
if [ -z "$node_path" ]; then
print_error "Could not find Node.js executable path. Cannot create service."
return 1
fi
# Find npm path (needed for systemd ExecStart)
local npm_path
npm_path=$(command -v npm)
if [ -z "$npm_path" ]; then
print_warning "Could not find npm executable path. Service might require adjustment."
# Try to find it relative to node? Often in ../lib/node_modules/npm/bin/npm-cli.js
local node_dir
node_dir=$(dirname "$node_path")
# This is a common structure, adjust if needed
npm_path="$node_dir/npm" # Adjust if npm is installed elsewhere
if ! command -v "$npm_path" &> /dev/null; then
print_error "Could not reliably find npm executable path. Cannot create service."
return 1
fi
print_info "Found npm at $npm_path"
fi
print_info "Creating/Updating service file at $service_file..."
# Use a HEREDOC to write the service file contents
cat << EOF > "$service_file"
[Unit]
Description=Pulse for Proxmox VE Monitoring Application
After=network.target
[Service]
Type=simple
User=$PULSE_USER
Group=$PULSE_USER
WorkingDirectory=$PULSE_DIR
# Load environment variables from .env file in the working directory
# Using explicit path instead of %W due to potential expansion issues
EnvironmentFile=$PULSE_DIR/.env
# Start command using absolute paths found earlier
ExecStart=$node_path $npm_path run start
# Restart policy
Restart=on-failure
RestartSec=5
# Environment (optional, if needed, but .env should handle this)
# Environment="NODE_ENV=production"
# Standard output/error logging
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
if [ $? -ne 0 ]; then
print_error "Failed to create systemd service file $service_file."
return 1
fi
# Set permissions for the service file
chmod 644 "$service_file"
print_info "Reloading systemd daemon..."
systemctl daemon-reload
# Skip enable/start if in update mode
if [ "$update_mode" = true ]; then
print_info "(Update mode: Skipping service enable/start)"
return 0
fi
print_info "Enabling $SERVICE_NAME to start on boot..."
if systemctl enable "$SERVICE_NAME" > /dev/null 2>&1; then
print_success "Service enabled successfully."
else
print_error "Failed to enable systemd service."
# Attempt cleanup of service file?
# rm -f "$service_file"
return 1
fi
print_info "Starting $SERVICE_NAME..."
if systemctl start "$SERVICE_NAME"; then
print_success "Service started successfully."
else
print_error "Failed to start systemd service."
print_warning "Please check the service status using: systemctl status $SERVICE_NAME"
print_warning "And check the logs using: journalctl -u $SERVICE_NAME"
# Don't necessarily exit, user might be able to fix it.
return 1 # Indicate start failure
fi
return 0
}
# --- Final Steps Functions --- (setup_cron_update, prompt_for_cron_setup, final_instructions)
setup_cron_update() {
local cron_schedule=""
local cron_command=""
local script_path="$SCRIPT_ABS_PATH"
local escaped_script_path # For grep/sed patterns
local cron_identifier="# Pulse-Auto-Update ($SCRIPT_NAME)" # Identifier comment
local escaped_cron_identifier
if [ -z "$script_path" ] || [ ! -f "$script_path" ]; then
print_warning "Could not reliably determine script path for cron job. Skipping auto-update setup."
return 1
fi
# Escape necessary characters for sed pattern matching
escaped_cron_identifier=$(sed 's/[/.*^$]/\\&/g' <<< "$cron_identifier")
print_info "Choose update frequency:"
echo " 1) Daily"
echo " 2) Weekly"
echo " 3) Monthly"
echo " 4) Never (Cancel)"
read -p "Enter your choice (1-4): " freq_choice
case $freq_choice in
1) cron_schedule="@daily" ;;
2) cron_schedule="@weekly" ;;
3) cron_schedule="@monthly" ;;
4) print_info "Automatic updates setup cancelled."; return 0 ;;
*) print_error "Invalid choice. Skipping auto-update setup."; return 1 ;;
esac
# Construct the cron command
cron_command="$cron_schedule /usr/bin/bash $script_path --update >> $LOG_FILE 2>&1"
# --- Improved Cron Job Handling ---
print_info "Checking/Updating cron job for Pulse automatic updates..."
# Get current crontab content or empty string if none exists
current_cron=$(crontab -l -u root 2>/dev/null || true)
# Use sed to remove the identifier line and the line immediately following it.
# The pattern looks for the exact identifier comment at the beginning of a line (^).
# If found, it reads the Next line (N) and Deletes both (d).
filtered_cron=$(echo "$current_cron" | sed "/^${escaped_cron_identifier}$/{N;d;}")
# Prepare the new crontab content
# If filtered_cron is empty after removal, avoid leading newline. Otherwise, add newline before appending.
if [ -z "$filtered_cron" ]; then
new_cron_content=$(printf "%s\n%s" "$cron_identifier" "$cron_command")
else
new_cron_content=$(printf "%s\n%s\n%s" "$filtered_cron" "$cron_identifier" "$cron_command")
fi
# Load the new crontab content
echo "$new_cron_content" | crontab -u root -
if [ $? -eq 0 ]; then
print_success "Cron job for automatic updates set successfully."
print_info "Update logs will be written to $LOG_FILE"
# Ensure log file exists and is writable
touch "$LOG_FILE" || print_warning "Could not touch log file $LOG_FILE"
chown root:root "$LOG_FILE" || print_warning "Could not chown log file $LOG_FILE to root"
chmod 644 "$LOG_FILE" || print_warning "Could not chmod log file $LOG_FILE"
else
print_error "Failed to update cron job. Please check crontab configuration manually."
return 1
fi
# --- End Improved Cron Job Handling ---
return 0
}
prompt_for_cron_setup() {
# Only prompt if not running in update mode
if [ "$MODE_UPDATE" = false ]; then
echo "" # Add spacing
read -p "Do you want to set up automatic updates for Pulse? (Y/n): " setup_cron_confirm
if [[ ! "$setup_cron_confirm" =~ ^[Nn]$ ]]; then # Proceed if not 'N' or 'n' (Default Yes)
setup_cron_update
else
print_info "Skipping automatic update setup."
fi
fi
}
final_instructions() {
# Try to get the IP address of the container
local ip_address
ip_address=$(hostname -I | awk '{print $1}') # Get the first IP address
local port_value
# Read the port from the .env file, fallback to default if not found/readable
local env_path="$PULSE_DIR/.env"
if [ -f "$env_path" ] && grep -q '^PORT=' "$env_path"; then
port_value=$(grep '^PORT=' "$env_path" | cut -d'=' -f2)
else
port_value="7655" # Default port
fi
echo ""
print_success "Pulse for Proxmox VE installation/update complete!"
echo "-------------------------------------------------------------"
print_info "You should be able to access the Pulse dashboard at:"
if [ -n "$ip_address" ]; then
echo " http://$ip_address:$port_value"
else
echo " http://<YOUR-LXC-IP>:$port_value"
print_warning "Could not automatically determine the LXC IP address."
fi
echo ""
print_info "The Pulse service ($SERVICE_NAME) is running and enabled on boot."
print_info "To check the status: sudo systemctl status $SERVICE_NAME"
print_info "To view logs: sudo journalctl -u $SERVICE_NAME -f"
print_info "Configuration file: $PULSE_DIR/.env"
echo "-------------------------------------------------------------"
}
# --- Main Execution --- Refactored
check_root
# Determine absolute path of the script early
if command -v readlink &> /dev/null && readlink -f "$0" &> /dev/null; then
SCRIPT_ABS_PATH=$(readlink -f "$0")
else
# Basic fallback if readlink -f is not available or fails
if [[ "$0" == /* ]]; then
SCRIPT_ABS_PATH="$0"
else
SCRIPT_ABS_PATH="$(pwd)/$0"
fi
# Verify the fallback path
if [ ! -f "$SCRIPT_ABS_PATH" ]; then
print_warning "Warning: Could not reliably determine absolute script path using fallback."
SCRIPT_ABS_PATH="" # Clear path if unsure
fi
fi
# Check installation status and determine user's desired action first
check_installation_status_and_determine_action # Sets the INSTALL_MODE variable
# --- Execute Action Based on INSTALL_MODE ---
case "$INSTALL_MODE" in
"remove")
print_info "Proceeding with removal..."
perform_remove
exit $?
;;
"cancel")
print_info "Operation cancelled by user."
exit 0
;;
"error")
# Error message already printed
print_error "Exiting due to error."
exit 1
;;
"install" | "update")
# Only install dependencies if installing or updating
print_info "Proceeding with install/update. Installing prerequisites..."
apt_update_upgrade || exit 1
install_dependencies || exit 1
setup_node || exit 1
create_pulse_user || exit 1
print_success "Prerequisites installed."
# Now perform the specific action
if [ "$INSTALL_MODE" = "install" ]; then
print_info "Starting installation..."
# --- Installation specific steps ---
print_info "Cloning Pulse repository into $PULSE_DIR..."
if git clone https://github.com/rcourtman/Pulse.git "$PULSE_DIR" > /dev/null 2>&1; then
print_success "Repository cloned successfully."
else
print_error "Failed to clone repository."
exit 1
fi
install_npm_deps || exit 1 # Installs root and server (NOW INCLUDES DEV)
# Build CSS after dependencies
print_info "Building CSS assets..."
cd "$PULSE_DIR" || { print_error "Failed to cd to $PULSE_DIR before building CSS"; exit 1; }
if ! npm run build:css > /dev/null 2>&1; then
print_error "Failed to build CSS assets."
exit 1
else
print_success "CSS assets built."
fi
# Now cd back if needed, though subsequent steps might need PULSE_DIR
# cd ..
set_permissions || exit 1 # Set permissions AFTER building css
configure_environment || exit 1 # Prompt user for details
setup_systemd_service || exit 1 # Create, enable, start service
final_instructions
prompt_for_cron_setup # Ask about cron on fresh install
else # Update mode
print_info "Starting update..."
# --- Update specific steps ---
if perform_update; then
print_success "Pulse update completed successfully."
final_instructions
prompt_for_cron_setup
else
print_error "Pulse update failed."
exit 1
fi
fi
;;
*)
print_error "Internal script error: Unknown INSTALL_MODE '$INSTALL_MODE'"
exit 1
;;
esac
print_info "Script finished."