873 lines
29 KiB
Bash
873 lines
29 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) to display sensors information.
|
|
#
|
|
|
|
################### Configuration #############
|
|
|
|
# Display configuration for HDD, NVME, CPU
|
|
# Set to 0 to disable line breaks
|
|
# Note: use these settings only if the displayed layout is broken
|
|
CPU_ITEMS_PER_ROW=0
|
|
NVME_ITEMS_PER_ROW=0
|
|
HDD_ITEMS_PER_ROW=0
|
|
|
|
# Known CPU sensor names. They can be full or partial but should ensure unambiguous identification.
|
|
# Should new ones be added, also update logic in configure() function.
|
|
KNOWN_CPU_SENSORS=("coretemp-isa-" "k10temp-pci-")
|
|
|
|
# Overwrite default backup location
|
|
BACKUP_DIR=""
|
|
|
|
##################### DO NOT EDIT BELOW #######################
|
|
# Only to be used to debug on other systems. Save the "sensor -j" output into a json file.
|
|
# Information will be loaded for script configuration and presented in Proxmox.
|
|
|
|
# DEV NOTE: lm-sensors version >3.6.0 breakes properly formatted JSON output using 'sensors -j'. This implements a workaround using uses a python3 for formatting
|
|
|
|
DEBUG_REMOTE=false
|
|
DEBUG_JSON_FILE="/tmp/sensordata.json"
|
|
DEBUG_UPS_FILE="/tmp/upsc.txt"
|
|
|
|
# This script's working directory
|
|
SCRIPT_CWD="$(dirname "$(readlink -f "$0")")"
|
|
|
|
# Debug location
|
|
JSON_EXPORT_DIRECTORY="$SCRIPT_CWD"
|
|
JSON_EXPORT_FILENAME="sensorsdata.json"
|
|
|
|
# File paths
|
|
PVE_MANAGER_LIB_JS_FILE="/usr/share/pve-manager/js/pvemanagerlib.js"
|
|
PVE_MOD_JS_SOURCE_FILE="$SCRIPT_CWD/PveMod_PveNodeStatusView.js"
|
|
PVE_MOD_JS_TARGET_FILE="/usr/share/pve-manager/js/PveMod_PveNodeStatusView.js"
|
|
NODES_PM_FILE="/usr/share/perl5/PVE/API2/Nodes.pm"
|
|
PVE_SENSOR_INFO_MOD_FILE="/usr/share/perl5/PVE/API2/PveMod_SensorInfo.pm"
|
|
PVE_SENSOR_INFO_SOURCE_FILE="$SCRIPT_CWD/PveMod_SensorInfo.pm"
|
|
|
|
#region message tools
|
|
# Section header (bold)
|
|
function msgb() {
|
|
local message="$1"
|
|
echo -e "\e[1m${message}\e[0m"
|
|
}
|
|
|
|
# Info (green)
|
|
function info() {
|
|
local message="$1"
|
|
echo -e "\e[0;32m[info] ${message}\e[0m"
|
|
}
|
|
|
|
# Warning (yellow)
|
|
function warn() {
|
|
local message="$1"
|
|
echo -e "\e[0;33m[warning] ${message}\e[0m"
|
|
}
|
|
|
|
# Error (red)
|
|
function err() {
|
|
local message="$1"
|
|
echo -e "\e[0;31m[error] ${message}\e[0m"
|
|
exit 1
|
|
}
|
|
|
|
# Prompts (cyan or bold)
|
|
function ask() {
|
|
local prompt="$1"
|
|
local response
|
|
read -p $'\n\e[1;36m'"${prompt}:"$'\e[0m ' response
|
|
echo "$response"
|
|
}
|
|
#endregion message tools
|
|
|
|
# Function to display usage information
|
|
function usage {
|
|
msgb "\nUsage:\n$0 [install | uninstall | save-sensors-data]\n"
|
|
exit 1
|
|
}
|
|
|
|
# System checks
|
|
function check_root_privileges() {
|
|
[[ $EUID -eq 0 ]] || err "This script must be run as root. Please run it with 'sudo $0'."
|
|
info "Root privileges verified."
|
|
}
|
|
|
|
# Define a function to install packages
|
|
function install_packages {
|
|
# Check if the 'sensors' command is available on the system
|
|
if (! command -v sensors &>/dev/null); then
|
|
# If the 'sensors' command is not available, prompt the user to install lm-sensors
|
|
local choiceInstallLmSensors=$(ask "lm-sensors is not installed. Would you like to install it? (y/n)")
|
|
case "$choiceInstallLmSensors" in
|
|
[yY])
|
|
# If the user chooses to install lm-sensors, update the package list and install the package
|
|
apt-get update
|
|
apt-get install lm-sensors
|
|
;;
|
|
[nN])
|
|
# If the user chooses not to install lm-sensors, exit the script with a zero status code
|
|
msgb "Decided to not install lm-sensors. The mod cannot run without it. Exiting..."
|
|
err "lm-sensors is required. Exiting..."
|
|
;;
|
|
*)
|
|
# If the user enters an invalid input, print an error message and exit the script with a non-zero status code
|
|
err "Invalid input. Exiting..."
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# Check if lm-sensors is installed correctly and exit if not
|
|
if (! command -v sensors &>/dev/null); then
|
|
err "lm-sensors installation failed or 'sensors' command is not available. Please install lm-sensors manually and re-run the script."
|
|
fi
|
|
}
|
|
|
|
# Main configuration function to detect sensors and set up parameters
|
|
function configure {
|
|
SENSORS_DETECTED=false
|
|
local sensorsOutput
|
|
local sanitisedSensorsOutput
|
|
local upsOutput
|
|
local modelName
|
|
local upsConnection
|
|
|
|
install_packages
|
|
|
|
#### Collect sensor data ####
|
|
#region sensors collection
|
|
if [ "$DEBUG_REMOTE" = true ]; then
|
|
warn "Remote debugging is used. Sensor readings from dump file $DEBUG_JSON_FILE will be used."
|
|
warn "Remote debugging is used. UPS readings from dump file $DEBUG_UPS_FILE will be used."
|
|
sensorsOutput=$(cat "$DEBUG_JSON_FILE")
|
|
else
|
|
sensorsOutput=$(sensors -j 2>/dev/null)
|
|
fi
|
|
|
|
# Apply lm-sensors sanitization
|
|
sanitisedSensorsOutput=$(sanitize_sensors_output "$sensorsOutput")
|
|
|
|
if [ $? -ne 0 ]; then
|
|
err "Sensor output error.\n\nCommand output:\n${sanitisedSensorsOutput}\n\nExiting..."
|
|
fi
|
|
#endregion sensors collection
|
|
|
|
#### CPU ####
|
|
#region CPU setup
|
|
msgb "\n=== Detecting CPU temperature sensors ==="
|
|
ENABLE_CPU=false
|
|
local cpuList=""
|
|
local cpuCount=0
|
|
|
|
# Find all CPU sensors that match known patterns
|
|
for pattern in "${KNOWN_CPU_SENSORS[@]}"; do
|
|
found_sensors=$(echo "$sanitisedSensorsOutput" | grep -o "\"${pattern}[^\"]*\"" | sed 's/"//g')
|
|
if [ -n "$found_sensors" ]; then
|
|
while read -r sensor; do
|
|
if [ -n "$sensor" ]; then
|
|
cpuCount=$((cpuCount + 1))
|
|
if [ -z "$cpuList" ]; then
|
|
cpuList="$sensor"
|
|
else
|
|
cpuList="$cpuList,$sensor"
|
|
fi
|
|
ENABLE_CPU=true
|
|
fi
|
|
done <<< "$found_sensors"
|
|
fi
|
|
done
|
|
|
|
if [ "$ENABLE_CPU" = true ]; then
|
|
info "Detected CPU sensors ($cpuCount): $cpuList"
|
|
SENSORS_DETECTED=true
|
|
while true; do
|
|
local choice=$(ask "Display temperatures for all cores [C] or average per CPU [a] (some newer AMD variants support per die)? (C/a)")
|
|
case "$choice" in
|
|
[cC]|"")
|
|
CPU_TEMP_TARGET="Core"
|
|
info "Temperatures will be displayed for all cores."
|
|
break
|
|
;;
|
|
[aA])
|
|
CPU_TEMP_TARGET="Package"
|
|
info "An average temperature will be displayed per CPU."
|
|
break
|
|
;;
|
|
*)
|
|
warn "Invalid input, please choose C or a."
|
|
;;
|
|
esac
|
|
done
|
|
else
|
|
warn "No CPU temperature sensors found."
|
|
fi
|
|
#endregion cpu setup
|
|
|
|
#### Graphics ####
|
|
#region Graphics setup
|
|
msgb "\n=== Detecting Graphics information ==="
|
|
|
|
#region intel GPU setup
|
|
# Check for Intel GPU - ensure intel_gpu_top is installed
|
|
if command -v intel_gpu_top &>/dev/null; then
|
|
# detect all intel cards using intel_gpu_top -L. Show them line by line
|
|
local intelCards
|
|
|
|
# Get the output from intel_gpu_top -L, skip empty lines
|
|
intelCards=$(intel_gpu_top -L 2>/dev/null | grep -E '^card[0-9]+' || true)
|
|
|
|
if [[ -n "$intelCards" ]]; then
|
|
local cardCount=$(echo "$intelCards" | wc -l)
|
|
echo "Intel GPU(s) detected ($cardCount):"
|
|
echo "$intelCards" | while IFS= read -r line; do
|
|
# Extract card name, GPU model, and PCI info
|
|
if [[ $line =~ ^(card[0-9]+)[[:space:]]+(.+)[[:space:]]+pci:(.+)$ ]]; then
|
|
local cardName="${BASH_REMATCH[1]}"
|
|
local gpuModel="${BASH_REMATCH[2]}"
|
|
local pciInfo="${BASH_REMATCH[3]}"
|
|
|
|
echo " - Card: $cardName"
|
|
echo " Model: $gpuModel"
|
|
echo " PCI: $pciInfo"
|
|
else
|
|
# Fallback: just show the line as-is
|
|
echo " $line"
|
|
fi
|
|
done
|
|
ENABLE_INTEL_GPU_INFO=true
|
|
ENABLE_GPU_INFO=true
|
|
else
|
|
warn "No Intel GPUs detected by intel_gpu_top."
|
|
ENABLE_INTEL_GPU_INFO=false
|
|
fi
|
|
else
|
|
warn "intel_gpu_top command not found. Skipping Intel GPU information detection."
|
|
ENABLE_INTEL_GPU_INFO=false
|
|
fi
|
|
#endregion intel GPU setup
|
|
|
|
#region NVIDIA GPU setup
|
|
# Check for NVIDIA GPU - ensure nvidia-smi is installed
|
|
if command -v nvidia-smi &>/dev/null; then
|
|
# detect all NVIDIA cards using nvidia-smi -L
|
|
local nvidiaCards
|
|
|
|
# Get the output from nvidia-smi -L (lists GPUs)
|
|
nvidiaCards=$(nvidia-smi -L 2>/dev/null || true)
|
|
|
|
if [[ -n "$nvidiaCards" ]]; then
|
|
local cardCount=$(echo "$nvidiaCards" | wc -l)
|
|
echo "NVIDIA GPU(s) detected ($cardCount):"
|
|
echo "$nvidiaCards" | while IFS= read -r line; do
|
|
# Expected format: GPU 0: NVIDIA GeForce RTX 3080 (UUID: GPU-xxxxx)
|
|
if [[ $line =~ ^GPU\ ([0-9]+):\ (.+)\ \(UUID:\ (.+)\)$ ]]; then
|
|
local gpuIndex="${BASH_REMATCH[1]}"
|
|
local gpuModel="${BASH_REMATCH[2]}"
|
|
local gpuUUID="${BASH_REMATCH[3]}"
|
|
|
|
echo " - GPU $gpuIndex"
|
|
echo " Model: $gpuModel"
|
|
echo " UUID: $gpuUUID"
|
|
else
|
|
# Fallback: just show the line as-is
|
|
echo " $line"
|
|
fi
|
|
done
|
|
ENABLE_NVIDIA_GPU_INFO=true
|
|
ENABLE_GPU_INFO=true
|
|
else
|
|
warn "No NVIDIA GPUs detected by nvidia-smi."
|
|
ENABLE_NVIDIA_GPU_INFO=false
|
|
fi
|
|
else
|
|
warn "nvidia-smi command not found. Skipping NVIDIA GPU information detection."
|
|
ENABLE_NVIDIA_GPU_INFO=false
|
|
fi
|
|
#endregion NVIDIA GPU setup
|
|
|
|
#region AMD GPU setup
|
|
# not implemented yet
|
|
#endregion AMD GPU setup
|
|
|
|
#endregion Graphics setup
|
|
|
|
#### RAM ####
|
|
#region ram setup
|
|
msgb "\n=== Detecting RAM temperature sensors ==="
|
|
local ramList=$(echo "$sanitisedSensorsOutput" | grep -o '"SODIMM[^"]*"' | sed 's/"//g' | paste -sd, -)
|
|
local ramCount=$(grep -c '"SODIMM[^"]*"' <<<"$sanitisedSensorsOutput")
|
|
|
|
if [ "$ramCount" -gt 0 ]; then
|
|
info "Detected RAM sensors ($ramCount): $ramList"
|
|
ENABLE_RAM_TEMP=true
|
|
SENSORS_DETECTED=true
|
|
else
|
|
warn "No RAM temperature sensors found."
|
|
ENABLE_RAM_TEMP=false
|
|
fi
|
|
#endregion ram setup
|
|
|
|
#### HDD/SSD ####
|
|
#region hdd setup
|
|
msgb "\n=== Detecting HDD/SSD temperature sensors ==="
|
|
local hddList=($(echo "$sanitisedSensorsOutput" | grep -o '"drivetemp-scsi[^"]*"' | sed 's/"//g'))
|
|
if [ ${#hddList[@]} -gt 0 ]; then
|
|
info "Detected HDD/SSD sensors (${#hddList[@]}): $(IFS=,; echo "${hddList[*]}")"
|
|
ENABLE_HDD_TEMP=true
|
|
SENSORS_DETECTED=true
|
|
else
|
|
warn "No HDD/SSD temperature sensors found."
|
|
ENABLE_HDD_TEMP=false
|
|
fi
|
|
#endregion hdd setup
|
|
|
|
#### NVMe ####
|
|
#region nvme setup
|
|
msgb "\n=== Detecting NVMe temperature sensors ==="
|
|
local nvmeList=($(echo "$sanitisedSensorsOutput" | grep -o '"nvme[^"]*"' | sed 's/"//g'))
|
|
if [ ${#nvmeList[@]} -gt 0 ]; then
|
|
info "Detected NVMe sensors (${#nvmeList[@]}): $(IFS=,; echo "${nvmeList[*]}")"
|
|
ENABLE_NVME_TEMP=true
|
|
SENSORS_DETECTED=true
|
|
else
|
|
warn "No NVMe temperature sensors found."
|
|
ENABLE_NVME_TEMP=false
|
|
fi
|
|
#endregion nvme setup
|
|
|
|
#### Fans ####
|
|
#region fan setup
|
|
msgb "\n=== Detecting fan speed sensors ==="
|
|
|
|
local fanList=""
|
|
local fanCount=0
|
|
|
|
# Find all fan names that have fan*_input entries
|
|
fanList=$(echo "$sanitisedSensorsOutput" | grep -B2 '"fan[0-9]\+_input"' | grep '".*": {' | sed 's/.*"\([^"]*\)": {.*/\1/' | sort -u | paste -sd, -)
|
|
fanCount=$(grep -c 'fan[0-9]\+_input' <<<"$sanitisedSensorsOutput")
|
|
|
|
if [ "$fanCount" -gt 0 ]; then
|
|
info "Detected fan speed sensors ($fanCount): $fanList"
|
|
ENABLE_FAN_SPEED=true
|
|
SENSORS_DETECTED=true
|
|
|
|
local choice
|
|
choice=$(ask "Display fans reporting zero speed? (Y/n)")
|
|
case "$choice" in
|
|
[yY]|"")
|
|
DISPLAY_ZERO_SPEED_FANS=true
|
|
info "Zero-speed fans will be displayed."
|
|
;;
|
|
[nN])
|
|
DISPLAY_ZERO_SPEED_FANS=false
|
|
info "Only active fans will be displayed."
|
|
;;
|
|
*)
|
|
warn "Invalid input. Defaulting to show zero-speed fans."
|
|
DISPLAY_ZERO_SPEED_FANS=true
|
|
;;
|
|
esac
|
|
else
|
|
warn "No fan speed sensors found."
|
|
ENABLE_FAN_SPEED=false
|
|
fi
|
|
#endregion fan setup
|
|
|
|
#### Temperature Units ####
|
|
#region temp unit setup
|
|
msgb "\n=== Display temperature ==="
|
|
if [ "$SENSORS_DETECTED" = true ]; then
|
|
local unit=$(ask "Display temperatures in Celsius [C] or Fahrenheit [f]? (C/f)")
|
|
case "$unit" in
|
|
[cC]|"")
|
|
TEMP_UNIT="C"
|
|
info "Using Celsius."
|
|
;;
|
|
[fF])
|
|
TEMP_UNIT="F"
|
|
info "Using Fahrenheit."
|
|
;;
|
|
*)
|
|
warn "Invalid selection. Defaulting to Celsius."
|
|
TEMP_UNIT="C"
|
|
;;
|
|
esac
|
|
fi
|
|
#endregion temp unit setup
|
|
|
|
#### UPS ####
|
|
#region ups setup
|
|
local choiceUPS=$(ask "Enable UPS information? (y/N)")
|
|
case "$choiceUPS" in
|
|
[yY])
|
|
if [ "$DEBUG_REMOTE" = true ]; then
|
|
upsOutput=$(cat "$DEBUG_UPS_FILE")
|
|
info "Remote debugging: UPS readings from $DEBUG_UPS_FILE"
|
|
upsConnection="DEBUG_UPS"
|
|
else
|
|
upsConnection=$(ask "Enter UPS connection (e.g., upsname[@hostname[:port]])")
|
|
if ! command -v upsc &>/dev/null; then
|
|
err "The 'upsc' command is not available. Install 'nut-client'."
|
|
fi
|
|
upsOutput=$(upsc "$upsConnection" 2>&1)
|
|
fi
|
|
|
|
if echo "$upsOutput" | grep -q "device.model:"; then
|
|
modelName=$(echo "$upsOutput" | grep "device.model:" | cut -d':' -f2- | xargs)
|
|
ENABLE_UPS=true
|
|
info "Connected to UPS model: $modelName at $upsConnection."
|
|
else
|
|
warn "Failed to connect to UPS at '$upsConnection'."
|
|
ENABLE_UPS=false
|
|
fi
|
|
;;
|
|
[nN]|"")
|
|
ENABLE_UPS=false
|
|
info "UPS information will not be displayed."
|
|
;;
|
|
*)
|
|
warn "Invalid selection. UPS info will not be displayed."
|
|
ENABLE_UPS=false
|
|
;;
|
|
esac
|
|
#endregion ups setup
|
|
|
|
#### System Info ####
|
|
#region system info setup
|
|
msgb "\n=== Detecting System Information ==="
|
|
for i in 1 2; do
|
|
echo "type ${i})"
|
|
dmidecode -t "$i" | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print $1": "$2}'
|
|
done
|
|
local choiceSysInfo=$(ask "Enable system information? (1/2/n)")
|
|
case "$choiceSysInfo" in
|
|
[1]|"")
|
|
ENABLE_SYSTEM_INFO=true
|
|
SYSTEM_INFO_TYPE=1
|
|
info "System information will be displayed."
|
|
;;
|
|
[2])
|
|
ENABLE_SYSTEM_INFO=true
|
|
SYSTEM_INFO_TYPE=2
|
|
info "Motherboard information will be displayed."
|
|
;;
|
|
[nN])
|
|
ENABLE_SYSTEM_INFO=false
|
|
info "System information will NOT be displayed."
|
|
;;
|
|
*)
|
|
warn "Invalid selection. Defaulting to system information."
|
|
ENABLE_SYSTEM_INFO=true
|
|
SYSTEM_INFO_TYPE=1
|
|
;;
|
|
esac
|
|
#endregion system info setup
|
|
|
|
#### Final Check ####
|
|
#region final check
|
|
if [ "$SENSORS_DETECTED" = false ] && [ "$ENABLE_UPS" = false ] && [ "$ENABLE_SYSTEM_INFO" = false ]; then
|
|
err "No sensors detected, UPS or system info enabled. Exiting."
|
|
fi
|
|
#endregion final check
|
|
}
|
|
|
|
|
|
# Function to install the modification
|
|
function install_mod {
|
|
msgb "\n=== Preparing mod installation ==="
|
|
check_root_privileges
|
|
check_mod_installation
|
|
configure
|
|
perform_backup
|
|
|
|
#### Insert information retrieval code ####
|
|
msgb "\n=== Installing sensor info module ==="
|
|
install_sensor_monitor_module
|
|
insert_sensor_monitor_into_pve
|
|
|
|
#### Install UI modification module ####
|
|
msgb "\n=== Installing UI modification module ==="
|
|
install_node_status_view_module
|
|
|
|
msgb "\n=== Finalizing installation ==="
|
|
|
|
restart_proxy
|
|
info "Installation completed."
|
|
ask "Clear the browser cache to ensure all changes are visualized. (any key to continue)"
|
|
}
|
|
|
|
# Sanitize sensors output to handle common lm-sensors parsing issues
|
|
sanitize_sensors_output() {
|
|
local input="$1"
|
|
|
|
# Pipe the text into Perl:
|
|
# -0777 → "slurp mode": read the entire stream as one string so
|
|
# regexes can match across line breaks.
|
|
# -pe → loop over input, applying the script (-e) and printing.
|
|
# Apply python3 json.tool for proper formatting and validation
|
|
echo "$input" | perl -0777 -pe '
|
|
# Replace ERROR lines with placeholder values
|
|
s/ERROR:.+\s(\w+):\s(.+)/"$1": 0.000,/g;
|
|
s/ERROR:.+\s(\w+)!/"$1": 0.000,/g;
|
|
|
|
# Remove trailing commas before closing braces
|
|
s/,\s*(\})/$1/g;
|
|
|
|
# Replace NaN values with null
|
|
s/\bNaN\b/null/g;
|
|
|
|
# Fix duplicate SODIMM keys - handle both pretty and one-line JSON
|
|
s/"SODIMM"\s*:\s*\{\s*"temp(\d+)_input"/"SODIMM $1": {\n "temp$1_input"/g;
|
|
|
|
# Fix duplicate fan keys - handle both pretty and one-line JSON
|
|
s/"([^"]*Fan[^"]*)"\s*:\s*\{\s*"fan(\d+)_input"/"$1 $2": {\n "fan$2_input"/g;
|
|
' | python3 -m json.tool 2>/dev/null || echo "$input"
|
|
}
|
|
|
|
#region Sensor Monitor Module Installation
|
|
# Install and configure the Sensor Monitor Perl module
|
|
install_sensor_monitor_module() {
|
|
# Check if source file exists
|
|
if [[ ! -f "$PVE_SENSOR_INFO_SOURCE_FILE" ]]; then
|
|
err "Source file not found: $PVE_SENSOR_INFO_SOURCE_FILE"
|
|
fi
|
|
|
|
# Copy the module file
|
|
cp "$PVE_SENSOR_INFO_SOURCE_FILE" "$PVE_SENSOR_INFO_MOD_FILE" || err "Failed to copy $PVE_SENSOR_INFO_SOURCE_FILE to $PVE_SENSOR_INFO_MOD_FILE"
|
|
info "Copied Sensor Monitor module to $PVE_SENSOR_INFO_MOD_FILE"
|
|
|
|
# Convert boolean flags to Perl format (1 or 0)
|
|
local intel_enabled=$([[ "$ENABLE_INTEL_GPU_INFO" = true ]] && echo 1 || echo 0)
|
|
local nvidia_enabled=$([[ "$ENABLE_NVIDIA_GPU_INFO" = true ]] && echo 1 || echo 0)
|
|
local ups_enabled=$([[ "$ENABLE_UPS" = true ]] && echo 1 || echo 0)
|
|
local sensors_mode=$([[ "$DEBUG_REMOTE" = true ]] && echo 1 || echo 0)
|
|
|
|
# Determine UPS device name
|
|
local ups_device="${upsConnection:-ups@localhost}"
|
|
|
|
# Update configuration in the installed module
|
|
sed -i "
|
|
# Update GPU configuration
|
|
/intel_enabled =>/ s/=> [01],/=> $intel_enabled,/
|
|
/nvidia_enabled =>/ s/=> [01],/=> $nvidia_enabled,/
|
|
|
|
# Update UPS configuration
|
|
/enabled =>/ {
|
|
/ups => {/,/},/ {
|
|
/enabled =>/ s/=> [01],/=> $ups_enabled,/
|
|
}
|
|
}
|
|
/device_name =>/ s|=> '[^']*',|=> '$ups_device',|
|
|
" "$PVE_SENSOR_INFO_MOD_FILE"
|
|
|
|
if [[ $? -eq 0 ]]; then
|
|
info "Sensor Monitor module configured successfully."
|
|
else
|
|
warn "Failed to configure Sensor Monitor module settings."
|
|
fi
|
|
}
|
|
#endregion Sensor Monitor Module Installation
|
|
|
|
#region node info insertion
|
|
# Main insertion routine
|
|
insert_sensor_monitor_into_pve() {
|
|
#region PveSensorInfoMod heredoc
|
|
sed -i '/my \$dinfo = df('\''\/'\'', 1);/i\
|
|
# Collect sensor data from PveMod_SensorInfo\
|
|
# Bad practice to add use here, but cleaner implementation would require several extensive modifications.\
|
|
use PVE::API2::PveMod_SensorInfo;\
|
|
$res->{sensorsJSONOutput} = PVE::API2::PveMod_SensorInfo::get_sensors_stats();\
|
|
' "$NODES_PM_FILE"
|
|
#endregion PveSensorInfoMod heredoc
|
|
info "Sensor data retriever added to \"$NODES_PM_FILE\"."
|
|
|
|
# Add system information if enabled
|
|
if [[ $ENABLE_SYSTEM_INFO == true ]]; then
|
|
collect_system_info "$NODES_PM_FILE"
|
|
fi
|
|
}
|
|
|
|
# Collect system information
|
|
collect_system_info() {
|
|
local output_file="$1"
|
|
local systemInfoCmd
|
|
|
|
systemInfoCmd=$(dmidecode -t "${SYSTEM_INFO_TYPE}" \
|
|
| awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print $1": "$2}' \
|
|
| awk '{$1=$1};1' \
|
|
| sed 's/$/ |/' \
|
|
| paste -sd " " - \
|
|
| sed 's/ |$//')
|
|
#region system info heredoc
|
|
sed -i "/my \$dinfo = df('\/', 1);/i\\
|
|
# Add system information to response\\
|
|
\$res->{systemInfo} = \"$(echo "$systemInfoCmd")\";\\
|
|
" "$NODES_PM_FILE"
|
|
#endregion system info heredoc
|
|
info "System information retriever added to \"$output_file\"."
|
|
}
|
|
#endregion node info insertion
|
|
|
|
#region UI Module Installation
|
|
# Install the UI modification module
|
|
install_node_status_view_module() {
|
|
# Check if source file exists
|
|
if [[ ! -f "$PVE_MOD_JS_SOURCE_FILE" ]]; then
|
|
err "Source file not found: $PVE_MOD_JS_SOURCE_FILE"
|
|
fi
|
|
|
|
# Copy the JavaScript module to PVE manager directory
|
|
cp "$PVE_MOD_JS_SOURCE_FILE" "$PVE_MOD_JS_TARGET_FILE" || err "Failed to copy $PVE_MOD_JS_SOURCE_FILE to $PVE_MOD_JS_TARGET_FILE"
|
|
info "Copied UI module to $PVE_MOD_JS_TARGET_FILE"
|
|
|
|
# Comment out the original PVE.node.StatusView definition in pvemanagerlib.js
|
|
# This allows our custom module to provide the new definition
|
|
if grep -q "^Ext.define('PVE.node.StatusView'," "$PVE_MANAGER_LIB_JS_FILE" 2>/dev/null; then
|
|
# Find the start of the definition and comment it out until the matching closing brace
|
|
sed -i "/^Ext\.define('PVE\.node\.StatusView',/,/^});/s|^|// |" "$PVE_MANAGER_LIB_JS_FILE"
|
|
info "Commented out original StatusView definition in pvemanagerlib.js"
|
|
else
|
|
warn "Original StatusView definition not found in expected format in pvemanagerlib.js"
|
|
fi
|
|
|
|
# Add a dynamic script loader to load our custom module
|
|
# Insert before the commented-out Ext.define so it loads early
|
|
if ! grep -q "PveMod_PveNodeStatusView.js" "$PVE_MANAGER_LIB_JS_FILE" 2>/dev/null; then
|
|
# Use ExtJS Loader to dynamically load our custom module
|
|
sed -i "/^\/\/ Ext\.define('PVE\.node\.StatusView',/i\\
|
|
// Load custom PVE.node.StatusView from external module\\
|
|
Ext.Loader.loadScript({\\
|
|
url: '/pve2/js/PveMod_PveNodeStatusView.js',\\
|
|
onLoad: function() { console.log('Loaded PveMod_PveNodeStatusView.js'); },\\
|
|
onError: function() { console.error('Failed to load PveMod_PveNodeStatusView.js'); }\\
|
|
});\\
|
|
" "$PVE_MANAGER_LIB_JS_FILE"
|
|
info "Added dynamic loader for custom UI module in pvemanagerlib.js"
|
|
else
|
|
info "Custom UI module loader already present in pvemanagerlib.js"
|
|
fi
|
|
}
|
|
#endregion UI Module Installation
|
|
|
|
|
|
# Function to uninstall the modification
|
|
function uninstall_mod {
|
|
msgb "=== Uninstalling Mod ==="
|
|
|
|
check_root_privileges
|
|
|
|
if [[ -z $(grep -e "\$res->{sensorsOutput}" "$NODES_PM_FILE") ]] && [[ -z $(grep -e "\$res->{systemInfo}" "$NODES_PM_FILE") ]]; then
|
|
err "Mod is not installed."
|
|
fi
|
|
|
|
set_backup_directory
|
|
info "Restoring modified files..."
|
|
|
|
# Find the latest Nodes.pm file using the find command
|
|
local latest_nodes_pm=$(find "$BACKUP_DIR" -name "Nodes.pm.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}')
|
|
|
|
if [ -n "$latest_nodes_pm" ]; then
|
|
# Restore the latest Nodes.pm file
|
|
msgb "Restoring latest Nodes.pm from backup: $latest_nodes_pm to \"$NODES_PM_FILE\"."
|
|
cp "$latest_nodes_pm" "$NODES_PM_FILE"
|
|
info "Restored Nodes.pm successfully."
|
|
else
|
|
warn "No Nodes.pm backup files found."
|
|
fi
|
|
|
|
# Restore original pvemanagerlib.js (uncomment the StatusView definition and remove loader)
|
|
local latest_pvemanagerlibjs=$(find "$BACKUP_DIR" -name "pvemanagerlib.js.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}')
|
|
|
|
if [ -n "$latest_pvemanagerlibjs" ]; then
|
|
# Restore the latest pvemanagerlib.js file
|
|
msgb "Restoring latest pvemanagerlib.js from backup: $latest_pvemanagerlibjs to \"$PVE_MANAGER_LIB_JS_FILE\"."
|
|
cp "$latest_pvemanagerlibjs" "$PVE_MANAGER_LIB_JS_FILE"
|
|
info "Restored pvemanagerlib.js successfully."
|
|
else
|
|
warn "No pvemanagerlib.js backup files found."
|
|
fi
|
|
|
|
# Remove UI module files
|
|
if [ -f "$PVE_MOD_JS_TARGET_FILE" ]; then
|
|
msgb "Removing UI module: $PVE_MOD_JS_TARGET_FILE"
|
|
rm "$PVE_MOD_JS_TARGET_FILE"
|
|
info "Removed UI module successfully."
|
|
else
|
|
warn "UI module file not found: $PVE_MOD_JS_TARGET_FILE"
|
|
fi
|
|
|
|
# Remove Sensor Info Perl module
|
|
local latest_sensor_info_pm=$(find "$BACKUP_DIR" -name "PveMod_SensorInfo.pm.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}')
|
|
|
|
if [ -n "$latest_sensor_info_pm" ]; then
|
|
# Restore the latest PveMod_SensorInfo.pm file (if there's a backup)
|
|
msgb "Restoring latest PveMod_SensorInfo.pm from backup: $latest_sensor_info_pm to \"$PVE_SENSOR_INFO_MOD_FILE\"."
|
|
cp "$latest_sensor_info_pm" "$PVE_SENSOR_INFO_MOD_FILE"
|
|
info "Restored PveMod_SensorInfo.pm successfully."
|
|
elif [ -f "$PVE_SENSOR_INFO_MOD_FILE" ]; then
|
|
# No backup found but file exists, remove it
|
|
msgb "No PveMod_SensorInfo.pm backup found. Removing installed module: $PVE_SENSOR_INFO_MOD_FILE"
|
|
rm "$PVE_SENSOR_INFO_MOD_FILE"
|
|
info "Removed PveMod_SensorInfo.pm successfully."
|
|
else
|
|
warn "No PveMod_SensorInfo.pm backup files found and module not installed."
|
|
fi
|
|
|
|
if [ -n "$latest_nodes_pm" ] || [ -n "$latest_pvemanagerlibjs" ] || [ -f "$PVE_MOD_JS_TARGET_FILE" ] || [ -n "$latest_sensor_info_pm" ] || [ -f "$PVE_SENSOR_INFO_MOD_FILE" ]; then
|
|
# At least one file was modified, restart the proxy
|
|
restart_proxy
|
|
fi
|
|
|
|
ask "Clear the browser cache to ensure all changes are visualized. (any key to continue)"
|
|
}
|
|
|
|
# Function to check if the modification is installed
|
|
check_mod_installation() {
|
|
if [[ -n $(grep -F 'use PVE::API2::PveMod_SensorInfo' "$NODES_PM_FILE") ]] || \
|
|
[[ -n $(grep -F 'use PVE::API2::GPUMonitor' "$NODES_PM_FILE") ]] || \
|
|
[[ -n $(grep -F '$res->{sensorsJSONOutput}' "$NODES_PM_FILE") ]] || \
|
|
[[ -n $(grep -F '$res->{sensorsOutput}' "$NODES_PM_FILE") ]] || \
|
|
[[ -n $(grep -F '$res->{systemInfo}' "$NODES_PM_FILE") ]] || \
|
|
[[ -f "$PVE_MOD_JS_TARGET_FILE" ]]; then
|
|
err "Mod is already installed. Uninstall existing before installing."
|
|
fi
|
|
}
|
|
|
|
function restart_proxy {
|
|
# Restart pveproxy
|
|
info "Restarting PVE proxy..."
|
|
systemctl restart pveproxy
|
|
}
|
|
|
|
function save_sensors_data {
|
|
msgb "=== Saving Sensors Data ==="
|
|
|
|
# Check if JSON_EXPORT_DIRECTORY exists and is writable
|
|
if [[ ! -d "$JSON_EXPORT_DIRECTORY" || ! -w "$JSON_EXPORT_DIRECTORY" ]]; then
|
|
err "Directory $JSON_EXPORT_DIRECTORY does not exist or is not writable. No file could be saved."
|
|
return
|
|
fi
|
|
|
|
|
|
# Check if command exists
|
|
if (command -v sensors &>/dev/null); then
|
|
# Save sensors output
|
|
local debug_save_filename="sensorsdata.json"
|
|
local filepath="${JSON_EXPORT_DIRECTORY}/${debug_save_filename}"
|
|
msgb "Sensors data will be saved in $filepath"
|
|
|
|
# Prompt user for confirmation
|
|
local choiceContinue=$(ask "Do you wish to continue? (Y/n)")
|
|
case "$choiceContinue" in
|
|
[yY]|"")
|
|
echo "lm-sensors raw output:" >"$filepath"
|
|
sensorsOutput=$(sensors -j 2>/dev/null)
|
|
echo "$sensorsOutput" >>"$filepath"
|
|
echo -e "\n\nSanitised lm-sensors output:" >>"$filepath"
|
|
# Apply lm-sensors sanitization
|
|
sanitisedSensorsOutput=$(sanitize_sensors_output "$sensorsOutput")
|
|
echo "$sanitisedSensorsOutput" >>"$filepath"
|
|
info "Sensors data saved in $filepath."
|
|
;;
|
|
*)
|
|
warn "Operation cancelled by user."
|
|
;;
|
|
esac
|
|
else
|
|
err "Sensors is not installed. No file could be saved."
|
|
fi
|
|
echo
|
|
}
|
|
|
|
function set_backup_directory {
|
|
# Check if the BACKUP_DIR variable is set, if not, use the default backup
|
|
if [[ -z "$BACKUP_DIR" ]]; then
|
|
# If not set, use the default backup directory, which is based on the home directory and PVE-MODS
|
|
BACKUP_DIR="$HOME/PVE-MODS"
|
|
info "Using default backup directory: $BACKUP_DIR"
|
|
else
|
|
# If set, ensure it is a valid directory
|
|
if [[ ! -d "$BACKUP_DIR" ]]; then
|
|
err "The specified backup directory does not exist: $BACKUP_DIR"
|
|
fi
|
|
info "Using custom backup directory: $BACKUP_DIR"
|
|
fi
|
|
}
|
|
|
|
function create_backup_directory {
|
|
set_backup_directory
|
|
|
|
# Create the backup directory if it does not exist
|
|
if [[ ! -d "$BACKUP_DIR" ]]; then
|
|
mkdir -p "$BACKUP_DIR" 2>/dev/null || {
|
|
err "Failed to create backup directory: $BACKUP_DIR. Please check permissions."
|
|
}
|
|
info "Created backup directory: $BACKUP_DIR"
|
|
else
|
|
info "Backup directory already exists: $BACKUP_DIR"
|
|
fi
|
|
}
|
|
|
|
function create_file_backup() {
|
|
local source_file="$1"
|
|
local timestamp="$2"
|
|
local filename
|
|
|
|
filename=$(basename "$source_file")
|
|
local backup_file="$BACKUP_DIR/${filename}.$timestamp"
|
|
|
|
[[ -f "$source_file" ]] || err "Source file does not exist: $source_file"
|
|
[[ -r "$source_file" ]] || err "Cannot read source file: $source_file"
|
|
|
|
cp "$source_file" "$backup_file" || err "Failed to create backup: $backup_file"
|
|
|
|
# Verify backup integrity
|
|
if ! cmp -s "$source_file" "$backup_file"; then
|
|
err "Backup verification failed for: $backup_file"
|
|
fi
|
|
|
|
info "Created backup: $backup_file"
|
|
}
|
|
|
|
function perform_backup {
|
|
local timestamp
|
|
timestamp=$(date +%Y%m%d_%H%M%S)
|
|
|
|
msgb "\n=== Creating backups of modified files ==="
|
|
|
|
create_backup_directory
|
|
create_file_backup "$NODES_PM_FILE" "$timestamp"
|
|
create_file_backup "$PVE_MANAGER_LIB_JS_FILE" "$timestamp"
|
|
|
|
# Backup Sensor Info module if it exists
|
|
if [[ -f "$PVE_SENSOR_INFO_MOD_FILE" ]]; then
|
|
create_file_backup "$PVE_SENSOR_INFO_MOD_FILE" "$timestamp"
|
|
fi
|
|
}
|
|
|
|
# Process the arguments using a while loop and a case statement
|
|
executed=0
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
install)
|
|
executed=$(($executed + 1))
|
|
msgb "\nInstalling the Proxmox VE sensors display mod..."
|
|
install_packages
|
|
install_mod
|
|
;;
|
|
uninstall)
|
|
executed=$(($executed + 1))
|
|
msgb "\nUninstalling the Proxmox VE sensors display mod..."
|
|
uninstall_mod
|
|
;;
|
|
save-sensors-data)
|
|
executed=$(($executed + 1))
|
|
msgb "\nSaving current sensor readings in a file for debugging..."
|
|
save_sensors_data
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# If no arguments were provided or all arguments have been processed, print the usage message
|
|
if [[ $executed -eq 0 ]]; then
|
|
usage
|
|
fi |