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 3143E1FF0AF for ; Fri, 06 Nov 2026 01:57:40 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id B085D216DF; Fri, 06 Nov 2026 01:51:18 +0100 (CET) From: Thomas Lamprecht To: pve-devel@lists.proxmox.com Subject: [PATCH v3 manager 57/76] ui: node: add SAN LUNs panel Date: Fri, 6 Nov 2026 00:31:32 +0100 Message-ID: <20261105233336.2178064-58-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: 1793926181394 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.304 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: QZQSYGBT3JXT4OB76N7C4IVW7T5CKTTZ X-Message-ID-Hash: QZQSYGBT3JXT4OB76N7C4IVW7T5CKTTZ 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 Expose the node inventory of SAN LUNs separately from local disk management. Show device identity, transport, size and usage, with local or unknown transports hidden unless requested. An empty list names the node and explains which connectivity and array settings to check. Allow node auditors and storage administrators to open the view, matching the inventory API permissions. Signed-off-by: Dietmar Maurer [TL: place the view under Node > SAN; use the shared SAN renderers and scan fields, the scan path-count names and node-specific empty-list guidance] Signed-off-by: Thomas Lamprecht --- www/manager6/Makefile | 1 + www/manager6/node/Config.js | 15 ++++ www/manager6/node/SANLuns.js | 155 +++++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 www/manager6/node/SANLuns.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index b60cfa569..2e1b23fed 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -247,6 +247,7 @@ JSSRC= \ node/Directory.js \ node/LVM.js \ node/LVMThin.js \ + node/SANLuns.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..3a84e96f6 100644 --- a/www/manager6/node/Config.js +++ b/www/manager6/node/Config.js @@ -357,6 +357,21 @@ Ext.define('PVE.node.Config', { groups: ['storage'], xtype: 'pveZFSList', }, + ); + } + + // the san-luns scan accepts either privilege + if (caps.nodes['Sys.Audit'] || caps.storage['Datastore.Allocate']) { + me.items.push({ + xtype: 'pveSANLunList', + title: 'SAN', + itemId: 'san', + iconCls: 'fa fa-cubes', + }); + } + + if (caps.nodes['Sys.Audit']) { + me.items.push( { xtype: 'pveNodeCephStatus', title: 'Ceph', diff --git a/www/manager6/node/SANLuns.js b/www/manager6/node/SANLuns.js new file mode 100644 index 000000000..593bfc519 --- /dev/null +++ b/www/manager6/node/SANLuns.js @@ -0,0 +1,155 @@ +Ext.define('PVE.node.SANLunList', { + extend: 'Ext.grid.GridPanel', + xtype: 'pveSANLunList', + + stateful: true, + stateId: 'grid-node-san-luns', + + disableSelection: true, + + emptyDeviceText: function (showAll) { + let text = Ext.String.format( + gettext( + 'No SAN devices found on node "{0}". Check SAN connections and array access, then press Reload.', + ), + Ext.htmlEncode(this.nodename), + ); + if (!showAll) { + text += + ' ' + + gettext( + '"Show all devices" also lists local devices and devices with an unknown transport.', + ); + } + return text; + }, + + viewConfig: { + trackOver: false, + }, + + columns: [ + { + header: gettext('Device'), + dataIndex: 'devpath', + flex: 2, + renderer: PVE.SANUtils.render_san_lun_device, + }, + { + header: gettext('Transport'), + dataIndex: 'transport', + width: 100, + renderer: PVE.SANUtils.render_san_transport, + }, + { + header: gettext('Size'), + dataIndex: 'size', + width: 100, + renderer: Proxmox.Utils.render_size, + }, + { + header: gettext('Vendor') + '/' + gettext('Model'), + dataIndex: 'model', + flex: 1, + renderer: (value, metaData, rec) => + Ext.htmlEncode(`${rec.get('vendor') || ''} ${value || ''}`.trim()), + }, + { + header: 'WWN', + dataIndex: 'wwn', + width: 200, + }, + { + header: gettext('Serial'), + dataIndex: 'serial', + width: 140, + hidden: true, + }, + { + header: gettext('Usage'), + dataIndex: 'usage', + flex: 2, + renderer: PVE.SANUtils.render_san_lun_usage, + }, + ], + + reload: function () { + this.getStore().load(); + }, + + listeners: { + activate: function () { + this.reload(); + }, + }, + + initComponent: function () { + let me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw 'no node name specified'; + } + + me.emptyText = me.emptyDeviceText(false); + + let sanTransports = { + fc: 1, + sas: 1, + iscsi: 1, + 'nvme-fc': 1, + 'nvme-tcp': 1, + 'nvme-rdma': 1, + 'nvme-loop': 1, + }; + + let sanTransportFilter = new Ext.util.Filter({ + id: 'san-transport', + filterFn: (rec) => !!sanTransports[rec.get('transport')], + }); + + let store = Ext.create('Ext.data.Store', { + fields: [...PVE.SANUtils.lun_fields, 'transport', 'serial', 'wwn'], + proxy: { + type: 'proxmox', + url: `/api2/json/nodes/${me.nodename}/scan/san-luns`, + }, + filters: [sanTransportFilter], + sorters: 'devpath', + }); + me.store = store; + + me.tbar = [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: () => me.reload(), + }, + '->', + { + xtype: 'proxmoxcheckbox', + boxLabel: gettext('Show all devices'), + autoEl: { + tag: 'div', + 'data-qtip': gettext('Also list devices with local or unknown transports.'), + }, + listeners: { + change: function (f, value) { + me.setEmptyText(me.emptyDeviceText(value)); + let filters = store.getFilters(); + if (value) { + filters.remove(sanTransportFilter); + } else { + filters.add(sanTransportFilter); + } + }, + }, + }, + ]; + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, store); + me.reload(); + }, +}); -- 2.47.3