public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Joaquin Varela <joaquinvarela@neatech.ar>
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	[thread overview]
Message-ID: <20261005002609.571-7-joaquinvarela@neatech.ar> (raw)
In-Reply-To: <20261005002609.571-1-joaquinvarela@neatech.ar>

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 <joaquinvarela@neatech.ar>
---

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]:<base64>:) 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();
+    },
+});



      parent reply	other threads:[~2026-10-05  0:27 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-05  0:26 [PATCH storage v3 0/4] add ZFS over NVMe/TCP storage plugin Joaquin Varela
2026-10-05  0:26 ` [PATCH storage v3 1/4] zfsnvme: " Joaquin Varela
2026-10-05  0:26 ` [PATCH storage v3 2/4] test: add zfsnvme plugin tests Joaquin Varela
2026-10-05  0:26 ` [PATCH storage v3 3/4] zfsnvme: fence target commands of abandoned transactions Joaquin Varela
2026-10-05  0:26 ` [PATCH storage v3 4/4] zfsnvme: wait up to 30 seconds for the shared storage lock Joaquin Varela
2026-10-05  0:26 ` [PATCH docs v3] storage: document ZFS over NVMe/TCP Joaquin Varela
2026-10-05  0:26 ` Joaquin Varela [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261005002609.571-7-joaquinvarela@neatech.ar \
    --to=joaquinvarela@neatech.ar \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal