]> git.proxmox.com Git - pve-cluster.git/blobdiff - data/PVE/API2/ClusterConfig.pm
Add cluster join API version check
[pve-cluster.git] / data / PVE / API2 / ClusterConfig.pm
index e05fd556e93789e7539855c6f1543bd3b3dc256b..205252fa06e52ffac39f6a901913aa1eed247202 100644 (file)
@@ -58,20 +58,43 @@ __PACKAGE__->register_method({
            { name => 'totem' },
            { name => 'join' },
            { name => 'qdevice' },
+           { name => 'apiversion' },
        ];
 
        return $result;
     }});
 
+__PACKAGE__->register_method ({
+    name => 'join_api_version',
+    path => 'apiversion',
+    method => 'GET',
+    description => "Return the version of the cluster join API available on this node.",
+    permissions => {
+       check => ['perm', '/', [ 'Sys.Audit' ]],
+    },
+    parameters => {
+       additionalProperties => 0,
+       properties => {},
+    },
+    returns => {
+       type => 'integer',
+       minimum => 0,
+       description => "Cluster Join API version, currently " . PVE::Cluster::Setup::JOIN_API_VERSION,
+    },
+    code => sub {
+       return PVE::Cluster::Setup::JOIN_API_VERSION;
+    }});
+
 __PACKAGE__->register_method ({
     name => 'create',
     path => '',
     method => 'POST',
     protected => 1,
-    description => "Generate new cluster configuration.",
+    description => "Generate new cluster configuration. If no links given,"
+                . " default to local IP address as link0.",
     parameters => {
        additionalProperties => 0,
-       properties => {
+       properties => PVE::Corosync::add_corosync_link_properties({
            clustername => {
                description => "The name of the cluster.",
                type => 'string', format => 'pve-node',
@@ -84,9 +107,7 @@ __PACKAGE__->register_method ({
                minimum => 1,
                optional => 1,
            },
-           link0 => get_standard_option('corosync-link'),
-           link1 => get_standard_option('corosync-link'),
-       },
+       }),
     },
     returns => { type => 'string' },
     code => sub {
@@ -110,7 +131,7 @@ __PACKAGE__->register_method ({
            my $nodename = PVE::INotify::nodename();
 
            # get the corosync basis config for the new cluster
-           my $config = PVE::Corosync::create_conf($nodename, %$param);
+           my $config = PVE::Corosync::create_conf($nodename, $param);
 
            print "Writing corosync config to /etc/pve/corosync.conf\n";
            PVE::Corosync::atomic_write_conf($config);
@@ -194,7 +215,7 @@ __PACKAGE__->register_method ({
     description => "Adds a node to the cluster configuration. This call is for internal use.",
     parameters => {
        additionalProperties => 0,
-       properties => {
+       properties => PVE::Corosync::add_corosync_link_properties({
            node => get_standard_option('pve-node'),
            nodeid => get_standard_option('corosync-nodeid'),
            votes => {
@@ -208,9 +229,18 @@ __PACKAGE__->register_method ({
                description => "Do not throw error if node already exists.",
                optional => 1,
            },
-           link0 => get_standard_option('corosync-link'),
-           link1 => get_standard_option('corosync-link'),
-       },
+           new_node_ip => {
+               type => 'string',
+               description => "IP Address of node to add. Used as fallback if no links are given.",
+               format => 'ip',
+               optional => 1,
+           },
+           apiversion => {
+               type => 'integer',
+               description => 'The JOIN_API_VERSION of the new node.',
+               optional => 1,
+           },
+       }),
     },
     returns => {
        type => "object",
@@ -232,6 +262,14 @@ __PACKAGE__->register_method ({
     code => sub {
        my ($param) = @_;
 
+       $param->{apiversion} //= 0;
+       if ($param->{apiversion} < (PVE::Cluster::Setup::JOIN_API_VERSION -
+           PVE::Cluster::Setup::JOIN_API_AGE_AS_CLUSTER)) {
+           die "unsupported old API version on joining node ($param->{apiversion},"
+               . " cluster node has " . PVE::Cluster::Setup::JOIN_API_VERSION
+               . "), please upgrade before joining\n";
+       }
+
        PVE::Cluster::check_cfs_quorum();
 
        my $vc_errors;
@@ -256,7 +294,7 @@ __PACKAGE__->register_method ({
                while (my ($k, $v) = each %$nodelist) {
                    next if $k eq $name; # allows re-adding a node if force is set
 
-                   for my $linknumber (0..1) {
+                   for my $linknumber (0..PVE::Corosync::MAX_LINK_INDEX) {
                        my $id = "ring${linknumber}_addr";
                        next if !defined($v->{$id});
 
@@ -266,20 +304,63 @@ __PACKAGE__->register_method ({
                }
            };
 
-           my $link0 = PVE::Corosync::parse_corosync_link($param->{link0});
-           my $link1 = PVE::Corosync::parse_corosync_link($param->{link1});
+           my $links = PVE::Corosync::extract_corosync_link_args($param);
+           foreach my $link (values %$links) {
+               $check_duplicate_addr->($link);
+           }
 
-           $check_duplicate_addr->($link0);
-           $check_duplicate_addr->($link1);
+           # Make sure that the newly added node has links compatible with the
+           # rest of the cluster. To start, extract all links that currently
+           # exist. Check any node, all have the same links here (because of
+           # verify_conf above).
+           my $node_options = (values %$nodelist)[0];
+           my $cluster_links = {};
+           foreach my $opt (keys %$node_options) {
+               my ($linktype, $linkid) = PVE::Corosync::parse_link_entry($opt);
+               next if !defined($linktype);
+               $cluster_links->{$linkid} = $node_options->{$opt};
+           }
 
-           # FIXME: handle all links (0-7), they're all independent now
-           $link0->{address} //= $name if exists($totem_cfg->{interface}->{0});
+           # in case no fallback IP was passed, but instead only a single link,
+           # treat it as fallback to allow link-IDs to be matched automatically
+           # FIXME: remove in 8.0 or when joining an old node not supporting
+           # new_node_ip becomes infeasible otherwise
+           my $legacy_fallback = 0;
+           if (!$param->{new_node_ip} && scalar(%$links) == 1 && $param->{apiversion} == 0) {
+               my $passed_link_id = (keys %$links)[0];
+               my $passed_link = delete $links->{$passed_link_id};
+               $param->{new_node_ip} = $passed_link->{address};
+               $legacy_fallback = 1;
+           }
 
-           die "corosync: using 'link1' parameter needs a interface with linknumber '1' configured!\n"
-               if $link1 && !defined($totem_cfg->{interface}->{1});
+           if (scalar(%$links)) {
+               # verify specified links exist and none are missing
+               for my $linknum (0..PVE::Corosync::MAX_LINK_INDEX) {
+                   my $have_cluster_link = defined($cluster_links->{$linknum});
+                   my $have_new_link = defined($links->{$linknum});
 
-           die "corosync: totem interface with linknumber 1 configured but 'link1' parameter not defined!\n"
-               if defined($totem_cfg->{interface}->{1}) && !defined($link1);
+                   die "corosync: link $linknum exists in cluster config but wasn't specified for new node\n"
+                       if $have_cluster_link && !$have_new_link;
+                   die "corosync: link $linknum specified for new node doesn't exist in cluster config\n"
+                       if !$have_cluster_link && $have_new_link;
+               }
+           } else {
+               # when called without any link parameters, fall back to
+               # new_node_ip, if all existing nodes only have a single link too
+               die "no links and no fallback ip (new_node_ip) given, cannot join cluster\n"
+                   if !$param->{new_node_ip};
+
+               my $cluster_link_count = scalar(%$cluster_links);
+               if ($cluster_link_count == 1) {
+                   my $linknum = (keys %$cluster_links)[0];
+                   $links->{$linknum} = { address => $param->{new_node_ip} };
+               } else {
+                   die "cluster has $cluster_link_count links, but only 1 given"
+                       if $legacy_fallback;
+                   die "no links given but multiple links found on other nodes,"
+                     . " fallback only supported on single-link clusters\n";
+               }
+           }
 
            if (defined(my $res = $nodelist->{$name})) {
                $param->{nodeid} = $res->{nodeid} if !$param->{nodeid};
@@ -317,13 +398,15 @@ __PACKAGE__->register_method ({
            warn $@ if $@;
 
            $nodelist->{$name} = {
-               ring0_addr => $link0->{address},
                nodeid => $param->{nodeid},
                name => $name,
            };
-           $nodelist->{$name}->{ring1_addr} = $link1->{address} if defined($link1);
            $nodelist->{$name}->{quorum_votes} = $param->{votes} if $param->{votes};
 
+           foreach my $link (keys %$links) {
+               $nodelist->{$name}->{"ring${link}_addr"} = $links->{$link}->{address};
+           }
+
            PVE::Cluster::log_msg('notice', 'root@pam', "adding node $name to cluster");
 
            PVE::Corosync::update_nodelist($conf, $nodelist);
@@ -505,10 +588,12 @@ __PACKAGE__->register_method ({
     path => 'join',
     method => 'POST',
     protected => 1,
-    description => "Joins this node into an existing cluster.",
+    description => "Joins this node into an existing cluster. If no links are"
+                . " given, default to IP resolved by node's hostname on single"
+                . " link (fallback fails for clusters with multiple links).",
     parameters => {
        additionalProperties => 0,
-       properties => {
+       properties => PVE::Corosync::add_corosync_link_properties({
            hostname => {
                type => 'string',
                description => "Hostname (or IP) of an existing cluster member."
@@ -525,17 +610,13 @@ __PACKAGE__->register_method ({
                description => "Do not throw error if node already exists.",
                optional => 1,
            },
-           link0 => get_standard_option('corosync-link', {
-               default => "IP resolved by node's hostname",
-           }),
-           link1 => get_standard_option('corosync-link'),
            fingerprint => get_standard_option('fingerprint-sha256'),
            password => {
                description => "Superuser (root) password of peer node.",
                type => 'string',
                maxLength => 128,
            },
-       },
+       }),
     },
     returns => { type => 'string' },
     code => sub {