[PATCH v2 storage 1/1] fix #254: iscsi: allow to configure multiple portals

ykonotopov at gnome.org ykonotopov at gnome.org
Sun Oct 15 20:32:16 CEST 2023


From: Yuri Konotopov <ykonotopov at gnome.org>

Signed-off-by: Yuri Konotopov <ykonotopov at gnome.org>
---
 PVE/API2/Storage/Scan.pm   |   2 +-
 PVE/Storage.pm             |  10 +-
 PVE/Storage/ISCSIPlugin.pm | 207 ++++++++++++++++++++++++++++++++-----
 3 files changed, 187 insertions(+), 32 deletions(-)

diff --git a/PVE/API2/Storage/Scan.pm b/PVE/API2/Storage/Scan.pm
index d7a8743..1f9773c 100644
--- a/PVE/API2/Storage/Scan.pm
+++ b/PVE/API2/Storage/Scan.pm
@@ -305,7 +305,7 @@ __PACKAGE__->register_method({
 	    node => get_standard_option('pve-node'),
 	    portal => {
 		description => "The iSCSI portal (IP or DNS name with optional port).",
-		type => 'string', format => 'pve-storage-portal-dns',
+		type => 'string', format => 'pve-storage-portal-dns-list',
 	    },
 	},
     },
diff --git a/PVE/Storage.pm b/PVE/Storage.pm
index cec3996..0043507 100755
--- a/PVE/Storage.pm
+++ b/PVE/Storage.pm
@@ -1428,12 +1428,14 @@ sub resolv_portal {
 sub scan_iscsi {
     my ($portal_in) = @_;
 
-    my $portal;
-    if (!($portal = resolv_portal($portal_in))) {
-	die "unable to parse/resolve portal address '${portal_in}'\n";
+    my @portals = PVE::Tools::split_list($portal_in);
+    for my $portal (@portals) {
+	if (!resolv_portal($portal)) {
+	    die "unable to parse/resolve portal address '${portal}'\n";
+	}
     }
 
-    return PVE::Storage::ISCSIPlugin::iscsi_discovery($portal);
+    return PVE::Storage::ISCSIPlugin::iscsi_discovery(\@portals);
 }
 
 sub storage_default_format {
diff --git a/PVE/Storage/ISCSIPlugin.pm b/PVE/Storage/ISCSIPlugin.pm
index a79fcb0..6f4309f 100644
--- a/PVE/Storage/ISCSIPlugin.pm
+++ b/PVE/Storage/ISCSIPlugin.pm
@@ -18,6 +18,65 @@ use base qw(PVE::Storage::Plugin);
 my $ISCSIADM = '/usr/bin/iscsiadm';
 $ISCSIADM = undef if ! -X $ISCSIADM;
 
+my $iscsi_cfg = "/etc/pve/iscsi.cfg";
+
+sub read_config {
+    my ($filename, $raw) = @_;
+
+    my $cfg = {};
+
+    return $cfg if ! -f $iscsi_cfg;
+
+    my $content = PVE::Tools::file_get_contents($iscsi_cfg);
+    return $cfg if !defined($content);
+
+    my @lines = split /\n/, $content;
+
+    my $target;
+
+    for my $line (@lines) {
+	$line =~ s/#.*$//;
+	$line =~ s/^\s+//;
+	$line =~ s/^;.*$//;
+	$line =~ s/\s+$//;
+	next if !$line;
+
+	$target = $1 if $line =~ m/^\[(\S+)\]$/;
+	if (!$target) {
+	    warn "no target - skip: $line\n";
+	    next;
+	}
+
+	if (!defined($cfg->{$target})) {
+	    $cfg->{$target} = [];
+	}
+
+	if ($line =~ m/^((?:$IPV4RE|\[$IPV6RE\]):\d+)$/) {
+	    push @{$cfg->{$target}}, $1;
+	}
+    }
+
+    return $cfg;
+}
+
+sub write_config {
+    my ($cfg) = @_;
+
+    my $out = '';
+
+    for my $target (sort keys %$cfg) {
+	$out .= "[$target]\n";
+	for my $portal (sort @{$cfg->{$target}}) {
+	    $out .= "$portal\n";
+	}
+	$out .= "\n";
+    }
+
+    PVE::Tools::file_set_contents($iscsi_cfg, $out);
+
+    return;
+}
+
 sub check_iscsi_support {
     my $noerr = shift;
 
@@ -45,11 +104,10 @@ sub iscsi_session_list {
     eval {
 	run_command($cmd, errmsg => 'iscsi session scan failed', outfunc => sub {
 	    my $line = shift;
-
-	    if ($line =~ m/^tcp:\s+\[(\S+)\]\s+\S+\s+(\S+)(\s+\S+)?\s*$/) {
-		my ($session, $target) = ($1, $2);
+	    if ($line =~ m/^tcp:\s+\[(\S+)\]\s+((?:$IPV4RE|\[$IPV6RE\]):\d+)\,\S+\s+(\S+)(\s+\S+)?\s*$/) {
+		my ($session, $portal, $target) = ($1, $2, $3);
 		# there can be several sessions per target (multipath)
-		push @{$res->{$target}}, $session;
+		push @{$res->{$target}}, [$session, $portal];
 	    }
 	});
     };
@@ -68,42 +126,68 @@ sub iscsi_test_portal {
     return PVE::Network::tcp_ping($server, $port || 3260, 2);
 }
 
-sub iscsi_discovery {
-    my ($portal) = @_;
+sub iscsi_portals {
+    my ($target) = @_;
 
     check_iscsi_support ();
 
-    my $res = {};
-    return $res if !iscsi_test_portal($portal); # fixme: raise exception here?
-
-    my $cmd = [$ISCSIADM, '--mode', 'discovery', '--type', 'sendtargets', '--portal', $portal];
+    my $res = [];
+    my $cmd = [$ISCSIADM, '--mode', 'node'];
     run_command($cmd, outfunc => sub {
 	my $line = shift;
 
 	if ($line =~ m/^((?:$IPV4RE|\[$IPV6RE\]):\d+)\,\S+\s+(\S+)\s*$/) {
-	    my $portal = $1;
-	    my $target = $2;
-	    # one target can have more than one portal (multipath).
-	    push @{$res->{$target}}, $portal;
+	    my ($portal, $portal_target) = ($1, $2);
+	    if ($portal_target eq $target) {
+		push @{$res}, $portal;
+	    }
 	}
     });
 
     return $res;
 }
 
+sub iscsi_discovery {
+    my ($portals) = @_;
+
+    check_iscsi_support ();
+
+    my $res = {};
+    for my $portal ($portals->@*) {
+	next if !iscsi_test_portal($portal); # fixme: raise exception here?
+
+	my $cmd = [$ISCSIADM, '--mode', 'discovery', '--type', 'sendtargets', '--portal', $portal];
+	run_command($cmd, outfunc => sub {
+	    my $line = shift;
+
+	    if ($line =~ m/^((?:$IPV4RE|\[$IPV6RE\]):\d+)\,\S+\s+(\S+)\s*$/) {
+		my $portal = $1;
+		my $target = $2;
+		# one target can have more than one portal (multipath).
+		push @{$res->{$target}}, $portal;
+	    }
+	});
+
+	# In case of multipath we want to exit on any portal available
+	last;
+    }
+
+    return $res;
+}
+
 sub iscsi_login {
-    my ($target, $portal_in) = @_;
+    my ($target, $portals) = @_;
 
     check_iscsi_support();
 
-    eval { iscsi_discovery($portal_in); };
+    eval { iscsi_discovery($portals); };
     warn $@ if $@;
 
     run_command([$ISCSIADM, '--mode', 'node', '--targetname',  $target, '--login']);
 }
 
 sub iscsi_logout {
-    my ($target, $portal) = @_;
+    my ($target) = @_;
 
     check_iscsi_support();
 
@@ -133,7 +217,7 @@ sub iscsi_session_rescan {
     }
 
     foreach my $session (@$session_list) {
-	my $cmd = [$ISCSIADM, '--mode', 'session', '--sid', $session, '--rescan'];
+	my $cmd = [$ISCSIADM, '--mode', 'session', '--sid', $session->@[0], '--rescan'];
 	eval { run_command($cmd, outfunc => sub {}); };
 	warn $@ if $@;
     }
@@ -245,8 +329,8 @@ sub properties {
 	    type => 'string',
 	},
 	portal => {
-	    description => "iSCSI portal (IP or DNS name with optional port).",
-	    type => 'string', format => 'pve-storage-portal-dns',
+	    description => "iSCSI portal (IP or DNS name with optional port). For multipath, multiple portals can be specified.",
+	    type => 'string', format => 'pve-storage-portal-dns-list',
 	},
     };
 }
@@ -264,6 +348,33 @@ sub options {
 
 # Storage implementation
 
+sub on_add_hook {
+    my ($class, $storeid, $scfg, %param) = @_;
+
+    my @portals = PVE::Tools::split_list($scfg->{portal});
+    my $target = $scfg->{target};
+
+    my $portal_cfg = read_config();
+    $portal_cfg->{$target} = \@portals;
+
+    write_config($portal_cfg);
+
+    return;
+}
+
+sub on_delete_hook {
+    my ($class, $storeid, $scfg) = @_;
+
+    my $portal_cfg = read_config();
+    my $target = $scfg->{target};
+
+    delete $portal_cfg->{$target};
+
+    write_config($portal_cfg);
+
+    return;
+}
+
 sub parse_volname {
     my ($class, $volname) = @_;
 
@@ -365,6 +476,21 @@ sub iscsi_session {
     return $cache->{iscsi_sessions}->{$target};
 }
 
+sub iscsi_session_portals {
+    my ($cache, $target) = @_;
+
+    my $res = [];
+    my $sessions = iscsi_session($cache, $target);
+
+    if (defined($sessions)) {
+	for my $session ($sessions->@*) {
+	    push @{$res}, $session->@[1];
+	}
+    }
+
+    return $res;
+}
+
 sub status {
     my ($class, $storeid, $scfg, $cache) = @_;
 
@@ -379,14 +505,37 @@ sub activate_storage {
 
     return if !check_iscsi_support(1);
 
-    my $session = iscsi_session($cache, $scfg->{target});
 
-    if (!defined ($session)) {
-	eval { iscsi_login($scfg->{target}, $scfg->{portal}); };
+    my $sessions = iscsi_session($cache, $scfg->{target});
+    my $portal_cfg = read_config();
+
+    my @portals = PVE::Tools::split_list($scfg->{portal});
+    if (defined($portal_cfg->{$scfg->{target}})) {
+	@portals = @{$portal_cfg->{$scfg->{target}}};
+    }
+
+    if (!defined ($sessions)) {
+	eval { iscsi_login($scfg->{target}, \@portals); };
 	warn $@ if $@;
     } else {
+	my @discovered_portals = @{iscsi_portals($scfg->{target})};
+	my @session_portals = @{iscsi_session_portals($cache, $scfg->{target})};
+
+	for my $discovered_portal (@discovered_portals) {
+	    if (!grep(/^\Q$discovered_portal\E$/, @session_portals)) {
+		eval { iscsi_login($scfg->{target}, \@discovered_portals); };
+		warn $@ if $@;
+		last;
+	    }
+	}
+
+	if(join(",", sort(@discovered_portals)) ne join(",", sort(@portals))) {
+	    $portal_cfg->{$scfg->{target}} = \@discovered_portals;
+	    write_config($portal_cfg);
+	}
+
 	# make sure we get all devices
-	iscsi_session_rescan($session);
+	iscsi_session_rescan($sessions);
     }
 }
 
@@ -396,15 +545,19 @@ sub deactivate_storage {
     return if !check_iscsi_support(1);
 
     if (defined(iscsi_session($cache, $scfg->{target}))) {
-	iscsi_logout($scfg->{target}, $scfg->{portal});
+	iscsi_logout($scfg->{target});
     }
 }
 
 sub check_connection {
     my ($class, $storeid, $scfg) = @_;
 
-    my $portal = $scfg->{portal};
-    return iscsi_test_portal($portal);
+    for my $portal (PVE::Tools::split_list($scfg->{portal})) {
+	my $result = iscsi_test_portal($portal);
+	return $result if $result;
+    }
+
+    return 0;
 }
 
 sub volume_resize {
-- 
2.41.0





More information about the pve-devel mailing list