#!/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 ############# # 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="/root/PVE-MOD2/Backup" ##################### 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 local ramList ramCount msgb "\n=== Detecting RAM temperature sensors ===" ramList=$(echo "$sanitisedSensorsOutput" | grep -o '"SODIMM[^"]*"' | sed 's/"//g' | paste -sd, -) 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 insert_system_info_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() { local intel_enabled nvidia_enabled ups_enabled sensors_mode # 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) intel_enabled=$([[ "$ENABLE_INTEL_GPU_INFO" = true ]] && echo 1 || echo 0) nvidia_enabled=$([[ "$ENABLE_NVIDIA_GPU_INFO" = true ]] && echo 1 || echo 0) ups_enabled=$([[ "$ENABLE_UPS" = true ]] && echo 1 || echo 0) 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->{PveMod_JsonSensorInfo} = PVE::API2::PVEMod_SensorInfo::get_sensors_info();\ $res->{PveMod_graphicsInfo} = PVE::API2::PVEMod_SensorInfo::get_pve_mod_version();\ $res->{PveMod_upsInfo} = PVE::API2::PVEMod_SensorInfo::get_ups_info();\ ' "$NODES_PM_FILE" #endregion PveSensorInfoMod heredoc info "Sensor data retriever added to \"$NODES_PM_FILE\"." } # Collect system information insert_system_info_into_pve() { local output_file="$1" local systemInfoCmd if [[ $ENABLE_SYSTEM_INFO == false ]]; then return fi 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->{pveMod_sensorInfo_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 to load it 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->{PveMod_SensorInfo_JSON}" "$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::PVEMod_SensorInfo' "$NODES_PM_FILE") ]] || \ [[ -n $(grep -F '$res->{sensorsJSONOutput}' "$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