8 use Net
::IP
qw(ip_is_ipv6);
9 use Scalar
::Util
qw(weaken);
10 use Socket
qw(AF_INET AF_INET6 inet_ntop);
15 use PVE
::Tools
qw($IPV4RE $IPV6RE);
17 my $basedir = "/etc/pve";
18 our $link_addr_re = qw
/^(ring|link)(\d+)_addr$/;
20 my $conf_array_sections = {
25 my $corosync_link_format = {
28 type
=> 'string', format
=> 'address',
29 format_description
=> 'IP',
30 description
=> "Hostname (or IP) of this corosync link address.",
38 description
=> "The priority for the link when knet is used in 'passive'"
39 . " mode (default). Lower value means higher priority. Only"
40 . " valid for cluster create, ignored on node add.",
43 my $corosync_link_desc = {
44 type
=> 'string', format
=> $corosync_link_format,
45 description
=> "Address and priority information of a single corosync link."
46 . " (up to 8 links supported; link0..link7)",
49 PVE
::JSONSchema
::register_standard_option
("corosync-link", $corosync_link_desc);
51 sub parse_corosync_link
{
54 return undef if !defined($value);
56 return PVE
::JSONSchema
::parse_property_string
($corosync_link_format, $value);
59 sub print_corosync_link
{
62 return undef if !defined($link);
64 return PVE
::JSONSchema
::print_property_string
($link, $corosync_link_format);
67 use constant MAX_LINK_INDEX
=> 7;
69 sub add_corosync_link_properties
{
72 for my $lnum (0..MAX_LINK_INDEX
) {
73 $prop->{"link$lnum"} = PVE
::JSONSchema
::get_standard_option
("corosync-link");
79 sub extract_corosync_link_args
{
83 for my $lnum (0..MAX_LINK_INDEX
) {
84 $links->{$lnum} = parse_corosync_link
($args->{"link$lnum"})
85 if $args->{"link$lnum"};
91 # a very simply parser ...
93 my ($filename, $raw) = @_;
97 my $digest = Digest
::SHA
::sha1_hex
(defined($raw) ?
$raw : '');
105 my @tokens = split(/\s/, $raw);
107 my $conf = { 'main' => {} };
110 my $section = $conf->{main
};
112 while (defined(my $token = shift @tokens)) {
113 my $nexttok = $tokens[0];
115 if ($nexttok && ($nexttok eq '{')) {
116 shift @tokens; # skip '{'
117 my $new_section = {};
118 if ($conf_array_sections->{$token}) {
119 $section->{$token} = [] if !defined($section->{$token});
120 push @{$section->{$token}}, $new_section;
121 } elsif (!defined($section->{$token})) {
122 $section->{$token} = $new_section;
124 die "section '$token' already exists and not marked as array!\n";
126 push @$stack, $section;
127 $section = $new_section;
132 $section = pop @$stack;
133 die "parse error - uncexpected '}'\n" if !$section;
138 die "missing ':' after key '$key'\n" if ! ($key =~ s/:$//);
140 die "parse error - no value for '$key'\n" if !defined($nexttok);
141 my $value = shift @tokens;
143 $section->{$key} = $value;
146 # make working with the config way easier
147 my ($totem, $nodelist) = $conf->{main
}->@{"totem", "nodelist"};
149 $nodelist->{node
} = {
151 $_->{name
} // $_->{ring0_addr
} => $_
152 } @{$nodelist->{node
}}
154 $totem->{interface
} = {
156 $_->{linknumber
} // $_->{ringnumber
} => $_
157 } @{$totem->{interface
}}
160 $conf->{digest
} = $digest;
166 my ($filename, $conf) = @_;
168 my $c = clone
($conf->{main
}) // die "no main section";
170 # retransform back for easier dumping
171 my $hash_to_array = sub {
173 return [ $hash->@{sort keys %$hash} ];
176 $c->{nodelist
}->{node
} = &$hash_to_array($c->{nodelist
}->{node
});
177 $c->{totem
}->{interface
} = &$hash_to_array($c->{totem
}->{interface
});
179 my $dump_section_weak;
180 $dump_section_weak = sub {
181 my ($section, $prefix) = @_;
185 foreach my $k (sort keys %$section) {
186 my $v = $section->{$k};
187 if (ref($v) eq 'HASH') {
188 $raw .= $prefix . "$k {\n";
189 $raw .= $dump_section_weak->($v, "$prefix ");
190 $raw .= $prefix . "}\n";
191 $raw .= "\n" if !$prefix; # add extra newline at 1st level only
192 } elsif (ref($v) eq 'ARRAY') {
193 foreach my $child (@$v) {
194 $raw .= $prefix . "$k {\n";
195 $raw .= $dump_section_weak->($child, "$prefix ");
196 $raw .= $prefix . "}\n";
199 die "got undefined value for key '$k'!\n" if !defined($v);
200 $raw .= $prefix . "$k: $v\n";
202 die "unexpected reference in config hash: $k => ". ref($v) ."\n";
208 my $dump_section = $dump_section_weak;
209 weaken
($dump_section_weak);
211 my $raw = $dump_section->($c, '');
216 # read only - use atomic_write_conf method to write
217 PVE
::Cluster
::cfs_register_file
('corosync.conf', \
&parse_conf
);
219 PVE
::Cluster
::cfs_register_file
('corosync.conf.new', \
&parse_conf
,
222 sub check_conf_exists
{
225 my $exists = -f
"$basedir/corosync.conf";
227 die "Error: Corosync config '$basedir/corosync.conf' does not exist - is this node part of a cluster?\n"
228 if !$noerr && !$exists;
233 sub update_nodelist
{
234 my ($conf, $nodelist) = @_;
236 $conf->{main
}->{nodelist
}->{node
} = $nodelist;
238 atomic_write_conf
($conf);
243 return clone
($conf->{main
}->{nodelist
}->{node
});
248 return clone
($conf->{main
}->{totem
});
251 # caller must hold corosync.conf cfs lock if used in read-modify-write cycle
252 sub atomic_write_conf
{
253 my ($conf, $no_increase_version) = @_;
255 if (!$no_increase_version) {
256 die "invalid corosync config: unable to read config version\n"
257 if !defined($conf->{main
}->{totem
}->{config_version
});
258 $conf->{main
}->{totem
}->{config_version
}++;
261 PVE
::Cluster
::cfs_write_file
("corosync.conf.new", $conf);
263 rename("/etc/pve/corosync.conf.new", "/etc/pve/corosync.conf")
264 || die "activating corosync.conf.new failed - $!\n";
267 # for creating a new cluster with the current node
268 # params are those from the API/CLI cluster create call
270 my ($nodename, $param) = @_;
272 my $clustername = $param->{clustername
};
273 my $nodeid = $param->{nodeid
} || 1;
274 my $votes = $param->{votes
} || 1;
276 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
278 my $links = extract_corosync_link_args
($param);
280 # if no links given, fall back to local IP as link0
281 $links->{0} = { address
=> $local_ip_address }
286 version
=> 2, # protocol version
288 cluster_name
=> $clustername,
290 ip_version
=> 'ipv4-6',
291 link_mode
=> 'passive',
299 quorum_votes
=> $votes,
304 provider
=> 'corosync_votequorum',
311 my $totem = $conf->{totem
};
312 my $node = $conf->{nodelist
}->{node
}->{$nodename};
314 foreach my $lnum (keys %$links) {
315 my $link = $links->{$lnum};
317 $totem->{interface
}->{$lnum} = { linknumber
=> $lnum };
319 my $prio = $link->{priority
};
320 $totem->{interface
}->{$lnum}->{knet_link_priority
} = $prio if $prio;
322 $node->{"ring${lnum}_addr"} = $link->{address
};
325 return { main
=> $conf };
328 # returns (\@errors, \@warnings) to the caller, does *not* 'die' or 'warn'
329 # verification was successful if \@errors is empty
336 my $nodelist = nodelist
($conf);
338 push @errors, "no nodes found";
339 return (\
@errors, \
@warnings);
342 my $totem = $conf->{main
}->{totem
};
344 push @errors, "no totem found";
345 return (\
@errors, \
@warnings);
348 if ((!defined($totem->{secauth
}) || $totem->{secauth
} ne 'on') &&
349 (!defined($totem->{crypto_cipher
}) || $totem->{crypto_cipher
} eq 'none')) {
350 push @warnings, "warning: authentication/encryption is not explicitly enabled"
351 . " (secauth / crypto_cipher / crypto_hash)";
354 my $interfaces = $totem->{interface
};
356 my $verify_link_ip = sub {
357 my ($key, $link, $node) = @_;
358 my ($resolved_ip, undef) = resolve_hostname_like_corosync
($link, $conf);
359 if (!defined($resolved_ip)) {
360 push @warnings, "warning: unable to resolve $key '$link' for node '$node'"
361 . " to an IP address according to Corosync's resolve strategy -"
362 . " cluster could fail on restart!";
363 } elsif ($resolved_ip ne $link) {
364 push @warnings, "warning: $key '$link' for node '$node' resolves to"
365 . " '$resolved_ip' - consider replacing it with the currently"
366 . " resolved IP address for stability";
370 # sort for output order stability
371 my @node_names = sort keys %$nodelist;
374 foreach my $node (@node_names) {
375 my $options = $nodelist->{$node};
376 foreach my $opt (keys %$options) {
377 my ($linktype, $linkid) = parse_link_entry
($opt);
378 next if !defined($linktype);
379 $node_links->{$node}->{$linkid} = {
380 name
=> "${linktype}${linkid}_addr",
381 addr
=> $options->{$opt},
387 # if interfaces are defined, *all* links must have a matching interface
388 # definition, and vice versa
389 for my $link (0..MAX_LINK_INDEX
) {
390 my $have_interface = defined($interfaces->{$link});
391 foreach my $node (@node_names) {
392 my $linkdef = $node_links->{$node}->{$link};
393 if (defined($linkdef)) {
394 $verify_link_ip->($linkdef->{name
}, $linkdef->{addr
}, $node);
395 if (!$have_interface) {
396 push @errors, "node '$node' has '$linkdef->{name}', but"
397 . " there is no interface number $link configured";
400 if ($have_interface) {
401 push @errors, "node '$node' is missing address for"
402 . "interface number $link";
408 # without interfaces, only check that links are consistent among nodes
409 for my $link (0..MAX_LINK_INDEX
) {
410 my $nodes_with_link = {};
411 foreach my $node (@node_names) {
412 my $linkdef = $node_links->{$node}->{$link};
413 if (defined($linkdef)) {
414 $verify_link_ip->($linkdef->{name
}, $linkdef->{addr
}, $node);
415 $nodes_with_link->{$node} = 1;
419 if (%$nodes_with_link) {
420 foreach my $node (@node_names) {
421 if (!defined($nodes_with_link->{$node})) {
422 push @errors, "node '$node' is missing link $link,"
423 . " which is configured on other nodes";
430 return (\
@errors, \
@warnings);
433 # returns ($linktype, $linkid) with $linktype being 'ring' for now, and possibly
434 # 'link' with upcoming corosync versions
435 sub parse_link_entry
{
437 return (undef, undef) if $opt !~ $link_addr_re;
441 sub for_all_corosync_addresses
{
442 my ($corosync_conf, $ip_version, $func) = @_;
444 my $nodelist = nodelist
($corosync_conf);
445 return if !defined($nodelist);
447 # iterate sorted to make rules deterministic (for change detection)
448 foreach my $node_name (sort keys %$nodelist) {
449 my $node_config = $nodelist->{$node_name};
450 foreach my $node_key (sort keys %$node_config) {
451 if ($node_key =~ $link_addr_re) {
452 my $node_address = $node_config->{$node_key};
454 my($ip, $version) = resolve_hostname_like_corosync
($node_address, $corosync_conf);
455 next if !defined($ip);
456 next if defined($version) && defined($ip_version) && $version != $ip_version;
458 $func->($node_name, $ip, $version, $node_key);
464 # NOTE: Corosync actually only resolves on startup or config change, but we
465 # currently do not have an easy way to synchronize our behaviour to that.
466 sub resolve_hostname_like_corosync
{
467 my ($hostname, $corosync_conf) = @_;
469 my $corosync_strategy = $corosync_conf->{main
}->{totem
}->{ip_version
};
470 $corosync_strategy = lc ($corosync_strategy // "ipv6-4");
472 my $match_ip_and_version = sub {
475 return undef if !defined($addr);
477 if ($addr =~ m/^$IPV4RE$/) {
479 } elsif ($addr =~ m/^$IPV6RE$/) {
486 my ($resolved_ip, $ip_version) = $match_ip_and_version->($hostname);
488 return ($resolved_ip, $ip_version) if defined($resolved_ip);
494 eval { @resolved_raw = PVE
::Tools
::getaddrinfo_all
($hostname); };
496 return undef if ($@ || !@resolved_raw);
498 foreach my $socket_info (@resolved_raw) {
499 next if !$socket_info->{addr
};
501 my ($family, undef, $host) = PVE
::Tools
::unpack_sockaddr_in46
($socket_info->{addr
});
503 if ($family == AF_INET
&& !defined($resolved_ip4)) {
504 $resolved_ip4 = inet_ntop
(AF_INET
, $host);
505 } elsif ($family == AF_INET6
&& !defined($resolved_ip6)) {
506 $resolved_ip6 = inet_ntop
(AF_INET6
, $host);
509 last if defined($resolved_ip4) && defined($resolved_ip6);
512 # corosync_strategy specifies the order in which IP addresses are resolved
513 # by corosync. We need to match that order, to ensure we create firewall
514 # rules for the correct address family.
515 if ($corosync_strategy eq "ipv4") {
516 $resolved_ip = $resolved_ip4;
517 } elsif ($corosync_strategy eq "ipv6") {
518 $resolved_ip = $resolved_ip6;
519 } elsif ($corosync_strategy eq "ipv6-4") {
520 $resolved_ip = $resolved_ip6 // $resolved_ip4;
521 } elsif ($corosync_strategy eq "ipv4-6") {
522 $resolved_ip = $resolved_ip4 // $resolved_ip6;
525 return $match_ip_and_version->($resolved_ip);