use strict;
use warnings;
-use Digest::SHA;
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";
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. Lower value means higher priority.",
+ },
+};
+my $corosync_link_desc = {
+ type => 'string', format => $corosync_link_format,
+ description => "Address and priority information of a single corosync link.",
+ 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);
+}
+
# a very simply parser ...
sub parse_conf {
my ($filename, $raw) = @_;
# 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 { $_->{ringnumber} => $_ } @{$totem->{interface}} };
+
+ $nodelist->{node} = {
+ map {
+ $_->{name} // $_->{ring0_addr} => $_
+ } @{$nodelist->{node}}
+ };
+ $totem->{interface} = {
+ map {
+ $_->{linknumber} // $_->{ringnumber} => $_
+ } @{$totem->{interface}}
+ };
$conf->{digest} = $digest;
return $conf;
}
-my $dump_section;
-$dump_section = 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($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($child, "$prefix ");
- $raw .= $prefix . "}\n";
- }
- } elsif (!ref($v)) {
- $raw .= $prefix . "$k: $v\n";
- } else {
- die "unexpected reference in config hash: $k => ". ref($v) ."\n";
- }
- }
-
- return $raw;
-};
-
sub write_conf {
my ($filename, $conf) = @_;
$c->{nodelist}->{node} = &$hash_to_array($c->{nodelist}->{node});
$c->{totem}->{interface} = &$hash_to_array($c->{totem}->{interface});
- my $raw = &$dump_section($c, '');
+ 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;
}
my $votes = $param{votes} || 1;
my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
- my $ring0_addr = $param{ring0_addr} // $local_ip_address;
- my $bindnet0_addr = $param{bindnet0_addr} // $local_ip_address;
- my $use_ipv6 = ip_is_ipv6($ring0_addr);
- die "ring 0 addresses must be from same IP family!\n"
- if $use_ipv6 != ip_is_ipv6($bindnet0_addr);
+ my $link0 = PVE::Cluster::parse_corosync_link($param{link0});
+ $link0->{address} //= $local_ip_address;
my $conf = {
totem => {
secauth => 'on',
cluster_name => $clustername,
config_version => 0,
- ip_version => $use_ipv6 ? 'ipv6' : 'ipv4',
+ ip_version => 'ipv4-6',
interface => {
0 => {
- bindnetaddr => $bindnet0_addr,
- ringnumber => 0,
+ linknumber => 0,
},
},
},
name => $nodename,
nodeid => $nodeid,
quorum_votes => $votes,
- ring0_addr => $ring0_addr,
+ ring0_addr => $link0->{address},
},
},
},
debug => 'off',
},
};
+ my $totem = $conf->{totem};
- die "Param bindnet1_addr set but ring1_addr not specified!\n"
- if (defined($param{bindnet1_addr}) && !defined($param{ring1_addr}));
-
- my $ring1_addr = $param{ring1_addr};
- my $bindnet1_addr = $param{bindnet1_addr} // $param{ring1_addr};
-
- if ($bindnet1_addr) {
- die "ring 1 addresses must be from same IP family as ring 0!\n"
- if $use_ipv6 != ip_is_ipv6($bindnet1_addr) ||
- $use_ipv6 != ip_is_ipv6($ring1_addr);
+ $totem->{interface}->{0}->{knet_link_priority} = $link0->{priority}
+ if defined($link0->{priority});
+ my $link1 = PVE::Cluster::parse_corosync_link($param{link1});
+ if ($link1->{address}) {
$conf->{totem}->{interface}->{1} = {
- bindnetaddr => $bindnet1_addr,
- ringnumber => 1,
+ linknumber => 1,
};
- $conf->{totem}->{rrp_mode} = 'passive';
- $conf->{nodelist}->{node}->{$nodename}->{ring1_addr} = $ring1_addr;
+ $totem->{link_mode} = 'passive';
+ $totem->{interface}->{1}->{knet_link_priority} = $link1->{priority}
+ if defined($link1->{priority});
+ $conf->{nodelist}->{node}->{$nodename}->{ring1_addr} = $link1->{address};
}
return { main => $conf };
}
+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 =~ /^(ring|link)\d+_addr$/) {
+ 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;