1047 lines
34 KiB
Bash
1047 lines
34 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 #############
|
|
# 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/PveSensors/PveMod_pvemanagerlib.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/PveSensors/PveMod_SensorInfo.pm"
|
|
GPU_RRD_DIR="/var/lib/rrdcached/db/pve-mod-gpu"
|
|
|
|
# New modular architecture paths
|
|
PVEMOD_SOURCES_DIR="$SCRIPT_CWD/PveSensors"
|
|
PVEMOD_INSTALL_DIR="/usr/share/perl5/PVE/PVEMod"
|
|
PVEMOD_COLLECTOR_DIR="$PVEMOD_INSTALL_DIR/Collector"
|
|
PVEMOD_CONFIG_FILE="$PVEMOD_INSTALL_DIR/Config.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."
|
|
}
|
|
|
|
# Check if a tool is installed; if not, offer to install it via apt.
|
|
# Usage: _check_or_install_tool <cmd> <pkg> <description>
|
|
# Returns 0 if the tool is available (or successfully installed), 1 otherwise.
|
|
function _check_or_install_tool {
|
|
local cmd="$1"
|
|
local pkg="$2"
|
|
local description="$3"
|
|
|
|
if command -v "$cmd" &>/dev/null; then
|
|
info "$description is installed."
|
|
return 0
|
|
fi
|
|
|
|
local choice
|
|
choice=$(ask "$description is not installed. Would you like to install it? (y/N)")
|
|
case "$choice" in
|
|
[yY])
|
|
apt-get update
|
|
apt-get install -y "$pkg"
|
|
if command -v "$cmd" &>/dev/null; then
|
|
info "$description installed successfully."
|
|
return 0
|
|
else
|
|
warn "$description installation failed. The section will be skipped."
|
|
return 1
|
|
fi
|
|
;;
|
|
*)
|
|
info "Skipping $description installation."
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Check if nvidia-smi is available. NVIDIA GPU monitoring requires the NVIDIA drivers
|
|
# to be installed manually; this function does not attempt installation via apt.
|
|
# Returns 0 if nvidia-smi is available, 1 otherwise.
|
|
function _check_nvidia_tool {
|
|
if command -v nvidia-smi &>/dev/null; then
|
|
info "nvidia-smi is installed."
|
|
return 0
|
|
fi
|
|
warn "nvidia-smi not found. NVIDIA GPU monitoring requires the NVIDIA drivers to be installed."
|
|
info "Refer to your system or Proxmox documentation for NVIDIA driver installation."
|
|
return 1
|
|
}
|
|
|
|
# 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
|
|
|
|
local lm_sensors_ok=false
|
|
_check_or_install_tool sensors lm-sensors "lm-sensors" && lm_sensors_ok=true
|
|
|
|
if [[ "$lm_sensors_ok" == true ]]; then
|
|
#### 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
|
|
|
|
#### 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
|
|
|
|
fi #end lm-sensors block 1
|
|
|
|
#### Graphics ####
|
|
#region Graphics setup
|
|
msgb "\n=== Detecting Graphics information ==="
|
|
|
|
#region intel GPU setup
|
|
if _check_or_install_tool intel_gpu_top igt-gpu-tools "Intel GPU tools (igt-gpu-tools)"; 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
|
|
ENABLE_INTEL_GPU_INFO=false
|
|
fi
|
|
#endregion intel GPU setup
|
|
|
|
#region NVIDIA GPU setup
|
|
if _check_nvidia_tool; 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
|
|
ENABLE_NVIDIA_GPU_INFO=false
|
|
fi
|
|
#endregion NVIDIA GPU setup
|
|
|
|
#region AMD GPU setup
|
|
if _check_or_install_tool radeontop radeontop "AMD GPU tools (radeontop)"; then
|
|
# AMD GPU detection is not yet implemented
|
|
ENABLE_AMD_GPU_INFO=false
|
|
else
|
|
ENABLE_AMD_GPU_INFO=false
|
|
fi
|
|
#endregion AMD GPU setup
|
|
|
|
#endregion Graphics setup
|
|
|
|
#### GPU Historical Data ####
|
|
#region gpu history setup
|
|
ENABLE_GPU_HISTORY=false
|
|
if [[ "$ENABLE_GPU_INFO" == true ]]; then
|
|
msgb "\n=== GPU Historical Data ==="
|
|
local choiceGpuHistory
|
|
choiceGpuHistory=$(ask "Store historical GPU data for graphs? (y/N)")
|
|
case "$choiceGpuHistory" in
|
|
[yY])
|
|
ENABLE_GPU_HISTORY=true
|
|
info "Historical GPU data will be stored."
|
|
;;
|
|
[nN]|"")
|
|
info "Historical GPU data will not be stored."
|
|
;;
|
|
*)
|
|
warn "Invalid selection. Historical GPU data will not be stored."
|
|
;;
|
|
esac
|
|
fi
|
|
#endregion gpu history 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_GPU_INFO" != true ] && [ "$ENABLE_UPS" = false ] && [ "$ENABLE_SYSTEM_INFO" = false ]; then
|
|
err "No sensors detected, no GPU info, no UPS and no 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
|
|
|
|
## Historical GPU data ##
|
|
if [[ "$ENABLE_GPU_HISTORY" == true ]]; then
|
|
install_gpu_history
|
|
|
|
# todo
|
|
|
|
# add nodes.pm index list — add gpumeta after gpurrddata:
|
|
|
|
fi
|
|
|
|
#### 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 API facade source file exists
|
|
if [[ ! -f "$PVE_SENSOR_INFO_SOURCE_FILE" ]]; then
|
|
err "Source file not found: $PVE_SENSOR_INFO_SOURCE_FILE"
|
|
fi
|
|
|
|
# Create PVEMod module directories
|
|
mkdir -p "$PVEMOD_INSTALL_DIR" || err "Failed to create directory: $PVEMOD_INSTALL_DIR"
|
|
mkdir -p "$PVEMOD_COLLECTOR_DIR" || err "Failed to create directory: $PVEMOD_COLLECTOR_DIR"
|
|
info "PVEMod module directories ready."
|
|
|
|
# Copy the API facade module
|
|
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 API facade to $PVE_SENSOR_INFO_MOD_FILE"
|
|
|
|
# Copy the new modular support files
|
|
local -a module_files=(
|
|
"PVEMod_Config.pm:$PVEMOD_INSTALL_DIR/Config.pm"
|
|
"PVEMod_Utils.pm:$PVEMOD_INSTALL_DIR/Utils.pm"
|
|
"PVEMod_Store.pm:$PVEMOD_INSTALL_DIR/Store.pm"
|
|
"PVEMod_ProcessManager.pm:$PVEMOD_INSTALL_DIR/ProcessManager.pm"
|
|
"PVEMod_Collectors/Intel.pm:$PVEMOD_COLLECTOR_DIR/Intel.pm"
|
|
"PVEMod_Collectors/Nvidia.pm:$PVEMOD_COLLECTOR_DIR/Nvidia.pm"
|
|
"PVEMod_Collectors/LmSensors.pm:$PVEMOD_COLLECTOR_DIR/LmSensors.pm"
|
|
"PVEMod_Collectors/Amd.pm:$PVEMOD_COLLECTOR_DIR/Amd.pm"
|
|
"PVEMod_Collectors/Ups.pm:$PVEMOD_COLLECTOR_DIR/Ups.pm"
|
|
)
|
|
for entry in "${module_files[@]}"; do
|
|
local src="$PVEMOD_SOURCES_DIR/${entry%%:*}"
|
|
local dst="${entry##*:}"
|
|
if [[ ! -f "$src" ]]; then
|
|
err "Module source file not found: $src"
|
|
fi
|
|
cp "$src" "$dst" || err "Failed to copy $src to $dst"
|
|
info "Copied $(basename "$src") to $dst"
|
|
done
|
|
|
|
# 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)
|
|
|
|
# Determine UPS device name
|
|
local ups_device="${upsConnection:-ups@localhost}"
|
|
|
|
# Patch configuration values in Config.pm (the canonical config location)
|
|
sed -i "
|
|
/intel_enabled =>/ s/=> [01],/=> $intel_enabled,/
|
|
/nvidia_enabled =>/ s/=> [01],/=> $nvidia_enabled,/
|
|
/enabled =>/ s/=> [01],/=> $ups_enabled,/
|
|
/device_name =>/ s|=> '[^']*',|=> '$ups_device',|
|
|
" "$PVEMOD_CONFIG_FILE"
|
|
|
|
if [[ $? -eq 0 ]]; then
|
|
info "Config.pm patched successfully."
|
|
else
|
|
warn "Failed to patch Config.pm 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
|
|
|
|
# Comment out the original PVE.node.Summary definition in pvemanagerlib.js
|
|
if grep -q "^Ext.define('PVE.node.Summary'," "$PVE_MANAGER_LIB_JS_FILE" 2>/dev/null; then
|
|
sed -i "/^Ext\.define('PVE\.node\.Summary',/,/^});/s|^|// |" "$PVE_MANAGER_LIB_JS_FILE"
|
|
info "Commented out original Summary definition in pvemanagerlib.js"
|
|
else
|
|
warn "Original Summary 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() { },\\
|
|
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
|
|
|
|
|
|
#region historical GPU data
|
|
# Install GPU historical data storage: RRD directory + API endpoint in Nodes.pm
|
|
install_gpu_history() {
|
|
msgb "\n=== Installing GPU historical data support ==="
|
|
|
|
# Create and configure the RRD directory
|
|
mkdir -p "$GPU_RRD_DIR" || err "Failed to create GPU RRD directory: $GPU_RRD_DIR"
|
|
chown www-data:www-data "$GPU_RRD_DIR" || err "Failed to set ownership on: $GPU_RRD_DIR"
|
|
info "GPU RRD directory ready: $GPU_RRD_DIR"
|
|
|
|
# Register gpurrddata in the node method list
|
|
sed -i "s/{ name => 'rrddata' },/{ name => 'rrddata' },\n { name => 'gpurrddata' },/" "$NODES_PM_FILE" \
|
|
|| err "Failed to register gpurrddata method in $NODES_PM_FILE"
|
|
|
|
# Insert the gpurrddata API method definition after the rrddata method
|
|
sed -i "/\"pve-node-9\.0\/\$param->{node}\", \$param->{timeframe}, \$param->{cf},/{
|
|
n
|
|
n
|
|
a\\
|
|
\\
|
|
__PACKAGE__->register_method({\\
|
|
name => 'gpurrddata',\\
|
|
path => 'gpurrddata',\\
|
|
method => 'GET',\\
|
|
protected => 1,\\
|
|
proxyto => 'node',\\
|
|
permissions => {\\
|
|
check => ['perm', '/nodes/{node}', ['Sys.Audit']],\\
|
|
},\\
|
|
description => \"Read GPU RRD statistics\",\\
|
|
parameters => {\\
|
|
additionalProperties => 0,\\
|
|
properties => {\\
|
|
node => get_standard_option('pve-node'),\\
|
|
card => {\\
|
|
description => \"The GPU card identifier (e.g. card0, nvidia0).\",\\
|
|
type => 'string',\\
|
|
pattern => '[a-zA-Z0-9]+',\\
|
|
},\\
|
|
timeframe => {\\
|
|
description => \"Specify the time frame you are interested in.\",\\
|
|
type => 'string',\\
|
|
enum => ['hour', 'day', 'week', 'month', 'year', 'decade'],\\
|
|
},\\
|
|
cf => {\\
|
|
description => \"The RRD consolidation function\",\\
|
|
type => 'string',\\
|
|
enum => ['AVERAGE', 'MAX'],\\
|
|
optional => 1,\\
|
|
},\\
|
|
},\\
|
|
},\\
|
|
returns => {\\
|
|
type => \"array\",\\
|
|
items => {\\
|
|
type => \"object\",\\
|
|
properties => {},\\
|
|
},\\
|
|
},\\
|
|
code => sub {\\
|
|
my (\$param) = \@_;\\
|
|
my \$nodename = PVE::INotify::nodename();\\
|
|
my \$card = \$param->{card};\\
|
|
die \"invalid card name\\n\" unless \$card =~ /^[a-zA-Z0-9]+\$/;\\
|
|
return PVE::RRD::create_rrd_data(\\
|
|
\"pve-mod-gpu/\$nodename/\$card\", \$param->{timeframe}, \$param->{cf},\\
|
|
);\\
|
|
},\\
|
|
});
|
|
}" "$NODES_PM_FILE" \
|
|
|| err "Failed to insert gpurrddata method in $NODES_PM_FILE"
|
|
|
|
info "GPU historical data API endpoint added to $NODES_PM_FILE"
|
|
}
|
|
#endregion historical GPU data
|
|
|
|
# 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
|
|
|
|
# Remove PVEMod support modules (Config, Utils, Store, ProcessManager, Collectors)
|
|
if [[ -d "$PVEMOD_INSTALL_DIR" ]]; then
|
|
msgb "Removing PVEMod module directory: $PVEMOD_INSTALL_DIR"
|
|
rm -rf "$PVEMOD_INSTALL_DIR"
|
|
info "Removed PVEMod module directory."
|
|
fi
|
|
|
|
# Remove GPU RRD directory (contains historical GPU data — destroyed permanently on uninstall)
|
|
if [[ -d "$GPU_RRD_DIR" ]]; then
|
|
msgb "Removing GPU RRD directory: $GPU_RRD_DIR"
|
|
rm -rf "$GPU_RRD_DIR"
|
|
info "Removed GPU RRD directory."
|
|
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 |