8 use Socket
qw(AF_INET AF_INET6 inet_ntop);
9 use Net
::IP
qw(ip_is_ipv6);
14 use PVE
::Tools
qw($IPV4RE $IPV6RE);
16 my $basedir = "/etc/pve";
18 my $conf_array_sections = {
23 my $corosync_link_format = {
26 type
=> 'string', format
=> 'address',
27 format_description
=> 'IP',
28 description
=> "Hostname (or IP) of this corosync link address.",
36 description
=> "The priority for the link when knet is used in 'passive' mode. Lower value means higher priority.",
39 my $corosync_link_desc = {
40 type
=> 'string', format
=> $corosync_link_format,
41 description
=> "Address and priority information of a single corosync link.",
44 PVE
::JSONSchema
::register_standard_option
("corosync-link", $corosync_link_desc);
46 sub parse_corosync_link
{
49 return undef if !defined($value);
51 return PVE
::JSONSchema
::parse_property_string
($corosync_link_format, $value);
54 # a very simply parser ...
56 my ($filename, $raw) = @_;
60 my $digest = Digest
::SHA
::sha1_hex
(defined($raw) ?
$raw : '');
68 my @tokens = split(/\s/, $raw);
70 my $conf = { 'main' => {} };
73 my $section = $conf->{main
};
75 while (defined(my $token = shift @tokens)) {
76 my $nexttok = $tokens[0];
78 if ($nexttok && ($nexttok eq '{')) {
79 shift @tokens; # skip '{'
81 if ($conf_array_sections->{$token}) {
82 $section->{$token} = [] if !defined($section->{$token});
83 push @{$section->{$token}}, $new_section;
84 } elsif (!defined($section->{$token})) {
85 $section->{$token} = $new_section;
87 die "section '$token' already exists and not marked as array!\n";
89 push @$stack, $section;
90 $section = $new_section;
95 $section = pop @$stack;
96 die "parse error - uncexpected '}'\n" if !$section;
101 die "missing ':' after key '$key'\n" if ! ($key =~ s/:$//);
103 die "parse error - no value for '$key'\n" if !defined($nexttok);
104 my $value = shift @tokens;
106 $section->{$key} = $value;
109 # make working with the config way easier
110 my ($totem, $nodelist) = $conf->{main
}->@{"totem", "nodelist"};
112 $nodelist->{node
} = {
114 $_->{name
} // $_->{ring0_addr
} => $_
115 } @{$nodelist->{node
}}
117 $totem->{interface
} = {
119 $_->{linknumber
} // $_->{ringnumber
} => $_
120 } @{$totem->{interface
}}
123 $conf->{digest
} = $digest;
129 $dump_section = sub {
130 my ($section, $prefix) = @_;
134 foreach my $k (sort keys %$section) {
135 my $v = $section->{$k};
136 if (ref($v) eq 'HASH') {
137 $raw .= $prefix . "$k {\n";
138 $raw .= &$dump_section($v, "$prefix ");
139 $raw .= $prefix . "}\n";
140 $raw .= "\n" if !$prefix; # add extra newline at 1st level only
141 } elsif (ref($v) eq 'ARRAY') {
142 foreach my $child (@$v) {
143 $raw .= $prefix . "$k {\n";
144 $raw .= &$dump_section($child, "$prefix ");
145 $raw .= $prefix . "}\n";
148 die "got undefined value for key '$k'!\n" if !defined($v);
149 $raw .= $prefix . "$k: $v\n";
151 die "unexpected reference in config hash: $k => ". ref($v) ."\n";
159 my ($filename, $conf) = @_;
161 my $c = clone
($conf->{main
}) // die "no main section";
163 # retransform back for easier dumping
164 my $hash_to_array = sub {
166 return [ $hash->@{sort keys %$hash} ];
169 $c->{nodelist
}->{node
} = &$hash_to_array($c->{nodelist
}->{node
});
170 $c->{totem
}->{interface
} = &$hash_to_array($c->{totem
}->{interface
});
172 my $raw = &$dump_section($c, '');
177 # read only - use atomic_write_conf method to write
178 PVE
::Cluster
::cfs_register_file
('corosync.conf', \
&parse_conf
);
180 PVE
::Cluster
::cfs_register_file
('corosync.conf.new', \
&parse_conf
,
183 sub check_conf_exists
{
186 $silent = $silent // 0;
188 my $exists = -f
"$basedir/corosync.conf";
190 warn "Corosync config '$basedir/corosync.conf' does not exist - is this node part of a cluster?\n"
191 if !$silent && !$exists;
196 sub update_nodelist
{
197 my ($conf, $nodelist) = @_;
199 $conf->{main
}->{nodelist
}->{node
} = $nodelist;
201 atomic_write_conf
($conf);
206 return clone
($conf->{main
}->{nodelist
}->{node
});
211 return clone
($conf->{main
}->{totem
});
214 # caller must hold corosync.conf cfs lock if used in read-modify-write cycle
215 sub atomic_write_conf
{
216 my ($conf, $no_increase_version) = @_;
218 if (!$no_increase_version) {
219 die "invalid corosync config: unable to read config version\n"
220 if !defined($conf->{main
}->{totem
}->{config_version
});
221 $conf->{main
}->{totem
}->{config_version
}++;
224 PVE
::Cluster
::cfs_write_file
("corosync.conf.new", $conf);
226 rename("/etc/pve/corosync.conf.new", "/etc/pve/corosync.conf")
227 || die "activating corosync.conf.new failed - $!\n";
230 # for creating a new cluster with the current node
231 # params are those from the API/CLI cluster create call
233 my ($nodename, %param) = @_;
235 my $clustername = $param{clustername
};
236 my $nodeid = $param{nodeid
} || 1;
237 my $votes = $param{votes
} || 1;
239 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
241 my $link0 = PVE
::Cluster
::parse_corosync_link
($param{link0
});
242 $link0->{address
} //= $local_ip_address;
246 version
=> 2, # protocol version
248 cluster_name
=> $clustername,
250 ip_version
=> 'ipv4-6',
262 quorum_votes
=> $votes,
263 ring0_addr
=> $link0->{address
},
268 provider
=> 'corosync_votequorum',
275 my $totem = $conf->{totem
};
277 $totem->{interface
}->{0}->{knet_link_priority
} = $link0->{priority
}
278 if defined($link0->{priority
});
280 my $link1 = PVE
::Cluster
::parse_corosync_link
($param{link1
});
281 if ($link1->{address
}) {
282 $conf->{totem
}->{interface
}->{1} = {
285 $totem->{link_mode
} = 'passive';
286 $totem->{interface
}->{1}->{knet_link_priority
} = $link1->{priority
}
287 if defined($link1->{priority
});
288 $conf->{nodelist
}->{node
}->{$nodename}->{ring1_addr
} = $link1->{address
};
291 return { main
=> $conf };
294 sub for_all_corosync_addresses
{
295 my ($corosync_conf, $ip_version, $func) = @_;
297 my $nodelist = nodelist
($corosync_conf);
298 return if !defined($nodelist);
300 # iterate sorted to make rules deterministic (for change detection)
301 foreach my $node_name (sort keys %$nodelist) {
302 my $node_config = $nodelist->{$node_name};
303 foreach my $node_key (sort keys %$node_config) {
304 if ($node_key =~ /^(ring|link)\d+_addr$/) {
305 my $node_address = $node_config->{$node_key};
307 my($ip, $version) = resolve_hostname_like_corosync
($node_address, $corosync_conf);
308 next if !defined($ip);
309 next if defined($version) && defined($ip_version) && $version != $ip_version;
311 $func->($node_name, $ip, $version, $node_key);
317 # NOTE: Corosync actually only resolves on startup or config change, but we
318 # currently do not have an easy way to synchronize our behaviour to that.
319 sub resolve_hostname_like_corosync
{
320 my ($hostname, $corosync_conf) = @_;
322 my $corosync_strategy = $corosync_conf->{main
}->{totem
}->{ip_version
};
323 $corosync_strategy = lc ($corosync_strategy // "ipv6-4");
325 my $match_ip_and_version = sub {
328 return undef if !defined($addr);
330 if ($addr =~ m/^$IPV4RE$/) {
332 } elsif ($addr =~ m/^$IPV6RE$/) {
339 my ($resolved_ip, $ip_version) = $match_ip_and_version->($hostname);
341 return ($resolved_ip, $ip_version) if defined($resolved_ip);
347 eval { @resolved_raw = PVE
::Tools
::getaddrinfo_all
($hostname); };
349 return undef if ($@ || !@resolved_raw);
351 foreach my $socket_info (@resolved_raw) {
352 next if !$socket_info->{addr
};
354 my ($family, undef, $host) = PVE
::Tools
::unpack_sockaddr_in46
($socket_info->{addr
});
356 if ($family == AF_INET
&& !defined($resolved_ip4)) {
357 $resolved_ip4 = inet_ntop
(AF_INET
, $host);
358 } elsif ($family == AF_INET6
&& !defined($resolved_ip6)) {
359 $resolved_ip6 = inet_ntop
(AF_INET6
, $host);
362 last if defined($resolved_ip4) && defined($resolved_ip6);
365 # corosync_strategy specifies the order in which IP addresses are resolved
366 # by corosync. We need to match that order, to ensure we create firewall
367 # rules for the correct address family.
368 if ($corosync_strategy eq "ipv4") {
369 $resolved_ip = $resolved_ip4;
370 } elsif ($corosync_strategy eq "ipv6") {
371 $resolved_ip = $resolved_ip6;
372 } elsif ($corosync_strategy eq "ipv6-4") {
373 $resolved_ip = $resolved_ip6 // $resolved_ip4;
374 } elsif ($corosync_strategy eq "ipv4-6") {
375 $resolved_ip = $resolved_ip4 // $resolved_ip6;
378 return $match_ip_and_version->($resolved_ip);