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 C905B1FF0ED for ; Fri, 31 Jul 2026 12:24:22 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 08B292176B; Fri, 31 Jul 2026 12:22:09 +0200 (CEST) From: Dietmar Maurer To: pve-devel@lists.proxmox.com Subject: [RFC pve-manager 19/27] ui: storage wizard: add iSCSI support Date: Fri, 31 Jul 2026 12:21:48 +0200 Message-ID: <20260731102156.3947857-20-dietmar@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260731102156.3947857-1-dietmar@proxmox.com> References: <20260731102156.3947857-1-dietmar@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 2 AWL -0.199 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) KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 1.274 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Message-ID-Hash: FBLMSMSQCIRYBELNV3S2WRVFZFQAQVBN X-Message-ID-Hash: FBLMSMSQCIRYBELNV3S2WRVFZFQAQVBN X-MailFrom: dietmar@zilli.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: Using an iSCSI SAN properly usually means two storage entries: an iSCSI storage for the connection and a shared LVM storage on top of a LUN, with the backend creating the volume group automatically. The existing dialogs leave this setup to the user; the wizard asks how the SAN space should be used and defaults to the shared LVM setup, with direct LUN usage as alternative. For the LVM setup the submit chain first creates the iSCSI storage, because LUNs can only be listed through an existing storage entry. When the target provides more than one LUN, a small prompt asks which one to use. If a later step fails, the wizard stays open and explains what was already created; retrying skips the completed steps. Removing the already created iSCSI storage again is intentionally not attempted. Signed-off-by: Dietmar Maurer --- www/manager6/Makefile | 1 + www/manager6/storage/wizard/IScsi.js | 351 +++++++++++++++++++++++++++ 2 files changed, 352 insertions(+) create mode 100644 www/manager6/storage/wizard/IScsi.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 3020ca05..e0a11e5e 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -381,6 +381,7 @@ JSSRC= \ storage/wizard/CommonSettings.js \ storage/wizard/NFS.js \ storage/wizard/CIFS.js \ + storage/wizard/IScsi.js \ Workspace.js \ # end of JSSRC list diff --git a/www/manager6/storage/wizard/IScsi.js b/www/manager6/storage/wizard/IScsi.js new file mode 100644 index 00000000..9ffb5005 --- /dev/null +++ b/www/manager6/storage/wizard/IScsi.js @@ -0,0 +1,351 @@ +Ext.define('PVE.storage.wizard.IScsiTarget', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveStorageWizardIScsiTarget', + + onlineHelp: 'storage_open_iscsi', + + initComponent: function () { + let me = this; + + me.column1 = [ + { + xtype: 'textfield', + name: 'portal', + fieldLabel: 'Portal', + allowBlank: false, + listeners: { + change: { + fn: function (f, value) { + let targetField = me.down('field[name=target]'); + targetField.setDisabled(!value); + targetField.setPortal(value); + targetField.setValue(''); + }, + buffer: 500, + }, + }, + }, + { + xtype: 'pveIScsiScan', + name: 'target', + fieldLabel: gettext('Target'), + disabled: true, + showNodeSelector: false, + }, + ]; + + me.column2 = []; + if (!PVE.Utils.isStandaloneNode()) { + me.column2.push({ + xtype: 'pveStorageScanNodeSelector', + listeners: { + change: function (f, value) { + me.down('field[name=target]').setNodeName(value); + let wizard = me.up('window'); + wizard.scanNode = value; + wizard.down('field[name=nodes]').setValue(value); + }, + }, + }); + } + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.wizard.IScsiUsage', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveStorageWizardIScsiUsage', + + onlineHelp: 'storage_open_iscsi', + + items: [ + { + xtype: 'displayfield', + value: gettext('How should the SAN space be used?'), + }, + { + xtype: 'radiogroup', + columns: 1, + vertical: true, + items: [ + { + boxLabel: + `${gettext('Shared LVM on a LUN (recommended)')} - ` + + gettext( + 'Create an LVM volume group on a LUN. Disk images are allocated as logical volumes, usable from all nodes.', + ), + name: 'usage', + inputValue: 'lvm', + checked: true, + margin: '0 0 10 0', + }, + { + boxLabel: + `${gettext('Use LUNs directly')} - ` + + gettext( + 'Each existing LUN is used one-to-one as a disk image. LUN management stays on the SAN.', + ), + name: 'usage', + inputValue: 'direct', + }, + ], + listeners: { + change: function (rg, value) { + rg.up('window').getViewModel().set('usage', value.usage); + }, + }, + }, + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext( + 'This creates two storage entries: an iSCSI storage for the SAN connection and an LVM storage on the selected LUN.', + ), + bind: { + hidden: '{!isLvmUsage}', + }, + }, + ], +}); + +Ext.define('PVE.storage.wizard.LunPrompt', { + extend: 'Ext.window.Window', + + modal: true, + resizable: false, + width: 500, + layout: 'anchor', + bodyPadding: 10, + + title: gettext('Select Base Volume (LUN)'), + + // config: nodename, storage, callback(volid) + + initComponent: function () { + let me = this; + + me.items = [ + { + xtype: 'displayfield', + userCls: 'pmx-hint', + anchor: '100%', + value: gettext( + 'The iSCSI target provides more than one LUN, select the one to use for the LVM volume group.', + ), + }, + { + xtype: 'pveStorageLunSelector', + itemId: 'lunSelector', + fieldLabel: gettext('Base Volume'), + anchor: '100%', + nodename: me.nodename, + listeners: { + // setting the storage after render triggers the load, + // passing it as config would not + afterrender: function (f) { + f.setStorage(me.storage); + }, + change: function (f, value) { + me.down('#okButton').setDisabled(!value); + }, + }, + }, + ]; + + me.buttons = [ + { + text: gettext('OK'), + itemId: 'okButton', + disabled: true, + handler: function () { + let volid = me.down('#lunSelector').getValue(); + me.close(); + me.callback(volid); + }, + }, + { + text: gettext('Cancel'), + handler: () => me.close(), + }, + ]; + + me.callParent(); + }, +}); + +PVE.storage.wizard.types.iscsi = { + text: 'iSCSI', + description: gettext('Block storage (LUNs) on a SAN, accessed over the network.'), + group: 'san', + apiType: 'iscsi', + viewModel: { + data: { + usage: 'lvm', + }, + formulas: { + isLvmUsage: (get) => get('usage') === 'lvm', + }, + }, + steps: () => [ + { + xtype: 'pveStorageWizardIScsiTarget', + title: gettext('Target'), + }, + { + xtype: 'pveStorageWizardIScsiUsage', + title: gettext('Usage'), + }, + ], + settings: { + onlineHelp: 'storage_open_iscsi', + cts: ['images', 'rootdir'], + defaultContent: ['images', 'rootdir'], + contentFieldConfig: { + bind: { + disabled: '{!isLvmUsage}', + hidden: '{!isLvmUsage}', + }, + }, + advancedColumn1: [ + { + xtype: 'textfield', + name: 'base-storage', + fieldLabel: gettext('iSCSI storage ID'), + emptyText: gettext('ID') + '-base', + vtype: 'StorageId', + allowBlank: true, + bind: { + disabled: '{!isLvmUsage}', + hidden: '{!isLvmUsage}', + }, + }, + ], + }, + summaryNotes: function (values) { + if (values.usage !== 'lvm') { + return []; + } + let baseId = values['base-storage'] || `${values.storage}-base`; + return [ + Ext.String.format( + gettext( + 'Two storage entries will be created: iSCSI storage "{0}" and LVM storage "{1}".', + ), + baseId, + values.storage, + ), + gettext( + 'The selected LUN is initialized as an LVM volume group. Existing data on the LUN will be destroyed!', + ), + gettext( + 'If the target provides more than one LUN, you will be asked to select one after the iSCSI storage was created.', + ), + ]; + }, + submit: function (wizard, values) { + let usage = values.usage; + delete values.usage; + + if (usage === 'direct') { + delete values['base-storage']; + values.type = 'iscsi'; + values.content = 'images'; + wizard.runSubmitChain([(next) => wizard.createStorage(values, next)]); + return; + } + + let baseId = values['base-storage'] || `${values.storage}-base`; + delete values['base-storage']; + + let nodesList = values.nodes ? [].concat(values.nodes) : []; + let contentNode = wizard.scanNode || nodesList[0] || Proxmox.NodeName; + + let baseCreatedHint = Ext.String.format( + gettext( + 'The iSCSI storage "{0}" was already created. You can complete the setup later by adding an LVM storage with a base volume via Add -> LVM.', + ), + Ext.htmlEncode(baseId), + ); + + wizard.runSubmitChain([ + (next) => { + if (wizard.createdBaseStorage) { + next(); + return; + } + wizard.createStorage( + { + storage: baseId, + type: 'iscsi', + portal: values.portal, + target: values.target, + content: 'none', + nodes: values.nodes, + }, + () => { + wizard.createdBaseStorage = true; + next(); + }, + ); + }, + (next) => { + if (wizard.baseVolume) { + next(); + return; + } + Proxmox.Utils.API2Request({ + url: `/nodes/${contentNode}/storage/${baseId}/content`, + method: 'GET', + params: { content: 'images' }, + waitMsgTarget: wizard, + failure: (response) => + Ext.Msg.alert( + gettext('Error'), + `${baseCreatedHint}

${response.htmlStatus}`, + ), + success: function (response) { + let luns = response.result.data || []; + if (luns.length === 0) { + Ext.Msg.alert( + gettext('Error'), + gettext('No LUN found on the iSCSI target.') + + `

${baseCreatedHint}`, + ); + return; + } + if (luns.length === 1) { + wizard.baseVolume = luns[0].volid; + next(); + return; + } + Ext.create('PVE.storage.wizard.LunPrompt', { + autoShow: true, + nodename: contentNode, + storage: baseId, + callback: function (volid) { + wizard.baseVolume = volid; + next(); + }, + }); + }, + }); + }, + (next) => + wizard.createStorage( + { + storage: values.storage, + type: 'lvm', + vgname: values.storage, + base: wizard.baseVolume, + shared: 1, + content: values.content, + nodes: values.nodes, + disable: values.disable, + }, + next, + baseCreatedHint, + ), + ]); + }, +}; -- 2.47.3