Upstream proxmox-git/pve-cluster state of the touched files before a0a405c3b7

This commit is contained in:
upstream committed 2026-09-25 11:34:50 +02:00
commit ad7bbf925f
1 file changed
+537
+537
View File
@@ -0,0 +1,537 @@
package PVE::Corosync;
use strict;
use warnings;
use Clone 'clone';
use Digest::SHA;
use Net::IP qw(ip_is_ipv6);
use Scalar::Util qw(weaken);
use Socket qw(AF_INET AF_INET6 inet_ntop);
use PVE::Cluster;
use PVE::JSONSchema;
use PVE::Tools;
use PVE::Tools qw($IPV4RE $IPV6RE);
my $basedir = "/etc/pve";
our $link_addr_re = qw/^(ring|link)(\d+)_addr$/;
my $conf_array_sections = {
node => 1,
interface => 1,
};
my $corosync_link_format = {
address => {
default_key => 1,
type => 'string',
format => 'address',
format_description => 'IP',
description => "Hostname (or IP) of this corosync link address.",
},
priority => {
optional => 1,
type => 'integer',
minimum => 0,
maximum => 255,
default => 0,
description => "The priority for the link when knet is used in 'passive'"
. " mode (default). Lower value means higher priority. Only"
. " valid for cluster create, ignored on node add.",
},
};
my $corosync_link_desc = {
type => 'string',
format => $corosync_link_format,
description => "Address and priority information of a single corosync link."
. " (up to 8 links supported; link0..link7)",
optional => 1,
};
PVE::JSONSchema::register_standard_option("corosync-link", $corosync_link_desc);
sub parse_corosync_link {
my ($value) = @_;
return undef if !defined($value);
return PVE::JSONSchema::parse_property_string($corosync_link_format, $value);
}
sub print_corosync_link {
my ($link) = @_;
return undef if !defined($link);
return PVE::JSONSchema::print_property_string($link, $corosync_link_format);
}
use constant MAX_LINK_INDEX => 7;
sub add_corosync_link_properties {
my ($prop) = @_;
for my $lnum (0 .. MAX_LINK_INDEX) {
$prop->{"link$lnum"} = PVE::JSONSchema::get_standard_option("corosync-link");
}
return $prop;
}
sub extract_corosync_link_args {
my ($args) = @_;
my $links = {};
for my $lnum (0 .. MAX_LINK_INDEX) {
$links->{$lnum} = parse_corosync_link($args->{"link$lnum"})
if $args->{"link$lnum"};
}
return $links;
}
# a very simply parser ...
sub parse_conf {
my ($filename, $raw) = @_;
return {} if !$raw;
my $digest = Digest::SHA::sha1_hex(defined($raw) ? $raw : '');
$raw =~ s/#.*$//mg;
$raw =~ s/\r?\n/ /g;
$raw =~ s/\s+/ /g;
$raw =~ s/^\s+//;
$raw =~ s/\s*$//;
my @tokens = split(/\s/, $raw);
my $conf = { 'main' => {} };
my $stack = [];
my $section = $conf->{main};
while (defined(my $token = shift @tokens)) {
my $nexttok = $tokens[0];
if ($nexttok && ($nexttok eq '{')) {
shift @tokens; # skip '{'
my $new_section = {};
if ($conf_array_sections->{$token}) {
$section->{$token} = [] if !defined($section->{$token});
push @{ $section->{$token} }, $new_section;
} elsif (!defined($section->{$token})) {
$section->{$token} = $new_section;
} else {
die "section '$token' already exists and not marked as array!\n";
}
push @$stack, $section;
$section = $new_section;
next;
}
if ($token eq '}') {
$section = pop @$stack;
die "parse error - uncexpected '}'\n" if !$section;
next;
}
my $key = $token;
die "missing ':' after key '$key'\n" if !($key =~ s/:$//);
die "parse error - no value for '$key'\n" if !defined($nexttok);
my $value = shift @tokens;
$section->{$key} = $value;
}
# make working with the config way easier
my ($totem, $nodelist) = $conf->{main}->@{ "totem", "nodelist" };
$nodelist->{node} = {
map {
$_->{name} // $_->{ring0_addr} => $_
} @{ $nodelist->{node} }
};
$totem->{interface} = {
map {
$_->{linknumber} // $_->{ringnumber} => $_
} @{ $totem->{interface} }
};
$conf->{digest} = $digest;
return $conf;
}
sub write_conf {
my ($filename, $conf) = @_;
my $c = clone($conf->{main}) // die "no main section";
# retransform back for easier dumping
my $hash_to_array = sub {
my ($hash) = @_;
return [$hash->@{ sort keys %$hash }];
};
$c->{nodelist}->{node} = &$hash_to_array($c->{nodelist}->{node});
$c->{totem}->{interface} = &$hash_to_array($c->{totem}->{interface});
my $dump_section_weak;
$dump_section_weak = sub {
my ($section, $prefix) = @_;
my $raw = '';
foreach my $k (sort keys %$section) {
my $v = $section->{$k};
if (ref($v) eq 'HASH') {
$raw .= $prefix . "$k {\n";
$raw .= $dump_section_weak->($v, "$prefix ");
$raw .= $prefix . "}\n";
$raw .= "\n" if !$prefix; # add extra newline at 1st level only
} elsif (ref($v) eq 'ARRAY') {
foreach my $child (@$v) {
$raw .= $prefix . "$k {\n";
$raw .= $dump_section_weak->($child, "$prefix ");
$raw .= $prefix . "}\n";
}
} elsif (!ref($v)) {
die "got undefined value for key '$k'!\n" if !defined($v);
$raw .= $prefix . "$k: $v\n";
} else {
die "unexpected reference in config hash: $k => " . ref($v) . "\n";
}
}
return $raw;
};
my $dump_section = $dump_section_weak;
weaken($dump_section_weak);
my $raw = $dump_section->($c, '');
return $raw;
}
# read only - use atomic_write_conf method to write
PVE::Cluster::cfs_register_file('corosync.conf', \&parse_conf);
# this is read/write
PVE::Cluster::cfs_register_file('corosync.conf.new', \&parse_conf, \&write_conf);
sub check_conf_exists {
my ($noerr) = @_;
my $exists = -f "$basedir/corosync.conf";
die
"Error: Corosync config '$basedir/corosync.conf' does not exist - is this node part of a cluster?\n"
if !$noerr && !$exists;
return $exists;
}
sub update_nodelist {
my ($conf, $nodelist) = @_;
$conf->{main}->{nodelist}->{node} = $nodelist;
atomic_write_conf($conf);
}
sub nodelist {
my ($conf) = @_;
return clone($conf->{main}->{nodelist}->{node});
}
sub totem_config {
my ($conf) = @_;
return clone($conf->{main}->{totem});
}
# caller must hold corosync.conf cfs lock if used in read-modify-write cycle
sub atomic_write_conf {
my ($conf, $no_increase_version) = @_;
if (!$no_increase_version) {
die "invalid corosync config: unable to read config version\n"
if !defined($conf->{main}->{totem}->{config_version});
$conf->{main}->{totem}->{config_version}++;
}
PVE::Cluster::cfs_write_file("corosync.conf.new", $conf);
rename("/etc/pve/corosync.conf.new", "/etc/pve/corosync.conf")
|| die "activating corosync.conf.new failed - $!\n";
}
# for creating a new cluster with the current node
# params are those from the API/CLI cluster create call
sub create_conf {
my ($nodename, $param) = @_;
my $clustername = $param->{clustername};
my $nodeid = $param->{nodeid} || 1;
my $votes = $param->{votes} || 1;
my $token_coefficient = $param->{'token-coefficient'};
my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
my $links = extract_corosync_link_args($param);
# if no links given, fall back to local IP as link0
$links->{0} = { address => $local_ip_address }
if !%$links;
my $conf = {
totem => {
version => 2, # protocol version
secauth => 'on',
cluster_name => $clustername,
config_version => 0,
ip_version => 'ipv4-6',
link_mode => 'passive',
interface => {},
},
nodelist => {
node => {
$nodename => {
name => $nodename,
nodeid => $nodeid,
quorum_votes => $votes,
},
},
},
quorum => {
provider => 'corosync_votequorum',
},
logging => {
to_syslog => 'yes',
debug => 'off',
},
};
my $totem = $conf->{totem};
my $node = $conf->{nodelist}->{node}->{$nodename};
foreach my $lnum (keys %$links) {
my $link = $links->{$lnum};
$totem->{interface}->{$lnum} = { linknumber => $lnum };
my $prio = $link->{priority};
$totem->{interface}->{$lnum}->{knet_link_priority} = $prio if $prio;
$node->{"ring${lnum}_addr"} = $link->{address};
}
$totem->{token_coefficient} = $token_coefficient if defined($token_coefficient);
return { main => $conf };
}
# returns (\@errors, \@warnings) to the caller, does *not* 'die' or 'warn'
# verification was successful if \@errors is empty
sub verify_conf {
my ($conf) = @_;
my @errors = ();
my @warnings = ();
my $nodelist = nodelist($conf);
if (!$nodelist) {
push @errors, "no nodes found";
return (\@errors, \@warnings);
}
my $totem = $conf->{main}->{totem};
if (!$totem) {
push @errors, "no totem found";
return (\@errors, \@warnings);
}
if (
(!defined($totem->{secauth}) || $totem->{secauth} ne 'on')
&& (!defined($totem->{crypto_cipher}) || $totem->{crypto_cipher} eq 'none')
) {
push @warnings, "warning: authentication/encryption is not explicitly enabled"
. " (secauth / crypto_cipher / crypto_hash)";
}
my $interfaces = $totem->{interface};
my $verify_link_ip = sub {
my ($key, $link, $node) = @_;
my ($resolved_ip, undef) = resolve_hostname_like_corosync($link, $conf);
if (!defined($resolved_ip)) {
push @warnings,
"warning: unable to resolve $key '$link' for node '$node'"
. " to an IP address according to Corosync's resolve strategy -"
. " cluster could fail on restart!";
} elsif ($resolved_ip ne $link) {
push @warnings,
"warning: $key '$link' for node '$node' resolves to"
. " '$resolved_ip' - consider replacing it with the currently"
. " resolved IP address for stability";
}
};
# sort for output order stability
my @node_names = sort keys %$nodelist;
my $node_links = {};
foreach my $node (@node_names) {
my $options = $nodelist->{$node};
foreach my $opt (keys %$options) {
my ($linktype, $linkid) = parse_link_entry($opt);
next if !defined($linktype);
$node_links->{$node}->{$linkid} = {
name => "${linktype}${linkid}_addr",
addr => $options->{$opt},
};
}
}
if (%$interfaces) {
# if interfaces are defined, *all* links must have a matching interface
# definition, and vice versa
for my $link (0 .. MAX_LINK_INDEX) {
my $have_interface = defined($interfaces->{$link});
foreach my $node (@node_names) {
my $linkdef = $node_links->{$node}->{$link};
if (defined($linkdef)) {
$verify_link_ip->($linkdef->{name}, $linkdef->{addr}, $node);
if (!$have_interface) {
push @errors, "node '$node' has '$linkdef->{name}', but"
. " there is no interface number $link configured";
}
} else {
if ($have_interface) {
push @errors,
"node '$node' is missing address for" . "interface number $link";
}
}
}
}
} else {
# without interfaces, only check that links are consistent among nodes
for my $link (0 .. MAX_LINK_INDEX) {
my $nodes_with_link = {};
foreach my $node (@node_names) {
my $linkdef = $node_links->{$node}->{$link};
if (defined($linkdef)) {
$verify_link_ip->($linkdef->{name}, $linkdef->{addr}, $node);
$nodes_with_link->{$node} = 1;
}
}
if (%$nodes_with_link) {
foreach my $node (@node_names) {
if (!defined($nodes_with_link->{$node})) {
push @errors, "node '$node' is missing link $link,"
. " which is configured on other nodes";
}
}
}
}
}
return (\@errors, \@warnings);
}
# returns ($linktype, $linkid) with $linktype being 'ring' for now, and possibly
# 'link' with upcoming corosync versions
sub parse_link_entry {
my ($opt) = @_;
return (undef, undef) if $opt !~ $link_addr_re;
return ($1, $2);
}
sub for_all_corosync_addresses {
my ($corosync_conf, $ip_version, $func) = @_;
my $nodelist = nodelist($corosync_conf);
return if !defined($nodelist);
# iterate sorted to make rules deterministic (for change detection)
foreach my $node_name (sort keys %$nodelist) {
my $node_config = $nodelist->{$node_name};
foreach my $node_key (sort keys %$node_config) {
if ($node_key =~ $link_addr_re) {
my $node_address = $node_config->{$node_key};
my ($ip, $version) = resolve_hostname_like_corosync($node_address, $corosync_conf);
next if !defined($ip);
next if defined($version) && defined($ip_version) && $version != $ip_version;
$func->($node_name, $ip, $version, $node_key);
}
}
}
}
# NOTE: Corosync actually only resolves on startup or config change, but we
# currently do not have an easy way to synchronize our behaviour to that.
sub resolve_hostname_like_corosync {
my ($hostname, $corosync_conf) = @_;
my $corosync_strategy = $corosync_conf->{main}->{totem}->{ip_version};
$corosync_strategy = lc($corosync_strategy // "ipv6-4");
my $match_ip_and_version = sub {
my ($addr) = @_;
return undef if !defined($addr);
if ($addr =~ m/^$IPV4RE$/) {
return ($addr, 4);
} elsif ($addr =~ m/^$IPV6RE$/) {
return ($addr, 6);
}
return undef;
};
my ($resolved_ip, $ip_version) = $match_ip_and_version->($hostname);
return ($resolved_ip, $ip_version) if defined($resolved_ip);
my $resolved_ip4;
my $resolved_ip6;
my @resolved_raw;
eval { @resolved_raw = PVE::Tools::getaddrinfo_all($hostname); };
return undef if ($@ || !@resolved_raw);
foreach my $socket_info (@resolved_raw) {
next if !$socket_info->{addr};
my ($family, undef, $host) = PVE::Tools::unpack_sockaddr_in46($socket_info->{addr});
if ($family == AF_INET && !defined($resolved_ip4)) {
$resolved_ip4 = inet_ntop(AF_INET, $host);
} elsif ($family == AF_INET6 && !defined($resolved_ip6)) {
$resolved_ip6 = inet_ntop(AF_INET6, $host);
}
last if defined($resolved_ip4) && defined($resolved_ip6);
}
# corosync_strategy specifies the order in which IP addresses are resolved
# by corosync. We need to match that order, to ensure we create firewall
# rules for the correct address family.
if ($corosync_strategy eq "ipv4") {
$resolved_ip = $resolved_ip4;
} elsif ($corosync_strategy eq "ipv6") {
$resolved_ip = $resolved_ip6;
} elsif ($corosync_strategy eq "ipv6-4") {
$resolved_ip = $resolved_ip6 // $resolved_ip4;
} elsif ($corosync_strategy eq "ipv4-6") {
$resolved_ip = $resolved_ip4 // $resolved_ip6;
}
return $match_ip_and_version->($resolved_ip);
}
1;