From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 1261C1FF14C for ; Fri, 26 Jun 2026 14:11:35 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A26E0F9A4; Fri, 26 Jun 2026 14:10:56 +0200 (CEST) From: Thomas Lamprecht To: pve-devel@lists.proxmox.com Subject: [PATCH manager 13/13] ui: node: show multipath maps and their paths under Disks Date: Fri, 26 Jun 2026 14:07:43 +0200 Message-ID: <20260626121000.2095591-14-t.lamprecht@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260626121000.2095591-1-t.lamprecht@proxmox.com> References: <20260626121000.2095591-1-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: 1782475802074 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.005 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment 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: ZZ3RUVDYZ4WIXV2OHU4ZL5NIW7P34CTX X-Message-ID-Hash: ZZ3RUVDYZ4WIXV2OHU4ZL5NIW7P34CTX 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 --- www/manager6/Makefile | 1 + www/manager6/node/Config.js | 7 ++ www/manager6/node/Multipath.js | 163 +++++++++++++++++++++++++++++++++ 3 files changed, 171 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 f6cd87492..29a6b9a77 100644 --- a/www/manager6/node/Config.js +++ b/www/manager6/node/Config.js @@ -355,6 +355,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..51c257d42 --- /dev/null +++ b/www/manager6/node/Multipath.js @@ -0,0 +1,163 @@ +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) => v || rec.data.wwid, + }, + { + text: 'WWID', + dataIndex: 'wwid', + flex: 1, + }, + { + 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) => 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) => 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`, + // the endpoint wraps the maps in a supported/running envelope + reader: { rootProperty: 'data.maps' }, + }, + }); + + 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 raw = rstore.getProxy().getReader().rawData || {}; + let info = raw.data || raw; + 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