rework apply and revert patches to patch

This commit is contained in:
Meliox 2026-06-08 22:01:28 +02:00
parent 5ce7fc928d
commit d74e016f90
18 changed files with 571 additions and 408 deletions

2
debian/control vendored
View File

@ -8,7 +8,7 @@ Homepage: https://github.com/Meliox/PVE-mods
Package: pve-mod Package: pve-mod
Architecture: all Architecture: all
Depends: ${misc:Depends}, perl, librrds-perl Depends: ${misc:Depends}, perl, librrds-perl, patch
Recommends: lm-sensors, nut-client Recommends: lm-sensors, nut-client
Suggests: intel-gpu-tools Suggests: intel-gpu-tools
Description: Proxmox VE UI modifications and sensor monitoring Description: Proxmox VE UI modifications and sensor monitoring

View File

@ -1 +1,3 @@
/etc/pve-mod/pve-mod.conf /etc/pve-mod/pve-mod.conf
/etc/pve-mod/conf.d/node_info.conf
/etc/pve-mod/conf.d/nag_screen.conf

View File

@ -1,9 +1,15 @@
#!/bin/bash #!/bin/bash
set -e set -e
NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm" DEFAULT_DIR="/usr/share/pve-mod"
DEFAULT_CONF="/usr/share/pve-mod/pve-mod.conf.default" CONF_DIR="/etc/pve-mod"
USER_CONF="/etc/pve-mod/pve-mod.conf"
# Config file pairs to compare on upgrade: "<default-file>|<user-file>"
CONF_PAIRS="
${DEFAULT_DIR}/pve-mod.conf.default|${CONF_DIR}/pve-mod.conf
${DEFAULT_DIR}/conf.d/node_info.conf.default|${CONF_DIR}/conf.d/node_info.conf
${DEFAULT_DIR}/conf.d/nag_screen.conf.default|${CONF_DIR}/conf.d/nag_screen.conf
"
# Extracts "section.key" pairs from an INI file, one per line. # Extracts "section.key" pairs from an INI file, one per line.
_extract_conf_keys() { _extract_conf_keys() {
@ -17,30 +23,48 @@ _extract_conf_keys() {
done < "$file" done < "$file"
} }
# Warns about keys present in the default config but missing from the user's config. # Warns about keys present in a default config but missing from the user's copy.
_check_new_config_keys() { _check_one_pair() {
[ -f "$DEFAULT_CONF" ] || return 0 local default_conf="$1" user_conf="$2"
[ -f "$USER_CONF" ] || return 0 [ -f "$default_conf" ] || return 0
[ -f "$user_conf" ] || return 0
local new_keys="" section="" key default_val local new_keys="" section="" key line
while IFS= read -r line; do while IFS= read -r line; do
case "$line" in case "$line" in
'#'*|'') continue ;; '#'*|'') continue ;;
'['*']') section="${line#[}"; section="${section%]}"; continue ;; '['*']') section="${line#[}"; section="${section%]}"; continue ;;
*'='*) *'='*)
key="${line%%=*}" key="${line%%=*}"
if ! _extract_conf_keys "$USER_CONF" | grep -qF "${section}.${key}"; then if ! _extract_conf_keys "$user_conf" | grep -qF "${section}.${key}"; then
new_keys="${new_keys} [${section}] ${line}\n" new_keys="${new_keys} [${section}] ${line} (${user_conf})\n"
fi fi
;; ;;
esac esac
done < "$DEFAULT_CONF" done < "$default_conf"
if [ -n "$new_keys" ]; then if [ -n "$new_keys" ]; then
printf "%b" "$new_keys"
fi
}
# Warns about any new config keys across the main config and all per-mod configs.
_check_new_config_keys() {
local all_new="" pair default_conf user_conf
while IFS= read -r pair; do
[ -n "$pair" ] || continue
default_conf="${pair%%|*}"
user_conf="${pair#*|}"
all_new="${all_new}$(_check_one_pair "$default_conf" "$user_conf")"
done <<EOF
$CONF_PAIRS
EOF
if [ -n "$all_new" ]; then
echo "" echo ""
echo "pve-mod: New configuration options are available since your last install:" echo "pve-mod: New configuration options are available since your last install:"
printf "%b" "$new_keys" printf "%b" "$all_new"
echo "pve-mod: Run 'pve-mod-configure' to configure them, or add them manually to $USER_CONF" echo "pve-mod: Run 'pve-mod-configure' to configure them, or add them manually."
echo "" echo ""
fi fi
} }

39
debian/rules vendored
View File

@ -31,17 +31,46 @@ override_dh_install:
# JS module (rename to match loader reference) # JS module (rename to match loader reference)
install -Dm644 src/PVENodeInfo/PveMod_pvemanagerlib.js \ install -Dm644 src/PVENodeInfo/PveMod_pvemanagerlib.js \
debian/pve-mod/usr/share/pve-manager/js/PveMod_PveNodeStatusView.js debian/pve-mod/usr/share/pve-manager/js/PveMod_PveNodeStatusView.js
# Patch helpers # Patch helpers (generic, manifest-driven)
install -Dm755 src/PVENodeInfo/apply-patches.sh \ install -Dm755 src/Scripts/apply-patches.sh \
debian/pve-mod/usr/lib/pve-mod/apply-patches.sh debian/pve-mod/usr/lib/pve-mod/apply-patches.sh
install -Dm755 src/PVENodeInfo/revert-patches.sh \ install -Dm755 src/Scripts/revert-patches.sh \
debian/pve-mod/usr/lib/pve-mod/revert-patches.sh debian/pve-mod/usr/lib/pve-mod/revert-patches.sh
# node_info mod patches + manifest + hooks
install -Dm644 src/PVENodeInfo/patches/patches.list \
debian/pve-mod/usr/lib/pve-mod/patches/node_info/patches.list
install -Dm644 src/PVENodeInfo/patches/01-nodes-pm-sensors.patch \
debian/pve-mod/usr/lib/pve-mod/patches/node_info/01-nodes-pm-sensors.patch
install -Dm644 src/PVENodeInfo/patches/02-nodes-pm-GPU-RRD-history.patch \
debian/pve-mod/usr/lib/pve-mod/patches/node_info/02-nodes-pm-GPU-RRD-history.patch
install -Dm644 src/PVENodeInfo/patches/03-pvemanager-js-sensors.patch \
debian/pve-mod/usr/lib/pve-mod/patches/node_info/03-pvemanager-js-sensors.patch
install -Dm755 src/PVENodeInfo/patches/post-apply.sh \
debian/pve-mod/usr/lib/pve-mod/patches/node_info/post-apply.sh
# nag_screen mod patches + manifest + hooks
install -Dm644 src/NagScreen/patches/patches.list \
debian/pve-mod/usr/lib/pve-mod/patches/nag_screen/patches.list
install -Dm644 src/NagScreen/patches/01-proxmoxlib-js-nagscreen.patch \
debian/pve-mod/usr/lib/pve-mod/patches/nag_screen/01-proxmoxlib-js-nagscreen.patch
install -Dm755 src/NagScreen/patches/post-apply.sh \
debian/pve-mod/usr/lib/pve-mod/patches/nag_screen/post-apply.sh
install -Dm755 src/NagScreen/patches/post-revert.sh \
debian/pve-mod/usr/lib/pve-mod/patches/nag_screen/post-revert.sh
# Configure tool # Configure tool
install -Dm755 src/Scripts/pve-mod-configure \ install -Dm755 src/Scripts/pve-mod-configure \
debian/pve-mod/usr/sbin/pve-mod-configure debian/pve-mod/usr/sbin/pve-mod-configure
# Default config (conffile for user edits) # Main config (conffile for user edits)
install -Dm644 src/pve-mod.conf \ install -Dm644 src/pve-mod.conf \
debian/pve-mod/etc/pve-mod/pve-mod.conf debian/pve-mod/etc/pve-mod/pve-mod.conf
# Reference copy for upgrade key-diff (not a conffile — always updated) # Per-mod configs under conf.d (conffiles for user edits)
install -Dm644 src/PVENodeInfo/node_info.conf \
debian/pve-mod/etc/pve-mod/conf.d/node_info.conf
install -Dm644 src/NagScreen/nag_screen.conf \
debian/pve-mod/etc/pve-mod/conf.d/nag_screen.conf
# Reference copies for upgrade key-diff (not conffiles — always updated)
install -Dm644 src/pve-mod.conf \ install -Dm644 src/pve-mod.conf \
debian/pve-mod/usr/share/pve-mod/pve-mod.conf.default debian/pve-mod/usr/share/pve-mod/pve-mod.conf.default
install -Dm644 src/PVENodeInfo/node_info.conf \
debian/pve-mod/usr/share/pve-mod/conf.d/node_info.conf.default
install -Dm644 src/NagScreen/nag_screen.conf \
debian/pve-mod/usr/share/pve-mod/conf.d/nag_screen.conf.default

View File

@ -0,0 +1,3 @@
# pve-mod :: nag_screen mod configuration
# The nag-screen mod has no tunable settings; this file is a placeholder
# kept for consistency with the per-mod conf.d layout.

View File

@ -0,0 +1,4 @@
# pve-mod :: nag_screen patch manifest
# Format: <patch-file> [section.key=value]
01-proxmoxlib-js-nagscreen.patch

View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
# post-apply hook for the nag_screen mod.
# Runs after nag_screen patches are applied. Provided env: MOD_CONF, STASH_DIR,
# CONFD_DIR. Replaces the minified proxmoxlib with a symlink to the patched
# unminified copy so the browser serves the patched code. The original minified
# file is stashed (it is a non-patch binary replacement, so `patch -R` cannot
# restore it). Exit codes: 0 = no change, 100 = changed, other = error.
set -u
PROXMOXLIB_JS="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
PROXMOXLIB_MIN_JS="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
STASH_DIR="${STASH_DIR:-/var/lib/pve-mod/backup}"
if [[ ! -L "$PROXMOXLIB_MIN_JS" ]]; then
mkdir -p "$STASH_DIR"
if [[ -f "$PROXMOXLIB_MIN_JS" ]]; then
mv "$PROXMOXLIB_MIN_JS" \
"$STASH_DIR/proxmoxlib.min.js.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
fi
ln -sf "$PROXMOXLIB_JS" "$PROXMOXLIB_MIN_JS"
echo "[pve-mod] Symlinked proxmoxlib.min.js -> proxmoxlib.js"
exit 100
fi
exit 0

View File

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# post-revert hook for the nag_screen mod.
# Runs before nag_screen patches are reverted. Provided env: MOD_CONF,
# STASH_DIR, CONFD_DIR. Removes the min.js symlink and restores the original
# minified file from the stash. Exit codes: 0 = no change, 100 = changed.
set -u
PROXMOXLIB_MIN_JS="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
STASH_DIR="${STASH_DIR:-/var/lib/pve-mod/backup}"
if [[ -L "$PROXMOXLIB_MIN_JS" ]]; then
rm -f "$PROXMOXLIB_MIN_JS"
latest=$(find "$STASH_DIR" -name "proxmoxlib.min.js.*" -type f -printf '%T+ %p\n' 2>/dev/null \
| sort -r | head -n1 | awk '{print $2}')
if [[ -n "$latest" ]]; then
cp "$latest" "$PROXMOXLIB_MIN_JS"
echo "[pve-mod] Restored proxmoxlib.min.js from stash"
fi
exit 100
fi
exit 0

View File

@ -1,261 +0,0 @@
#!/usr/bin/env bash
# /usr/lib/pve-mod/apply-patches.sh
#
# Applies pve-mod patches to Proxmox VE system files.
# Reads /etc/pve-mod/pve-mod.conf to determine which modules are enabled.
# Idempotent: safe to call multiple times (e.g. from apt hook after PVE upgrade).
CONF_FILE="/etc/pve-mod/pve-mod.conf"
BACKUP_DIR="/var/lib/pve-mod/backup"
NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm"
PVE_MANAGER_JS="/usr/share/pve-manager/js/pvemanagerlib.js"
PVE_MOD_JS="/usr/share/pve-manager/js/PveMod_PveNodeStatusView.js"
PROXMOXLIB_JS="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
PROXMOXLIB_MIN_JS="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
GPU_RRD_DIR="/var/lib/rrdcached/db/pve-mod-gpu"
info() { echo "[pve-mod] $*"; }
warn() { echo "[pve-mod] WARNING: $*" >&2; }
# Read one value from the INI config file; prints $default if not found.
read_conf() {
local section="$1" key="$2" default="${3:-0}"
if [[ ! -f "$CONF_FILE" ]]; then
echo "$default"
return
fi
local val
val=$(awk -F= -v sec="[$section]" -v k="$key" '
/^\[/ { in_sec = ($0 == sec) }
in_sec && /^[^#=]+=/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $1)
if ($1 == k) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
print $2; exit
}
}
' "$CONF_FILE")
echo "${val:-$default}"
}
backup_file() {
local src="$1"
[[ -f "$src" ]] || return 0
local name ts
name=$(basename "$src")
ts=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
cp "$src" "$BACKUP_DIR/${name}.${ts}"
info "Backed up $(basename "$src")$BACKUP_DIR/${name}.${ts}"
}
# ── Read enabled modules ──────────────────────────────────────────────────────
NODE_INFO=$(read_conf modules node_info 0)
NAG_SCREEN=$(read_conf modules nag_screen 0)
GPU_HISTORY=$(read_conf gpu gpu_history 0)
CHANGED=false
# ── node-info: Nodes.pm ───────────────────────────────────────────────────────
if [[ "$NODE_INFO" == "1" ]]; then
if ! grep -qF "use PVE::API2::PVEMod_SensorInfo" "$NODES_PM" 2>/dev/null; then
backup_file "$NODES_PM"
python3 - "$NODES_PM" <<'PYEOF'
import sys, re
path = sys.argv[1]
content = open(path).read()
if 'use PVE::API2::PVEMod_SensorInfo' in content:
sys.exit(0)
m = re.search(r'^([ \t]*)my \$dinfo = df\(\'\/\', 1\);', content, re.MULTILINE)
if not m:
print("ERROR: Anchor 'my $dinfo = df' not found in Nodes.pm", file=sys.stderr)
sys.exit(1)
indent = m.group(1)
insertion = (
f"{indent}# Collect sensor data from PveMod_SensorInfo\n"
f"{indent}use PVE::API2::PVEMod_SensorInfo;\n"
f"{indent}$res->{{PveMod_JsonSensorInfo}} = PVE::API2::PVEMod_SensorInfo::get_sensors_info();\n"
f"{indent}$res->{{PveMod_Version}} = PVE::API2::PVEMod_SensorInfo::get_pve_mod_version();\n"
f"{indent}$res->{{PveMod_graphicsInfo}} = PVE::API2::PVEMod_SensorInfo::get_graphics_info();\n"
f"{indent}$res->{{PveMod_upsInfo}} = PVE::API2::PVEMod_SensorInfo::get_ups_info();\n"
f"{indent}$res->{{PveMod_systemInfo}} = PVE::API2::PVEMod_SensorInfo::get_system_information();\n"
)
content = content[:m.start()] + insertion + content[m.start():]
open(path, 'w').write(content)
PYEOF
info "Patched Nodes.pm"
CHANGED=true
fi
# ── node-info: pvemanagerlib.js ───────────────────────────────────────────
if ! grep -qF "PveMod_PveNodeStatusView.js" "$PVE_MANAGER_JS" 2>/dev/null; then
backup_file "$PVE_MANAGER_JS"
python3 - "$PVE_MANAGER_JS" <<'PYEOF'
import sys, re
path = sys.argv[1]
content = open(path).read()
if 'PveMod_PveNodeStatusView.js' in content:
sys.exit(0)
# Comment out original StatusView definition
content = re.sub(
r"(?m)^(Ext\.define\('PVE\.node\.StatusView',.*?^}\);)",
lambda m: '\n'.join('// ' + line for line in m.group(1).split('\n')),
content, flags=re.DOTALL
)
# Comment out original Summary definition
content = re.sub(
r"(?m)^(Ext\.define\('PVE\.node\.Summary',.*?^}\);)",
lambda m: '\n'.join('// ' + line for line in m.group(1).split('\n')),
content, flags=re.DOTALL
)
# Insert dynamic loader before the now-commented StatusView block
loader = (
"// Load custom PVE.node.StatusView from external module\n"
"Ext.Loader.loadScript({\n"
" url: '/pve2/js/PveMod_PveNodeStatusView.js',\n"
" onLoad: function() { },\n"
" onError: function() { console.error('Failed to load PveMod_PveNodeStatusView.js'); }\n"
"});\n"
)
content = re.sub(
r"(// Ext\.define\('PVE\.node\.StatusView',)",
loader + r'\1',
content, count=1
)
open(path, 'w').write(content)
PYEOF
info "Patched pvemanagerlib.js"
CHANGED=true
fi
fi
# ── node-info: GPU RRD history ────────────────────────────────────────────────
if [[ "$NODE_INFO" == "1" && "$GPU_HISTORY" == "1" ]]; then
if ! grep -qF "gpurrddata" "$NODES_PM" 2>/dev/null; then
# Register method in the node sub-path list
sed -i "s/{ name => 'rrddata' },/{ name => 'rrddata' },\n { name => 'gpurrddata' },/" "$NODES_PM"
# Append gpurrddata method definition after the rrddata code block
python3 - "$NODES_PM" <<'PYEOF'
import sys, re
path = sys.argv[1]
content = open(path).read()
if 'gpurrddata' in content:
sys.exit(0)
method = r"""
__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},
);
},
});
"""
# Insert before the final 1; at end of file
content = re.sub(r'\n1;\s*$', method + '\n1;\n', content)
open(path, 'w').write(content)
PYEOF
mkdir -p "$GPU_RRD_DIR"
chown www-data:www-data "$GPU_RRD_DIR" 2>/dev/null || true
info "Installed gpurrddata API endpoint"
CHANGED=true
fi
fi
# ── nag-screen: proxmoxlib.js ─────────────────────────────────────────────────
if [[ "$NAG_SCREEN" == "1" ]]; then
if ! grep -qF "// disable subscription nag screen" "$PROXMOXLIB_JS" 2>/dev/null; then
backup_file "$PROXMOXLIB_JS"
python3 - "$PROXMOXLIB_JS" <<'PYEOF'
import sys, re
path = sys.argv[1]
content = open(path).read()
if '// disable subscription nag screen' in content:
sys.exit(0)
m = re.search(r'(checked_command:\s*function\s*\(orig_cmd\)\s*\{)', content)
if not m:
print("ERROR: checked_command pattern not found in proxmoxlib.js", file=sys.stderr)
sys.exit(1)
insert = "\n\t\t\t// disable subscription nag screen\n\t\t\torig_cmd();\n\t\t\treturn;"
pos = m.end()
content = content[:pos] + insert + content[pos:]
open(path, 'w').write(content)
PYEOF
info "Patched proxmoxlib.js (nag screen)"
CHANGED=true
fi
if [[ ! -L "$PROXMOXLIB_MIN_JS" ]]; then
backup_file "$PROXMOXLIB_MIN_JS"
mv "$PROXMOXLIB_MIN_JS" "$BACKUP_DIR/proxmoxlib.min.js.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
ln -sf "$PROXMOXLIB_JS" "$PROXMOXLIB_MIN_JS"
info "Symlinked proxmoxlib.min.js → proxmoxlib.js"
CHANGED=true
fi
fi
# ── restart pveproxy if anything changed ──────────────────────────────────────
if [[ "$CHANGED" == "true" ]]; then
info "Restarting pveproxy..."
systemctl restart pveproxy 2>/dev/null || true
fi

View File

@ -0,0 +1,45 @@
# pve-mod :: node_info mod configuration
# Settings for the node-info / sensor-monitoring mod.
# Managed by pve-mod-configure. Re-run to update.
[gpu]
intel_enabled=0
nvidia_enabled=0
amd_enabled=0
gpu_history=0
[lm_sensors]
enabled=0
enable_cpu=0
cpu_temp_target=Core
enable_ram_temp=0
enable_hdd_temp=0
enable_nvme_temp=0
enable_fan_speed=0
display_zero_speed_fans=0
temp_unit=C
[ups]
enabled=0
device_name=ups@localhost
[system_info]
enabled=0
type=1
# Debug mode: when a collector's mode is 1, the real tool is not required.
# Data is read from the file path instead. Useful for development/testing.
[debug]
lm_sensors_mode=0
lm_sensors_output_file=/tmp/sensors-output.json
intel_mode=0
intel_devices_file=/tmp/intel-gpu-devices.json
nvidia_mode=0
nvidia_devices_file=/tmp/nvidia-smi-devices.csv
nvidia_output_file=/tmp/nvidia-smi-output.csv
amd_mode=0
amd_devices_file=/tmp/amd-gpu-devices.json
ups_mode=0
ups_output_file=/tmp/ups-output.json
log_enabled=0
log_file=/tmp/pve-mod-debug.log

View File

@ -0,0 +1,9 @@
# pve-mod :: node_info patch manifest
# Format: <patch-file> [section.key=value]
# Patches are applied top-to-bottom. An optional condition (read from this mod's
# conf.d file, /etc/pve-mod/conf.d/node_info.conf) gates a patch; it is applied
# only when the key equals the given value.
01-nodes-pm-sensors.patch
02-nodes-pm-GPU-RRD-history.patch gpu.gpu_history=1
03-pvemanager-js-sensors.patch

View File

@ -0,0 +1,34 @@
#!/usr/bin/env bash
# post-apply hook for the node_info mod.
# Runs after node_info patches are applied. Provided env: MOD_CONF, STASH_DIR,
# CONFD_DIR. Creates the GPU RRD storage directory when GPU history is enabled.
# Exit codes: 0 = no change, 100 = changed (restart pveproxy), other = error.
set -u
GPU_RRD_DIR="/var/lib/rrdcached/db/pve-mod-gpu"
read_conf() {
local file="$1" section="$2" key="$3" default="${4:-0}"
[[ -f "$file" ]] || { echo "$default"; return; }
local val
val=$(awk -F= -v sec="[$section]" -v k="$key" '
/^\[/ { in_sec = ($0 == sec) }
in_sec && /^[^#=]+=/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $1)
if ($1 == k) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit }
}
' "$file")
echo "${val:-$default}"
}
gpu_history="$(read_conf "${MOD_CONF:-/etc/pve-mod/conf.d/node_info.conf}" gpu gpu_history 0)"
if [[ "$gpu_history" == "1" && ! -d "$GPU_RRD_DIR" ]]; then
mkdir -p "$GPU_RRD_DIR"
chown www-data:www-data "$GPU_RRD_DIR" 2>/dev/null || true
echo "[pve-mod] Created GPU RRD directory: $GPU_RRD_DIR"
exit 100
fi
exit 0

View File

@ -1,69 +0,0 @@
#!/usr/bin/env bash
# /usr/lib/pve-mod/revert-patches.sh
#
# Reverts all patches applied by apply-patches.sh.
# Restores PVE system files from backups in /var/lib/pve-mod/backup/.
# Called by prerm before package files are removed.
BACKUP_DIR="/var/lib/pve-mod/backup"
NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm"
PVE_MANAGER_JS="/usr/share/pve-manager/js/pvemanagerlib.js"
PVE_MOD_JS="/usr/share/pve-manager/js/PveMod_PveNodeStatusView.js"
PROXMOXLIB_JS="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
PROXMOXLIB_MIN_JS="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
info() { echo "[pve-mod] $*"; }
warn() { echo "[pve-mod] WARNING: $*" >&2; }
restore_latest() {
local name="$1" target="$2"
local latest
latest=$(find "$BACKUP_DIR" -name "${name}.*" -type f -printf '%T+ %p\n' 2>/dev/null \
| sort -r | head -n1 | awk '{print $2}')
if [[ -n "$latest" ]]; then
cp "$latest" "$target"
info "Restored $(basename "$target") from backup"
else
warn "No backup found for ${name}; $target not restored"
fi
}
CHANGED=false
# ── node-info: Nodes.pm ───────────────────────────────────────────────────────
if grep -qF "use PVE::API2::PVEMod_SensorInfo" "$NODES_PM" 2>/dev/null; then
restore_latest "Nodes.pm" "$NODES_PM"
CHANGED=true
fi
# ── node-info: pvemanagerlib.js ───────────────────────────────────────────────
if grep -qF "PveMod_PveNodeStatusView.js" "$PVE_MANAGER_JS" 2>/dev/null; then
restore_latest "pvemanagerlib.js" "$PVE_MANAGER_JS"
CHANGED=true
fi
# ── node-info: JS module file ─────────────────────────────────────────────────
if [[ -f "$PVE_MOD_JS" ]]; then
rm -f "$PVE_MOD_JS"
info "Removed PveMod_PveNodeStatusView.js"
CHANGED=true
fi
# ── nag-screen: proxmoxlib.min.js symlink ────────────────────────────────────
if [[ -L "$PROXMOXLIB_MIN_JS" ]]; then
rm -f "$PROXMOXLIB_MIN_JS"
restore_latest "proxmoxlib.min.js" "$PROXMOXLIB_MIN_JS"
CHANGED=true
fi
# ── nag-screen: proxmoxlib.js ────────────────────────────────────────────────
if grep -qF "// disable subscription nag screen" "$PROXMOXLIB_JS" 2>/dev/null; then
restore_latest "proxmoxlib.js" "$PROXMOXLIB_JS"
CHANGED=true
fi
# ── restart pveproxy if anything changed ─────────────────────────────────────
if [[ "$CHANGED" == "true" ]]; then
info "Restarting pveproxy..."
systemctl restart pveproxy 2>/dev/null || true
fi

View File

@ -0,0 +1,206 @@
#!/usr/bin/env bash
# /usr/lib/pve-mod/apply-patches.sh
#
# Generic patch applier for pve-mod.
#
# Reads /etc/pve-mod/pve-mod.conf [modules] to learn which mods are enabled,
# then applies each enabled mod's patches from /usr/lib/pve-mod/patches/<mod>/.
#
# Patch convention: every .patch uses a/<path> b/<path> headers and is applied
# with `patch -p1 -F0 -d /` (zero fuzz: line offsets tolerated, fuzzy context
# matching disabled). Each mod folder contains a 'patches.list' manifest and
# optional 'post-apply.sh' / 'post-revert.sh' hooks.
#
# Per-mod atomicity: before applying, every active patch of a mod is dry-run.
# If any one cannot be applied (or is already partially/broken-applied), the
# whole mod is reverted to a clean state and reported as failed - a mod is
# never left half-applied.
#
# Idempotent: a patch that is already applied is detected (reverse dry-run) and
# skipped, so this script is safe to run repeatedly (e.g. from the dpkg trigger
# after a pve-manager upgrade).
#
# Hook exit-code convention: a hook returns 0 (no change), 100 (made a change,
# triggers a pveproxy restart), or any other code (error).
set -u
# Root paths (overridable via environment, mainly for testing).
PVE_MOD_ROOT="${PVE_MOD_ROOT:-/}"
MAIN_CONF="${PVE_MOD_MAIN_CONF:-/etc/pve-mod/pve-mod.conf}"
CONFD_DIR="${PVE_MOD_CONFD_DIR:-/etc/pve-mod/conf.d}"
PATCHES_DIR="${PVE_MOD_PATCHES_DIR:-/usr/lib/pve-mod/patches}"
# Storage for non-patch replaced files (e.g. nag-screen's minified proxmoxlib).
# Patched text files need no backups - `patch -R` reverts them.
STASH_DIR="${PVE_MOD_STASH_DIR:-/var/lib/pve-mod/backup}"
info() { echo "[pve-mod] $*"; }
warn() { echo "[pve-mod] WARNING: $*" >&2; }
# read_conf <file> <section> <key> [default]
# Prints one value from an INI file, or the default if absent.
read_conf() {
local file="$1" section="$2" key="$3" default="${4:-0}"
if [[ ! -f "$file" ]]; then
echo "$default"
return
fi
local val
val=$(awk -F= -v sec="[$section]" -v k="$key" '
/^\[/ { in_sec = ($0 == sec) }
in_sec && /^[^#=]+=/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $1)
if ($1 == k) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
print $2; exit
}
}
' "$file")
echo "${val:-$default}"
}
# List the keys of the [modules] section in the main config, one per line.
list_modules() {
[[ -f "$MAIN_CONF" ]] || return 0
awk -F= '
/^\[/ { in_sec = ($0 == "[modules]") }
in_sec && /^[^#=]+=/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $1)
print $1
}
' "$MAIN_CONF"
}
# Patch-state helpers (all use zero fuzz for deterministic detection).
_dry_forward() { patch -p1 -F0 -d "$PVE_MOD_ROOT" -f --dry-run -s < "$1" >/dev/null 2>&1; }
_dry_reverse() { patch -R -p1 -F0 -d "$PVE_MOD_ROOT" -f --dry-run -s < "$1" >/dev/null 2>&1; }
is_applied() { _dry_reverse "$1"; }
can_apply() { _dry_forward "$1"; }
do_apply() { patch -p1 -F0 -d "$PVE_MOD_ROOT" -f -s < "$1"; }
do_revert() { patch -R -p1 -F0 -d "$PVE_MOD_ROOT" -f -s < "$1"; }
CHANGED=false
FAILED=false
# Run a mod hook honouring the exit-code convention. Sets CHANGED / FAILED.
run_hook() {
local hook="$1" mod_conf="$2"
[[ -x "$hook" ]] || return 0
STASH_DIR="$STASH_DIR" MOD_CONF="$mod_conf" CONFD_DIR="$CONFD_DIR" "$hook"
local rc=$?
case "$rc" in
0) ;;
100) CHANGED=true ;;
*) warn " hook $(basename "$hook") reported an error (exit $rc)"; FAILED=true ;;
esac
}
# ── main ──────────────────────────────────────────────────────────────────────
if ! command -v patch >/dev/null 2>&1; then
warn "'patch' command not found; cannot apply mods. Install the 'patch' package."
exit 1
fi
for mod in $(list_modules); do
[[ "$(read_conf "$MAIN_CONF" modules "$mod" 0)" == "1" ]] || continue
mod_dir="$PATCHES_DIR/$mod"
manifest="$mod_dir/patches.list"
mod_conf="$CONFD_DIR/$mod.conf"
if [[ ! -f "$manifest" ]]; then
warn "No patch manifest for enabled mod '$mod' ($manifest); skipping."
FAILED=true
continue
fi
info "Checking mod: $mod"
# Build the list of active patch files (those whose condition is met).
active=()
preflight_ok=true
while IFS= read -r line; do
# Strip comments and surrounding whitespace; skip blanks.
line="${line%%#*}"
line="$(echo "$line" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')"
[[ -z "$line" ]] && continue
# Format: <patch-file> [section.key=value]
patch_name="${line%%[[:space:]]*}"
condition=""
if [[ "$line" == *[[:space:]]* ]]; then
condition="$(echo "${line#"$patch_name"}" | sed -E 's/^[[:space:]]+//')"
fi
# Evaluate optional condition against the mod's conf.d file.
if [[ -n "$condition" ]]; then
local_key="${condition%%=*}"
want="${condition#*=}"
sect="${local_key%%.*}"
ckey="${local_key#*.}"
if [[ "$(read_conf "$mod_conf" "$sect" "$ckey" 0)" != "$want" ]]; then
info " skip $patch_name (condition $condition not met)"
continue
fi
fi
patch_file="$mod_dir/$patch_name"
if [[ ! -f "$patch_file" ]]; then
warn " patch file missing: $patch_file"
preflight_ok=false
continue
fi
active+=("$patch_file")
done < "$manifest"
# Preflight dry-run: every active patch must be already applied or cleanly
# appliable. Otherwise the mod cannot be installed atomically.
to_apply=()
if [[ "$preflight_ok" == "true" ]]; then
for pf in "${active[@]}"; do
if is_applied "$pf"; then
continue
elif can_apply "$pf"; then
to_apply+=("$pf")
else
warn " $(basename "$pf") does not apply cleanly"
preflight_ok=false
break
fi
done
fi
# If preflight failed, revert the whole mod back to a clean state so it is
# never left half-applied.
if [[ "$preflight_ok" != "true" ]]; then
warn "Mod '$mod': preflight failed - reverting mod to clean state."
for (( i=${#active[@]}-1 ; i>=0 ; i-- )); do
pf="${active[$i]}"
if is_applied "$pf"; then
do_revert "$pf" && { info " reverted $(basename "$pf")"; CHANGED=true; }
fi
done
run_hook "$mod_dir/post-revert.sh" "$mod_conf"
FAILED=true
continue
fi
# Apply the outstanding patches.
if [[ ${#to_apply[@]} -gt 0 ]]; then
for pf in "${to_apply[@]}"; do
do_apply "$pf" && { info " applied $(basename "$pf")"; CHANGED=true; }
done
else
info " already up to date"
fi
run_hook "$mod_dir/post-apply.sh" "$mod_conf"
done
if [[ "$CHANGED" == "true" ]]; then
info "Restarting pveproxy..."
systemctl restart pveproxy 2>/dev/null || true
fi
[[ "$FAILED" == "true" ]] && exit 1
exit 0

View File

@ -7,6 +7,9 @@
set -euo pipefail set -euo pipefail
CONF_FILE="/etc/pve-mod/pve-mod.conf" CONF_FILE="/etc/pve-mod/pve-mod.conf"
CONFD_DIR="/etc/pve-mod/conf.d"
NODE_INFO_CONF="${CONFD_DIR}/node_info.conf"
NAG_SCREEN_CONF="${CONFD_DIR}/nag_screen.conf"
APPLY_PATCHES="/usr/lib/pve-mod/apply-patches.sh" APPLY_PATCHES="/usr/lib/pve-mod/apply-patches.sh"
NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm" NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm"
@ -24,11 +27,9 @@ ask() {
} }
bool() { [[ "$1" == true ]] && echo 1 || echo 0; } bool() { [[ "$1" == true ]] && echo 1 || echo 0; }
# Loads every value from an existing config file into the wizard's variables, _load_conf_debug() {
# so that re-running the tool preserves settings the user does not change. [[ -f "$NODE_INFO_CONF" ]] || return 0
_load_conf() { local in_debug=0 line key val
[[ -f "$CONF_FILE" ]] || return 0
local section="" line key val
while IFS= read -r line; do while IFS= read -r line; do
case "$line" in case "$line" in
'#'*|'') continue ;; '#'*|'') continue ;;
@ -78,7 +79,7 @@ _load_conf() {
debug.log_enabled) DEBUG_LOG="$val" ;; debug.log_enabled) DEBUG_LOG="$val" ;;
debug.log_file) DEBUG_LOG_FILE="$val" ;; debug.log_file) DEBUG_LOG_FILE="$val" ;;
esac esac
done < "$CONF_FILE" done < "$NODE_INFO_CONF"
} }
#endregion helpers #endregion helpers
@ -390,15 +391,33 @@ configure_node_info() {
#region write config #region write config
write_config() { write_config() {
mkdir -p "$(dirname "$CONF_FILE")" mkdir -p "$(dirname "$CONF_FILE")" "$CONFD_DIR"
# Main config: which mods are enabled + global trigger/service settings.
cat > "$CONF_FILE" <<EOF cat > "$CONF_FILE" <<EOF
# pve-mod configuration file # pve-mod main configuration file
# Managed by pve-mod-configure. Re-run to update. # Managed by pve-mod-configure. Re-run to update.
#
# This file only declares which mods are enabled. Each mod keeps its own
# settings in ${CONFD_DIR}/<mod>.conf
[modules] [modules]
node_info=${MOD_NODE_INFO} node_info=${MOD_NODE_INFO}
nag_screen=${MOD_NAG_SCREEN} nag_screen=${MOD_NAG_SCREEN}
[pve_trigger]
enabled=${PVE_TRIGGER_ENABLED}
[service]
mode=embedded
EOF
info "Main configuration saved to $CONF_FILE"
# node_info mod config.
cat > "$NODE_INFO_CONF" <<EOF
# pve-mod :: node_info mod configuration
# Managed by pve-mod-configure. Re-run to update.
[gpu] [gpu]
intel_enabled=${ENABLE_INTEL_GPU_INFO} intel_enabled=${ENABLE_INTEL_GPU_INFO}
nvidia_enabled=${ENABLE_NVIDIA_GPU_INFO} nvidia_enabled=${ENABLE_NVIDIA_GPU_INFO}
@ -424,12 +443,6 @@ device_name=${UPS_DEVICE_NAME}
enabled=${ENABLE_SYSTEM_INFO} enabled=${ENABLE_SYSTEM_INFO}
type=${SYSTEM_INFO_TYPE} type=${SYSTEM_INFO_TYPE}
[pve_trigger]
enabled=${PVE_TRIGGER_ENABLED}
[service]
mode=embedded
# Debug mode: when a collector's mode is 1, the real tool is not required. # Debug mode: when a collector's mode is 1, the real tool is not required.
# Data is read from the file path instead. Useful for development/testing. # Data is read from the file path instead. Useful for development/testing.
[debug] [debug]
@ -447,7 +460,16 @@ ups_output_file=${DEBUG_UPS_FILE}
log_enabled=${DEBUG_LOG} log_enabled=${DEBUG_LOG}
log_file=${DEBUG_LOG_FILE} log_file=${DEBUG_LOG_FILE}
EOF EOF
info "Configuration saved to $CONF_FILE" info "node_info configuration saved to $NODE_INFO_CONF"
# nag_screen mod config (placeholder; no tunable settings).
if [[ ! -f "$NAG_SCREEN_CONF" ]]; then
cat > "$NAG_SCREEN_CONF" <<EOF
# pve-mod :: nag_screen mod configuration
# The nag-screen mod has no tunable settings; this file is a placeholder
# kept for consistency with the per-mod conf.d layout.
EOF
fi
} }
#endregion write config #endregion write config

View File

@ -0,0 +1,101 @@
#!/usr/bin/env bash
# /usr/lib/pve-mod/revert-patches.sh
#
# Generic patch reverter for pve-mod.
#
# Reverses every patch found under /usr/lib/pve-mod/patches/<mod>/ (regardless
# of whether the mod is currently enabled), using `patch -R -p1 -F0 -d /`.
# Patches are reverted in reverse manifest order. Patched text files need no
# backups - `patch -R` restores them exactly. Non-patch actions (e.g. the
# nag-screen min.js symlink) are undone by each mod's 'post-revert.sh' hook.
#
# Called by prerm before the package's files are removed.
#
# Hook exit-code convention: a hook returns 0 (no change), 100 (made a change,
# triggers a pveproxy restart), or any other code (error).
set -u
# Root paths (overridable via environment, mainly for testing).
PVE_MOD_ROOT="${PVE_MOD_ROOT:-/}"
CONFD_DIR="${PVE_MOD_CONFD_DIR:-/etc/pve-mod/conf.d}"
PATCHES_DIR="${PVE_MOD_PATCHES_DIR:-/usr/lib/pve-mod/patches}"
# Storage for non-patch replaced files (e.g. nag-screen's minified proxmoxlib).
STASH_DIR="${PVE_MOD_STASH_DIR:-/var/lib/pve-mod/backup}"
info() { echo "[pve-mod] $*"; }
warn() { echo "[pve-mod] WARNING: $*" >&2; }
# Revert a single patch. Returns 0 if a change was made, 1 otherwise.
revert_one_patch() {
local patch="$1"
# Not applied? (a clean forward apply means the change is absent)
if patch -p1 -F0 -d "$PVE_MOD_ROOT" -f --dry-run -s < "$patch" >/dev/null 2>&1; then
return 1
fi
if patch -R -p1 -F0 -d "$PVE_MOD_ROOT" -f --dry-run -s < "$patch" >/dev/null 2>&1; then
patch -R -p1 -F0 -d "$PVE_MOD_ROOT" -f -s < "$patch"
return 0
fi
warn " $(basename "$patch") could not be reverted cleanly; manual cleanup may be needed"
return 1
}
# ── main ──────────────────────────────────────────────────────────────────────
if ! command -v patch >/dev/null 2>&1; then
warn "'patch' command not found; cannot revert mods."
exit 0
fi
[[ -d "$PATCHES_DIR" ]] || exit 0
CHANGED=false
for mod_dir in "$PATCHES_DIR"/*/; do
[[ -d "$mod_dir" ]] || continue
mod="$(basename "$mod_dir")"
manifest="$mod_dir/patches.list"
mod_conf="$CONFD_DIR/$mod.conf"
# Run the post-revert hook first (undo non-patch actions such as symlinks).
hook="$mod_dir/post-revert.sh"
if [[ -x "$hook" ]]; then
info "Running post-revert hook for $mod"
STASH_DIR="$STASH_DIR" MOD_CONF="$mod_conf" CONFD_DIR="$CONFD_DIR" "$hook"
rc=$?
case "$rc" in
0) ;;
100) CHANGED=true ;;
*) warn "post-revert hook for $mod reported an error (exit $rc)" ;;
esac
fi
[[ -f "$manifest" ]] || continue
info "Reverting mod: $mod"
# Collect patch names (ignore conditions and comments), then reverse order.
mapfile -t patches < <(
while IFS= read -r line; do
line="${line%%#*}"
line="$(echo "$line" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')"
[[ -z "$line" ]] && continue
echo "${line%%[[:space:]]*}"
done < "$manifest"
)
for (( i=${#patches[@]}-1 ; i>=0 ; i-- )); do
patch_name="${patches[$i]}"
patch_file="$mod_dir/$patch_name"
[[ -f "$patch_file" ]] || continue
if revert_one_patch "$patch_file"; then
info " reverted $patch_name"
CHANGED=true
fi
done
done
if [[ "$CHANGED" == "true" ]]; then
info "Restarting pveproxy..."
systemctl restart pveproxy 2>/dev/null || true
fi
exit 0

View File

@ -1,55 +1,19 @@
# pve-mod configuration file # pve-mod main configuration file
# Run 'pve-mod-configure' to set values interactively. # Run 'pve-mod-configure' to set values interactively.
# All flags are 0 (disabled) by default; pve-mod-configure enables them. #
# This file only declares which mods are enabled. Each mod keeps its own
# settings in /etc/pve-mod/conf.d/<mod>.conf
#
# When a mod flag below is 1, its patches are (re)applied on install and,
# if [pve_trigger] enabled=1, after every pve-manager upgrade.
[modules] [modules]
node_info=0 node_info=0
nag_screen=0 nag_screen=0
[gpu] # Re-apply patches automatically after a pve-manager upgrade (dpkg trigger).
intel_enabled=0
nvidia_enabled=0
amd_enabled=0
gpu_history=0
[lm_sensors]
enabled=0
enable_cpu=0
cpu_temp_target=Core
enable_ram_temp=0
enable_hdd_temp=0
enable_nvme_temp=0
enable_fan_speed=0
display_zero_speed_fans=0
temp_unit=C
[ups]
enabled=0
device_name=ups@localhost
[system_info]
enabled=0
type=1
[pve_trigger] [pve_trigger]
enabled=0 enabled=0
[service] [service]
mode=embedded mode=embedded
# Debug mode: when a mode is 1, the real tool is not required.
# Data is read from the file path instead. Useful for development/testing.
[debug]
lm_sensors_mode=0
lm_sensors_output_file=/tmp/sensors-output.json
intel_mode=0
intel_devices_file=/tmp/intel-gpu-devices.json
nvidia_mode=0
nvidia_devices_file=/tmp/nvidia-smi-devices.csv
nvidia_output_file=/tmp/nvidia-smi-output.csv
amd_mode=0
amd_devices_file=/tmp/amd-gpu-devices.json
ups_mode=0
ups_output_file=/tmp/ups-output.json
log_enabled=0
log_file=/tmp/pve-mod-debug.log