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 F1BB61FF0AF for ; Fri, 06 Nov 2026 01:57:00 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id EDD1D21CB6; Fri, 06 Nov 2026 01:51:16 +0100 (CET) From: Thomas Lamprecht To: pve-devel@lists.proxmox.com Subject: [PATCH v3 manager 58/76] ui: san luns: show multipath path state Date: Fri, 6 Nov 2026 00:31:33 +0100 Message-ID: <20261105233336.2178064-59-t.lamprecht@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261105233336.2178064-1-t.lamprecht@proxmox.com> References: <20261105233336.2178064-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: 1793926181465 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.307 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) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust 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: 3CCOCJSVVQJ5ZEGVDNZRJRYHYVDRQLP6 X-Message-ID-Hash: 3CCOCJSVVQJ5ZEGVDNZRJRYHYVDRQLP6 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: From: Dietmar Maurer Show active and total path counts with a warning when they differ, also in the Fibre Channel wizard through the shared renderer. Expand rows to show path states and fault counters, with checker state and WWPNs for multipathd or ANA state and controller addresses for native NVMe multipath. Administrators can inspect these without running "multipath -ll" or reading sysfs. Signed-off-by: Dietmar Maurer [TL: use the active path count under the name the scan uses and a shared helper for the detail table markup] Signed-off-by: Thomas Lamprecht --- www/manager6/SANUtils.js | 20 +++++++++- www/manager6/node/SANLuns.js | 76 +++++++++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/www/manager6/SANUtils.js b/www/manager6/SANUtils.js index 3b31ec614..25c733263 100644 --- a/www/manager6/SANUtils.js +++ b/www/manager6/SANUtils.js @@ -7,6 +7,7 @@ Ext.define('PVE.SANUtils', { 'vendor', 'model', 'paths-total', + 'paths-active', 'usage', 'used-by-storage', 'vgname', @@ -15,12 +16,29 @@ Ext.define('PVE.SANUtils', { 'by-id', ], + // headers and cells are HTML + render_detail_table: function (headers, rows) { + let header = headers.map((text) => `${text}`).join(''); + let body = rows.map((cells) => `${cells.join('')}`).join(''); + return `${header}${body}
`; + }, + render_san_lun_device: function (value, metaData, rec) { let text = Ext.htmlEncode(value); // only multipath mapped devices report their path count let paths = rec.get('paths-total'); if (paths) { - text += ` (${paths} ${ngettext('path', 'paths', paths)})`; + let active = rec.get('paths-active'); + if (Ext.isNumeric(active) && active < paths) { + let qtip = Ext.htmlEncode( + gettext('Some paths failed, the multipath device is degraded'), + ); + text = + ` ` + + `${text} (${active}/${paths} ${gettext('paths')})`; + } else { + text += ` (${paths} ${ngettext('path', 'paths', paths)})`; + } } return text; }, diff --git a/www/manager6/node/SANLuns.js b/www/manager6/node/SANLuns.js index 593bfc519..fe30125d8 100644 --- a/www/manager6/node/SANLuns.js +++ b/www/manager6/node/SANLuns.js @@ -28,6 +28,73 @@ Ext.define('PVE.node.SANLunList', { trackOver: false, }, + plugins: [ + { + ptype: 'rowexpander', + expandOnDblClick: false, + scrollIntoViewOnExpand: false, + rowBodyTpl: [ + '{[this.renderDetails(values)]}', + { + renderDetails: function (values) { + let enc = Ext.htmlEncode; + let lines = []; + if (values.wwn) { + lines.push(`WWN: ${enc(values.wwn)}`); + } + if (values.serial) { + lines.push(`${gettext('Serial')}: ${enc(values.serial)}`); + } + let faults = values['path-faults']; + if (Ext.isNumeric(faults)) { + lines.push( + `${gettext('Path Failures Since Map Creation')}: ${enc(faults)}`, + ); + } + let paths = values['path-list'] || []; + if (paths.length) { + let goodStates = { active: 1, live: 1 }; + let goodAna = { optimized: 1, 'non-optimized': 1 }; + let columns = [ + { key: 'device', label: gettext('Path') }, + { + key: 'state', + label: gettext('State'), + bad: (path) => !goodStates[path.state], + }, + { key: 'checker-state', label: gettext('Checker') }, + { + key: 'ana-state', + label: 'ANA', + bad: (path) => !goodAna[path['ana-state']], + }, + { key: 'host-wwpn', label: 'Host WWPN' }, + { key: 'target-wwpn', label: 'Target WWPN' }, + { key: 'address', label: gettext('Address') }, + ].filter((col) => paths.some((path) => path[col.key] !== undefined)); + let rows = paths.map((path) => + columns.map((col) => { + let text = enc(path[col.key] ?? ''); + if (text !== '' && col.bad && col.bad(path)) { + text = ` ${text}`; + } + return text; + }), + ); + lines.push( + PVE.SANUtils.render_detail_table( + columns.map((col) => col.label), + rows, + ), + ); + } + return lines.join('
'); + }, + }, + ], + }, + ], + columns: [ { header: gettext('Device'), @@ -109,7 +176,14 @@ Ext.define('PVE.node.SANLunList', { }); let store = Ext.create('Ext.data.Store', { - fields: [...PVE.SANUtils.lun_fields, 'transport', 'serial', 'wwn'], + fields: [ + ...PVE.SANUtils.lun_fields, + 'transport', + 'serial', + 'wwn', + 'path-faults', + 'path-list', + ], proxy: { type: 'proxmox', url: `/api2/json/nodes/${me.nodename}/scan/san-luns`, -- 2.47.3