1 package PVE
::API2
::ClusterConfig
;
10 use PVE
::RPCEnvironment
;
11 use PVE
::JSONSchema
qw(get_standard_option);
13 use PVE
::APIClient
::LWP
;
15 use PVE
::Cluster
::Setup
;
19 use base
qw(PVE::RESTHandler);
21 my $clusterconf = "/etc/pve/corosync.conf";
22 my $authfile = "/etc/corosync/authkey";
23 my $local_cluster_lock = "/var/lock/pvecm.lock";
27 description
=> "Node id for this node.",
31 PVE
::JSONSchema
::register_standard_option
("corosync-nodeid", $nodeid_desc);
33 __PACKAGE__-
>register_method({
37 description
=> "Directory index.",
39 check
=> ['perm', '/', [ 'Sys.Audit' ]],
42 additionalProperties
=> 0,
51 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
60 { name
=> 'qdevice' },
61 { name
=> 'apiversion' },
67 __PACKAGE__-
>register_method ({
68 name
=> 'join_api_version',
71 description
=> "Return the version of the cluster join API available on this node.",
73 check
=> ['perm', '/', [ 'Sys.Audit' ]],
76 additionalProperties
=> 0,
82 description
=> "Cluster Join API version, currently " . PVE
::Cluster
::Setup
::JOIN_API_VERSION
,
85 return PVE
::Cluster
::Setup
::JOIN_API_VERSION
;
88 __PACKAGE__-
>register_method ({
93 description
=> "Generate new cluster configuration. If no links given,"
94 . " default to local IP address as link0.",
96 additionalProperties
=> 0,
97 properties
=> PVE
::Corosync
::add_corosync_link_properties
({
99 description
=> "The name of the cluster.",
100 type
=> 'string', format
=> 'pve-node',
103 nodeid
=> get_standard_option
('corosync-nodeid'),
106 description
=> "Number of votes for this node.",
112 returns
=> { type
=> 'string' },
116 die "cluster config '$clusterconf' already exists\n" if -f
$clusterconf;
118 my $rpcenv = PVE
::RPCEnvironment
::get
();
119 my $authuser = $rpcenv->get_user();
123 PVE
::Cluster
::Setup
::setup_sshd_config
(1);
124 PVE
::Cluster
::Setup
::setup_rootsshconfig
();
125 PVE
::Cluster
::Setup
::setup_ssh_keys
();
127 PVE
::Tools
::run_command
(['/usr/sbin/corosync-keygen', '-lk', $authfile])
129 die "no authentication key available\n" if -f
!$authfile;
131 my $nodename = PVE
::INotify
::nodename
();
133 # get the corosync basis config for the new cluster
134 my $config = PVE
::Corosync
::create_conf
($nodename, $param);
136 print "Writing corosync config to /etc/pve/corosync.conf\n";
137 PVE
::Corosync
::atomic_write_conf
($config);
139 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
140 PVE
::Cluster
::Setup
::ssh_merge_keys
();
141 PVE
::Cluster
::Setup
::gen_pve_node_files
($nodename, $local_ip_address);
142 PVE
::Cluster
::Setup
::ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
144 print "Restart corosync and cluster filesystem\n";
145 PVE
::Tools
::run_command
('systemctl restart corosync pve-cluster');
149 PVE
::Tools
::lock_file
($local_cluster_lock, 10, $code);
153 return $rpcenv->fork_worker('clustercreate', $param->{clustername
}, $authuser, $worker);
156 __PACKAGE__-
>register_method({
160 description
=> "Corosync node list.",
162 check
=> ['perm', '/', [ 'Sys.Audit' ]],
165 additionalProperties
=> 0,
173 node
=> { type
=> 'string' },
176 links
=> [ { rel
=> 'child', href
=> "{node}" } ],
182 my $conf = PVE
::Cluster
::cfs_read_file
('corosync.conf');
183 my $nodelist = PVE
::Corosync
::nodelist
($conf);
185 return PVE
::RESTHandler
::hash_to_array
($nodelist, 'node');
188 # lock method to ensure local and cluster wide atomicity
189 # if we're a single node cluster just lock locally, we have no other cluster
190 # node which we could contend with, else also acquire a cluster wide lock
191 my $config_change_lock = sub {
194 PVE
::Tools
::lock_file
($local_cluster_lock, 10, sub {
195 PVE
::Cluster
::cfs_update
(1);
196 my $members = PVE
::Cluster
::get_members
();
197 if (scalar(keys %$members) > 1) {
198 my $res = PVE
::Cluster
::cfs_lock_file
('corosync.conf', 10, $code);
200 # cfs_lock_file only sets $@ but lock_file doesn't propagates $@ unless we die here
201 die $@ if defined($@);
210 __PACKAGE__-
>register_method ({
212 path
=> 'nodes/{node}',
215 description
=> "Adds a node to the cluster configuration. This call is for internal use.",
217 additionalProperties
=> 0,
218 properties
=> PVE
::Corosync
::add_corosync_link_properties
({
219 node
=> get_standard_option
('pve-node'),
220 nodeid
=> get_standard_option
('corosync-nodeid'),
223 description
=> "Number of votes for this node",
229 description
=> "Do not throw error if node already exists.",
234 description
=> "IP Address of node to add. Used as fallback if no links are given.",
240 description
=> 'The JOIN_API_VERSION of the new node.',
248 corosync_authkey
=> {
265 $param->{apiversion
} //= 0;
266 PVE
::Cluster
::Setup
::assert_node_can_join_our_version
($param->{apiversion
});
268 PVE
::Cluster
::check_cfs_quorum
();
274 my $conf = PVE
::Cluster
::cfs_read_file
("corosync.conf");
275 my $nodelist = PVE
::Corosync
::nodelist
($conf);
276 my $totem_cfg = PVE
::Corosync
::totem_config
($conf);
278 ($vc_errors, $vc_warnings) = PVE
::Corosync
::verify_conf
($conf);
279 die if scalar(@$vc_errors);
281 my $name = $param->{node
};
283 # ensure we do not reuse an address, that can crash the whole cluster!
284 my $check_duplicate_addr = sub {
286 return if !defined($link) || !defined($link->{address
});
287 my $addr = $link->{address
};
289 while (my ($k, $v) = each %$nodelist) {
290 next if $k eq $name; # allows re-adding a node if force is set
292 for my $linknumber (0..PVE
::Corosync
::MAX_LINK_INDEX
) {
293 my $id = "ring${linknumber}_addr";
294 next if !defined($v->{$id});
296 die "corosync: address '$addr' already used on link $id by node '$k'\n"
297 if $v->{$id} eq $addr;
302 my $links = PVE
::Corosync
::extract_corosync_link_args
($param);
303 foreach my $link (values %$links) {
304 $check_duplicate_addr->($link);
307 # Make sure that the newly added node has links compatible with the
308 # rest of the cluster. To start, extract all links that currently
309 # exist. Check any node, all have the same links here (because of
310 # verify_conf above).
311 my $node_options = (values %$nodelist)[0];
312 my $cluster_links = {};
313 foreach my $opt (keys %$node_options) {
314 my ($linktype, $linkid) = PVE
::Corosync
::parse_link_entry
($opt);
315 next if !defined($linktype);
316 $cluster_links->{$linkid} = $node_options->{$opt};
319 # in case no fallback IP was passed, but instead only a single link,
320 # treat it as fallback to allow link-IDs to be matched automatically
321 # FIXME: remove in 8.0 or when joining an old node not supporting
322 # new_node_ip becomes infeasible otherwise
323 my $legacy_fallback = 0;
324 if (!$param->{new_node_ip
} && scalar(%$links) == 1 && $param->{apiversion
} == 0) {
325 my $passed_link_id = (keys %$links)[0];
326 my $passed_link = delete $links->{$passed_link_id};
327 $param->{new_node_ip
} = $passed_link->{address
};
328 $legacy_fallback = 1;
331 if (scalar(%$links)) {
332 # verify specified links exist and none are missing
333 for my $linknum (0..PVE
::Corosync
::MAX_LINK_INDEX
) {
334 my $have_cluster_link = defined($cluster_links->{$linknum});
335 my $have_new_link = defined($links->{$linknum});
337 die "corosync: link $linknum exists in cluster config but wasn't specified for new node\n"
338 if $have_cluster_link && !$have_new_link;
339 die "corosync: link $linknum specified for new node doesn't exist in cluster config\n"
340 if !$have_cluster_link && $have_new_link;
343 # when called without any link parameters, fall back to
344 # new_node_ip, if all existing nodes only have a single link too
345 die "no links and no fallback ip (new_node_ip) given, cannot join cluster\n"
346 if !$param->{new_node_ip
};
348 my $cluster_link_count = scalar(%$cluster_links);
349 if ($cluster_link_count == 1) {
350 my $linknum = (keys %$cluster_links)[0];
351 $links->{$linknum} = { address
=> $param->{new_node_ip
} };
353 die "cluster has $cluster_link_count links, but only 1 given"
355 die "no links given but multiple links found on other nodes,"
356 . " fallback only supported on single-link clusters\n";
360 if (defined(my $res = $nodelist->{$name})) {
361 $param->{nodeid
} = $res->{nodeid
} if !$param->{nodeid
};
362 $param->{votes
} = $res->{quorum_votes
} if !defined($param->{votes
});
364 if ($res->{quorum_votes
} == $param->{votes
} &&
365 $res->{nodeid
} == $param->{nodeid
} && $param->{force
}) {
366 print "forcing overwrite of configured node '$name'\n";
368 die "can't add existing node '$name'\n";
370 } elsif (!$param->{nodeid
}) {
375 foreach my $v (values %$nodelist) {
376 if ($v->{nodeid
} eq $nodeid) {
385 $param->{nodeid
} = $nodeid;
388 $param->{votes
} = 1 if !defined($param->{votes
});
390 PVE
::Cluster
::Setup
::gen_local_dirs
($name);
392 eval { PVE
::Cluster
::Setup
::ssh_merge_keys
(); };
395 $nodelist->{$name} = {
396 nodeid
=> $param->{nodeid
},
399 $nodelist->{$name}->{quorum_votes
} = $param->{votes
} if $param->{votes
};
401 foreach my $link (keys %$links) {
402 $nodelist->{$name}->{"ring${link}_addr"} = $links->{$link}->{address
};
405 PVE
::Cluster
::log_msg
('notice', 'root@pam', "adding node $name to cluster");
407 PVE
::Corosync
::update_nodelist
($conf, $nodelist);
410 $config_change_lock->($code);
412 # If vc_errors is set, we died because of verify_conf.
413 # Raise this error, since it contains more information than just a single-line string.
414 if (defined($vc_errors) && scalar(@$vc_errors)) {
417 my ($type, @arr) = @_;
418 return if !scalar(@arr);
419 $err_hash->{"${type}$_"} = $arr[$_] for 0..$#arr;
422 $add_errs->("warning", @$vc_warnings);
423 $add_errs->("error", @$vc_errors);
425 PVE
::Exception
::raise
("invalid corosync.conf\n", errors
=> $err_hash);
431 corosync_authkey
=> PVE
::Tools
::file_get_contents
($authfile),
432 corosync_conf
=> PVE
::Tools
::file_get_contents
($clusterconf),
433 warnings
=> $vc_warnings,
440 __PACKAGE__-
>register_method ({
442 path
=> 'nodes/{node}',
445 description
=> "Removes a node from the cluster configuration.",
447 additionalProperties
=> 0,
449 node
=> get_standard_option
('pve-node'),
452 returns
=> { type
=> 'null' },
456 my $local_node = PVE
::INotify
::nodename
();
457 die "Cannot delete myself from cluster!\n" if $param->{node
} eq $local_node;
459 PVE
::Cluster
::check_cfs_quorum
();
462 my $conf = PVE
::Cluster
::cfs_read_file
("corosync.conf");
463 my $nodelist = PVE
::Corosync
::nodelist
($conf);
468 foreach my $tmp_node (keys %$nodelist) {
469 my $d = $nodelist->{$tmp_node};
470 my $ring0_addr = $d->{ring0_addr
};
471 my $ring1_addr = $d->{ring1_addr
};
472 if (($tmp_node eq $param->{node
}) ||
473 (defined($ring0_addr) && ($ring0_addr eq $param->{node
})) ||
474 (defined($ring1_addr) && ($ring1_addr eq $param->{node
}))) {
476 $nodeid = $d->{nodeid
};
481 die "Node/IP: $param->{node} is not a known host of the cluster.\n"
484 PVE
::Cluster
::log_msg
('notice', 'root@pam', "deleting node $node from cluster");
486 delete $nodelist->{$node};
488 # allowed to fail when node is already shut down!
490 PVE
::Tools
::run_command
(['corosync-cfgtool','-k', $nodeid])
494 PVE
::Corosync
::update_nodelist
($conf, $nodelist);
497 $config_change_lock->($code);
503 __PACKAGE__-
>register_method ({
507 check
=> ['perm', '/', [ 'Sys.Audit' ]],
510 description
=> "Get information needed to join this cluster over the connected node.",
512 additionalProperties
=> 0,
514 node
=> get_standard_option
('pve-node', {
515 description
=> "The node for which the joinee gets the nodeinfo. ",
516 default => "current connected node",
523 additionalProperties
=> 0,
529 additionalProperties
=> 1,
531 name
=> get_standard_option
('pve-node'),
532 nodeid
=> get_standard_option
('corosync-nodeid'),
533 ring0_addr
=> get_standard_option
('corosync-link'),
534 quorum_votes
=> { type
=> 'integer', minimum
=> 0 },
535 pve_addr
=> { type
=> 'string', format
=> 'ip' },
536 pve_fp
=> get_standard_option
('fingerprint-sha256'),
540 preferred_node
=> get_standard_option
('pve-node'),
541 totem
=> { type
=> 'object' },
542 config_digest
=> { type
=> 'string' },
548 my $nodename = $param->{node
} // PVE
::INotify
::nodename
();
550 PVE
::Cluster
::cfs_update
(1);
551 my $conf = PVE
::Cluster
::cfs_read_file
('corosync.conf');
553 # FIXME: just return undef or empty object in PVE 8.0 (check if manager can handle it!)
554 PVE
::Exception
::raise
(
555 'node is not in a cluster, no join info available!',
556 code
=> HTTP
::Status
::HTTP_FAILED_DEPENDENCY
,
557 ) if !($conf && $conf->{main
});
559 my $totem_cfg = $conf->{main
}->{totem
} // {};
560 my $nodelist = $conf->{main
}->{nodelist
}->{node
} // {};
561 my $corosync_config_digest = $conf->{digest
};
563 die "unknown node '$nodename'\n" if ! $nodelist->{$nodename};
565 foreach my $name (keys %$nodelist) {
566 my $node = $nodelist->{$name};
567 $node->{pve_fp
} = PVE
::Cluster
::get_node_fingerprint
($name);
568 $node->{pve_addr
} = scalar(PVE
::Cluster
::remote_node_ip
($name));
572 nodelist
=> [ values %$nodelist ],
573 preferred_node
=> $nodename,
575 config_digest
=> $corosync_config_digest,
581 __PACKAGE__-
>register_method ({
586 description
=> "Joins this node into an existing cluster. If no links are"
587 . " given, default to IP resolved by node's hostname on single"
588 . " link (fallback fails for clusters with multiple links).",
590 additionalProperties
=> 0,
591 properties
=> PVE
::Corosync
::add_corosync_link_properties
({
594 description
=> "Hostname (or IP) of an existing cluster member."
596 nodeid
=> get_standard_option
('corosync-nodeid'),
599 description
=> "Number of votes for this node",
605 description
=> "Do not throw error if node already exists.",
608 fingerprint
=> get_standard_option
('fingerprint-sha256'),
610 description
=> "Superuser (root) password of peer node.",
616 returns
=> { type
=> 'string' },
620 my $rpcenv = PVE
::RPCEnvironment
::get
();
621 my $authuser = $rpcenv->get_user();
625 PVE
::Tools
::lock_file
($local_cluster_lock, 10, \
&PVE
::Cluster
::Setup
::join, $param);
629 return $rpcenv->fork_worker('clusterjoin', undef, $authuser, $worker);
633 __PACKAGE__-
>register_method({
637 description
=> "Get corosync totem protocol settings.",
639 check
=> ['perm', '/', [ 'Sys.Audit' ]],
642 additionalProperties
=> 0,
653 my $conf = PVE
::Cluster
::cfs_read_file
('corosync.conf');
655 my $totem_cfg = $conf->{main
}->{totem
};
660 __PACKAGE__-
>register_method ({
665 description
=> 'Get QDevice status',
667 check
=> ['perm', '/', [ 'Sys.Audit' ]],
670 additionalProperties
=> 0,
680 my $socket_path = "/var/run/corosync-qdevice/corosync-qdevice.sock";
681 return $result if !-S
$socket_path;
683 my $qdevice_socket = IO
::Socket
::UNIX-
>new(
685 Peer
=> $socket_path,
688 print $qdevice_socket "status verbose\n";
692 "Last poll call" => 1,
698 while (my $line = <$qdevice_socket>) {
700 next if $line =~ /^\s/;
701 if ($line =~ /^(.*?)\s*:\s*(.*)$/) {
702 $result->{$1} = $2 if $qdevice_keys->{$1};
708 #TODO: possibly add setup and remove methods