public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Fabian Ebner <f.ebner@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v2 manager 14/14] fix #2422: allow multiple Ceph public networks
Date: Mon, 10 May 2021 14:18:26 +0200	[thread overview]
Message-ID: <20210510121826.8543-15-f.ebner@proxmox.com> (raw)
In-Reply-To: <20210510121826.8543-1-f.ebner@proxmox.com>

From: Alwin Antreich <a.antreich@proxmox.com>

Multiple public networks can be defined in the ceph.conf. The networks need to
be routed to each other.

Support handling multiple IPs for a single monitor. By default, one address from
each public network is selected for monitor creation, but, as before, it can be
overwritten with the mon-address parameter, now taking a list of addresses.

On removal, make sure the all addresses are removed from the mon_host entry in
the ceph configuration.

Originally-by: Alwin Antreich <a.antreich@proxmox.com>
[handling of multiple addresses]
Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
---

Changes from v1:
    * adapt to new handling of IPv6 addresses
    * make overwrite_ips unique

Sorry if the patch is a bit messy to review, what it essentially is, is creating
loops over nets/addresses around the old existing logic.

Viewing with a word-based diff might help in some places.


Older changes:

Changes from original patch as suggested by Fabian G.:
    * split list after truth check for $pubnet
    * avoid long post-if statement
    * style/indentation fixes
    * verify cidr

Other changes from original patch:
    * test/select IPs from each public network
    * return and handle a list of IPs, so that it's not necessary to create a
      monitor for each public network
    * also handle multiple addresses upon removal

 PVE/API2/Ceph/MON.pm | 164 +++++++++++++++++++++++++++++--------------
 1 file changed, 111 insertions(+), 53 deletions(-)

diff --git a/PVE/API2/Ceph/MON.pm b/PVE/API2/Ceph/MON.pm
index 6244655d..12c9caf0 100644
--- a/PVE/API2/Ceph/MON.pm
+++ b/PVE/API2/Ceph/MON.pm
@@ -20,8 +20,11 @@ use PVE::API2::Ceph::MGR;
 
 use base qw(PVE::RESTHandler);
 
-my $find_mon_ip = sub {
-    my ($cfg, $rados, $node, $overwrite_ip) = @_;
+my $find_mon_ips = sub {
+    my ($cfg, $rados, $node, $mon_address) = @_;
+
+    my $overwrite_ips = [ PVE::Tools::split_list($mon_address) ];
+    $overwrite_ips = PVE::Network::unique_ips($overwrite_ips);
 
     my $pubnet;
     if ($rados) {
@@ -34,32 +37,60 @@ my $find_mon_ip = sub {
     $pubnet //= $cfg->{global}->{public_network};
 
     if (!$pubnet) {
-	return $overwrite_ip // PVE::Cluster::remote_node_ip($node);
+	if (scalar(@{$overwrite_ips})) {
+	    return $overwrite_ips;
+	} else {
+	   # don't refactor into '[ PVE::Cluster::remote... ]' as it uses wantarray
+	   my $ip = PVE::Cluster::remote_node_ip($node);
+	   return [ $ip ];
+	}
     }
 
-    my $allowed_ips = PVE::Network::get_local_ip_from_cidr($pubnet);
-    $allowed_ips = PVE::Network::unique_ips($allowed_ips);
+    my $public_nets = [ PVE::Tools::split_list($pubnet) ];
+    if (scalar(@{$public_nets}) > 1) {
+	warn "Multiple Ceph public networks detected on $node: $pubnet\n";
+	warn "Networks must be capable of routing to each other.\n";
+    }
+
+    my $res = [];
+
+    if (!scalar(@{$overwrite_ips})) { # auto-select one address for each public network
+	for my $net (@{$public_nets}) {
+	    $net = PVE::JSONSchema::pve_verify_cidr($net);
+
+	    my $allowed_ips = PVE::Network::get_local_ip_from_cidr($net);
+	    $allowed_ips = PVE::Network::unique_ips($allowed_ips);
 
-    die "No active IP found for the requested ceph public network '$pubnet' on node '$node'\n"
-	if scalar(@$allowed_ips) < 1;
+	    die "No active IP found for the requested ceph public network '$net' on node '$node'\n"
+		if scalar(@$allowed_ips) < 1;
 
-    if (!$overwrite_ip) {
-	if (scalar(@$allowed_ips) == 1) {
-	    return $allowed_ips->[0];
+	    if (scalar(@$allowed_ips) == 1) {
+		push @{$res}, $allowed_ips->[0];
+	    } else {
+		die "Multiple IPs for ceph public network '$net' detected on $node:\n".
+		    join("\n", @$allowed_ips) ."\nuse 'mon-address' to specify one of them.\n";
+	    }
 	}
-	die "Multiple IPs for ceph public network '$pubnet' detected on $node:\n".
-	    join("\n", @$allowed_ips) ."\nuse 'mon-address' to specify one of them.\n";
-    } else {
-	$overwrite_ip = PVE::Network::canonical_ip($overwrite_ip);
+    } else { # check if overwrite IPs are active and in any of the public networks
+	my $allowed_list = [];
+
+	for my $net (@{$public_nets}) {
+	    $net = PVE::JSONSchema::pve_verify_cidr($net);
 
-	if (grep { $_ eq $overwrite_ip } @$allowed_ips) {
-	    return $overwrite_ip;
+	    push @{$allowed_list}, @{PVE::Network::get_local_ip_from_cidr($net)};
 	}
-	die "Monitor IP '$overwrite_ip' not in ceph public network '$pubnet'\n"
-	    if !PVE::Network::is_ip_in_cidr($overwrite_ip, $pubnet);
 
-	die "Specified monitor IP '$overwrite_ip' not configured or up on $node!\n";
+	my $allowed_ips = PVE::Network::unique_ips($allowed_list);
+
+	for my $overwrite_ip (@{$overwrite_ips}) {
+	    die "Specified monitor IP '$overwrite_ip' not configured or up on $node!\n"
+		if !grep { $_ eq $overwrite_ip } @{$allowed_ips};
+
+	    push @{$res}, $overwrite_ip;
+	}
     }
+
+    return $res;
 };
 
 my $ips_from_mon_host = sub {
@@ -87,9 +118,7 @@ my $ips_from_mon_host = sub {
 };
 
 my $assert_mon_prerequisites = sub {
-    my ($cfg, $monhash, $monid, $monip) = @_;
-
-    $monip = PVE::Network::canonical_ip($monip);
+    my ($cfg, $monhash, $monid, $monips) = @_;
 
     my $used_ips = {};
 
@@ -107,7 +136,10 @@ my $assert_mon_prerequisites = sub {
 	$used_ips->{$ip} = 1;
     }
 
-    die "monitor address '$monip' already in use\n" if $used_ips->{$monip};
+    for my $monip (@{$monips}) {
+	$monip = PVE::Network::canonical_ip($monip);
+	die "monitor address '$monip' already in use\n" if $used_ips->{$monip};
+    }
 
     if (defined($monhash->{$monid})) {
 	die "monitor '$monid' already exists\n";
@@ -246,9 +278,9 @@ __PACKAGE__->register_method ({
 		description => "The ID for the monitor, when omitted the same as the nodename",
 	    },
 	    'mon-address' => {
-		description => 'Overwrites autodetected monitor IP address. ' .
-		               'Must be in the public network of ceph.',
-		type => 'string', format => 'ip',
+		description => 'Overwrites autodetected monitor IP address(es). ' .
+		               'Must be in the public network(s) of Ceph.',
+		type => 'string', format => 'ip-list',
 		optional => 1,
 	    },
 	},
@@ -276,9 +308,9 @@ __PACKAGE__->register_method ({
 
 	my $monid = $param->{monid} // $param->{node};
 	my $monsection = "mon.$monid";
-	my $ip = $find_mon_ip->($cfg, $rados, $param->{node}, $param->{'mon-address'});
+	my $ips = $find_mon_ips->($cfg, $rados, $param->{node}, $param->{'mon-address'});
 
-	$assert_mon_prerequisites->($cfg, $monhash, $monid, $ip);
+	$assert_mon_prerequisites->($cfg, $monhash, $monid, $ips);
 
 	my $worker = sub  {
 	    my $upid = shift;
@@ -291,7 +323,7 @@ __PACKAGE__->register_method ({
 		    $rados = PVE::RADOS->new(timeout => PVE::Ceph::Tools::get_config('long_rados_timeout'));
 		}
 		$monhash = PVE::Ceph::Services::get_services_info('mon', $cfg, $rados);
-		$assert_mon_prerequisites->($cfg, $monhash, $monid, $ip);
+		$assert_mon_prerequisites->($cfg, $monhash, $monid, $ips);
 
 		my $client_keyring = PVE::Ceph::Tools::get_or_create_admin_keyring();
 		my $mon_keyring = PVE::Ceph::Tools::get_config('pve_mon_key_path');
@@ -329,22 +361,31 @@ __PACKAGE__->register_method ({
 		    run_command(['chown', 'ceph:ceph', $mondir]);
 
 		    my $is_first_address = !defined($rados);
-		    if (Net::IP::ip_is_ipv6($ip)) {
-			$cfg->{global}->{ms_bind_ipv6} = 'true';
-			$cfg->{global}->{ms_bind_ipv4} = 'false' if $is_first_address;
-		    } else {
-			$cfg->{global}->{ms_bind_ipv4} = 'true';
-			$cfg->{global}->{ms_bind_ipv6} = 'false' if $is_first_address;
-		    }
 
-		    my $monaddr = Net::IP::ip_is_ipv6($ip) ? "[$ip]" : $ip;
+		    my $monaddrs = [];
+
+		    for my $ip (@{$ips}) {
+			if (Net::IP::ip_is_ipv6($ip)) {
+			    $cfg->{global}->{ms_bind_ipv6} = 'true';
+			    $cfg->{global}->{ms_bind_ipv4} = 'false' if $is_first_address;
+			} else {
+			    $cfg->{global}->{ms_bind_ipv4} = 'true';
+			    $cfg->{global}->{ms_bind_ipv6} = 'false' if $is_first_address;
+			}
+
+			my $monaddr = Net::IP::ip_is_ipv6($ip) ? "[$ip]" : $ip;
+			push @{$monaddrs}, "v2:$monaddr:3300";
+			push @{$monaddrs}, "v1:$monaddr:6789";
+
+			$is_first_address = 0;
+		    }
 
 		    my $monmaptool_cmd = [
 			'monmaptool',
 			'--clobber',
 			'--addv',
 			$monid,
-			"[v2:$monaddr:3300,v1:$monaddr:6789]",
+			"[" . join(',', @{$monaddrs}) . "]",
 			'--print',
 			$monmap,
 		    ];
@@ -385,10 +426,10 @@ __PACKAGE__->register_method ({
 			$monhost .= " " . $monhash->{$mon}->{addr};
 		    }
 		}
-		$monhost .= " $ip";
+		$monhost .= " " . join(' ', @{$ips});
 		$cfg->{global}->{mon_host} = $monhost;
 		# The IP is needed in the ceph.conf for the first boot
-		$cfg->{$monsection}->{public_addr} = $ip;
+		$cfg->{$monsection}->{public_addr} = $ips->[0];
 
 		cfs_write_file('ceph.conf', $cfg);
 
@@ -482,12 +523,27 @@ __PACKAGE__->register_method ({
 		$monstat = $rados->mon_command({ prefix => 'quorum_status' });
 		$monlist = $monstat->{monmap}->{mons};
 
-		my $addr;
+		my $addrs = [];
+
+		my $add_addr = sub {
+		    my ($addr) = @_;
+
+		    # extract the ip without port and nonce (if present)
+		    ($addr) = $addr =~ m|^(.*):\d+(/\d+)?$|;
+		    ($addr) = $addr =~ m|^\[?(.*?)\]?$|; # remove brackets
+		    push @{$addrs}, $addr;
+		};
+
 		for my $mon (@$monlist) {
 		    if ($mon->{name} eq $monid) {
-			$addr = $mon->{public_addr} // $mon->{addr};
-			($addr) = $addr =~ m|^(.*):\d+/\d+$|; # extract the ip without port/nonce
-			($addr) = $addr =~ m|^\[?(.*?)\]?$|; # remove brackets
+			if ($mon->{public_addrs} && $mon->{public_addrs}->{addrvec}) {
+			    my $addrvec = $mon->{public_addrs}->{addrvec};
+			    for my $addr (@{$addrvec}) {
+				$add_addr->($addr->{addr});
+			    }
+			} else {
+			    $add_addr->($mon->{public_addr} // $mon->{addr});
+			}
 			last;
 		    }
 		}
@@ -502,18 +558,20 @@ __PACKAGE__->register_method ({
 
 		# delete from mon_host
 		if (my $monhost = $cfg->{global}->{mon_host}) {
-		    $monhost = $remove_addr_from_mon_host->($monhost, $addr);
+		    my $mon_host_ips = $ips_from_mon_host->($cfg->{global}->{mon_host});
 
-		    # also remove matching IPs that differ syntactically
-		    if (PVE::JSONSchema::pve_verify_ip($addr, 1)) {
-			$addr = PVE::Network::canonical_ip($addr);
+		    for my $addr (@{$addrs}) {
+			$monhost = $remove_addr_from_mon_host->($monhost, $addr);
 
-			my $mon_host_ips = $ips_from_mon_host->($cfg->{global}->{mon_host});
+			# also remove matching IPs that differ syntactically
+			if (PVE::JSONSchema::pve_verify_ip($addr, 1)) {
+			    $addr = PVE::Network::canonical_ip($addr);
 
-			for my $mon_host_ip (@{$mon_host_ips}) {
-			    # match canonical addresses, but remove as present in mon_host
-			    if (PVE::Network::canonical_ip($mon_host_ip) eq $addr) {
-				$monhost = $remove_addr_from_mon_host->($monhost, $mon_host_ip);
+			    for my $mon_host_ip (@{$mon_host_ips}) {
+				# match canonical addresses, but remove as present in mon_host
+				if (PVE::Network::canonical_ip($mon_host_ip) eq $addr) {
+				    $monhost = $remove_addr_from_mon_host->($monhost, $mon_host_ip);
+				}
 			    }
 			}
 		    }
-- 
2.20.1





  parent reply	other threads:[~2021-05-10 12:19 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-05-10 12:18 [pve-devel] [PATCH-SERIES v2 common/manager] " Fabian Ebner
2021-05-10 12:18 ` [pve-devel] [PATCH v2 common 01/14] network: is_ip_in_cidr: correctly handle the CIDR being a singleton range Fabian Ebner
2021-05-10 12:18 ` [pve-devel] [PATCH v2 common 02/14] network: is_ip_in_cidr: avoid warning when versions don't match Fabian Ebner
2021-05-10 12:18 ` [pve-devel] [PATCH v2 common 03/14] network: add canonical_ip function Fabian Ebner
2021-05-10 12:18 ` [pve-devel] [PATCH v2 common 04/14] network: add unique_ips function Fabian Ebner
2021-05-10 12:18 ` [pve-devel] [PATCH v2 manager 05/14] api: ceph: mon: split up arguments for run_command Fabian Ebner
2021-05-10 12:18 ` [pve-devel] [PATCH v2 manager 06/14] api: ceph: create mon: handle ms_bind_ipv* options more generally Fabian Ebner
2021-05-10 12:18 ` [pve-devel] [PATCH v2 manager 07/14] api: ceph: create mon: factor out monmaptool command Fabian Ebner
2021-05-10 12:18 ` [pve-devel] [PATCH v2 manager 08/14] api: ceph: create mon: explicitly add subsequent monitors to the monmap Fabian Ebner
2021-05-10 12:18 ` [pve-devel] [PATCH v2 manager 09/14] api: ceph: mon: fix handling of IPv6 addresses in find_mon_ip Fabian Ebner
2021-05-10 12:18 ` [pve-devel] [PATCH v2 manager 10/14] api: ceph: mon: add ips_from_mon_host helper Fabian Ebner
2021-05-10 12:18 ` [pve-devel] [PATCH v2 manager 11/14] api: ceph: mon: fix handling of IPv6 addresses in assert_mon_prerequisites Fabian Ebner
2021-05-10 12:18 ` [pve-devel] [PATCH v2 manager 12/14] api: ceph: mon: factor out mon_host regex address removal Fabian Ebner
2021-05-10 12:18 ` [pve-devel] [PATCH v2 manager 13/14] api: ceph: mon: fix handling of IPv6 addresses in destroymon Fabian Ebner
2021-05-10 12:18 ` Fabian Ebner [this message]
2021-06-17 13:21 ` [pve-devel] applied-partially: [PATCH-SERIES v2 common/manager] fix #2422: allow multiple Ceph public networks Thomas Lamprecht
2021-06-18 15:14 ` [pve-devel] " Thomas Lamprecht

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=20210510121826.8543-15-f.ebner@proxmox.com \
    --to=f.ebner@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