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