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 4AB891FF0AF for ; Fri, 06 Nov 2026 01:59:52 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 78D49216F4; Fri, 06 Nov 2026 01:51:24 +0100 (CET) From: Thomas Lamprecht To: pve-devel@lists.proxmox.com Subject: [PATCH v3 manager 75/76] ui: san: show the local users of a LUN in its usage tooltip Date: Fri, 6 Nov 2026 00:31:50 +0100 Message-ID: <20261105233336.2178064-76-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: 1793926182741 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.294 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: OW6JEQOIDEF3F5UZWLL5DC5BNQD4DELW X-Message-ID-Hash: OW6JEQOIDEF3F5UZWLL5DC5BNQD4DELW 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: The SAN LUN lists showed whether a LUN holds data, but not what uses it on the node, for example a logical volume that LVM activated on one path of a multipath map. Only the refusals named such users. Add the users, or the failed check for them, to the tooltip of the usage column in Node > SAN and in the setup wizards. Signed-off-by: Thomas Lamprecht --- test/SANUtils_test.js | 42 ++++++++++++++++++++- www/manager6/SANUtils.js | 40 +++++++++++++------- www/manager6/storage/wizard/FibreChannel.js | 6 ++- www/manager6/storage/wizard/LunSelect.js | 1 + 4 files changed, 73 insertions(+), 16 deletions(-) diff --git a/test/SANUtils_test.js b/test/SANUtils_test.js index 86f97e9bd..f60e23b73 100644 --- a/test/SANUtils_test.js +++ b/test/SANUtils_test.js @@ -7,7 +7,13 @@ const { test } = require('node:test'); // load PVE.SANUtils with stubs of the ExtJS and translation helpers it uses function sanUtils(translations = {}) { const sandbox = { - PVE: {}, + PVE: { + window: { + SANLunWipe: { + texts: () => ({ unchecked: 'Could not check whether this node uses the LUN.' }), + }, + }, + }, gettext: (text) => translations[text] ?? text, Ext: { apply: (target, source) => Object.assign(target, source), @@ -105,3 +111,37 @@ test('LUNs not offered for a new volume group', () => { 'an existing volume group stays available for reuse', ); }); + +test('local users of a LUN in its usage tooltip', () => { + const utils = sanUtils(); + const row = (inUse, users = '') => ({ + usage: 'lvm', + vgname: 'vg1', + 'in-use': inUse, + 'in-use-by': users, + }); + const tip = (lun, options) => utils.san_lun_usage(lun, options).tip; + + assert.doesNotMatch(tip(row('no')), /use/i, 'nothing for a LUN the node does not use'); + assert.match( + tip(row('yes', 'sdb held by vg1-vm--100--disk--0')), + /
In use on this node: sdb held by vg1-vm--100--disk--0$/, + 'the users, in the node view', + ); + assert.match( + tip(row('yes', 'dm-5'), { node: 'n1' }), + /
In use on node "n1": dm-5$/, + 'the users and the scan node, in a wizard', + ); + assert.match( + tip(row('unknown', 'check failed: timeout')), + /
Could not check whether this node uses the LUN\.$/, + 'a failed check', + ); + const refusal = 'In use on this node: dm-5'; + assert.equal( + tip(row('yes', 'dm-5'), { node: 'n1', refusal }).split('
').length, + 2, + 'not again next to the refusal to wipe that names the users', + ); +}); diff --git a/www/manager6/SANUtils.js b/www/manager6/SANUtils.js index d3a50c1a9..dbef08c84 100644 --- a/www/manager6/SANUtils.js +++ b/www/manager6/SANUtils.js @@ -367,27 +367,39 @@ Ext.define('PVE.SANUtils', { return Number(response?.result?.status) === 403; }, + // The local users of a LUN or the failed check, undefined if the scan node does not use it. + // Without a node, the texts are those of the node's own views, which speak of "this node". + san_lun_users_text: function (row, node) { + if (row['in-use'] === 'no') { + return undefined; + } + if (row['in-use'] === 'yes') { + let users = row['in-use-by'] ?? gettext('unknown'); + return node === undefined + ? Ext.String.format(gettext('In use on this node: {0}'), users) + : Ext.String.format(gettext('In use on node "{0}": {1}'), node, users); + } + return node === undefined + ? PVE.window.SANLunWipe.texts(row).unchecked + : Ext.String.format(gettext('Could not check usage on node "{0}"'), node); + }, + // A node refuses a new volume group on an unused LUN that it uses or could not check, so the // setup should not offer it. Returns the reason, or undefined for any other LUN. san_lun_in_use_reason: function (row, node) { - if (row.usage !== 'unused' || row['in-use'] === 'no') { - return undefined; - } - if (row['in-use'] === 'yes') { - return Ext.String.format( - gettext('In use on node "{0}": {1}'), - node, - row['in-use-by'] ?? gettext('unknown'), - ); - } - return Ext.String.format(gettext('Could not check usage on node "{0}"'), node); + return row.usage === 'unused' ? PVE.SANUtils.san_lun_users_text(row, node) : undefined; }, - // returns { text, tip } as HTML; an attribution to ownStorage alone does not count as use, and - // refusal says why the row cannot be wiped - san_lun_usage: function (row, { ownStorage, refusal } = {}) { + // returns { text, tip } as HTML; an attribution to ownStorage alone does not count as use, + // refusal says why the row cannot be wiped, and node is the scan node outside the node's views + san_lun_usage: function (row, { ownStorage, refusal, node } = {}) { let text = PVE.SANUtils.san_lun_usage_text(row, ownStorage); let tooltip = [text]; + let users = PVE.SANUtils.san_lun_users_text(row, node); + // the refusal to wipe a LUN in use names its users already + if (users && refusal !== PVE.SANUtils.san_lun_users_text(row)) { + tooltip.push(Ext.htmlEncode(users)); + } if (refusal) { tooltip.push(Ext.htmlEncode(refusal)); } diff --git a/www/manager6/storage/wizard/FibreChannel.js b/www/manager6/storage/wizard/FibreChannel.js index 265bc4a78..01b8590f4 100644 --- a/www/manager6/storage/wizard/FibreChannel.js +++ b/www/manager6/storage/wizard/FibreChannel.js @@ -505,7 +505,11 @@ Ext.define('PVE.storage.wizard.FCLunSelect', { renderer: function (value, metaData, rec) { let reason = rec.get('reason'); if (!reason) { - return PVE.SANUtils.render_san_lun_usage(value, metaData, rec); + let { text, tip } = PVE.SANUtils.san_lun_usage(rec.data, { + node: rec.get('scanNode'), + }); + metaData.tdAttr = `data-qtip="${Ext.htmlEncode(tip)}"`; + return text; } let text = Ext.htmlEncode(reason); metaData.tdAttr = `data-qtip="${Ext.htmlEncode(text)}"`; diff --git a/www/manager6/storage/wizard/LunSelect.js b/www/manager6/storage/wizard/LunSelect.js index f8eba9e0a..dc7639803 100644 --- a/www/manager6/storage/wizard/LunSelect.js +++ b/www/manager6/storage/wizard/LunSelect.js @@ -282,6 +282,7 @@ Ext.define('PVE.storage.wizard.LunSelect', { // every LUN of the storage carries its attribution ownStorage: me.storage, refusal, + node: me.node, }); metaData.tdAttr = `data-qtip="${Ext.htmlEncode(tip)}"`; return text; -- 2.47.3