From ec0ec5fc4ab38122a444b07160ccbf436e66f35e Mon Sep 17 00:00:00 2001 From: Meliox Date: Mon, 6 Apr 2026 20:31:33 +0200 Subject: [PATCH] first implementation of displaying graphed GPU data --- PveMod_SensorInfo.pm | 151 +++++++++++- PveMod_pvemanagerlib.js | 502 +++++++++++++++++++++++++++++++++++++++- pve-mod-gui-sensors.sh | 120 ++++++++++ 3 files changed, 764 insertions(+), 9 deletions(-) diff --git a/PveMod_SensorInfo.pm b/PveMod_SensorInfo.pm index b04609d..9cdb483 100644 --- a/PveMod_SensorInfo.pm +++ b/PveMod_SensorInfo.pm @@ -6,7 +6,9 @@ use JSON; use POSIX qw(WNOHANG); use Time::HiRes qw(time); use Fcntl qw(:flock O_CREAT O_EXCL O_WRONLY); -use File::Path qw(remove_tree); +use File::Path qw(remove_tree make_path); +use PVE::INotify; +use RRDs; # debug configuration - set to 0 to disable all _debug output my $DEBUG_ENABLED = 1; @@ -508,6 +510,7 @@ sub _collector_for_intel_device { # Write to device-specific file _safe_write_json($device_state_file, $device_data); + _update_intel_gpu_rrd($device->{card}, $stats); } } } @@ -517,14 +520,145 @@ sub _collector_for_intel_device { exit 0; } -# Parse information for graphical presentation. +# ============================================================================ +# RRD Support for GPU metrics +# ============================================================================ + +my $RRD_SOCKET = '/var/run/rrdcached.sock'; +my $RRD_BASE = '/var/lib/rrdcached/db/pve-mod-gpu'; + +sub _get_nodename { + return PVE::INotify::nodename(); +} + +sub _gpu_rrd_path { + my ($card) = @_; + return "$RRD_BASE/" . _get_nodename() . "/$card"; +} + +sub _ensure_intel_gpu_rrd { + my ($card) = @_; + my $path = _gpu_rrd_path($card); + return if -f $path; + + my $dir = "$RRD_BASE/" . _get_nodename(); + make_path($dir, { mode => 0755 }) unless -d $dir; + + RRDs::create( + $path, + '--step', '1', + 'DS:freq_req:GAUGE:120:0:U', + 'DS:freq_act:GAUGE:120:0:U', + 'DS:rc6:GAUGE:120:0:100', + 'DS:power_gpu:GAUGE:120:0:U', + 'DS:power_pkg:GAUGE:120:0:U', + 'DS:render_busy:GAUGE:120:0:100', + 'DS:blitter_busy:GAUGE:120:0:100', + 'DS:video_busy:GAUGE:120:0:100', + 'DS:videnh_busy:GAUGE:120:0:100', + 'RRA:AVERAGE:0.5:1:1440', + 'RRA:AVERAGE:0.5:60:1440', + 'RRA:AVERAGE:0.5:1800:1344', + 'RRA:AVERAGE:0.5:21600:1464', + 'RRA:AVERAGE:0.5:604800:520', + 'RRA:MAX:0.5:1:1440', + 'RRA:MAX:0.5:60:1440', + 'RRA:MAX:0.5:1800:1344', + 'RRA:MAX:0.5:21600:1464', + 'RRA:MAX:0.5:604800:520', + ); + my $err = RRDs::error(); + _debug(__LINE__, "Created Intel GPU RRD $path: " . ($err // 'OK')); +} + +sub _update_intel_gpu_rrd { + my ($card, $stats) = @_; + _ensure_intel_gpu_rrd($card); + my $path = _gpu_rrd_path($card); + + my $freq_req = $stats->{frequency}{requested} // 'U'; + my $freq_act = $stats->{frequency}{actual} // 'U'; + my $rc6 = $stats->{rc6}{value} // 'U'; + my $power_gpu = $stats->{power}{GPU} // 'U'; + my $power_pkg = $stats->{power}{Package} // 'U'; + my $render_busy = $stats->{engines}{'Render/3D'}{busy} // 'U'; + my $blitter = $stats->{engines}{Blitter}{busy} // 'U'; + my $video = $stats->{engines}{Video}{busy} // 'U'; + my $videnh = $stats->{engines}{VideoEnhance}{busy} // 'U'; + + my @daemon_args = (-S $RRD_SOCKET) ? ('--daemon', "unix:$RRD_SOCKET") : (); + RRDs::update( + $path, + @daemon_args, + "N:$freq_req:$freq_act:$rc6:$power_gpu:$power_pkg:$render_busy:$blitter:$video:$videnh", + ); + my $err = RRDs::error(); + _debug(__LINE__, "RRD update intel $card: $err") if $err; +} + +sub _ensure_nvidia_gpu_rrd { + my ($index) = @_; + my $card = "nvidia$index"; + my $path = _gpu_rrd_path($card); + return if -f $path; + + my $dir = "$RRD_BASE/" . _get_nodename(); + make_path($dir, { mode => 0755 }) unless -d $dir; + + RRDs::create( + $path, + '--step', '1', + 'DS:gpu_util:GAUGE:120:0:100', + 'DS:mem_util:GAUGE:120:0:100', + 'DS:mem_used:GAUGE:120:0:U', + 'DS:mem_total:GAUGE:120:0:U', + 'DS:power_draw:GAUGE:120:0:U', + 'DS:power_limit:GAUGE:120:0:U', + 'DS:temp_gpu:GAUGE:120:0:U', + 'DS:fan_speed:GAUGE:120:0:100', + 'RRA:AVERAGE:0.5:1:1440', + 'RRA:AVERAGE:0.5:60:1440', + 'RRA:AVERAGE:0.5:1800:1344', + 'RRA:AVERAGE:0.5:21600:1464', + 'RRA:AVERAGE:0.5:604800:520', + 'RRA:MAX:0.5:1:1440', + 'RRA:MAX:0.5:60:1440', + 'RRA:MAX:0.5:1800:1344', + 'RRA:MAX:0.5:21600:1464', + 'RRA:MAX:0.5:604800:520', + ); + my $err = RRDs::error(); + _debug(__LINE__, "Created NVIDIA GPU RRD $path: " . ($err // 'OK')); +} + +sub _update_nvidia_gpu_rrd { + my ($index, $stats) = @_; + _ensure_nvidia_gpu_rrd($index); + my $card = "nvidia$index"; + my $path = _gpu_rrd_path($card); + + my $gpu_util = $stats->{utilization}{gpu} // 'U'; + my $mem_util = $stats->{utilization}{memory} // 'U'; + my $mem_used = $stats->{memory}{used} // 'U'; + my $mem_total = $stats->{memory}{total} // 'U'; + my $power_draw = $stats->{power}{draw} // 'U'; + my $power_limit = $stats->{power}{limit} // 'U'; + my $temp_gpu = $stats->{temperature}{gpu} // 'U'; + my $fan_speed = $stats->{fan}{speed} // 'U'; + + my @daemon_args = (-S $RRD_SOCKET) ? ('--daemon', "unix:$RRD_SOCKET") : (); + RRDs::update( + $path, + @daemon_args, + "N:$gpu_util:$mem_util:$mem_used:$mem_total:$power_draw:$power_limit:$temp_gpu:$fan_speed", + ); + my $err = RRDs::error(); + _debug(__LINE__, "RRD update nvidia$index: $err") if $err; +} + +# Parse information for graphical presentation. sub _parse_graphic_info { my ($line) = @_; - - # Create a RRD Database (One-Time Setup) - - # Collect intel GPU data and save it into the database - return undef; } @@ -752,6 +886,7 @@ sub _get_and_write_nvidia_stats { # Write to device-specific file _safe_write_json($device_state_file, $device_data); + _update_nvidia_gpu_rrd($device_index, $stats); } unless (@all_stats) { @@ -1990,4 +2125,4 @@ END { } } -1; \ No newline at end of file +1; diff --git a/PveMod_pvemanagerlib.js b/PveMod_pvemanagerlib.js index d87059b..39504d5 100644 --- a/PveMod_pvemanagerlib.js +++ b/PveMod_pvemanagerlib.js @@ -624,7 +624,7 @@ Ext.define('PVE.node.StatusView', { nvmeData.forEach((data) => { let deviceName = data.model; if (data.serial) { - deviceName += ` (${data.serial})`; + deviceName += ` (${data.serial})`; } html += ''; html += `${deviceName}`; @@ -1031,4 +1031,504 @@ Ext.define('PVE.node.StatusView', { me.callParent(); }, +}); + +Ext.define('pve-rrd-gpu', { + extend: 'Ext.data.Model', + fields: [ + 'freq_req', 'freq_act', 'rc6', + 'power_gpu', 'power_pkg', + 'render_busy', 'blitter_busy', 'video_busy', 'videnh_busy', + 'gpu_util', 'mem_util', 'mem_used', 'mem_total', + 'power_draw', 'power_limit', 'temp_gpu', 'fan_speed', + { type: 'date', dateFormat: 'timestamp', name: 'time' }, + ], +}); + +Ext.define('PVE.data.GpuRRDStore', { + extend: 'Proxmox.data.RRDStore', + alias: 'store.pveGpuRRDStore', + + model: 'pve-rrd-gpu', + card: undefined, + + setRRDUrl: function(timeframe, cf) { + var me = this; + if (!me.rrdurl) { return; } + if (!timeframe) { timeframe = me.timeframe; } + if (!cf) { cf = me.cf; } + me.proxy.url = me.rrdurl + + '?card=' + encodeURIComponent(me.card) + + '&timeframe=' + timeframe + + '&cf=' + cf; + }, +}); + +Ext.define('PVE.node.GpuRRD', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeGpuRRD', + + layout: 'fit', + title: 'GPU', + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + var card = me.card || 'card0'; + var baseurl = '/api2/json/nodes/' + nodename + '/gpurrddata'; + var isNvidia = card.indexOf('nvidia') === 0; + + var store = Ext.create('PVE.data.GpuRRDStore', { + rrdurl: baseurl, + card: card, + }); + + var items; + if (isNvidia) { + items = [ + { + xtype: 'proxmoxRRDChart', + title: 'GPU & Memory Utilization', + fields: ['gpu_util', 'mem_util'], + fieldTitles: ['GPU %', 'Memory %'], + unit: 'percent', + store: store, + }, + { + xtype: 'proxmoxRRDChart', + title: 'Memory Usage (MiB)', + fields: ['mem_used', 'mem_total'], + fieldTitles: ['Used', 'Total'], + store: store, + }, + { + xtype: 'proxmoxRRDChart', + title: 'Power Draw (W)', + fields: ['power_draw', 'power_limit'], + fieldTitles: ['Draw', 'Limit'], + store: store, + }, + { + xtype: 'proxmoxRRDChart', + title: 'Temperature & Fan', + fields: ['temp_gpu', 'fan_speed'], + fieldTitles: ['Temp (°C)', 'Fan %'], + store: store, + }, + ]; + } else { + items = [ + { + xtype: 'proxmoxRRDChart', + title: 'GPU Frequency (MHz)', + fields: ['freq_req', 'freq_act'], + fieldTitles: ['Requested', 'Actual'], + store: store, + }, + { + xtype: 'proxmoxRRDChart', + title: 'Engine Busy', + fields: ['render_busy', 'blitter_busy', 'video_busy', 'videnh_busy'], + fieldTitles: ['Render/3D %', 'Blitter %', 'Video %', 'VideoEnh %'], + unit: 'percent', + store: store, + }, + { + xtype: 'proxmoxRRDChart', + title: 'Power (W)', + fields: ['power_gpu', 'power_pkg'], + fieldTitles: ['GPU', 'Package'], + store: store, + }, + { + xtype: 'proxmoxRRDChart', + title: 'RC6 Residency', + fields: ['rc6'], + fieldTitles: ['RC6 %'], + unit: 'percent', + store: store, + }, + ]; + } + + Ext.apply(me, { + items: [{ + xtype: 'container', + layout: { + type: 'vbox', + align: 'stretch', + }, + items: items, + }], + }); + + me.callParent(); + + me.on('activate', function() { store.startUpdate(); }); + me.on('deactivate', function() { store.stopUpdate(); }); + me.on('destroy', function() { store.stopUpdate(); }); + }, +}); + +Ext.define('PVE.node.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeSummary', + + scrollable: true, + bodyPadding: 5, + + showVersions: function () { + var me = this; + + var nodename = me.pveSelNode.data.node; + + var view = Ext.createWidget('component', { + autoScroll: true, + id: 'pkgversions', + padding: 5, + style: { + 'white-space': 'pre', + 'font-family': 'monospace', + }, + }); + + var win = Ext.create('Ext.window.Window', { + title: gettext('Package versions'), + width: 600, + height: 600, + layout: 'fit', + modal: true, + items: [view], + buttons: [ + { + xtype: 'button', + iconCls: 'fa fa-clipboard', + handler: function (button) { + window + .getSelection() + .selectAllChildren(document.getElementById('pkgversions')); + document.execCommand('copy'); + }, + text: gettext('Copy'), + }, + { + text: gettext('Ok'), + handler: function () { + this.up('window').close(); + }, + }, + ], + }); + + Proxmox.Utils.API2Request({ + waitMsgTarget: me, + url: `/nodes/${nodename}/apt/versions`, + method: 'GET', + failure: function (response, opts) { + win.close(); + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function (response, opts) { + win.show(); + let text = ''; + Ext.Array.each(response.result.data, function (rec) { + let version = 'not correctly installed'; + let pkg = rec.Package; + if (rec.OldVersion && rec.CurrentState === 'Installed') { + version = rec.OldVersion; + } + if (rec.RunningKernel) { + text += `${pkg}: ${version} (running kernel: ${rec.RunningKernel})\n`; + } else if (rec.ManagerVersion) { + text += `${pkg}: ${version} (running version: ${rec.ManagerVersion})\n`; + } else { + text += `${pkg}: ${version}\n`; + } + }); + + view.update(Ext.htmlEncode(text)); + }, + }); + }, + + updateRepositoryStatus: function () { + let me = this; + let repoStatus = me.nodeStatus.down('#repositoryStatus'); + + let nodename = me.pveSelNode.data.node; + + Proxmox.Utils.API2Request({ + url: `/nodes/${nodename}/apt/repositories`, + method: 'GET', + failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + success: (response) => + repoStatus.setRepositoryInfo(response.result.data['standard-repos']), + }); + + Proxmox.Utils.API2Request({ + url: `/nodes/${nodename}/subscription`, + method: 'GET', + failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + success: function (response, opts) { + const res = response.result; + const subscription = res?.data?.status.toLowerCase() === 'active'; + repoStatus.setSubscriptionStatus(subscription); + }, + }); + }, + + initComponent: function () { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw 'no node name specified'; + } + + if (!me.statusStore) { + throw 'no status storage specified'; + } + + var rstore = me.statusStore; + + var version_btn = new Ext.Button({ + text: gettext('Package versions'), + handler: function () { + Proxmox.Utils.checked_command(function () { + me.showVersions(); + }); + }, + }); + + var rrdstore = Ext.create('Proxmox.data.RRDStore', { + rrdurl: '/api2/json/nodes/' + nodename + '/rrddata', + model: 'pve-rrd-node', + }); + + var gpurrdstore = Ext.create('PVE.data.GpuRRDStore', { + rrdurl: '/api2/json/nodes/' + nodename + '/gpurrddata', + card: 'card0', + }); + + let nodeStatus = Ext.create('PVE.node.StatusView', { + xtype: 'pveNodeStatus', + rstore: rstore, + width: 770, + pveSelNode: me.pveSelNode, + }); + + Ext.apply(me, { + tbar: [version_btn, '->', { xtype: 'proxmoxRRDTypeSelector' }], + nodeStatus: nodeStatus, + items: [ + { + xtype: 'container', + itemId: 'itemcontainer', + layout: 'column', + minWidth: 700, + defaults: { + minHeight: 360, + padding: 5, + columnWidth: 1, + }, + items: [ + nodeStatus, + { + xtype: 'proxmoxRRDChart', + title: gettext('CPU Usage'), + fields: ['cpu', 'iowait'], + fieldTitles: [gettext('CPU usage'), gettext('IO delay')], + unit: 'percent', + store: rrdstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Server Load'), + fields: ['loadavg'], + fieldTitles: [gettext('Load average')], + store: rrdstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Memory usage'), + fields: [ + { + yField: 'memtotal', + title: gettext('Total'), + tooltip: { + trackMouse: true, + renderer: function (toolTip, record, item) { + let value = record.get('memtotal'); + + if (value === null) { + toolTip.setHtml(gettext('No Data')); + } else { + let total = Proxmox.Utils.format_size(value); + let time = new Date(record.get('time')); + + let avail = record.get('memavailable'); + let availText = ''; + if (Ext.isNumeric(avail)) { + let v = Proxmox.Utils.format_size(avail); + availText = ` (${gettext('Available')}: ${v})`; + } + + toolTip.setHtml( + `${gettext('Total')}: ${total}${availText}
${time}`, + ); + } + }, + }, + }, + { + yField: 'memused', + title: gettext('Used'), + tooltip: { + trackMouse: true, + renderer: function (toolTip, record, item) { + let value = record.get('memused'); + + if (value === null) { + toolTip.setHtml(gettext('No Data')); + } else { + let total = Proxmox.Utils.format_size(value); + let time = new Date(record.get('time')); + + let arc = record.get('arcsize'); + let arcText = ''; + if (Ext.isNumeric(arc) && arc > 1024 * 1024) { + let v = Proxmox.Utils.format_size(value - arc); + arcText = ` (${gettext('Without ZFS ARC')}: ${v})`; + } + + toolTip.setHtml( + `${gettext('Used')}: ${total}${arcText}
${time}`, + ); + } + }, + }, + }, + 'arcsize', + { + type: 'line', + fill: false, + yField: 'memavailable', + title: gettext('Available'), + style: { + lineWidth: 2.5, + opacity: 1, + }, + }, + ], + fieldTitles: [ + gettext('Total'), + gettext('Used'), + gettext('ZFS ARC'), + gettext('Available'), + ], + colors: ['#94ae0a', '#115fa6', '#24AD9A', '#bbde0d'], + unit: 'bytes', + powerOfTwo: true, + store: rrdstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Network Traffic'), + fields: ['netin', 'netout'], + fieldTitles: [gettext('Incoming'), gettext('Outgoing')], + store: rrdstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('CPU Pressure Stall'), + fieldTitles: ['Some'], + fields: ['pressurecpusome'], + colors: ['#FFD13E', '#A61120'], + store: rrdstore, + unit: 'percent', + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('IO Pressure Stall'), + fieldTitles: ['Some', 'Full'], + fields: ['pressureiosome', 'pressureiofull'], + colors: ['#FFD13E', '#A61120'], + store: rrdstore, + unit: 'percent', + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Memory Pressure Stall'), + fieldTitles: ['Some', 'Full'], + fields: ['pressurememorysome', 'pressurememoryfull'], + colors: ['#FFD13E', '#A61120'], + store: rrdstore, + unit: 'percent', + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('GPU Frequency (MHz)'), + fields: ['freq_req', 'freq_act'], + fieldTitles: [gettext('Requested'), gettext('Actual')], + store: gpurrdstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('GPU Engine Busy'), + fields: ['render_busy', 'blitter_busy', 'video_busy', 'videnh_busy'], + fieldTitles: [gettext('Render/3D'), gettext('Blitter'), gettext('Video'), gettext('VideoEnh')], + unit: 'percent', + store: gpurrdstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('GPU Power (W)'), + fields: ['power_gpu', 'power_pkg'], + fieldTitles: [gettext('GPU'), gettext('Package')], + store: gpurrdstore, + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('GPU RC6 Residency'), + fields: ['rc6'], + fieldTitles: [gettext('RC6 %')], + unit: 'percent', + store: gpurrdstore, + }, + ], + listeners: { + resize: function (panel) { + Proxmox.Utils.updateColumns(panel); + }, + }, + }, + ], + listeners: { + activate: function () { + rstore.setInterval(1000); + rstore.startUpdate(); + rrdstore.startUpdate(); + gpurrdstore.startUpdate(); + }, + destroy: function () { + rstore.setInterval(5000); + rrdstore.stopUpdate(); + gpurrdstore.stopUpdate(); + }, + }, + }); + + me.updateRepositoryStatus(); + + me.callParent(); + + let sp = Ext.state.Manager.getProvider(); + me.mon(sp, 'statechange', function (provider, key, value) { + if (key !== 'summarycolumns') { + return; + } + Proxmox.Utils.updateColumns(me.getComponent('itemcontainer')); + }); + }, }); \ No newline at end of file diff --git a/pve-mod-gui-sensors.sh b/pve-mod-gui-sensors.sh index 4e43457..22e0e86 100644 --- a/pve-mod-gui-sensors.sh +++ b/pve-mod-gui-sensors.sh @@ -35,6 +35,7 @@ PVE_MOD_JS_TARGET_FILE="/usr/share/pve-manager/js/PveMod_PveNodeStatusView.js" NODES_PM_FILE="/usr/share/perl5/PVE/API2/Nodes.pm" PVE_SENSOR_INFO_MOD_FILE="/usr/share/perl5/PVE/API2/PveMod_SensorInfo.pm" PVE_SENSOR_INFO_SOURCE_FILE="$SCRIPT_CWD/PveMod_SensorInfo.pm" +GPU_RRD_DIR="/var/lib/rrdcached/db/pve-mod-gpu" #region message tools # Section header (bold) @@ -281,6 +282,28 @@ function configure { #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 + #### RAM #### #region ram setup local ramList ramCount @@ -477,6 +500,11 @@ function install_mod { insert_sensor_monitor_into_pve insert_system_info_into_pve + ## Historical GPU data ## + if [[ "$ENABLE_GPU_HISTORY" == true ]]; then + install_gpu_history + fi + #### Install UI modification module #### msgb "\n=== Installing UI modification module ===" install_node_status_view_module @@ -624,6 +652,14 @@ install_node_status_view_module() { 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 @@ -643,6 +679,83 @@ Ext.Loader.loadScript({\\ } #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 ===" @@ -706,6 +819,13 @@ function uninstall_mod { warn "No PveMod_SensorInfo.pm backup files found and module not installed." 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