Files
ProxMenux/install_proxmenux.sh
MacRimi 371f61fa08 install_proxmenux: clear stale beta_version.txt on every stable install
A user who rode the beta channel and later switched back to stable
keeps a leftover beta_version.txt under /usr/local/share/proxmenux/.
The `menu` launcher's beta-mode update check (`check_updates_beta`)
short-circuits when that file isn't present, but it stays put across
stable installs and updates today, so the user keeps seeing the
"Beta update available" prompt on top of the legitimate stable one
even though they're no longer on the beta channel.

Drop the marker on every stable install/update, in both the update
path (around the `cp ./version.txt` near the scripts-tree wipe) and
the fresh-install path (the symmetric block lower in the file).
The comment about which files survive a scripts-tree wipe is
updated to no longer mention beta_version.txt, since that's exactly
what we're removing.

If the user re-opts into the beta program, install_proxmenux_beta
re-creates the file — this only clears stale state that the user no
longer has any way to update from anyway.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 16:34:20 +02:00

1236 lines
42 KiB
Bash
Executable File

#!/bin/bash
# ==========================================================
# ProxMenux - A menu-driven toolkit for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Contributors : cod378
# Subproject : ProxMenux Monitor (System Health & Web Dashboard)
# Copyright : (c) 2024-2025 MacRimi
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.4
# Last Updated : 12/11/2025
# ==========================================================
# Description:
# This script installs and configures ProxMenux, a menu-driven
# toolkit for managing and optimizing Proxmox VE servers.
#
# - Ensures the script is run with root privileges.
# - Displays an installation confirmation prompt.
# - Installs required dependencies:
# • whiptail (interactive terminal menus)
# • curl (downloads and connectivity checks)
# • jq (JSON parsing)
# • Python 3 + venv (for translation support)
# - Creates the ProxMenux base directories and configuration files:
# • $BASE_DIR/config.json
# • $BASE_DIR/cache.json
# - Copies local project files into the target paths (offline mode by default):
# • scripts/* → $BASE_DIR/scripts/
# • utils.sh → $BASE_DIR/scripts/utils.sh
# • menu → $INSTALL_DIR/menu (main launcher)
# • install_proxmenux.sh → $BASE_DIR/install_proxmenux.sh
# - Sets correct permissions for all executables.
# - Displays the final instruction on how to start ProxMenux ("menu").
#
# Notes:
# - This installer supports both offline and online setups.
# - ProxMenux Monitor can be installed later as an optional module
# to provide real-time system monitoring and a web dashboard.
# ==========================================================
# Configuration ============================================
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
INSTALL_DIR="/usr/local/bin"
BASE_DIR="/usr/local/share/proxmenux"
CONFIG_FILE="$BASE_DIR/config.json"
CACHE_FILE="$BASE_DIR/cache.json"
UTILS_FILE="$BASE_DIR/utils.sh"
LOCAL_VERSION_FILE="$BASE_DIR/version.txt"
MENU_SCRIPT="menu"
VENV_PATH="/opt/googletrans-env"
MONITOR_INSTALL_DIR="$BASE_DIR"
MONITOR_RUNTIME_DIR="$BASE_DIR/monitor-app"
MONITOR_SERVICE_FILE="/etc/systemd/system/proxmenux-monitor.service"
MONITOR_PORT=8008
# Offline installer envs
REPO_URL="https://github.com/MacRimi/ProxMenux.git"
TEMP_DIR="/tmp/proxmenux-install-$$"
# Load utility functions
NEON_PURPLE_BLUE="\033[38;5;99m"
WHITE="\033[38;5;15m"
RESET="\033[0m"
DARK_GRAY="\033[38;5;244m"
ORANGE="\033[38;5;208m"
YW="\033[33m"
YWB="\033[1;33m"
GN="\033[1;92m"
RD="\033[01;31m"
CL="\033[m"
BL="\033[36m"
DGN="\e[32m"
BGN="\e[1;32m"
DEF="\e[1;36m"
CUS="\e[38;5;214m"
BOLD="\033[1m"
BFR="\\r\\033[K"
HOLD="-"
BOR=" | "
CM="${GN}${CL}"
TAB=" "
# Create and display spinner
spinner() {
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local spin_i=0
local interval=0.1
printf "\e[?25l"
local color="${YW}"
while true; do
printf "\r ${color}%s${CL}" "${frames[spin_i]}"
spin_i=$(( (spin_i + 1) % ${#frames[@]} ))
sleep "$interval"
done
}
# Function to simulate typing effect
type_text() {
local text="$1"
local delay=0.05
for ((i=0; i<${#text}; i++)); do
echo -n "${text:$i:1}"
sleep $delay
done
echo
}
# Display info message with spinner
msg_info() {
local msg="$1"
echo -ne "${TAB}${YW}${HOLD}${msg}"
spinner &
SPINNER_PID=$!
}
# Display info2 message
msg_info2() {
local msg="$1"
echo -e "${TAB}${BOLD}${YW}${HOLD}${msg}${CL}"
}
# Display title script
msg_title() {
local msg="$1"
echo -e "\n"
echo -e "${TAB}${BOLD}${HOLD}${BOR}${msg}${BOR}${HOLD}${CL}"
echo -e "\n"
}
# Display warning or highlighted information message
msg_warn() {
if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then
kill $SPINNER_PID > /dev/null
fi
printf "\e[?25h"
local msg="$1"
echo -e "${BFR}${TAB}${CL} ${YWB}${msg}${CL}"
}
# Display success message
msg_ok() {
if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then
kill $SPINNER_PID > /dev/null
fi
printf "\e[?25h"
local msg="$1"
echo -e "${BFR}${TAB}${CM}${GN}${msg}${CL}"
}
# Display error message
msg_error() {
if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then
kill $SPINNER_PID > /dev/null
fi
printf "\e[?25h"
local msg="$1"
echo -e "${BFR}${TAB}${RD}[ERROR] ${msg}${CL}"
}
show_proxmenux_logo() {
clear
if [[ -z "$SSH_TTY" && -z "$(who am i | awk '{print $NF}' | grep -E '([0-9]{1,3}\.){3}[0-9]{1,3}')" ]]; then
# Logo for terminal noVNC
LOGO=$(cat << "EOF"
\e[0m\e[38;2;61;61;61m▆\e[38;2;60;60;60m▄\e[38;2;54;54;54m▂\e[0m \e[38;2;0;0;0m \e[0m \e[38;2;54;54;54m▂\e[38;2;60;60;60m▄\e[38;2;61;61;61m▆\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[38;2;61;61;61;48;2;37;37;37m▇\e[0m\e[38;2;60;60;60m▅\e[38;2;56;56;56m▃\e[38;2;37;37;37m▁ \e[38;2;36;36;36m▁\e[38;2;56;56;56m▃\e[38;2;60;60;60m▅\e[38;2;61;61;61;48;2;37;37;37m▇\e[48;2;62;62;62m \e[0m\e[7m\e[38;2;60;60;60m▁\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[7m\e[38;2;61;61;61m▂\e[0m\e[38;2;62;62;62;48;2;61;61;61m┈\e[48;2;62;62;62m \e[48;2;61;61;61m┈\e[0m\e[38;2;60;60;60m▆\e[38;2;57;57;57m▄\e[38;2;48;48;48m▂\e[0m \e[38;2;47;47;47m▂\e[38;2;57;57;57m▄\e[38;2;60;60;60m▆\e[38;2;62;62;62;48;2;61;61;61m┈\e[48;2;62;62;62m \e[48;2;61;61;61m┈\e[0m\e[7m\e[38;2;60;60;60m▂\e[38;2;57;57;57m▄\e[38;2;47;47;47m▆\e[0m \e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[7m\e[38;2;39;39;39m▇\e[38;2;57;57;57m▅\e[38;2;60;60;60m▃\e[0m\e[38;2;40;40;40;48;2;61;61;61m▁\e[48;2;62;62;62m \e[38;2;54;54;54;48;2;61;61;61m┊\e[48;2;62;62;62m \e[38;2;39;39;39;48;2;61;61;61m▁\e[0m\e[7m\e[38;2;60;60;60m▃\e[38;2;57;57;57m▅\e[38;2;38;38;38m▇\e[0m \e[38;2;193;60;2m▃\e[38;2;217;67;2m▅\e[38;2;225;70;2m▇\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[0m \e[38;2;203;63;2m▄\e[38;2;147;45;1m▂\e[0m \e[7m\e[38;2;55;55;55m▆\e[38;2;60;60;60m▄\e[38;2;61;61;61m▂\e[38;2;60;60;60m▄\e[38;2;55;55;55m▆\e[0m \e[38;2;144;44;1m▂\e[38;2;202;62;2m▄\e[38;2;219;68;2m▆\e[38;2;231;72;3;48;2;226;70;2m┈\e[48;2;231;72;3m \e[48;2;225;70;2m▉\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[7m\e[38;2;121;37;1m▉\e[0m\e[38;2;0;0;0;48;2;231;72;3m \e[0m\e[38;2;221;68;2m▇\e[38;2;208;64;2m▅\e[38;2;212;66;2m▂\e[38;2;123;37;0m▁\e[38;2;211;65;2m▂\e[38;2;207;64;2m▅\e[38;2;220;68;2m▇\e[48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m┈\e[0m\e[7m\e[38;2;221;68;2m▂\e[0m\e[38;2;44;13;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[0m \e[7m\e[38;2;190;59;2m▅\e[38;2;216;67;2m▃\e[38;2;225;70;2m▁\e[0m\e[38;2;95;29;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;230;71;2m┈\e[48;2;231;72;3m \e[0m\e[7m\e[38;2;225;70;2m▁\e[38;2;216;67;2m▃\e[38;2;191;59;2m▅\e[0m \e[38;2;0;0;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏ \e[0m \e[7m\e[38;2;172;53;1m▆\e[38;2;213;66;2m▄\e[38;2;219;68;2m▂\e[38;2;213;66;2m▄\e[38;2;174;54;2m▆\e[0m \e[38;2;0;0;0m \e[0m \e[38;2;0;0;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏ \e[0m \e[38;2;0;0;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
\e[7m\e[38;2;52;52;52m▆\e[38;2;59;59;59m▄\e[38;2;61;61;61m▂\e[0m\e[38;2;31;31;31m▏ \e[0m \e[7m\e[38;2;228;71;2m▂\e[38;2;221;69;2m▄\e[38;2;196;60;2m▆\e[0m
EOF
)
TEXT=(
""
""
"${BOLD}ProxMenux${RESET}"
""
"${BOLD}${NEON_PURPLE_BLUE}An Interactive Menu for${RESET}"
"${BOLD}${NEON_PURPLE_BLUE}Proxmox VE management${RESET}"
""
""
""
""
)
mapfile -t logo_lines <<< "$LOGO"
for i in {0..9}; do
echo -e "${TAB}${logo_lines[i]} ${WHITE}${RESET} ${TEXT[i]}"
done
echo -e
else
# Logo for terminal SSH
TEXT=(
""
""
""
""
"${BOLD}ProxMenux${RESET}"
""
"${BOLD}${NEON_PURPLE_BLUE}An Interactive Menu for${RESET}"
"${BOLD}${NEON_PURPLE_BLUE}Proxmox VE management${RESET}"
""
""
""
""
""
""
)
LOGO=(
"${DARK_GRAY}░░░░ ░░░░${RESET}"
"${DARK_GRAY}░░░░░░░ ░░░░░░ ${RESET}"
"${DARK_GRAY}░░░░░░░░░░░ ░░░░░░░ ${RESET}"
"${DARK_GRAY}░░░░ ░░░░░░ ░░░░░░ ${ORANGE}░░${RESET}"
"${DARK_GRAY}░░░░ ░░░░░░░ ${ORANGE}░░▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ░░░ ${ORANGE}░▒▒▒▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}▒▒▒░ ░▒▒▒▒▒▒▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}░▒▒▒▒▒ ▒▒▒▒▒░░ ▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}░░▒▒▒▒▒▒▒░░ ▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}░░░ ▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}▒▒▒░${RESET}"
"${DARK_GRAY} ░░ ${ORANGE}░░ ${RESET}"
)
for i in {0..12}; do
echo -e "${TAB}${LOGO[i]}${RESET} ${TEXT[i]}"
done
echo -e
fi
}
# ==========================================================
cleanup_corrupted_files() {
if [ -f "$CONFIG_FILE" ] && ! jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
echo "Cleaning up corrupted configuration file..."
rm -f "$CONFIG_FILE"
fi
if [ -f "$CACHE_FILE" ] && ! jq empty "$CACHE_FILE" >/dev/null 2>&1; then
echo "Cleaning up corrupted cache file..."
rm -f "$CACHE_FILE"
fi
}
# Cleanup function
cleanup() {
if [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
fi
}
# Set trap to ensure cleanup on exit
trap cleanup EXIT
# ==========================================================
check_existing_installation() {
local has_venv=false
local has_config=false
local has_language=false
local has_menu=false
if [ -f "$INSTALL_DIR/$MENU_SCRIPT" ]; then
has_menu=true
fi
if [ -d "$VENV_PATH" ] && [ -f "$VENV_PATH/bin/activate" ]; then
has_venv=true
fi
if [ -f "$CONFIG_FILE" ]; then
if jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
has_config=true
local current_language=$(jq -r '.language // empty' "$CONFIG_FILE" 2>/dev/null)
if [[ -n "$current_language" && "$current_language" != "null" && "$current_language" != "empty" ]]; then
has_language=true
fi
else
echo "Warning: Corrupted config file detected, removing..."
rm -f "$CONFIG_FILE"
fi
fi
if [ "$has_venv" = true ] && [ "$has_language" = true ]; then
echo "translation"
elif [ "$has_menu" = true ] && [ "$has_venv" = false ]; then
echo "normal"
elif [ "$has_menu" = true ]; then
echo "unknown"
else
echo "none"
fi
}
uninstall_proxmenux() {
local install_type="$1"
local force_clean="$2"
if [ "$force_clean" != "force" ]; then
if ! whiptail --title "Uninstall ProxMenux" --yesno "Are you sure you want to uninstall ProxMenux?" 10 60; then
return 1
fi
fi
echo "Uninstalling ProxMenux..."
if systemctl is-active --quiet proxmenux-monitor.service; then
echo "Stopping ProxMenux Monitor service..."
systemctl stop proxmenux-monitor.service
fi
if systemctl is-enabled --quiet proxmenux-monitor.service 2>/dev/null; then
echo "Disabling ProxMenux Monitor service..."
systemctl disable proxmenux-monitor.service
fi
if [ -f "$MONITOR_SERVICE_FILE" ]; then
echo "Removing ProxMenux Monitor service file..."
rm -f "$MONITOR_SERVICE_FILE"
systemctl daemon-reload
fi
if [ -d "$MONITOR_INSTALL_DIR" ]; then
echo "Removing ProxMenux Monitor directory..."
rm -rf "$MONITOR_INSTALL_DIR"
fi
if [ -f "$VENV_PATH/bin/activate" ]; then
echo "Removing googletrans and virtual environment..."
source "$VENV_PATH/bin/activate"
pip uninstall -y googletrans >/dev/null 2>&1
deactivate
rm -rf "$VENV_PATH"
fi
if [ "$install_type" = "translation" ] && [ "$force_clean" != "force" ]; then
DEPS_TO_REMOVE=$(whiptail --title "Remove Translation Dependencies" --checklist \
"Select translation-specific dependencies to remove:" 15 60 3 \
"python3-venv" "Python virtual environment" OFF \
"python3-pip" "Python package installer" OFF \
"python3" "Python interpreter" OFF \
3>&1 1>&2 2>&3)
if [ -n "$DEPS_TO_REMOVE" ]; then
echo "Removing selected dependencies..."
read -r -a DEPS_ARRAY <<< "$(echo "$DEPS_TO_REMOVE" | tr -d '"')"
for dep in "${DEPS_ARRAY[@]}"; do
echo "Removing $dep..."
apt-mark auto "$dep" >/dev/null 2>&1
apt-get -y --purge autoremove "$dep" >/dev/null 2>&1
done
apt-get autoremove -y --purge >/dev/null 2>&1
fi
fi
rm -f "$INSTALL_DIR/$MENU_SCRIPT"
rm -rf "$BASE_DIR"
[ -f /root/.bashrc.bak ] && mv /root/.bashrc.bak /root/.bashrc
if [ -f /etc/motd.bak ]; then
mv /etc/motd.bak /etc/motd
else
sed -i '/This system is optimised by: ProxMenux/d' /etc/motd
fi
echo "ProxMenux has been uninstalled."
return 0
}
handle_installation_change() {
local current_type="$1"
local new_type="$2"
if [ "$current_type" = "$new_type" ]; then
return 0
fi
case "$current_type-$new_type" in
"translation-1"|"translation-normal")
if whiptail --title "Installation Type Change" \
--yesno "Switch from Translation to Normal Version?\n\nThis will remove translation components." 10 60; then
echo "Preparing for installation type change..."
uninstall_proxmenux "translation" "force" >/dev/null 2>&1
return 0
else
return 1
fi
;;
"normal-2"|"normal-translation")
if whiptail --title "Installation Type Change" \
--yesno "Switch from Normal to Translation Version?\n\nThis will add translation components." 10 60; then
return 0
else
return 1
fi
;;
*)
return 0
;;
esac
}
update_config() {
local component="$1"
local status="$2"
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local tracked_components=("dialog" "curl" "jq" "git" "python3" "python3-venv" "python3-pip" "virtual_environment" "pip" "googletrans" "proxmenux_monitor")
if [[ " ${tracked_components[@]} " =~ " ${component} " ]]; then
mkdir -p "$(dirname "$CONFIG_FILE")"
if [ ! -f "$CONFIG_FILE" ] || ! jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
echo '{}' > "$CONFIG_FILE"
fi
local tmp_file=$(mktemp)
if jq --arg comp "$component" --arg stat "$status" --arg time "$timestamp" \
'.[$comp] = {status: $stat, timestamp: $time}' "$CONFIG_FILE" > "$tmp_file" 2>/dev/null; then
mv "$tmp_file" "$CONFIG_FILE"
else
echo '{}' > "$CONFIG_FILE"
jq --arg comp "$component" --arg stat "$status" --arg time "$timestamp" \
'.[$comp] = {status: $stat, timestamp: $time}' "$CONFIG_FILE" > "$tmp_file" && mv "$tmp_file" "$CONFIG_FILE"
fi
[ -f "$tmp_file" ] && rm -f "$tmp_file"
fi
}
show_progress() {
local step="$1"
local total="$2"
local message="$3"
echo -e "\n${BOLD}${BL}${TAB}Installing ProxMenux: Step $step of $total${CL}"
echo
msg_info2 "$message"
}
select_language() {
if [ -f "$CONFIG_FILE" ] && jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
local existing_language=$(jq -r '.language // empty' "$CONFIG_FILE" 2>/dev/null)
if [[ -n "$existing_language" && "$existing_language" != "null" && "$existing_language" != "empty" ]]; then
LANGUAGE="$existing_language"
msg_ok "Using existing language configuration: $LANGUAGE"
return 0
fi
fi
LANGUAGE=$(whiptail --title "Select Language" --menu "Choose a language for the menu:" 20 60 12 \
"en" "English (Recommended)" \
"es" "Spanish" \
"fr" "French" \
"de" "German" \
"it" "Italian" \
"pt" "Portuguese" 3>&1 1>&2 2>&3)
if [ -z "$LANGUAGE" ]; then
msg_error "No language selected. Exiting."
exit 1
fi
mkdir -p "$(dirname "$CONFIG_FILE")"
if [ ! -f "$CONFIG_FILE" ] || ! jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
echo '{}' > "$CONFIG_FILE"
fi
local tmp_file=$(mktemp)
if jq --arg lang "$LANGUAGE" '. + {language: $lang}' "$CONFIG_FILE" > "$tmp_file" 2>/dev/null; then
mv "$tmp_file" "$CONFIG_FILE"
else
echo "{\"language\": \"$LANGUAGE\"}" > "$CONFIG_FILE"
fi
[ -f "$tmp_file" ] && rm -f "$tmp_file"
msg_ok "Language set to: $LANGUAGE"
}
# Show installation confirmation for new installations
show_installation_confirmation() {
local install_type="$1"
case "$install_type" in
"1")
if whiptail --title "ProxMenux - Normal Version Installation" \
--yesno "ProxMenux Normal Version will install:\n\n• dialog (interactive menus) - Official Debian package\n• curl (file downloads) - Official Debian package\n• jq (JSON processing) - Official Debian package\n• ProxMenux core files (/usr/local/share/proxmenux)\n• ProxMenux Monitor (Web dashboard on port 8008)\n\nThis is a lightweight installation with minimal dependencies.\n\nProceed with installation?" 20 70; then
return 0
else
return 1
fi
;;
"2")
if whiptail --title "ProxMenux - Translation Version Installation" \
--yesno "ProxMenux Translation Version will install:\n\n• dialog (interactive menus)\n• curl (file downloads)\n• jq (JSON processing)\n• python3 + python3-venv + python3-pip\n• Google Translate library (googletrans)\n• Virtual environment (/opt/googletrans-env)\n• Translation cache system\n• ProxMenux core files\n• ProxMenux Monitor (Web dashboard on port 8008)\n\nThis version requires more dependencies for translation support.\n\nProceed with installation?" 20 70; then
return 0
else
return 1
fi
;;
esac
}
get_server_ip() {
local ip
# Try to get the primary IP address
ip=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+')
if [ -z "$ip" ]; then
# Fallback: get first non-loopback IP
ip=$(hostname -I | awk '{print $1}')
fi
if [ -z "$ip" ]; then
# Last resort: use localhost
ip="localhost"
fi
echo "$ip"
}
detect_latest_appimage() {
local appimage_dir="$TEMP_DIR/AppImage"
if [ ! -d "$appimage_dir" ]; then
return 1
fi
local latest_appimage=$(find "$appimage_dir" -name "ProxMenux-*.AppImage" -type f | sort -V | tail -1)
if [ -z "$latest_appimage" ]; then
return 1
fi
echo "$latest_appimage"
return 0
}
get_appimage_version() {
local appimage_path="$1"
local filename=$(basename "$appimage_path")
# Match any dotted number sequence + optional pre-release suffix
# (e.g. "-beta"). The previous `[0-9]+\.[0-9]+\.[0-9]+` was hardcoded
# to three segments and dropped both the fourth segment AND the
# `-beta` suffix on a name like `ProxMenux-1.2.1.2-beta.AppImage`.
local version=$(echo "$filename" | grep -oP 'ProxMenux-\K[0-9]+(?:\.[0-9]+)+(?:-[A-Za-z0-9]+)?')
echo "$version"
}
# ── AppImage runtime extraction ────────────────────────────
# Extract the AppImage's squashfs to a stable directory and run AppRun
# directly. Avoids the FUSE mount under /tmp/.mount_ProxMe<random>, which
# trips Wazuh rule 521 / rkhunter "Possible kernel level rootkit" alerts
# (issue #101) — those scanners flag any directory that appears in
# readdir() but is hidden from lstat(), which is exactly what AppImage's
# FUSE mount layer looks like to them. Running from a plain extracted
# directory has the same files but no FUSE indirection, so the false
# positive disappears.
extract_appimage_to_runtime_dir() {
local appimage_path="$1"
local target_runtime_dir="$2"
local tmp_extract_dir
tmp_extract_dir=$(mktemp -d /tmp/proxmenux-extract.XXXXXX) || return 1
msg_info "Extracting AppImage runtime to ${target_runtime_dir}..."
if ! ( cd "$tmp_extract_dir" && "$appimage_path" --appimage-extract >/dev/null 2>&1 ); then
msg_error "Failed to extract AppImage."
rm -rf "$tmp_extract_dir"
return 1
fi
if [ ! -x "$tmp_extract_dir/squashfs-root/AppRun" ]; then
msg_error "Extracted AppImage missing AppRun."
rm -rf "$tmp_extract_dir"
return 1
fi
rm -rf "${target_runtime_dir}.new"
mv "$tmp_extract_dir/squashfs-root" "${target_runtime_dir}.new"
rm -rf "$tmp_extract_dir"
if [ -d "$target_runtime_dir" ]; then
rm -rf "${target_runtime_dir}.old"
mv "$target_runtime_dir" "${target_runtime_dir}.old"
fi
mv "${target_runtime_dir}.new" "$target_runtime_dir"
rm -rf "${target_runtime_dir}.old"
rm -f "$appimage_path"
msg_ok "AppImage runtime extracted (no FUSE mount; bypasses Wazuh rule 521)."
return 0
}
install_proxmenux_monitor() {
local appimage_source=$(detect_latest_appimage)
if [ -z "$appimage_source" ] || [ ! -f "$appimage_source" ]; then
msg_error "ProxMenux Monitor AppImage not found in $TEMP_DIR/AppImage/"
msg_warn "Please ensure the AppImage directory exists with ProxMenux-*.AppImage files."
update_config "proxmenux_monitor" "appimage_not_found"
return 1
fi
local appimage_version=$(get_appimage_version "$appimage_source")
if systemctl is-active --quiet proxmenux-monitor.service; then
systemctl stop proxmenux-monitor.service
fi
local service_exists=false
if [ -f "$MONITOR_SERVICE_FILE" ]; then
service_exists=true
fi
local sha256_file="$TEMP_DIR/AppImage/ProxMenux-Monitor.AppImage.sha256"
if [ -f "$sha256_file" ]; then
msg_info "Verifying AppImage integrity..."
local expected_hash=$(cat "$sha256_file" | grep -Eo '^[a-f0-9]+' | tr -d '\n')
local actual_hash=$(sha256sum "$appimage_source" | awk '{print $1}')
if [ "$expected_hash" != "$actual_hash" ]; then
msg_error "SHA256 verification failed! AppImage may be corrupted."
return 1
fi
msg_ok "SHA256 verification passed."
else
msg_warn "SHA256 checksum not available. Skipping verification."
fi
mkdir -p "$MONITOR_INSTALL_DIR"
local target_path="$MONITOR_INSTALL_DIR/ProxMenux-Monitor.AppImage"
cp "$appimage_source" "$target_path"
chmod +x "$target_path"
if ! extract_appimage_to_runtime_dir "$target_path" "$MONITOR_RUNTIME_DIR"; then
update_config "proxmenux_monitor" "extract_failed"
return 1
fi
msg_ok "ProxMenux Monitor v$appimage_version installed."
if [ "$service_exists" = false ]; then
return 0 # New installation - service needs to be created
else
# The v1.2.2 install layout extracts the AppImage into
# MONITOR_RUNTIME_DIR/ and runs AppRun out of that directory
# (`extract_appimage_to_runtime_dir` above), so the unit must
# point at AppRun — not at the bare AppImage. Existing users
# updating from v1.2.1.x stable still have a unit whose
# ExecStart targets `/usr/local/share/proxmenux/ProxMenux-Monitor.AppImage`
# which was fine when the AppImage was FUSE-mounted but breaks
# under PVE 9.x / Debian 13 (status=203/EXEC, GitHub issue #222).
# Rewrite the unit on every update — idempotent for users
# whose unit is already correct.
_proxmenux_rewrite_monitor_unit_for_apprun
systemctl start proxmenux-monitor.service
sleep 2
if systemctl is-active --quiet proxmenux-monitor.service; then
update_config "proxmenux_monitor" "updated"
return 2 # Update successful
else
msg_warn "Service failed to restart. Check: journalctl -u proxmenux-monitor"
update_config "proxmenux_monitor" "failed"
return 1
fi
fi
}
# Idempotent rewriter of the proxmenux-monitor unit file. Used by the
# update path in `install_proxmenux_monitor` so that existing
# installations updated to v1.2.2+ get their ExecStart corrected to
# point at the extracted AppRun even when the unit already exists.
# Mirrors `create_monitor_service`'s unit body so both code paths
# converge on the same file content. Returns 0 always; failures are
# logged so the surrounding flow can still attempt the start and
# report a more accurate failure to the user.
_proxmenux_rewrite_monitor_unit_for_apprun() {
local exec_path="$MONITOR_RUNTIME_DIR/AppRun"
if [ -f "$TEMP_DIR/systemd/proxmenux-monitor.service" ]; then
sed "s|ExecStart=.*|ExecStart=$exec_path|g" \
"$TEMP_DIR/systemd/proxmenux-monitor.service" > "$MONITOR_SERVICE_FILE"
else
cat > "$MONITOR_SERVICE_FILE" << EOF
[Unit]
Description=ProxMenux Monitor - Web Dashboard
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=$MONITOR_INSTALL_DIR
ExecStart=$exec_path
Restart=on-failure
RestartSec=10
Environment="PORT=$MONITOR_PORT"
[Install]
WantedBy=multi-user.target
EOF
fi
systemctl daemon-reload
return 0
}
create_monitor_service() {
local exec_path="$MONITOR_RUNTIME_DIR/AppRun"
if [ -f "$TEMP_DIR/systemd/proxmenux-monitor.service" ]; then
sed "s|ExecStart=.*|ExecStart=$exec_path|g" \
"$TEMP_DIR/systemd/proxmenux-monitor.service" > "$MONITOR_SERVICE_FILE"
msg_ok "Using service file from repository."
else
cat > "$MONITOR_SERVICE_FILE" << EOF
[Unit]
Description=ProxMenux Monitor - Web Dashboard
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=$MONITOR_INSTALL_DIR
ExecStart=$exec_path
Restart=on-failure
RestartSec=10
Environment="PORT=$MONITOR_PORT"
[Install]
WantedBy=multi-user.target
EOF
msg_ok "Created default service file."
fi
systemctl daemon-reload
systemctl enable proxmenux-monitor.service > /dev/null 2>&1
systemctl start proxmenux-monitor.service > /dev/null 2>&1
sleep 3
if systemctl is-active --quiet proxmenux-monitor.service; then
msg_ok "ProxMenux Monitor service started successfully."
update_config "proxmenux_monitor" "installed"
return 0
else
msg_warn "ProxMenux Monitor service failed to start."
msg_info2 "Check logs with: journalctl -u proxmenux-monitor -n 20"
msg_info2 "Check status with: systemctl status proxmenux-monitor"
update_config "proxmenux_monitor" "failed"
return 1
fi
}
install_normal_version() {
local total_steps=5
local current_step=1
show_progress $current_step $total_steps "Installing basic dependencies."
if ! command -v jq > /dev/null 2>&1; then
apt-get update > /dev/null 2>&1
if apt-get install -y jq > /dev/null 2>&1 && command -v jq > /dev/null 2>&1; then
update_config "jq" "installed"
else
local jq_url="https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64"
if wget -q -O /usr/local/bin/jq "$jq_url" 2>/dev/null && chmod +x /usr/local/bin/jq; then
if command -v jq > /dev/null 2>&1; then
update_config "jq" "installed_from_github"
else
msg_error "Failed to install jq. Please install it manually."
update_config "jq" "failed"
return 1
fi
else
msg_error "Failed to install jq from both APT and GitHub. Please install it manually."
update_config "jq" "failed"
return 1
fi
fi
else
update_config "jq" "already_installed"
fi
BASIC_DEPS=("dialog" "curl" "git")
if [ -z "${APT_UPDATED:-}" ]; then
apt-get update -y > /dev/null 2>&1 || true
APT_UPDATED=1
fi
for pkg in "${BASIC_DEPS[@]}"; do
# Strict per-package check — see comment in install_translation_version().
if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "ok installed"; then
if apt-get install -y "$pkg" > /dev/null 2>&1; then
update_config "$pkg" "installed"
else
msg_error "Failed to install $pkg. Please install it manually."
update_config "$pkg" "failed"
return 1
fi
else
update_config "$pkg" "already_installed"
fi
done
if ! command -v git > /dev/null 2>&1; then
msg_info "Installing git (required to clone the ProxMenux repository)."
if [ -z "${APT_UPDATED:-}" ]; then
apt-get update -y > /dev/null 2>&1 || true
APT_UPDATED=1
fi
if ! apt-get install -y git > /dev/null 2>&1; then
msg_error "Failed to install git. Please run 'apt-get install git' manually and rerun the installer."
update_config "git" "failed"
return 1
fi
if ! command -v git > /dev/null 2>&1; then
msg_error "Git is still not available after installation. Aborting to avoid a broken setup."
update_config "git" "failed"
return 1
fi
update_config "git" "installed"
else
update_config "git" "already_installed"
fi
msg_ok "jq, dialog, curl and git installed successfully."
((current_step++))
show_progress $current_step $total_steps "Install ProxMenux repository"
msg_info "Cloning ProxMenux repositoryy."
if ! git clone --depth 1 "$REPO_URL" "$TEMP_DIR" 2>/dev/null; then
msg_error "Failed to clone repository from $REPO_URL"
exit 1
fi
msg_ok "Repository cloned successfully."
cd "$TEMP_DIR"
((current_step++))
show_progress $current_step $total_steps "Creating directories and configuration"
mkdir -p "$BASE_DIR"
mkdir -p "$INSTALL_DIR"
if [ ! -f "$CONFIG_FILE" ]; then
echo '{}' > "$CONFIG_FILE"
fi
msg_ok "Directories and configuration created."
((current_step++))
show_progress $current_step $total_steps "Copying necessary files"
cp "./scripts/utils.sh" "$UTILS_FILE"
cp "./menu" "$INSTALL_DIR/$MENU_SCRIPT"
cp "./version.txt" "$LOCAL_VERSION_FILE"
cp "./install_proxmenux.sh" "$BASE_DIR/install_proxmenux.sh"
# A user that previously rode the beta train and then switched back
# to stable would still have a leftover beta_version.txt under
# $BASE_DIR, which makes the `menu` update check (check_updates_beta)
# offer a "Beta update available" prompt on top of the legitimate
# stable one. Clearing the marker on every stable install/update
# keeps the stable install honestly stable — if the user opts into
# the beta program again, the beta installer will recreate the file.
rm -f "$BASE_DIR/beta_version.txt"
# Wipe the scripts tree before copying so any file removed upstream
# (renamed, consolidated, deprecated) disappears from the user install.
# Only $BASE_DIR/scripts/ is cleared; config.json, cache.json,
# components_status.json, version.txt, monitor.db, smart/, oci/ and
# the AppImage live outside this path and are preserved.
rm -rf "$BASE_DIR/scripts"
mkdir -p "$BASE_DIR/scripts"
cp -r "./scripts/"* "$BASE_DIR/scripts/"
# Only .sh files need the executable bit. Applying +x recursively would
# also flag README.md, .json, .py etc. as executable for no reason.
find "$BASE_DIR/scripts" -type f -name '*.sh' -exec chmod +x {} +
chmod +x "$BASE_DIR/install_proxmenux.sh"
msg_ok "Necessary files created."
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
((current_step++))
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
install_proxmenux_monitor
local monitor_status=$?
if [ $monitor_status -eq 0 ]; then
create_monitor_service
fi
msg_ok "ProxMenux Normal Version installation completed successfully."
}
install_translation_version() {
local total_steps=5
local current_step=1
show_progress $current_step $total_steps "Language selection"
select_language
((current_step++))
show_progress $current_step $total_steps "Installing system dependencies"
if ! command -v jq > /dev/null 2>&1; then
apt-get update > /dev/null 2>&1
if apt-get install -y jq > /dev/null 2>&1 && command -v jq > /dev/null 2>&1; then
update_config "jq" "installed"
else
local jq_url="https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64"
if wget -q -O /usr/local/bin/jq "$jq_url" 2>/dev/null && chmod +x /usr/local/bin/jq; then
if command -v jq > /dev/null 2>&1; then
update_config "jq" "installed_from_github"
else
msg_error "Failed to install jq. Please install it manually."
update_config "jq" "failed"
return 1
fi
else
msg_error "Failed to install jq from both APT and GitHub. Please install it manually."
update_config "jq" "failed"
return 1
fi
fi
else
update_config "jq" "already_installed"
fi
DEPS=("dialog" "curl" "git" "python3" "python3-venv" "python3-pip")
for pkg in "${DEPS[@]}"; do
# `dpkg -l | grep -qw "$pkg"` treats `-` as a word boundary, so a
# query for `python3` would falsely match `python3-pip` and skip
# the real `python3` install. `dpkg-query -W -f='${Status}'` asks
# for the EXACT package and reports "install ok installed" only
# when truly present. Issue #205 traced back here.
if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "ok installed"; then
if apt-get install -y "$pkg" > /dev/null 2>&1; then
update_config "$pkg" "installed"
else
msg_error "Failed to install $pkg. Please install it manually."
update_config "$pkg" "failed"
return 1
fi
else
update_config "$pkg" "already_installed"
fi
done
msg_ok "jq, dialog, curl, git, python3, python3-venv and python3-pip installed successfully."
((current_step++))
show_progress $current_step $total_steps "Setting up translation environment"
if [ ! -d "$VENV_PATH" ] || [ ! -f "$VENV_PATH/bin/activate" ]; then
python3 -m venv --system-site-packages "$VENV_PATH" > /dev/null 2>&1
if [ ! -f "$VENV_PATH/bin/activate" ]; then
msg_error "Failed to create virtual environment. Please check your Python installation."
update_config "virtual_environment" "failed"
return 1
else
update_config "virtual_environment" "created"
fi
else
update_config "virtual_environment" "already_exists"
fi
source "$VENV_PATH/bin/activate"
if pip install --upgrade pip > /dev/null 2>&1; then
update_config "pip" "upgraded"
else
msg_error "Failed to upgrade pip."
update_config "pip" "upgrade_failed"
return 1
fi
if pip install --break-system-packages --no-cache-dir googletrans==4.0.0-rc1 > /dev/null 2>&1; then
update_config "googletrans" "installed"
else
msg_error "Failed to install googletrans. Please check your internet connection."
update_config "googletrans" "failed"
deactivate
return 1
fi
deactivate
show_progress $current_step $total_steps "Cloning ProxMenux repository"
if ! git clone --depth 1 "$REPO_URL" "$TEMP_DIR" 2>/dev/null; then
msg_error "Failed to clone repository from $REPO_URL"
exit 1
fi
msg_ok "Repository cloned successfully."
cd "$TEMP_DIR"
((current_step++))
show_progress $current_step $total_steps "Copying necessary files"
mkdir -p "$BASE_DIR"
mkdir -p "$INSTALL_DIR"
cp "./json/cache.json" "$CACHE_FILE"
msg_ok "Cache file copied with translations."
cp "./scripts/utils.sh" "$UTILS_FILE"
cp "./menu" "$INSTALL_DIR/$MENU_SCRIPT"
cp "./version.txt" "$LOCAL_VERSION_FILE"
cp "./install_proxmenux.sh" "$BASE_DIR/install_proxmenux.sh"
# Clear any leftover beta_version.txt — see the equivalent block
# in the update path above for the rationale (in short: prevents
# the menu from offering a phantom "Beta update available" after a
# user has switched back to the stable channel).
rm -f "$BASE_DIR/beta_version.txt"
mkdir -p "$BASE_DIR/scripts"
cp -r "./scripts/"* "$BASE_DIR/scripts/"
chmod -R +x "$BASE_DIR/scripts/"
chmod +x "$BASE_DIR/install_proxmenux.sh"
msg_ok "Necessary files created."
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
((current_step++))
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
install_proxmenux_monitor
local monitor_status=$?
if [ $monitor_status -eq 0 ]; then
create_monitor_service
elif [ $monitor_status -eq 2 ]; then
msg_ok "ProxMenux Monitor updated successfully."
fi
msg_ok "ProxMenux Translation Version installation completed successfully."
}
show_installation_options() {
local current_install_type
current_install_type=$(check_existing_installation)
local pve_version
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
local menu_title="ProxMenux Installation"
local menu_text="Choose installation type:"
if [ "$current_install_type" != "none" ]; then
case "$current_install_type" in
"translation")
menu_title="ProxMenux Update - Translation Version Detected"
;;
"normal")
menu_title="ProxMenux Update - Normal Version Detected"
;;
"unknown")
menu_title="ProxMenux Update - Existing Installation Detected"
;;
esac
fi
if [[ "$pve_version" -ge 9 ]]; then
INSTALL_TYPE=$(whiptail --backtitle "ProxMenux" --title "$menu_title" --menu "\n$menu_text" 14 70 2 \
"1" "Normal Version (English only)" 3>&1 1>&2 2>&3)
if [ -z "$INSTALL_TYPE" ]; then
show_proxmenux_logo
msg_warn "Installation cancelled."
exit 1
fi
else
INSTALL_TYPE=$(whiptail --backtitle "ProxMenux" --title "$menu_title" --menu "\n$menu_text" 14 70 2 \
"1" "Normal Version (English only)" \
"2" "Translation Version (Multi-language support)" 3>&1 1>&2 2>&3)
if [ -z "$INSTALL_TYPE" ]; then
show_proxmenux_logo
msg_warn "Installation cancelled."
exit 1
fi
fi
if [ -z "$INSTALL_TYPE" ]; then
show_proxmenux_logo
msg_warn "Installation cancelled."
exit 1
fi
if [ "$current_install_type" = "none" ]; then
if ! show_installation_confirmation "$INSTALL_TYPE"; then
show_proxmenux_logo
msg_warn "Installation cancelled."
exit 1
fi
fi
if ! handle_installation_change "$current_install_type" "$INSTALL_TYPE"; then
show_proxmenux_logo
msg_warn "Installation cancelled."
exit 1
fi
}
install_proxmenux() {
show_installation_options
case "$INSTALL_TYPE" in
"1")
show_proxmenux_logo
msg_title "Installing ProxMenux - Normal Version"
install_normal_version
;;
"2")
show_proxmenux_logo
msg_title "Installing ProxMenux - Translation Version"
install_translation_version
;;
*)
msg_error "Invalid option selected."
exit 1
;;
esac
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
# ── Legacy gpu-guard hookscript auto-cleanup ──────────────
# Previous ProxMenux versions attached a hookscript to VMs/LXCs with GPU
# passthrough; that reference in the guest .conf broke backup/restore to
# hosts without the snippet. The hookscript system has been removed.
# This silently purges any leftover references and the snippet file.
# Idempotent: does nothing on hosts that never had the legacy hook.
if [ -x "$LOCAL_SCRIPTS/global/cleanup_gpu_hookscripts.sh" ]; then
bash "$LOCAL_SCRIPTS/global/cleanup_gpu_hookscripts.sh" || true
fi
msg_title "ProxMenux has been installed successfully"
if systemctl is-active --quiet proxmenux-monitor.service; then
local server_ip=$(get_server_ip)
echo -e "${GN}🌐 ProxMenux Monitor activated${CL}: ${BL}http://${server_ip}:${MONITOR_PORT}${CL}"
echo
fi
echo -ne "${GN}"
type_text "To run ProxMenux, simply execute this command in the console or terminal:"
echo -e "${YWB} menu${CL}"
echo
# -------
exit 0
}
if [ "$(id -u)" -ne 0 ]; then
msg_error "This script must be run as root."
exit 1
fi
cleanup_corrupted_files
install_proxmenux