From 96bd5cab8b2b352b4a5ad87a090be4788609eb65 Mon Sep 17 00:00:00 2001 From: Meliox Date: Mon, 8 Jun 2026 22:01:28 +0200 Subject: [PATCH] rework apply and revert patches to patch --- debian/control | 2 +- debian/pve-mod.conffiles | 2 + debian/pve-mod.postinst | 50 +++- debian/rules | 39 ++- src/NagScreen/nag_screen.conf | 3 + .../patches/01-proxmoxlib-js-nagscreen.patch | 1 + src/NagScreen/patches/patches.list | 4 + src/NagScreen/patches/post-apply.sh | 26 ++ src/NagScreen/patches/post-revert.sh | 23 ++ src/PVENodeInfo/apply-patches.sh | 261 ------------------ src/PVENodeInfo/node_info.conf | 45 +++ src/PVENodeInfo/patches/patches.list | 9 + src/PVENodeInfo/patches/post-apply.sh | 34 +++ src/PVENodeInfo/revert-patches.sh | 69 ----- src/Scripts/apply-patches.sh | 206 ++++++++++++++ src/Scripts/pve-mod-configure | 46 ++- src/Scripts/revert-patches.sh | 101 +++++++ src/pve-mod.conf | 52 +--- 18 files changed, 569 insertions(+), 404 deletions(-) create mode 100644 src/NagScreen/nag_screen.conf create mode 100644 src/NagScreen/patches/patches.list create mode 100644 src/NagScreen/patches/post-apply.sh create mode 100644 src/NagScreen/patches/post-revert.sh delete mode 100644 src/PVENodeInfo/apply-patches.sh create mode 100644 src/PVENodeInfo/node_info.conf create mode 100644 src/PVENodeInfo/patches/patches.list create mode 100644 src/PVENodeInfo/patches/post-apply.sh delete mode 100644 src/PVENodeInfo/revert-patches.sh create mode 100644 src/Scripts/apply-patches.sh create mode 100644 src/Scripts/revert-patches.sh diff --git a/debian/control b/debian/control index d2d26a3..21f48d8 100644 --- a/debian/control +++ b/debian/control @@ -8,7 +8,7 @@ Homepage: https://github.com/Meliox/PVE-mods Package: pve-mod Architecture: all -Depends: ${misc:Depends}, perl, librrds-perl +Depends: ${misc:Depends}, perl, librrds-perl, patch Recommends: lm-sensors, nut-client Suggests: igt-gpu-tools Description: Proxmox VE UI modifications and sensor monitoring diff --git a/debian/pve-mod.conffiles b/debian/pve-mod.conffiles index 0c4d9d3..1db9445 100644 --- a/debian/pve-mod.conffiles +++ b/debian/pve-mod.conffiles @@ -1 +1,3 @@ /etc/pve-mod/pve-mod.conf +/etc/pve-mod/conf.d/node_info.conf +/etc/pve-mod/conf.d/nag_screen.conf diff --git a/debian/pve-mod.postinst b/debian/pve-mod.postinst index 7ca0c76..1140cb4 100644 --- a/debian/pve-mod.postinst +++ b/debian/pve-mod.postinst @@ -1,9 +1,15 @@ #!/bin/bash set -e -NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm" -DEFAULT_CONF="/usr/share/pve-mod/pve-mod.conf.default" -USER_CONF="/etc/pve-mod/pve-mod.conf" +DEFAULT_DIR="/usr/share/pve-mod" +CONF_DIR="/etc/pve-mod" + +# Config file pairs to compare on upgrade: "|" +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. _extract_conf_keys() { @@ -17,30 +23,48 @@ _extract_conf_keys() { done < "$file" } -# Warns about keys present in the default config but missing from the user's config. -_check_new_config_keys() { - [ -f "$DEFAULT_CONF" ] || return 0 - [ -f "$USER_CONF" ] || return 0 +# Warns about keys present in a default config but missing from the user's copy. +_check_one_pair() { + local default_conf="$1" user_conf="$2" + [ -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 case "$line" in '#'*|'') continue ;; '['*']') section="${line#[}"; section="${section%]}"; continue ;; *'='*) key="${line%%=*}" - if ! _extract_conf_keys "$USER_CONF" | grep -qF "${section}.${key}"; then - new_keys="${new_keys} [${section}] ${line}\n" + if ! _extract_conf_keys "$user_conf" | grep -qF "${section}.${key}"; then + new_keys="${new_keys} [${section}] ${line} (${user_conf})\n" fi ;; esac - done < "$DEFAULT_CONF" + done < "$default_conf" 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 < [section.key=value] + +01-proxmoxlib-js-nagscreen.patch diff --git a/src/NagScreen/patches/post-apply.sh b/src/NagScreen/patches/post-apply.sh new file mode 100644 index 0000000..c4f0b96 --- /dev/null +++ b/src/NagScreen/patches/post-apply.sh @@ -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 diff --git a/src/NagScreen/patches/post-revert.sh b/src/NagScreen/patches/post-revert.sh new file mode 100644 index 0000000..45c9df3 --- /dev/null +++ b/src/NagScreen/patches/post-revert.sh @@ -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 diff --git a/src/PVENodeInfo/apply-patches.sh b/src/PVENodeInfo/apply-patches.sh deleted file mode 100644 index cba11d7..0000000 --- a/src/PVENodeInfo/apply-patches.sh +++ /dev/null @@ -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 diff --git a/src/PVENodeInfo/node_info.conf b/src/PVENodeInfo/node_info.conf new file mode 100644 index 0000000..13c4ed4 --- /dev/null +++ b/src/PVENodeInfo/node_info.conf @@ -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 diff --git a/src/PVENodeInfo/patches/patches.list b/src/PVENodeInfo/patches/patches.list new file mode 100644 index 0000000..9049189 --- /dev/null +++ b/src/PVENodeInfo/patches/patches.list @@ -0,0 +1,9 @@ +# pve-mod :: node_info patch manifest +# Format: [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 diff --git a/src/PVENodeInfo/patches/post-apply.sh b/src/PVENodeInfo/patches/post-apply.sh new file mode 100644 index 0000000..62b3b5e --- /dev/null +++ b/src/PVENodeInfo/patches/post-apply.sh @@ -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 diff --git a/src/PVENodeInfo/revert-patches.sh b/src/PVENodeInfo/revert-patches.sh deleted file mode 100644 index d86097a..0000000 --- a/src/PVENodeInfo/revert-patches.sh +++ /dev/null @@ -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 diff --git a/src/Scripts/apply-patches.sh b/src/Scripts/apply-patches.sh new file mode 100644 index 0000000..e6dc887 --- /dev/null +++ b/src/Scripts/apply-patches.sh @@ -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//. +# +# Patch convention: every .patch uses a/ b/ 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
[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: [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 diff --git a/src/Scripts/pve-mod-configure b/src/Scripts/pve-mod-configure index 9911fb5..689c0db 100644 --- a/src/Scripts/pve-mod-configure +++ b/src/Scripts/pve-mod-configure @@ -7,6 +7,9 @@ set -euo pipefail 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" NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm" @@ -25,7 +28,7 @@ ask() { bool() { [[ "$1" == true ]] && echo 1 || echo 0; } _load_conf_debug() { - [[ -f "$CONF_FILE" ]] || return 0 + [[ -f "$NODE_INFO_CONF" ]] || return 0 local in_debug=0 line key val while IFS= read -r line; do case "$line" in @@ -50,7 +53,7 @@ _load_conf_debug() { log_enabled) DEBUG_LOG="$val" ;; log_file) DEBUG_LOG_FILE="$val" ;; esac - done < "$CONF_FILE" + done < "$NODE_INFO_CONF" } #endregion helpers @@ -346,15 +349,33 @@ configure_node_info() { #region 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" <.conf [modules] node_info=${MOD_NODE_INFO} 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" < "$NAG_SCREEN_CONF" </ (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 diff --git a/src/pve-mod.conf b/src/pve-mod.conf index 40c3022..2757128 100644 --- a/src/pve-mod.conf +++ b/src/pve-mod.conf @@ -1,55 +1,19 @@ -# pve-mod configuration file +# pve-mod main configuration file # 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/.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] node_info=0 nag_screen=0 -[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 - +# Re-apply patches automatically after a pve-manager upgrade (dpkg trigger). [pve_trigger] enabled=0 [service] 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