From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 67CC91FF142 for ; Fri, 03 Jul 2026 17:32:34 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 8DAE621210; Fri, 03 Jul 2026 17:31:27 +0200 (CEST) From: Thomas Lamprecht To: pve-devel@lists.proxmox.com Subject: [PATCH v2 manager 13/13] ui: node: show multipath maps and their paths under Disks Date: Fri, 3 Jul 2026 14:46:13 +0200 Message-ID: <20260703124707.1172980-15-t.lamprecht@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260703124707.1172980-2-t.lamprecht@proxmox.com> References: <20260703124707.1172980-2-t.lamprecht@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1783092616464 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.022 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: U4JVUHVYTBGN5IGCKUNU3AZQTYFNEP63 X-Message-ID-Hash: U4JVUHVYTBGN5IGCKUNU3AZQTYFNEP63 X-MailFrom: t.lamprecht@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Add a read-only per-node view of the device-mapper multipath maps and their individual paths, the detail behind the datacenter health matrix. Each map expands to its paths with their device, state and transport, so when the matrix flags a node as degraded an operator can see exactly which path is down without dropping to the shell. Signed-off-by: Thomas Lamprecht --- Changes in v2: - capitalize the 'Used By' column header - show the correct reason for an empty grid (not installed / daemon down) - HTML-encode the name, WWID, and transport renderers www/manager6/Makefile | 1 + www/manager6/node/Config.js | 7 ++ www/manager6/node/Multipath.js | 171 +++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 www/manager6/node/Multipath.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 1a3b845c8..9b5125a66 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -245,6 +245,7 @@ JSSRC= \ node/Directory.js \ node/LVM.js \ node/LVMThin.js \ + node/Multipath.js \ node/StatusView.js \ node/Subscription.js \ node/Summary.js \ diff --git a/www/manager6/node/Config.js b/www/manager6/node/Config.js index 217ee284c..bb1c30fd4 100644 --- a/www/manager6/node/Config.js +++ b/www/manager6/node/Config.js @@ -357,6 +357,13 @@ Ext.define('PVE.node.Config', { groups: ['storage'], xtype: 'pveZFSList', }, + { + xtype: 'pveMultipathStatus', + title: gettext('Multipath'), + itemId: 'multipath', + iconCls: 'fa fa-road', + groups: ['storage'], + }, { xtype: 'pveNodeCephStatus', title: 'Ceph', diff --git a/www/manager6/node/Multipath.js b/www/manager6/node/Multipath.js new file mode 100644 index 000000000..f21a91483 --- /dev/null +++ b/www/manager6/node/Multipath.js @@ -0,0 +1,171 @@ +Ext.define('PVE-node-multipath-map', { + extend: 'Ext.data.Model', + idProperty: 'wwid', + fields: [ + 'wwid', + 'name', + 'health', + 'transport', + 'used-by', + 'path-groups', + { name: 'paths-active', type: 'number' }, + { name: 'paths-total', type: 'number' }, + { name: 'size', type: 'number' }, + { + // pre-rendered path detail for the row expander; keeps the hyphenated map keys out + // of the XTemplate and recomputes when the paths change + name: 'pathsHtml', + calculate: function (data) { + let html = ''; + (data['path-groups'] || []).forEach((pg) => { + (pg.paths || []).forEach((p) => { + let state = p['dm-state'] || ''; + let icon = + state === 'active' ? 'check-circle good' : 'times-circle critical'; + let extra = [p['dev-state'], p.transport].filter((v) => v).join(', '); + html += + '
' + + ` ` + + `${Ext.htmlEncode(p.dev || '')} - ${Ext.htmlEncode(state)}` + + (extra ? ` (${Ext.htmlEncode(extra)})` : '') + + '
'; + }); + }); + return html; + }, + }, + ], +}); + +// Read-only per-node view of the device-mapper multipath maps and their paths, the detail behind +// the datacenter-wide health matrix. The map rows update in place via a DiffStore, so live +// path-state changes do not flicker or collapse the expanded rows. +Ext.define('PVE.node.MultipathStatus', { + extend: 'Ext.grid.Panel', + xtype: 'pveMultipathStatus', + + stateful: true, + stateId: 'grid-node-multipath', + + emptyText: gettext('No multipath maps on this node.'), + + plugins: [ + { + ptype: 'rowexpander', + rowBodyTpl: ['
{pathsHtml}
'], + }, + ], + + columns: [ + { + text: gettext('Name'), + dataIndex: 'name', + flex: 1, + renderer: (v, meta, rec) => Ext.htmlEncode(v || rec.data.wwid), + }, + { + text: 'WWID', + dataIndex: 'wwid', + flex: 1, + renderer: Ext.htmlEncode, + }, + { + text: gettext('Health'), + dataIndex: 'health', + width: 130, + renderer: PVE.Utils.render_multipath_health, + }, + { + text: gettext('Paths'), + width: 90, + align: 'right', + renderer: (v, meta, rec) => `${rec.data['paths-active']}/${rec.data['paths-total']}`, + }, + { + text: gettext('Transport'), + dataIndex: 'transport', + width: 90, + renderer: (v) => Ext.htmlEncode(v || ''), + }, + { + text: gettext('Size'), + dataIndex: 'size', + width: 100, + align: 'right', + renderer: (v) => (v ? Proxmox.Utils.format_size(v) : ''), + }, + { + text: gettext('Used By'), + dataIndex: 'used-by', + flex: 1, + renderer: (v) => Ext.htmlEncode(v || ''), + }, + ], + + initComponent: function () { + let me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw 'no node name specified'; + } + + let rstore = Ext.create('Proxmox.data.UpdateStore', { + interval: 3000, + storeid: `node-multipath-${me.nodename}`, + model: 'PVE-node-multipath-map', + proxy: { + type: 'proxmox', + url: `/api2/json/nodes/${me.nodename}/disks/multipath`, + reader: { + // maps come from data.maps; stash the supported/running envelope so the load + // handler can explain an empty grid (not installed / daemon down / no maps) + rootProperty: 'data.maps', + transform: (raw) => { + me.mpInfo = (raw && raw.data) || {}; + return raw; + }, + }, + }, + }); + + me.store = Ext.create('Proxmox.data.DiffStore', { + rstore: rstore, + sorters: [{ property: 'wwid' }], + }); + + me.tbar = [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: () => rstore.load(), + }, + ]; + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, rstore, true); + + // the empty grid means different things (no maps, daemon down, not installed); pull the + // reason from the envelope to guide the operator + me.mon(rstore, 'load', function (store, records, success) { + if (!success) { + return; + } + let info = me.mpInfo || {}; + let text; + if (!info.supported) { + text = gettext('multipath-tools is not installed on this node.'); + } else if (!info.running) { + text = gettext('The multipathd daemon is not running.'); + } else { + text = gettext('No multipath maps on this node.'); + } + me.setEmptyText(text); + }); + + me.on('activate', () => rstore.startUpdate()); + me.on('deactivate', () => rstore.stopUpdate()); + me.on('destroy', () => rstore.stopUpdate()); + }, +}); -- 2.47.3