From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 83FFE1FF0E2 for ; Thu, 30 Jul 2026 11:41:23 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 87DBE214AF; Thu, 30 Jul 2026 11:41:16 +0200 (CEST) From: Elias Huhsovitz To: pve-devel@lists.proxmox.com Subject: [PATCH manager v3 1/2] fix #6855: ui: storage: add mount options field for nfs amd smb/cifs Date: Thu, 30 Jul 2026 11:40:54 +0200 Message-ID: <20260730094055.57659-2-e.huhsovitz@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260730094055.57659-1-e.huhsovitz@proxmox.com> References: <20260730094055.57659-1-e.huhsovitz@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1785404462229 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.131 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_LOW -0.7 Sender listed at https://www.dnswl.org/, low 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: EI6AEBSQOYMFTWE7YOVOKNUJDDXXRED7 X-Message-ID-Hash: EI6AEBSQOYMFTWE7YOVOKNUJDDXXRED7 X-MailFrom: e.huhsovitz@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 CC: Elias Huhsovitz 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 text field for custom mount options to the NFS and CIFS storage configuration dialogs. This allows users to configure advanced mount parameters (like `nconnect`, `rdma`, or `seal`) directly via the web interface, without needing to use the CLI or manually edit storage.cfg. When nfs version or mount option is being changed: Display a hint to the user that changes will require remounting the storage to take effect. Reduce the usage of legacy JS practices, such as `var` & `+` string concatination in affected functions. Remove unused class property `options`. Signed-off-by: Elias Huhsovitz --- www/manager6/storage/CIFSEdit.js | 69 +++++++++++++++++-- www/manager6/storage/NFSEdit.js | 112 ++++++++++++++++++++++++------- 2 files changed, 152 insertions(+), 29 deletions(-) diff --git a/www/manager6/storage/CIFSEdit.js b/www/manager6/storage/CIFSEdit.js index 5fe3fefe..47e2118a 100644 --- a/www/manager6/storage/CIFSEdit.js +++ b/www/manager6/storage/CIFSEdit.js @@ -121,7 +121,7 @@ Ext.define('PVE.storage.CIFSInputPanel', { onlineHelp: 'storage_cifs', onGetValues: function (values) { - let me = this; + const me = this; if (values.password?.length === 0) { delete values.password; @@ -133,11 +133,39 @@ Ext.define('PVE.storage.CIFSInputPanel', { delete values.subdir; } + if (values.options) { + values.options = values.options + .split(',') + .map((opt) => opt.trim()) + .filter((opt, index, self) => opt !== '' && self.indexOf(opt) === index) + .join(','); + } + + if (!values.options) { + delete values.options; + if (!me.isCreate) { + values.delete = values.delete ? `${values.delete},options` : 'options'; + } + } + return me.callParent([values]); }, + setValues: function (values) { + const me = this; + + me.originalOptions = values.options || ''; + + me.callParent([values]); + + const hint = me.down('#mountOptionsHint'); + if (hint) { + hint.setHidden(true); + } + }, + initComponent: function () { - var me = this; + const me = this; me.column1 = [ { @@ -149,7 +177,7 @@ Ext.define('PVE.storage.CIFSInputPanel', { listeners: { change: function (f, value) { if (me.isCreate) { - let exportField = me.down('field[name=share]'); + const exportField = me.down('field[name=share]'); exportField.setServer(value); } }, @@ -166,7 +194,7 @@ Ext.define('PVE.storage.CIFSInputPanel', { if (!me.isCreate) { return; } - var exportField = me.down('field[name=share]'); + const exportField = me.down('field[name=share]'); exportField.setUsername(value); }, }, @@ -181,7 +209,7 @@ Ext.define('PVE.storage.CIFSInputPanel', { minLength: 1, listeners: { change: function (f, value) { - let exportField = me.down('field[name=share]'); + const exportField = me.down('field[name=share]'); exportField.setPassword(value); }, }, @@ -213,7 +241,7 @@ Ext.define('PVE.storage.CIFSInputPanel', { listeners: { change: function (f, value) { if (me.isCreate) { - let exportField = me.down('field[name=share]'); + const exportField = me.down('field[name=share]'); exportField.setDomain(value); } }, @@ -229,6 +257,35 @@ Ext.define('PVE.storage.CIFSInputPanel', { }, ]; + me.advancedColumn2 = [ + { + xtype: 'textfield', + name: 'options', + fieldLabel: gettext('Mount Options'), + emptyText: `${gettext('Example')}: rdma, seal`, + allowBlank: true, + listeners: { + change: function (field, newValue) { + const hint = me.down('#mountOptionsHint'); + if (hint) { + hint.setHidden(me.isCreate || (newValue || '') === me.originalOptions); + } + }, + }, + }, + ]; + + me.advancedColumnB = me.advancedColumnB || []; + me.advancedColumnB.unshift({ + xtype: 'displayfield', + itemId: 'mountOptionsHint', + userCls: 'pmx-hint', + hidden: true, + value: gettext( + 'Changes to mount options require remounting the storage to take effect.', + ), + }); + me.callParent(); }, }); diff --git a/www/manager6/storage/NFSEdit.js b/www/manager6/storage/NFSEdit.js index 72db156f..68f4a8ce 100644 --- a/www/manager6/storage/NFSEdit.js +++ b/www/manager6/storage/NFSEdit.js @@ -57,34 +57,37 @@ Ext.define('PVE.storage.NFSScan', { me.callParent(); }, }); - Ext.define('PVE.storage.NFSInputPanel', { extend: 'PVE.panel.StorageBase', onlineHelp: 'storage_nfs', - options: [], - onGetValues: function (values) { - var me = this; - - var i; - var res = []; - for (i = 0; i < me.options.length; i++) { - let item = me.options[i]; - if (!item.match(/^vers=(.*)$/)) { - res.push(item); - } + const me = this; + const res = []; + + if (values.options && values.options.trim() !== '') { + const opts = values.options.split(','); + opts.forEach((opt) => { + const cleanOpt = opt.trim(); + if (cleanOpt !== '' && !cleanOpt.match(/^vers=(.*)$/) && !res.includes(cleanOpt)) { + res.push(cleanOpt); + } + }); } + if (values.nfsversion && values.nfsversion !== '__default__') { - res.push('vers=' + values.nfsversion); + res.push(`vers=${values.nfsversion}`); } + delete values.nfsversion; - values.options = res.join(','); - if (values.options === '') { + + if (res.length > 0) { + values.options = res.join(','); + } else { delete values.options; if (!me.isCreate) { - values.delete = 'options'; + values.delete = values.delete ? `${values.delete},options` : 'options'; } } @@ -92,21 +95,42 @@ Ext.define('PVE.storage.NFSInputPanel', { }, setValues: function (values) { - var me = this; + const me = this; + if (values.options) { - me.options = values.options.split(','); - me.options.forEach(function (item) { - var match = item.match(/^vers=(.*)$/); + const opts = values.options.split(',').map((item) => item.trim()); + + opts.forEach((item) => { + const match = item.match(/^vers=(.*)$/); if (match) { values.nfsversion = match[1]; } }); + + // Filter out "vers=..." and empty strings + const otherOptions = opts.filter( + (item, index, self) => item !== '' && !item.match(/^vers=(.*)$/), + ); + values.options = otherOptions.join(','); + + if (values.options === '') { + delete values.options; + } + } + + me.originalOptions = values.options || ''; + me.originalNfsVersion = values.nfsversion || '__default__'; + + me.callParent([values]); + + const hint = me.down('#mountOptionsHint'); + if (hint) { + hint.setHidden(true); } - return me.callParent([values]); }, initComponent: function () { - var me = this; + const me = this; me.column1 = [ { @@ -118,7 +142,7 @@ Ext.define('PVE.storage.NFSInputPanel', { listeners: { change: function (f, value) { if (me.isCreate) { - let exportField = me.down('field[name=export]'); + const exportField = me.down('field[name=export]'); exportField.setServer(value); exportField.setValue(''); } @@ -143,6 +167,25 @@ Ext.define('PVE.storage.NFSInputPanel', { ]; me.advancedColumn2 = [ + { + xtype: 'textfield', + name: 'options', + fieldLabel: gettext('Mount Options'), + emptyText: `${gettext('Example')}: nconnect=4, soft`, + allowBlank: true, + listeners: { + change: function (field, newValue) { + const hint = me.down('#mountOptionsHint'); + if (hint) { + const currentVersion = + me.down('field[name=nfsversion]')?.getValue() || '__default__'; + const isOptionsChanged = (newValue || '') !== me.originalOptions; + const isVersionChanged = currentVersion !== me.originalNfsVersion; + hint.setHidden(me.isCreate || !(isOptionsChanged || isVersionChanged)); + } + }, + }, + }, { xtype: 'proxmoxKVComboBox', fieldLabel: gettext('NFS Version'), @@ -156,9 +199,32 @@ Ext.define('PVE.storage.NFSInputPanel', { ['4.1', '4.1'], ['4.2', '4.2'], ], + listeners: { + change: function (field, newValue) { + const hint = me.down('#mountOptionsHint'); + if (hint) { + const currentOptions = me.down('field[name=options]')?.getValue() || ''; + const isOptionsChanged = currentOptions !== me.originalOptions; + const isVersionChanged = + (newValue || '__default__') !== me.originalNfsVersion; + hint.setHidden(me.isCreate || !(isOptionsChanged || isVersionChanged)); + } + }, + }, }, ]; + me.advancedColumnB = me.advancedColumnB || []; + me.advancedColumnB.unshift({ + xtype: 'displayfield', + itemId: 'mountOptionsHint', + userCls: 'pmx-hint', + hidden: true, + value: gettext( + 'Changes to mount options require remounting the storage to take effect.', + ), + }); + me.callParent(); }, }); -- 2.47.3