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 A44581FF09C for ; Mon, 05 Oct 2026 02:27:15 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 4ADAF216D8; Mon, 05 Oct 2026 02:26:47 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=neatech-ar.20251104.gappssmtp.com; s=20251104; t=1791159984; x=1791764784; darn=lists.proxmox.com; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=bzYkLYaGljIRgBkSNqMm/lZs4C1msXURSTs0mWfvh9g=; b=RLeVdHeeD+3z0m0+lZ8ZmRAjQSomDKO6vAadwwvuScHJU1H4wnH9TDQM5/RDmQsuMx 1f2IApktO9d8x+ixGYYUZSMAi6lT09DWfe/Ty/cEFI74Lv4c+QE2ZT3twG5wx+oDMVUS PTxzRTTmRMJxNUTtnIrCb2baheJP5kdtDuWTHPlElfoCEBW9zEr6I21SY/G/H2smfKTz sgqna+tp8rt2ye/hhUzltM+1PLivNlO6SaUxSBNwVNalUEepVe8Y0/PaK0z0lqZGdY1C foM8M8/HD2qhf4urVuhW4Sp5aels54ZwzUbipXC96dGn6+0xf3rVW1vPgfEZM1qF0m3T ojuQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1791159984; x=1791764784; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=bzYkLYaGljIRgBkSNqMm/lZs4C1msXURSTs0mWfvh9g=; b=jm9S44vLHBxZ2g4v3/lpWcT5OjEjOxMXxl6R81x7POOMoqrM7zFAvy7xYR5I83pbV7 oDN+BjsbL7m3wLf9NR4vCl2+Gvj5hLc4KKzh4eTrCTgPt3DbGDZm+qk8VA97fyCPOmL+ YZT9+FeyiwbT7EXUvUXR3U78uoTcMVOpUS/aKI4M+j9WMC8MbTb3WWVq4AFH3+n7tQzY LdsGo2jGqnDGOHhNSxNv+/7fsiRrTKJZjiq+b/VY5PFg/I6Fj2Lf+WK9ROY79ndNIvsb 7mpkse4MRtcxabqolVQqgPK1qcQ41U399EUid6+Jvv1F5GicqA6/pAPKO3Vkm754jEcP YZFQ== X-Gm-Message-State: AFq9FYLq8DEVaBFjav2GF1WuFLyd5YAzMDO/wsed20NvxV+BuwEnYUFP Aj6JjLLAP8nnMIxeAfhUCkejuY7po6bV5Kn1oSm5nxr4OJDZxVZdIsHqqDPxNZoNYS677S/we+x alhoMxPc= X-Gm-Gg: AYBFou0HrBk5cDpHJ7J+6IDcslgozDEhm7wzaMWGAFh34Guu7iC03qxS2Lx5sX3TfW/ QW86zWdal5HV3HcbjsAxeY/j9VFWDqEvLt1y1ruLhqGkBQIRk2ZAjOXr0pcVRaGoBUUWOqhsQzJ 9Ag/Szmpj1JEpFkUNP5WiC/Tv8f+mEQM9vJ6u0IWSvV6cHi1544GTTaQVgTBrglBwTuA4O7PkYY aGclSnMEPoJYGRvHov6wij01xdIeLy/eFCe0ttXRPAFhgccbKvwOPJjjlPue5mVRo02zaU0oaap tCst8hBCsgwYKHmGYwKBucCEfK55+M9YKoCn/WwVxsYL1SaFtqve1E+xwRjpatE6I3pXT8L4geV wTxTmCp0PbQU2t5j1b3oBpq49G6Q9RJEvUgAeptwUWWc9bqsCJzyB/ydKt0Jqa2/FrZOYip32ma hRGPjAe3pc05JLZiisANdmFWPG1juDsdWhPeNixhjO+iatOjMGoNsPO/4cyEkR5jZU6m1v42QFW 9s4DOjoA+wpL4aeTFCKrsQOSadE91kbWuCLenw2GpouiiKW/OGT0roWm84zd/TKIUbrfQVAvATJ X-Received: by 2002:a05:6102:1627:b0:7b4:f1cd:80dd with SMTP id ada2fe7eead31-7c0f1cdc5c7mr1284783137.1.1791159983818; Sun, 04 Oct 2026 17:26:23 -0700 (PDT) From: Joaquin Varela To: pve-devel@lists.proxmox.com Subject: [PATCH manager v3] ui: storage: add ZFS over NVMe/TCP editor Date: Sun, 4 Oct 2026 21:26:09 -0300 Message-ID: <20261005002609.571-7-joaquinvarela@neatech.ar> X-Mailer: git-send-email 2.54.0.windows.1 In-Reply-To: <20261005002609.571-1-joaquinvarela@neatech.ar> References: <20261005002609.571-1-joaquinvarela@neatech.ar> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.476 Adjusted score from AWL reputation of From: address DKIM_SIGNED 0.1 Message has a DKIM or DK signature, not necessarily valid DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature DMARC_PASS -0.1 DMARC pass policy 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: XPOJHFEJKD7TL2H3DZVMOIOJW2PFD6OS X-Message-ID-Hash: XPOJHFEJKD7TL2H3DZVMOIOJW2PFD6OS X-MailFrom: joaquinvarela@neatech.ar 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: Add an input panel for the zfsnvme storage type, so that ZFS over NVMe/TCP storages can be added and edited in the web interface, and register it in the storage type list and the JavaScript bundle. The general tab covers the SSH server, ZFS pool, subsystem NQN, block size, thin provisioning, NVMe/TCP portals, host interfaces, allowed host NQNs, the DH-HMAC-CHAP key and the multipath I/O policy. The connection timeouts and the number of I/O queues are advanced options. Ranges and defaults are those of the storage plugin schema. Apply the backend's restrictions in the form, so that they show up before submitting: properties that are fixed after creation are read-only when editing, host NQNs can only be added, and a fast I/O fail timeout must not exceed a finite controller loss timeout. The key is only requested on creation. Since the backend does not support key rotation, editing neither shows nor resends it. The help button links to the storage_zfsnvme section of the admin guide, so generating OnlineHelpInfo.js requires a pve-doc-generator that includes it. Signed-off-by: Joaquin Varela --- v3, accompanying "[PATCH storage v3 0/4] add ZFS over NVMe/TCP storage plugin": - rebased onto current master; the three v2 patches are squashed - behavior changes against v2: - thin provisioning is unchecked by default, like the API default (the storage v3 series no longer sets sparse on creation) and the other ZFS editors - Allowed Host NQNs: when editing, a validator refuses to drop a host NQN that the storage already has, since the backend refuses it too; an edit-only hint says that host NQNs can only be added and that revoking a host needs a new storage with a new key - DH-HMAC-CHAP key: on creation the field checks the DHHC-1 format (DHHC-1:0[0-3]::) and is required; when editing it shows "Unchanged (rotation not supported)" and sends nothing (v2 showed "Configured"); the label is "DH-HMAC-CHAP Key" instead of "DHCHAP Key" - Fast I/O Fail Timeout: a validator refuses a value above a finite Controller Loss Timeout, revalidated when that timeout changes; an empty field shows "Off" - layout: I/O Queues moved to the first advanced column and shows "Default" when empty; the fast I/O fail sentence moved from the general hint to a new advanced hint, which also says that a Controller Loss Timeout of -1 retries forever - placeholders use example values only, and the portals placeholder no longer shows the default port - the options match the storage v3 series, whose schema did not change for the editor (nvme-host-ifaces and nvme-host-nqns are required in the schema now, which the editor already enforced); its online help needs the docs patch v2: https://lore.proxmox.com/pve-devel/cover.1785636980.git.joaquinvarela@neatech.ar/ www/manager6/Makefile | 1 + www/manager6/Utils.js | 6 + www/manager6/storage/ZFSNVMeEdit.js | 233 ++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+) create mode 100644 www/manager6/storage/ZFSNVMeEdit.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index d2ea786b..fa634804 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -376,6 +376,7 @@ JSSRC= \ storage/Summary.js \ storage/TemplateView.js \ storage/ZFSEdit.js \ + storage/ZFSNVMeEdit.js \ storage/ZFSPoolEdit.js \ storage/ESXIEdit.js \ Workspace.js \ diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js index 8b99371d..7022b8df 100644 --- a/www/manager6/Utils.js +++ b/www/manager6/Utils.js @@ -880,6 +880,12 @@ Ext.define('PVE.Utils', { faIcon: 'building', backups: false, }, + zfsnvme: { + name: 'ZFS over NVMe/TCP', + ipanel: 'ZFSNVMeInputPanel', + faIcon: 'building', + backups: false, + }, zfspool: { name: 'ZFS', ipanel: 'ZFSPoolInputPanel', diff --git a/www/manager6/storage/ZFSNVMeEdit.js b/www/manager6/storage/ZFSNVMeEdit.js new file mode 100644 index 00000000..c7649cfe --- /dev/null +++ b/www/manager6/storage/ZFSNVMeEdit.js @@ -0,0 +1,233 @@ +Ext.define('PVE.storage.ZFSNVMeInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_zfsnvme', + + onGetValues: function (values) { + if (this.isCreate) { + values.content = 'images'; + } + return this.callParent([values]); + }, + + initComponent: function () { + let me = this; + + let splitList = (value) => + (value || '') + .split(',') + .map((item) => item.trim()) + .filter((item) => item !== ''); + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'server', + fieldLabel: gettext('SSH Server'), + allowBlank: false, + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'pool', + fieldLabel: gettext('ZFS Pool'), + emptyText: 'tank/pve-nvme', + allowBlank: false, + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'subsysnqn', + fieldLabel: gettext('Subsystem NQN'), + emptyText: 'nqn.2026-01.com.example:pve-nvme', + allowBlank: false, + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'blocksize', + value: '16k', + fieldLabel: gettext('Block Size'), + validator: PVE.Utils.validateZfsBlocksize, + allowBlank: false, + }, + { + xtype: 'proxmoxcheckbox', + name: 'sparse', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Thin provision'), + }, + ]; + + me.column2 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'nvme-portals', + fieldLabel: gettext('NVMe/TCP Portals'), + emptyText: '192.0.2.10,198.51.100.10', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'nvme-host-ifaces', + fieldLabel: gettext('Host Interfaces'), + emptyText: 'ens1f0,ens1f1', + allowBlank: false, + }, + { + xtype: 'textfield', + name: 'nvme-host-nqns', + fieldLabel: gettext('Allowed Host NQNs'), + emptyText: 'nqn.2014-08.org.nvmexpress:uuid:...', + allowBlank: false, + validator: function (value) { + // the backend refuses to drop a host NQN from an existing storage + let current = splitList(value); + let removed = splitList(this.originalValue).filter( + (hostnqn) => !current.includes(hostnqn), + ); + if (removed.length) { + return Ext.String.format( + gettext('Host NQN {0} cannot be removed from an existing storage'), + Ext.htmlEncode(removed[0]), + ); + } + return true; + }, + }, + me.isCreate + ? { + xtype: 'textfield', + inputType: 'password', + name: 'dhchap-key', + fieldLabel: gettext('DH-HMAC-CHAP Key'), + emptyText: 'DHHC-1:xx:...:', + regex: /^DHHC-1:0[0-3]:[A-Za-z0-9+/]+={0,2}:$/, + regexText: gettext('Expected format: DHHC-1:xx:...:'), + allowBlank: false, + } + : { + xtype: 'displayfield', + fieldLabel: gettext('DH-HMAC-CHAP Key'), + value: gettext('Unchanged (rotation not supported)'), + }, + { + xtype: 'proxmoxKVComboBox', + name: 'nvme-iopolicy', + value: 'round-robin', + fieldLabel: gettext('I/O Policy'), + comboItems: [ + ['round-robin', 'round-robin'], + ['queue-depth', 'queue-depth'], + ['numa', 'numa'], + ], + allowBlank: false, + }, + ]; + + me.advancedColumn1 = [ + { + xtype: 'proxmoxintegerfield', + name: 'nvme-keep-alive-tmo', + value: 5, + minValue: 1, + maxValue: 120, + fieldLabel: gettext('Keep Alive Timeout'), + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'nvme-reconnect-delay', + value: 2, + minValue: 1, + maxValue: 120, + fieldLabel: gettext('Reconnect Delay'), + allowBlank: false, + }, + { + xtype: 'proxmoxintegerfield', + name: 'nvme-nr-io-queues', + minValue: 1, + maxValue: 1024, + fieldLabel: gettext('I/O Queues'), + emptyText: Proxmox.Utils.defaultText, + deleteEmpty: !me.isCreate, + allowBlank: true, + }, + ]; + + me.advancedColumn2 = [ + { + xtype: 'proxmoxintegerfield', + name: 'nvme-ctrl-loss-tmo', + value: 600, + minValue: -1, + maxValue: 86400, + fieldLabel: gettext('Controller Loss Timeout'), + allowBlank: false, + listeners: { + change: function (field) { + let panel = field.up('inputpanel'); + if (panel) { + panel.down('field[name=nvme-fast-io-fail-tmo]').validate(); + } + }, + }, + }, + { + xtype: 'proxmoxintegerfield', + name: 'nvme-fast-io-fail-tmo', + minValue: 0, + maxValue: 86400, + fieldLabel: gettext('Fast I/O Fail Timeout'), + emptyText: gettext('Off'), + deleteEmpty: !me.isCreate, + allowBlank: true, + validator: function (value) { + let panel = this.up('inputpanel'); + if (value === '' || !panel) { + return true; + } + let ctrlLossTmo = panel.down('field[name=nvme-ctrl-loss-tmo]').getValue(); + if (ctrlLossTmo === null || ctrlLossTmo < 0) { + return true; + } + return ( + Number(value) <= ctrlLossTmo || + gettext('Must not exceed the Controller Loss Timeout') + ); + }, + }, + ]; + + me.advancedColumnB = [ + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext( + 'A Controller Loss Timeout of -1 retries forever. Leave Fast I/O Fail Timeout empty to queue I/O until the controller is lost.', + ), + }, + ]; + + me.columnB = [ + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext( + 'List /etc/nvme/hostnqn from every allowed cluster node. Host interface names are matched to portals by position and must exist on every selected node.', + ), + }, + ]; + + if (!me.isCreate) { + me.columnB.push({ + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext( + 'Host NQNs can only be added. Revoking a host requires a new storage with a new key.', + ), + }); + } + + me.callParent(); + }, +});