all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Dietmar Maurer <dietmar@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [RFC pve-storage 14/27] api: add node-level iSCSI initiator target and session API
Date: Fri, 31 Jul 2026 12:21:43 +0200	[thread overview]
Message-ID: <20260731102156.3947857-15-dietmar@proxmox.com> (raw)
In-Reply-To: <20260731102156.3947857-1-dietmar@proxmox.com>

The iSCSI storage plugin manages session logins automatically on
storage activation, but nothing surfaces the resulting state: admins
cannot see which targets a node knows or is logged in to, how healthy
the sessions are, or which parameters were negotiated. Sessions and
node records also linger after their storage is removed from the
configuration, with no way to clean them up short of iscsiadm on the
shell.

Add a small observability-focused API at /nodes/{node}/iscsi: report
the initiator identity, list the known targets per portal with their
session state and the configured storage using them, read the
negotiated parameters of a session, rescan a session for new LUNs,
log out of a single session and remove the node records of a target
or of a single portal. The target list merges the initiator's node
records with the active sessions, so a target stays visible as
not-connected after a logout or while its portal is unreachable,
instead of silently disappearing.

Portals need special handling in two places, since iscsiadm reports
and accepts them in several representations: the list normalizes
them before merging (bracketed IPv6 in session listings, bare in
node listings, v4-mapped addresses for IPv4 portals reached over
IPv6 sockets), and record removal passes v4-mapped addresses back
with their IPv4 tail rewritten in hex groups, the only spelling
iscsiadm's portal parser accepts with a port.

Reads require Datastore.Allocate on /storage like the related
scan/iscsi and scan/san-luns endpoints, so the SAN views work with a
single privilege; logout, rescan and node record removal mutate node
state and require Sys.Modify.

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
 src/PVE/API2/ISCSI.pm          | 586 +++++++++++++++++++++++++++++++++
 src/PVE/API2/Makefile          |   1 +
 src/PVE/Storage/ISCSIPlugin.pm |  23 ++
 3 files changed, 610 insertions(+)
 create mode 100644 src/PVE/API2/ISCSI.pm

diff --git a/src/PVE/API2/ISCSI.pm b/src/PVE/API2/ISCSI.pm
new file mode 100644
index 0000000..6ce5e2a
--- /dev/null
+++ b/src/PVE/API2/ISCSI.pm
@@ -0,0 +1,586 @@
+package PVE::API2::ISCSI;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RESTHandler;
+use PVE::Storage;
+use PVE::Storage::ISCSIPlugin;
+use PVE::Tools qw(run_command file_get_contents file_read_firstline);
+
+use base qw(PVE::RESTHandler);
+
+# Node-level view of the local iSCSI initiator: report the initiator identity,
+# inspect the known targets with their session state and negotiated parameters,
+# rescan a session for new LUNs, log out of stale sessions and remove leftover
+# node records. Session logins are managed by the iSCSI storage plugin on
+# storage activation; this API only observes them and cleans up what no
+# configured storage owns anymore.
+
+my $ISCSIADM = '/usr/bin/iscsiadm';
+my $INITIATOR_NAME_FILE = '/etc/iscsi/initiatorname.iscsi';
+
+my $session_id_schema = {
+    description => "Kernel iSCSI session id.",
+    type => 'integer',
+    minimum => 0,
+};
+
+my $target_schema = {
+    description => "iSCSI target name (IQN or EUI).",
+    type => 'string',
+    minLength => 1,
+    maxLength => 255,
+};
+
+my $initiator_return = {
+    type => 'object',
+    properties => {
+        name => {
+            description => "The initiator IQN (InitiatorName).",
+            type => 'string',
+        },
+        alias => {
+            description => "The initiator alias (InitiatorAlias), if set.",
+            type => 'string',
+            optional => 1,
+        },
+    },
+};
+
+my $status_return = {
+    type => 'object',
+    properties => {
+        supported => {
+            description =>
+                "Whether iSCSI is supported, that is the open-iscsi package is installed.",
+            type => 'boolean',
+        },
+        initiator => {
+            %$initiator_return,
+            description => "The local initiator identity, if available.",
+            optional => 1,
+        },
+    },
+};
+
+my $target_return = {
+    type => 'object',
+    properties => {
+        target => {
+            description => "The target IQN.",
+            type => 'string',
+        },
+        portal => {
+            description => "The portal ('host:port') this entry refers to.",
+            type => 'string',
+        },
+        'session-id' => {
+            description => "The kernel session id; absent when there is no active session"
+                . " on this portal.",
+            type => 'integer',
+            optional => 1,
+        },
+        transport => {
+            description => "The transport the session runs over (for example 'tcp' or"
+                . " 'iser'); only set for active sessions.",
+            type => 'string',
+            optional => 1,
+        },
+        health => {
+            description => "The session health derived from the kernel session state:"
+                . " 'recovering' means the connection is down and iscsid is retrying the"
+                . " login, 'not-connected' means only a node record exists, without an"
+                . " active session.",
+            type => 'string',
+            enum => ['logged-in', 'recovering', 'logging-out', 'not-connected', 'unknown'],
+        },
+        'kernel-state' => {
+            description => "The raw kernel session state read from sysfs (for example"
+                . " 'LOGGED_IN'), if available.",
+            type => 'string',
+            optional => 1,
+        },
+        'used-by-storage' => {
+            description => "Storage identifier of a configured iSCSI-type storage using"
+                . " this target.",
+            type => 'string',
+            optional => 1,
+        },
+    },
+};
+
+my $session_params_return = {
+    type => 'object',
+    properties => {
+        'header-digest' => {
+            description => "The negotiated header digest.",
+            type => 'string',
+            optional => 1,
+        },
+        'data-digest' => {
+            description => "The negotiated data digest.",
+            type => 'string',
+            optional => 1,
+        },
+        'max-recv-data-segment-length' => {
+            description => "Max data segment length (bytes) the initiator accepts in a PDU.",
+            type => 'integer',
+            optional => 1,
+        },
+        'max-xmit-data-segment-length' => {
+            description => "Max data segment length (bytes) the target accepts in a PDU.",
+            type => 'integer',
+            optional => 1,
+        },
+        'first-burst-length' => {
+            description => "Max unsolicited data (bytes) sent with a command (first burst).",
+            type => 'integer',
+            optional => 1,
+        },
+        'max-burst-length' => {
+            description => "Max data (bytes) in one solicited burst.",
+            type => 'integer',
+            optional => 1,
+        },
+        'max-outstanding-r2t' => {
+            description => "Max number of outstanding Ready-To-Transfer PDUs.",
+            type => 'integer',
+            optional => 1,
+        },
+        'immediate-data' => {
+            description => "Whether unsolicited immediate data is allowed ('Yes'/'No').",
+            type => 'string',
+            optional => 1,
+        },
+        'initial-r2t' => {
+            description =>
+                "Whether the target requires an R2T before any data is sent ('Yes'/'No').",
+            type => 'string',
+            optional => 1,
+        },
+        'recovery-timeout' => {
+            description =>
+                "Seconds the session waits for path recovery before failing its commands.",
+            type => 'integer',
+            optional => 1,
+        },
+        'abort-timeout' => {
+            description => "Seconds to wait for a SCSI abort task to complete.",
+            type => 'integer',
+            optional => 1,
+        },
+        'lu-reset-timeout' => {
+            description => "Seconds to wait for a LUN reset to complete.",
+            type => 'integer',
+            optional => 1,
+        },
+        'tgt-reset-timeout' => {
+            description => "Seconds to wait for a target reset to complete.",
+            type => 'integer',
+            optional => 1,
+        },
+    },
+};
+
+my sub read_initiator_info {
+    my $content = eval { file_get_contents($INITIATOR_NAME_FILE) };
+    return undef if !defined($content);
+
+    my $res = {};
+    for my $line (split(/\n/, $content)) {
+        if ($line =~ m/^\s*InitiatorName\s*=\s*(\S+)\s*$/) {
+            $res->{name} = $1;
+        } elsif ($line =~ m/^\s*InitiatorAlias\s*=\s*(.+?)\s*$/) {
+            $res->{alias} = $1;
+        }
+    }
+    return defined($res->{name}) ? $res : undef;
+}
+
+my sub session_health {
+    my ($kernel_state) = @_;
+
+    return 'unknown' if !defined($kernel_state);
+    return 'logged-in' if $kernel_state eq 'LOGGED_IN';
+    return 'recovering' if $kernel_state eq 'FAILED';
+    return 'not-connected' if $kernel_state eq 'FREE';
+    return 'unknown';
+}
+
+# iscsiadm reports portals in several representations: session listings print
+# IPv6 addresses in brackets while node record listings do not, and connections
+# to IPv4 portals made over IPv6 sockets appear with a v4-mapped address
+# (::ffff:a.b.c.d). Normalize them so session and node record entries merge on
+# the same key, unmapping v4-mapped addresses and bracketing IPv6 addresses
+# like the storage configuration does.
+my sub normalize_portal {
+    my ($portal) = @_;
+    return $portal if $portal !~ m/^(.+):(\d+)$/;
+    my ($addr, $port) = ($1, $2);
+    $addr =~ s/^\[(.*)\]$/$1/;
+    $addr =~ s/^::ffff:(\d+\.\d+\.\d+\.\d+)$/$1/i;
+    $addr = "[$addr]" if $addr =~ m/:/;
+    return "$addr:$port";
+}
+
+# iscsiadm's --portal parser only accepts a port on a bracketed address, and
+# only takes the bracket path when the address contains no dots, so a stored
+# v4-mapped address (::ffff:a.b.c.d) cannot be passed back as printed in any
+# form. Rewrite an embedded IPv4 tail in hex groups: that spelling parses,
+# and iscsiadm resolves it back to the same address when matching records.
+my sub iscsiadm_portal_arg {
+    my ($portal) = @_;
+    my ($addr, $port) = $portal =~ m/^(.+):(\d+)$/;
+    return $portal if !defined($addr) || $addr !~ m/:/;
+    if ($addr =~ m/^(.*:)(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
+        $addr = sprintf("%s%x:%x", $1, ($2 << 8) + $3, ($4 << 8) + $5);
+    }
+    return "[$addr]:$port";
+}
+
+# One entry per known target and portal: the active sessions, merged with the
+# node records so a target stays visible as 'not-connected' after a logout or
+# while its portal is unreachable.
+my sub list_targets {
+    my $session_map = PVE::Storage::ISCSIPlugin::iscsi_session_list();
+    my $node_list = PVE::Storage::ISCSIPlugin::iscsi_node_list();
+
+    # map each target IQN to the first configured iSCSI-type storage using it
+    my $target_storage = {};
+    my $cfg = PVE::Storage::config();
+    for my $storeid (sort keys %{ $cfg->{ids} }) {
+        my $scfg = $cfg->{ids}->{$storeid};
+        next if $scfg->{type} ne 'iscsi' || !defined($scfg->{target});
+        $target_storage->{ $scfg->{target} } //= $storeid;
+    }
+
+    my $res = [];
+    my $have_session = {};
+    for my $target (sort keys %$session_map) {
+        for my $session (@{ $session_map->{$target} }) {
+            my $sid = int($session->{session_id});
+            my $state = file_read_firstline("/sys/class/iscsi_session/session${sid}/state");
+            my $portal = normalize_portal($session->{portal});
+            my $entry = {
+                target => $target,
+                'session-id' => $sid,
+                portal => $portal,
+                transport => $session->{transport},
+                health => session_health($state),
+            };
+            $entry->{'kernel-state'} = $state if defined($state);
+            $entry->{'used-by-storage'} = $target_storage->{$target}
+                if defined($target_storage->{$target});
+            push @$res, $entry;
+            $have_session->{"$target\0$portal"} = 1;
+        }
+    }
+
+    for my $node (@$node_list) {
+        my $portal = normalize_portal($node->{portal});
+        next if $have_session->{"$node->{target}\0$portal"};
+        my $entry = {
+            target => $node->{target},
+            portal => $portal,
+            health => 'not-connected',
+        };
+        $entry->{'used-by-storage'} = $target_storage->{ $node->{target} }
+            if defined($target_storage->{ $node->{target} });
+        push @$res, $entry;
+    }
+
+    return [sort { $a->{target} cmp $b->{target} || $a->{portal} cmp $b->{portal} } @$res];
+}
+
+# Map the interesting lines of 'iscsiadm -m session -P 2' (the 'Timeouts' and
+# 'Negotiated iSCSI params' sections) to the API field names.
+my $session_param_map = {
+    'HeaderDigest' => ['header-digest', 'string'],
+    'DataDigest' => ['data-digest', 'string'],
+    'MaxRecvDataSegmentLength' => ['max-recv-data-segment-length', 'integer'],
+    'MaxXmitDataSegmentLength' => ['max-xmit-data-segment-length', 'integer'],
+    'FirstBurstLength' => ['first-burst-length', 'integer'],
+    'MaxBurstLength' => ['max-burst-length', 'integer'],
+    'ImmediateData' => ['immediate-data', 'string'],
+    'InitialR2T' => ['initial-r2t', 'string'],
+    'MaxOutstandingR2T' => ['max-outstanding-r2t', 'integer'],
+    'Recovery Timeout' => ['recovery-timeout', 'integer'],
+    'Abort Timeout' => ['abort-timeout', 'integer'],
+    'LUN Reset Timeout' => ['lu-reset-timeout', 'integer'],
+    'Target Reset Timeout' => ['tgt-reset-timeout', 'integer'],
+};
+
+my sub read_session_params {
+    my ($session_id) = @_;
+
+    my $res = {};
+    run_command(
+        [$ISCSIADM, '--mode', 'session', '--sid', $session_id, '-P', '2'],
+        errmsg => "reading parameters of iSCSI session $session_id failed",
+        outfunc => sub {
+            my ($line) = @_;
+            return if $line !~ m/^\s*([^:]+?)\s*:\s*(.+?)\s*$/;
+            my ($key, $value) = ($1, $2);
+            my $field = $session_param_map->{$key} or return;
+            my ($name, $type) = @$field;
+            if ($type eq 'integer') {
+                return if $value !~ m/^\d+$/;
+                $value = int($value);
+            }
+            $res->{$name} = $value;
+        },
+    );
+    return $res;
+}
+
+__PACKAGE__->register_method({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    proxyto => 'node',
+    permissions => { user => 'all' },
+    description => "Node iSCSI initiator index.",
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            node => get_standard_option('pve-node'),
+        },
+    },
+    returns => {
+        type => 'array',
+        items => {
+            type => "object",
+            properties => {},
+        },
+        links => [{ rel => 'child', href => "{name}" }],
+    },
+    code => sub {
+        return [
+            { name => 'rescan' },
+            { name => 'session-logout' },
+            { name => 'session-params' },
+            { name => 'status' },
+            { name => 'target' },
+        ];
+    },
+});
+
+# Reads require Datastore.Allocate on /storage like the related scan/iscsi and
+# scan/san-luns endpoints, so the SAN views work with a single privilege.
+# Session logout and rescan mutate node state and require Sys.Modify.
+__PACKAGE__->register_method({
+    name => 'status',
+    path => 'status',
+    method => 'GET',
+    protected => 1,
+    proxyto => 'node',
+    description => "Report iSCSI support and, when open-iscsi is installed, the"
+        . " initiator identity.",
+    permissions => {
+        check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            node => get_standard_option('pve-node'),
+        },
+    },
+    returns => $status_return,
+    code => sub {
+        my $res = { supported => -x $ISCSIADM ? 1 : 0 };
+        if ($res->{supported}) {
+            my $initiator = read_initiator_info();
+            $res->{initiator} = $initiator if defined($initiator);
+        }
+        return $res;
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'targets',
+    path => 'target',
+    method => 'GET',
+    protected => 1,
+    proxyto => 'node',
+    description => "List the iSCSI targets known to the initiator, together with the state"
+        . " of their sessions.",
+    permissions => {
+        check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            node => get_standard_option('pve-node'),
+        },
+    },
+    returns => {
+        type => 'array',
+        items => $target_return,
+    },
+    code => sub {
+        return list_targets();
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'target_delete',
+    path => 'target',
+    method => 'DELETE',
+    protected => 1,
+    proxyto => 'node',
+    description => "Remove the node records of a target from the initiator database, either"
+        . " of a single portal or of the whole target. Requires the affected records to be"
+        . " logged out.",
+    permissions => {
+        check => ['perm', '/', ['Sys.Modify']],
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            node => get_standard_option('pve-node'),
+            target => $target_schema,
+            portal => {
+                description => "Only remove the node record of this portal.",
+                type => 'string',
+                minLength => 2,
+                maxLength => 256,
+                optional => 1,
+            },
+        },
+    },
+    returns => { type => 'null' },
+    code => sub {
+        my ($param) = @_;
+        my ($target, $portal) = ($param->{target}, $param->{portal});
+
+        my $sessions = PVE::Storage::ISCSIPlugin::iscsi_session_list()->{$target} // [];
+        for my $session (@$sessions) {
+            next if defined($portal) && normalize_portal($session->{portal}) ne $portal;
+            die "target '$target' still has an active session - log out first\n";
+        }
+
+        if (!defined($portal)) {
+            run_command(
+                [$ISCSIADM, '--mode', 'node', '--targetname', $target, '--op', 'delete'],
+                errmsg => "removing the node records of target '$target' failed",
+                outfunc => sub { },
+            );
+            return undef;
+        }
+
+        # look up the records by their normalized portal and address each one
+        # in the representation it is actually stored under
+        my @records =
+            grep { $_->{target} eq $target && normalize_portal($_->{portal}) eq $portal }
+            @{ PVE::Storage::ISCSIPlugin::iscsi_node_list() };
+        die "no node record for target '$target', portal '$portal' found\n"
+            if !scalar(@records);
+
+        for my $record (@records) {
+            run_command(
+                [
+                    $ISCSIADM,
+                    '--mode',
+                    'node',
+                    '--targetname',
+                    $target,
+                    '--portal',
+                    iscsiadm_portal_arg($record->{portal}),
+                    '--op',
+                    'delete',
+                ],
+                errmsg => "removing the node record of target '$target' failed",
+                outfunc => sub { },
+            );
+        }
+        return undef;
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'session_params',
+    path => 'session-params',
+    method => 'GET',
+    protected => 1,
+    proxyto => 'node',
+    description => "Read the parameters an active iSCSI session negotiated at login.",
+    permissions => {
+        check => ['perm', '/storage', ['Datastore.Allocate']],
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            node => get_standard_option('pve-node'),
+            'session-id' => $session_id_schema,
+        },
+    },
+    returns => $session_params_return,
+    code => sub {
+        my ($param) = @_;
+        return read_session_params($param->{'session-id'});
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'session_logout',
+    path => 'session-logout',
+    method => 'POST',
+    protected => 1,
+    proxyto => 'node',
+    description => "Log out of a single iSCSI session, tearing down one path of a target.",
+    permissions => {
+        check => ['perm', '/', ['Sys.Modify']],
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            node => get_standard_option('pve-node'),
+            'session-id' => $session_id_schema,
+        },
+    },
+    returns => { type => 'null' },
+    code => sub {
+        my ($param) = @_;
+        my $sid = $param->{'session-id'};
+        run_command(
+            [$ISCSIADM, '--mode', 'session', '--sid', $sid, '--logout'],
+            errmsg => "logout of iSCSI session $sid failed",
+            outfunc => sub { },
+        );
+        return undef;
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'rescan',
+    path => 'rescan',
+    method => 'POST',
+    protected => 1,
+    proxyto => 'node',
+    description => "Rescan an iSCSI session to pick up newly attached LUNs.",
+    permissions => {
+        check => ['perm', '/', ['Sys.Modify']],
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            node => get_standard_option('pve-node'),
+            'session-id' => $session_id_schema,
+        },
+    },
+    returns => { type => 'null' },
+    code => sub {
+        my ($param) = @_;
+        my $sid = $param->{'session-id'};
+        run_command(
+            [$ISCSIADM, '--mode', 'session', '--sid', $sid, '--rescan'],
+            errmsg => "rescan of iSCSI session $sid failed",
+            outfunc => sub { },
+        );
+        return undef;
+    },
+});
+
+1;
diff --git a/src/PVE/API2/Makefile b/src/PVE/API2/Makefile
index fe316c5..10e18f9 100644
--- a/src/PVE/API2/Makefile
+++ b/src/PVE/API2/Makefile
@@ -3,5 +3,6 @@
 .PHONY: install
 install:
 	install -D -m 0644 Disks.pm ${DESTDIR}${PERLDIR}/PVE/API2/Disks.pm
+	install -D -m 0644 ISCSI.pm ${DESTDIR}${PERLDIR}/PVE/API2/ISCSI.pm
 	make -C Storage install
 	make -C Disks install
diff --git a/src/PVE/Storage/ISCSIPlugin.pm b/src/PVE/Storage/ISCSIPlugin.pm
index 4fbc5ad..e9a585f 100644
--- a/src/PVE/Storage/ISCSIPlugin.pm
+++ b/src/PVE/Storage/ISCSIPlugin.pm
@@ -68,6 +68,29 @@ sub iscsi_session_list {
     return $res;
 }
 
+sub iscsi_node_list {
+    assert_iscsi_support();
+
+    my $res = [];
+    eval {
+        run_command(
+            [$ISCSIADM, '--mode', 'node'],
+            errmsg => 'iscsi node record scan failed',
+            outfunc => sub {
+                my $line = shift;
+                if ($line =~ $ISCSI_TARGET_RE) {
+                    push @$res, { portal => $1, target => $2 };
+                }
+            },
+        );
+    };
+    if (my $err = $@) {
+        die $err if $err !~ m/: No records found/i;
+    }
+
+    return $res;
+}
+
 sub iscsi_test_session {
     my ($sid) = @_;
 
-- 
2.47.3




  parent reply	other threads:[~2026-07-31 10:24 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 ` Dietmar Maurer [this message]
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 ` [RFC pve-manager 27/27] ui: san luns: show iSCSI targets and sessions Dietmar Maurer

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-15-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal