public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dietmar Maurer <dietmar@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [RFC pve-manager 27/27] ui: san luns: show iSCSI targets and sessions
Date: Fri, 31 Jul 2026 12:21:56 +0200	[thread overview]
Message-ID: <20260731102156.3947857-28-dietmar@proxmox.com> (raw)
In-Reply-To: <20260731102156.3947857-1-dietmar@proxmox.com>

Show the iSCSI targets known to the node's initiator in a collapsible
area below the SAN LUN list, since iSCSI sessions provide a share of
the listed LUNs. The view is a tree with one node per target and its
portals as leaves, keeping it compact for setups with many portals:
the leaves merge active sessions with the initiator's node records,
so a target stays visible as not connected after a logout or while
its portal is unreachable, and each target summarizes how many of its
portals are connected. Targets start collapsed; a reload keeps the
expansion and selection state. Leaves report session id, transport
and health; the toolbar shows the initiator IQN with a copy button,
as the SAN side needs it for LUN masking. An info window shows the
parameters a session negotiated at login.

Session logins are managed by the storage layer, so the offered
actions are a rescan for new or resized LUNs, logging out of a
session and removing the node records of a logged-out portal, or of
a whole logged-out target, the latter two mainly to clean up after
removed storages. The confirmations warn when a configured storage
still uses the target, as records and sessions come back on the next
activation. The actions are only offered with Sys.Modify, and the
whole area hides itself when open-iscsi is not installed, keeping FC
or NVMe only nodes unaffected.

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
 www/manager6/Makefile             |   1 +
 www/manager6/Utils.js             |  33 ++
 www/manager6/node/Config.js       |   4 +-
 www/manager6/node/ISCSITargets.js | 552 ++++++++++++++++++++++++++++++
 www/manager6/node/SANLuns.js      |  47 +++
 5 files changed, 635 insertions(+), 2 deletions(-)
 create mode 100644 www/manager6/node/ISCSITargets.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index c354b3aa..7dcb3c59 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -243,6 +243,7 @@ JSSRC= 							\
 	node/CmdMenu.js					\
 	node/Config.js					\
 	node/Directory.js				\
+	node/ISCSITargets.js				\
 	node/LVM.js					\
 	node/LVMThin.js					\
 	node/SANLuns.js					\
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index a34e1363..53aeb341 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1142,6 +1142,39 @@ Ext.define('PVE.Utils', {
             return Ext.htmlEncode(rec.get('detail') || gettext('in use'));
         },
 
+        render_iscsi_transport: function (value) {
+            if (!value) {
+                return '';
+            }
+            return value === 'iser' ? 'iSER' : value.toUpperCase();
+        },
+
+        // Render a session health value as icon plus label; a value this map
+        // does not know falls back to the raw string with the unknown icon.
+        render_iscsi_session_health: function (value) {
+            if (!value) {
+                return '';
+            }
+            let display = {
+                'logged-in': { icon: 'good fa-check', label: gettext('Logged in') },
+                recovering: { icon: 'warning fa-exclamation', label: gettext('Recovering') },
+                'logging-out': { icon: 'faded fa-sign-out', label: gettext('Logging out') },
+                'not-connected': { icon: 'faded fa-unlink', label: gettext('Not connected') },
+                unknown: { icon: 'faded fa-question', label: gettext('Unknown') },
+            }[value];
+            let icon = display?.icon ?? 'faded fa-question';
+            let label = display?.label ?? value;
+            return `<i class="fa fa-fw ${icon}"></i> ${Ext.htmlEncode(label)}`;
+        },
+
+        // Render a whole-second duration like "120 s"; empty for missing values.
+        render_seconds: function (value) {
+            if (value === undefined || value === null) {
+                return '';
+            }
+            return `${value} s`;
+        },
+
         render_serverity: function (value) {
             return PVE.Utils.log_severity_hash[value] || value;
         },
diff --git a/www/manager6/node/Config.js b/www/manager6/node/Config.js
index 6329241a..3ac599e8 100644
--- a/www/manager6/node/Config.js
+++ b/www/manager6/node/Config.js
@@ -359,10 +359,10 @@ Ext.define('PVE.node.Config', {
                 },
             );
 
-            // the san-luns scan endpoint requires Datastore.Allocate
+            // the san-luns scan and iscsi endpoints require Datastore.Allocate
             if (caps.storage['Datastore.Allocate']) {
                 me.items.push({
-                    xtype: 'pveSANLunList',
+                    xtype: 'pveSANLunPanel',
                     title: gettext('SAN LUNs'),
                     itemId: 'san-luns',
                     iconCls: 'fa fa-cubes',
diff --git a/www/manager6/node/ISCSITargets.js b/www/manager6/node/ISCSITargets.js
new file mode 100644
index 00000000..5a2d8265
--- /dev/null
+++ b/www/manager6/node/ISCSITargets.js
@@ -0,0 +1,552 @@
+// Tree of the iSCSI targets known to the node's initiator, with their portals
+// as leaves: the active sessions merged with the initiator's node records, so
+// a target stays visible as not connected after a logout or while its portal
+// is unreachable. An info window shows the parameters a session negotiated at
+// login. Session logins are managed by the iSCSI storage plugin on storage
+// activation; the offered actions are a rescan for new LUNs, logging out of a
+// session and removing the node records of a logged-out portal or target, the
+// latter two mainly to clean up after removed storages.
+
+// Read-only details for a single session: the negotiated parameters loaded from
+// the session-params endpoint, prefixed by the session identity. The identity
+// rows are seeded as defaults from the list record so they show before the load
+// completes.
+Ext.define('PVE.node.ISCSISessionParams', {
+    extend: 'Proxmox.grid.ObjectGrid',
+    xtype: 'pveISCSISessionParams',
+
+    viewConfig: {
+        enableTextSelection: true,
+    },
+
+    // The negotiated session parameters, loaded from the endpoint. The session
+    // identity rows are prepended per-instance in initComponent.
+    rows: {
+        'header-digest': { header: gettext('Header Digest') },
+        'data-digest': { header: gettext('Data Digest') },
+        'max-recv-data-segment-length': {
+            header: gettext('Max Receive Segment'),
+            renderer: Proxmox.Utils.render_size,
+        },
+        'max-xmit-data-segment-length': {
+            header: gettext('Max Transmit Segment'),
+            renderer: Proxmox.Utils.render_size,
+        },
+        'first-burst-length': {
+            header: gettext('First Burst Length'),
+            renderer: Proxmox.Utils.render_size,
+        },
+        'max-burst-length': {
+            header: gettext('Max Burst Length'),
+            renderer: Proxmox.Utils.render_size,
+        },
+        'max-outstanding-r2t': { header: gettext('Max Outstanding R2T') },
+        'immediate-data': { header: gettext('Immediate Data') },
+        'initial-r2t': { header: gettext('Initial R2T') },
+        'recovery-timeout': {
+            header: gettext('Recovery Timeout'),
+            renderer: PVE.Utils.render_seconds,
+        },
+        'abort-timeout': {
+            header: gettext('Abort Timeout'),
+            renderer: PVE.Utils.render_seconds,
+        },
+        'lu-reset-timeout': {
+            header: gettext('LUN Reset Timeout'),
+            renderer: PVE.Utils.render_seconds,
+        },
+        'tgt-reset-timeout': {
+            header: gettext('Target Reset Timeout'),
+            renderer: PVE.Utils.render_seconds,
+        },
+    },
+
+    initComponent: function () {
+        let me = this;
+
+        if (!me.nodename) {
+            throw 'no node name specified';
+        }
+        if (!me.sessionData) {
+            throw 'no session specified';
+        }
+
+        let session = me.sessionData;
+
+        me.url = `/api2/json/nodes/${me.nodename}/iscsi/session-params`;
+        me.extraParams = { 'session-id': session['session-id'] };
+
+        // prepend the session identity, seeded from the list record so it shows
+        // before the parameter load completes
+        me.rows = {
+            target: { header: gettext('Target'), defaultValue: session.target },
+            'session-id': {
+                header: gettext('Session ID'),
+                defaultValue: session['session-id'],
+            },
+            portal: { header: gettext('Portal'), defaultValue: session.portal },
+            transport: {
+                header: gettext('Transport'),
+                defaultValue: session.transport,
+                renderer: PVE.Utils.render_iscsi_transport,
+            },
+            health: {
+                header: gettext('Status'),
+                defaultValue: session.health,
+                renderer: PVE.Utils.render_iscsi_session_health,
+            },
+            'kernel-state': {
+                header: gettext('Kernel State'),
+                defaultValue: session['kernel-state'],
+            },
+            ...me.rows,
+        };
+
+        me.callParent();
+
+        me.rstore.load();
+    },
+});
+
+Ext.define('PVE.node.ISCSITargetTree', {
+    extend: 'Ext.tree.Panel',
+    xtype: 'pveISCSITargetTree',
+
+    stateful: true,
+    stateId: 'grid-node-iscsi-target',
+
+    rootVisible: false,
+
+    viewConfig: {
+        emptyText: gettext('No iSCSI targets found'),
+    },
+
+    selModel: { allowDeselect: true },
+
+    viewModel: {
+        data: {
+            // The bare initiator IQN, empty when none is available.
+            initiatorName: '',
+            // The text shown in the toolbar, including the alias if set.
+            initiatorText: '',
+        },
+    },
+
+    controller: {
+        xclass: 'Ext.app.ViewController',
+
+        // group the flat per-portal entries of the target endpoint into one
+        // tree node per target with its portals as leaves
+        buildChildren: function (rows, expandedTargets) {
+            let targets = {};
+            for (const row of rows) {
+                targets[row.target] ??= [];
+                targets[row.target].push(row);
+            }
+            return Object.keys(targets)
+                .sort()
+                .map((target) => {
+                    let portals = targets[target];
+                    // Failed and free sessions still have IDs, so derive connectivity from health.
+                    let connected = portals.filter((p) => p.health === 'logged-in').length;
+                    let sessions = portals.filter((p) => p['session-id'] !== undefined).length;
+                    return {
+                        text: target,
+                        target: target,
+                        isTarget: true,
+                        iconCls: 'fa fa-bullseye',
+                        expanded: !!expandedTargets[target],
+                        connectedCount: connected,
+                        sessionCount: sessions,
+                        totalPortals: portals.length,
+                        'used-by-storage': portals[0]['used-by-storage'],
+                        children: portals.map((p) => ({
+                            text: p.portal,
+                            isTarget: false,
+                            leaf: true,
+                            iconCls: 'fa fa-plug',
+                            target: p.target,
+                            portal: p.portal,
+                            'session-id': p['session-id'],
+                            transport: p.transport,
+                            health: p.health,
+                            'kernel-state': p['kernel-state'],
+                            'used-by-storage': p['used-by-storage'],
+                        })),
+                    };
+                });
+        },
+
+        // a stable key for a tree node, so the selection survives a reload
+        // that rebuilds the whole tree from scratch
+        nodeKey: function (node) {
+            return node.get('isTarget')
+                ? `target:${node.get('target')}`
+                : `portal:${node.get('target')}\0${node.get('portal')}`;
+        },
+
+        reloadData: function () {
+            let me = this;
+            let view = me.getView();
+            Proxmox.Utils.API2Request({
+                url: `/nodes/${view.nodename}/iscsi/target`,
+                method: 'GET',
+                waitMsgTarget: view,
+                failure: (response) => Proxmox.Utils.setErrorMask(view, response.htmlStatus),
+                success: function (response) {
+                    Proxmox.Utils.setErrorMask(view, false);
+                    let prev = view.getSelection()[0];
+                    let key = prev ? me.nodeKey(prev) : undefined;
+                    // targets start collapsed, but a reload keeps what the
+                    // user expanded by hand
+                    let expandedTargets = {};
+                    view.getRootNode().eachChild((n) => {
+                        if (n.isExpanded()) {
+                            expandedTargets[n.get('target')] = true;
+                        }
+                    });
+                    view.setRootNode({
+                        expanded: true,
+                        children: me.buildChildren(response.result.data, expandedTargets),
+                    });
+                    if (key) {
+                        let node = view
+                            .getRootNode()
+                            .findChildBy((n) => me.nodeKey(n) === key, me, true);
+                        if (node) {
+                            view.getSelectionModel().select(node);
+                        }
+                    }
+                },
+            });
+        },
+
+        onReload: function () {
+            this.getView().reload();
+        },
+
+        onRescan: function () {
+            let me = this;
+            let view = me.getView();
+            let rec = view.getSelection()[0];
+            Proxmox.Utils.API2Request({
+                url: `/nodes/${view.nodename}/iscsi/rescan`,
+                method: 'POST',
+                params: { 'session-id': rec.get('session-id') },
+                waitMsgTarget: view,
+                failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+                success: () => view.reload(),
+            });
+        },
+
+        onLogout: function () {
+            let me = this;
+            let view = me.getView();
+            let rec = view.getSelection()[0];
+            let msg = Ext.String.format(
+                gettext("Log out of the session on portal '{0}'?"),
+                Ext.htmlEncode(rec.get('portal')),
+            );
+            let storage = rec.get('used-by-storage');
+            if (storage) {
+                msg += `<br><br>${Ext.String.format(
+                    gettext(
+                        'Storage "{0}" uses this session - it will be logged in again' +
+                            ' automatically on the next storage activation.',
+                    ),
+                    Ext.htmlEncode(storage),
+                )}`;
+            }
+            Ext.Msg.confirm(gettext('Confirm'), msg, function (btn) {
+                if (btn !== 'yes') {
+                    return;
+                }
+                Proxmox.Utils.API2Request({
+                    url: `/nodes/${view.nodename}/iscsi/session-logout`,
+                    method: 'POST',
+                    params: { 'session-id': rec.get('session-id') },
+                    waitMsgTarget: view,
+                    failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+                    success: () => view.reload(),
+                });
+            });
+        },
+
+        // on a portal leaf only that portal's node record is removed, on a
+        // target node all records of the target
+        onRemove: function () {
+            let me = this;
+            let view = me.getView();
+            let rec = view.getSelection()[0];
+            let params = { target: rec.get('target') };
+            let msg;
+            if (rec.get('isTarget')) {
+                msg = Ext.String.format(
+                    gettext("Remove all initiator records for target '{0}'?"),
+                    Ext.htmlEncode(rec.get('target')),
+                );
+            } else {
+                params.portal = rec.get('portal');
+                msg = Ext.String.format(
+                    gettext("Remove the record for target '{0}', portal '{1}'?"),
+                    Ext.htmlEncode(rec.get('target')),
+                    Ext.htmlEncode(rec.get('portal')),
+                );
+            }
+            let storage = rec.get('used-by-storage');
+            if (storage) {
+                msg += `<br><br>${Ext.String.format(
+                    gettext(
+                        'Storage "{0}" uses this target - the records will be created again' +
+                            ' automatically on the next storage activation.',
+                    ),
+                    Ext.htmlEncode(storage),
+                )}`;
+            }
+            Ext.Msg.confirm(gettext('Confirm'), msg, function (btn) {
+                if (btn !== 'yes') {
+                    return;
+                }
+                Proxmox.Utils.API2Request({
+                    url: `/nodes/${view.nodename}/iscsi/target?${Ext.Object.toQueryString(params)}`,
+                    method: 'DELETE',
+                    waitMsgTarget: view,
+                    failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+                    success: () => view.reload(),
+                });
+            });
+        },
+
+        showInfo: function (rec) {
+            let view = this.getView();
+            if (rec.get('session-id') === undefined) {
+                return;
+            }
+            Ext.create('Ext.window.Window', {
+                title: gettext('Session Parameters'),
+                modal: true,
+                width: 500,
+                height: 600,
+                resizable: true,
+                layout: 'fit',
+                items: [
+                    {
+                        xtype: 'pveISCSISessionParams',
+                        nodename: view.nodename,
+                        sessionData: rec.data,
+                    },
+                ],
+            }).show();
+        },
+
+        onInfo: function () {
+            this.showInfo(this.getView().getSelection()[0]);
+        },
+
+        onItemDblClick: function (tree, rec) {
+            this.showInfo(rec);
+        },
+
+        onCopyInitiator: function () {
+            let name = this.getViewModel().get('initiatorName');
+            if (name) {
+                navigator.clipboard?.writeText(name);
+            }
+        },
+
+        setInitiator: function (status) {
+            let me = this;
+            let view = me.getView();
+            if (!status.supported) {
+                // without open-iscsi there are no targets to show, and nodes
+                // with FC or NVMe attached SANs only need not install it
+                view.hide();
+                return;
+            }
+            let initiator = status.initiator || {};
+            let text = initiator.name || Proxmox.Utils.unknownText;
+            if (initiator.alias) {
+                text += ` (${initiator.alias})`;
+            }
+            me.getViewModel().set({
+                initiatorName: initiator.name || '',
+                initiatorText: text,
+            });
+            view.iscsiSupported = true;
+            view.reload();
+        },
+
+        init: function (view) {
+            let me = this;
+            if (!view.nodename) {
+                throw 'no node name specified';
+            }
+            Proxmox.Utils.API2Request({
+                url: `/nodes/${view.nodename}/iscsi/status`,
+                method: 'GET',
+                failure: (response) => Proxmox.Utils.setErrorMask(view, response.htmlStatus),
+                success: (response) => me.setInitiator(response.result.data),
+            });
+        },
+    },
+
+    store: {
+        type: 'tree',
+        fields: [
+            'text',
+            'isTarget',
+            'connectedCount',
+            'sessionCount',
+            'totalPortals',
+            'target',
+            'portal',
+            'session-id',
+            'transport',
+            'health',
+            'kernel-state',
+            'used-by-storage',
+        ],
+        root: { expanded: true, children: [] },
+    },
+
+    // loading is deferred until the status query confirms open-iscsi is
+    // installed, and skipped entirely when it is not
+    reload: function () {
+        if (this.iscsiSupported) {
+            this.getController().reloadData();
+        }
+    },
+
+    listeners: {
+        activate: 'onReload',
+        itemdblclick: 'onItemDblClick',
+    },
+
+    columns: [
+        {
+            xtype: 'treecolumn',
+            text: gettext('Target'),
+            dataIndex: 'text',
+            flex: 3,
+        },
+        {
+            text: gettext('Session ID'),
+            dataIndex: 'session-id',
+            width: 110,
+        },
+        {
+            text: gettext('Transport'),
+            dataIndex: 'transport',
+            width: 100,
+            renderer: PVE.Utils.render_iscsi_transport,
+        },
+        {
+            text: gettext('Status'),
+            dataIndex: 'health',
+            width: 150,
+            renderer: function (value, metaData, rec) {
+                if (!rec.get('isTarget')) {
+                    return PVE.Utils.render_iscsi_session_health(value);
+                }
+                let connected = rec.get('connectedCount');
+                let total = rec.get('totalPortals');
+                if (connected === 0) {
+                    return `<i class="fa fa-fw faded fa-unlink"></i> ${gettext('Not connected')}`;
+                }
+                if (connected === total) {
+                    return `<i class="fa fa-fw good fa-check"></i> ${gettext('Connected')}`;
+                }
+                return `<i class="fa fa-fw warning fa-exclamation"></i> ${connected}/${total} ${gettext('connected')}`;
+            },
+        },
+        {
+            text: gettext('Storage'),
+            dataIndex: 'used-by-storage',
+            flex: 1,
+            renderer: (value, metaData, rec) =>
+                rec.get('isTarget') ? Ext.htmlEncode(value ?? '') : '',
+        },
+    ],
+
+    initComponent: function () {
+        let me = this;
+
+        let caps = Ext.state.Manager.get('GuiCap');
+        let hasSession = (rec) => !rec.get('isTarget') && rec.get('session-id') !== undefined;
+
+        let items = [
+            {
+                text: gettext('Reload'),
+                iconCls: 'fa fa-refresh',
+                handler: 'onReload',
+            },
+        ];
+        if (caps.nodes['Sys.Modify']) {
+            items.push(
+                {
+                    xtype: 'proxmoxButton',
+                    text: gettext('Rescan'),
+                    iconCls: 'fa fa-refresh',
+                    disabled: true,
+                    enableFn: hasSession,
+                    handler: 'onRescan',
+                },
+                {
+                    xtype: 'proxmoxButton',
+                    text: gettext('Logout'),
+                    iconCls: 'fa fa-sign-out',
+                    disabled: true,
+                    dangerous: true,
+                    enableFn: hasSession,
+                    handler: 'onLogout',
+                },
+                {
+                    xtype: 'proxmoxButton',
+                    text: gettext('Remove'),
+                    iconCls: 'fa fa-trash-o',
+                    disabled: true,
+                    dangerous: true,
+                    // a portal leaf must be logged out; a target node covers
+                    // all of its records, so every portal must be logged out
+                    enableFn: (rec) =>
+                        rec.get('isTarget') ? rec.get('sessionCount') === 0 : !hasSession(rec),
+                    handler: 'onRemove',
+                },
+            );
+        }
+        items.push(
+            {
+                xtype: 'proxmoxButton',
+                text: gettext('Info'),
+                iconCls: 'fa fa-info-circle',
+                disabled: true,
+                enableFn: hasSession,
+                handler: 'onInfo',
+            },
+            '->',
+            {
+                xtype: 'displayfield',
+                fieldLabel: gettext('Initiator Name'),
+                labelWidth: 100,
+                bind: {
+                    value: '{initiatorText}',
+                },
+            },
+            {
+                xtype: 'button',
+                iconCls: 'fa fa-clipboard',
+                tooltip: gettext('Copy to Clipboard'),
+                handler: 'onCopyInitiator',
+                bind: {
+                    disabled: '{!initiatorName}',
+                },
+            },
+        );
+        me.tbar = {
+            defaults: { parentXType: 'treepanel' },
+            items: items,
+        };
+
+        me.callParent();
+    },
+});
diff --git a/www/manager6/node/SANLuns.js b/www/manager6/node/SANLuns.js
index ce11c39b..0503a3f8 100644
--- a/www/manager6/node/SANLuns.js
+++ b/www/manager6/node/SANLuns.js
@@ -226,3 +226,50 @@ Ext.define('PVE.node.SANLunList', {
         me.reload();
     },
 });
+
+// The SAN observability page: the LUN list with the iSCSI target list of the
+// node below it, since iSCSI sessions provide a share of the listed LUNs.
+Ext.define('PVE.node.SANLunPanel', {
+    extend: 'Ext.panel.Panel',
+    xtype: 'pveSANLunPanel',
+
+    layout: 'border',
+
+    // the config panel activates the card itself, so propagate the reload to
+    // both views by hand
+    listeners: {
+        activate: function () {
+            this.items.each((panel) => panel.reload());
+        },
+    },
+
+    initComponent: function () {
+        let me = this;
+
+        let nodename = me.pveSelNode?.data?.node;
+        if (!nodename) {
+            throw 'no node name specified';
+        }
+
+        me.items = [
+            {
+                xtype: 'pveSANLunList',
+                region: 'center',
+                border: false,
+                pveSelNode: me.pveSelNode,
+            },
+            {
+                xtype: 'pveISCSITargetTree',
+                region: 'south',
+                title: gettext('iSCSI Targets'),
+                height: '35%',
+                collapsible: true,
+                animCollapse: false,
+                border: false,
+                nodename: nodename,
+            },
+        ];
+
+        me.callParent();
+    },
+});
-- 
2.47.3




      parent reply	other threads:[~2026-07-31 10:25 UTC|newest]

Thread overview: 28+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-31 10:21 [RFC pve-storage/proxmox-widget-toolkit/pve-manager 00/27] add guided remote storage setup and SAN visibility Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 01/27] diskmanage: collect disk transport type from lsblk Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 02/27] diskmanage: add helper to list multipath devices Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 03/27] diskmanage: qualify NVMe over fabrics transport Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 04/27] disks: list: add include-remote parameter Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 05/27] diskmanage: include iSCSI session devices in disk enumeration Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 06/27] diskmanage: link multipath member disks to their map device Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 07/27] iscsi: factor out session device map from device list Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 08/27] api: scan: add san-luns method listing SAN LUN candidates Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 09/27] disks: lvm: allow creating volume groups on multipath devices Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 10/27] diskmanage: add helper querying multipath path state Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 11/27] diskmanage: add helper querying NVMe native " Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 12/27] api: scan: san-luns: report " Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 13/27] iscsi plugin: list sessions of all transports and capture transport Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-storage 14/27] api: add node-level iSCSI initiator target and session API Dietmar Maurer
2026-07-31 10:21 ` [RFC proxmox-widget-toolkit 15/27] disk selectors: allow opting into remote devices Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 16/27] ui: storage: allow switching the scan node of the NFS/CIFS scan combos Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 17/27] ui: storage: add guided remote storage wizard with NFS support Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 18/27] ui: storage wizard: add SMB/CIFS support Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 19/27] ui: storage wizard: add iSCSI support Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 20/27] ui: storage wizard: add FC-attached SAN (shared LVM) support Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 21/27] ui: storage wizard: add ZFS over iSCSI support Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 22/27] ui: dc: storage: add remote storage wizard entry to the add menu Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 23/27] ui: node: add SAN LUNs panel Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 24/27] ui: san luns: show multipath path state Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 25/27] api: nodes: add iSCSI initiator API endpoint Dietmar Maurer
2026-07-31 10:21 ` [RFC pve-manager 26/27] pvenode: add iscsi commands Dietmar Maurer
2026-07-31 10:21 ` Dietmar Maurer [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=20260731102156.3947857-28-dietmar@proxmox.com \
    --to=dietmar@proxmox.com \
    --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