PVE-mods/src/PVENodeInfo/apply-patches.sh
2026-06-07 19:13:00 +02:00

262 lines
9.0 KiB
Bash

#!/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