]> git.proxmox.com Git - pve-cluster.git/blame - data/PVE/Corosync.pm
corosync: use correct default resolving strategy
[pve-cluster.git] / data / PVE / Corosync.pm
CommitLineData
b6973a89
TL
1package PVE::Corosync;
2
3use strict;
4use warnings;
5
6use Digest::SHA;
496de919 7use Clone 'clone';
c393636b 8use Socket qw(AF_INET AF_INET6 inet_ntop);
b01bc84d 9use Net::IP qw(ip_is_ipv6);
b6973a89
TL
10
11use PVE::Cluster;
c393636b
SR
12use PVE::Tools;
13use PVE::Tools qw($IPV4RE $IPV6RE);
b6973a89
TL
14
15my $basedir = "/etc/pve";
16
496de919
TL
17my $conf_array_sections = {
18 node => 1,
19 interface => 1,
20};
21
b6973a89
TL
22# a very simply parser ...
23sub parse_conf {
24 my ($filename, $raw) = @_;
25
26 return {} if !$raw;
27
28 my $digest = Digest::SHA::sha1_hex(defined($raw) ? $raw : '');
29
30 $raw =~ s/#.*$//mg;
31 $raw =~ s/\r?\n/ /g;
32 $raw =~ s/\s+/ /g;
33 $raw =~ s/^\s+//;
34 $raw =~ s/\s*$//;
35
36 my @tokens = split(/\s/, $raw);
37
496de919 38 my $conf = { 'main' => {} };
b6973a89
TL
39
40 my $stack = [];
496de919 41 my $section = $conf->{main};
b6973a89
TL
42
43 while (defined(my $token = shift @tokens)) {
44 my $nexttok = $tokens[0];
45
46 if ($nexttok && ($nexttok eq '{')) {
47 shift @tokens; # skip '{'
496de919
TL
48 my $new_section = {};
49 if ($conf_array_sections->{$token}) {
50 $section->{$token} = [] if !defined($section->{$token});
51 push @{$section->{$token}}, $new_section;
52 } elsif (!defined($section->{$token})) {
53 $section->{$token} = $new_section;
54 } else {
55 die "section '$token' already exists and not marked as array!\n";
56 }
b6973a89
TL
57 push @$stack, $section;
58 $section = $new_section;
59 next;
60 }
61
62 if ($token eq '}') {
63 $section = pop @$stack;
64 die "parse error - uncexpected '}'\n" if !$section;
65 next;
66 }
67
68 my $key = $token;
69 die "missing ':' after key '$key'\n" if ! ($key =~ s/:$//);
70
71 die "parse error - no value for '$key'\n" if !defined($nexttok);
72 my $value = shift @tokens;
73
496de919 74 $section->{$key} = $value;
b6973a89
TL
75 }
76
e7ecad20
TL
77 # make working with the config way easier
78 my ($totem, $nodelist) = $conf->{main}->@{"totem", "nodelist"};
034a1b11
TL
79
80 $nodelist->{node} = {
81 map {
82 $_->{name} // $_->{ring0_addr} => $_
83 } @{$nodelist->{node}}
84 };
85 $totem->{interface} = {
86 map {
87 $_->{linknumber} // $_->{ringnumber} => $_
88 } @{$totem->{interface}}
89 };
e7ecad20 90
b6973a89
TL
91 $conf->{digest} = $digest;
92
93 return $conf;
94}
95
96my $dump_section;
97$dump_section = sub {
98 my ($section, $prefix) = @_;
99
496de919 100 my $raw = '';
b6973a89 101
496de919
TL
102 foreach my $k (sort keys %$section) {
103 my $v = $section->{$k};
104 if (ref($v) eq 'HASH') {
105 $raw .= $prefix . "$k {\n";
106 $raw .= &$dump_section($v, "$prefix ");
107 $raw .= $prefix . "}\n";
108 $raw .= "\n" if !$prefix; # add extra newline at 1st level only
109 } elsif (ref($v) eq 'ARRAY') {
110 foreach my $child (@$v) {
111 $raw .= $prefix . "$k {\n";
112 $raw .= &$dump_section($child, "$prefix ");
113 $raw .= $prefix . "}\n";
114 }
115 } elsif (!ref($v)) {
1b8c5ae7 116 die "got undefined value for key '$k'!\n" if !defined($v);
496de919
TL
117 $raw .= $prefix . "$k: $v\n";
118 } else {
119 die "unexpected reference in config hash: $k => ". ref($v) ."\n";
120 }
b6973a89
TL
121 }
122
b6973a89 123 return $raw;
b6973a89
TL
124};
125
126sub write_conf {
127 my ($filename, $conf) = @_;
128
e7ecad20
TL
129 my $c = clone($conf->{main}) // die "no main section";
130
131 # retransform back for easier dumping
132 my $hash_to_array = sub {
133 my ($hash) = @_;
134 return [ $hash->@{sort keys %$hash} ];
135 };
b6973a89 136
e7ecad20
TL
137 $c->{nodelist}->{node} = &$hash_to_array($c->{nodelist}->{node});
138 $c->{totem}->{interface} = &$hash_to_array($c->{totem}->{interface});
139
140 my $raw = &$dump_section($c, '');
b6973a89
TL
141
142 return $raw;
143}
144
2b28b160 145# read only - use atomic_write_conf method to write
b6973a89
TL
146PVE::Cluster::cfs_register_file('corosync.conf', \&parse_conf);
147# this is read/write
148PVE::Cluster::cfs_register_file('corosync.conf.new', \&parse_conf,
149 \&write_conf);
150
151sub check_conf_exists {
152 my ($silent) = @_;
153
154 $silent = $silent // 0;
155
156 my $exists = -f "$basedir/corosync.conf";
157
158 warn "Corosync config '$basedir/corosync.conf' does not exist - is this node part of a cluster?\n"
159 if !$silent && !$exists;
160
161 return $exists;
162}
163
164sub update_nodelist {
165 my ($conf, $nodelist) = @_;
166
e7ecad20 167 $conf->{main}->{nodelist}->{node} = $nodelist;
b6973a89 168
2b28b160 169 atomic_write_conf($conf);
b6973a89
TL
170}
171
172sub nodelist {
173 my ($conf) = @_;
e7ecad20 174 return clone($conf->{main}->{nodelist}->{node});
b6973a89
TL
175}
176
b6973a89
TL
177sub totem_config {
178 my ($conf) = @_;
e7ecad20 179 return clone($conf->{main}->{totem});
b6973a89
TL
180}
181
2b28b160
TL
182# caller must hold corosync.conf cfs lock if used in read-modify-write cycle
183sub atomic_write_conf {
184 my ($conf, $no_increase_version) = @_;
185
186 if (!$no_increase_version) {
187 die "invalid corosync config: unable to read config version\n"
188 if !defined($conf->{main}->{totem}->{config_version});
189 $conf->{main}->{totem}->{config_version}++;
190 }
191
192 PVE::Cluster::cfs_write_file("corosync.conf.new", $conf);
193
194 rename("/etc/pve/corosync.conf.new", "/etc/pve/corosync.conf")
195 || die "activating corosync.conf.new failed - $!\n";
196}
197
b01bc84d
TL
198# for creating a new cluster with the current node
199# params are those from the API/CLI cluster create call
200sub create_conf {
201 my ($nodename, %param) = @_;
202
203 my $clustername = $param{clustername};
204 my $nodeid = $param{nodeid} || 1;
205 my $votes = $param{votes} || 1;
206
207 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
208 my $ring0_addr = $param{ring0_addr} // $local_ip_address;
916c10b3 209 my $bindnet0_addr = $param{bindnet0_addr} // $ring0_addr;
b01bc84d
TL
210
211 my $use_ipv6 = ip_is_ipv6($ring0_addr);
212 die "ring 0 addresses must be from same IP family!\n"
213 if $use_ipv6 != ip_is_ipv6($bindnet0_addr);
214
215 my $conf = {
216 totem => {
217 version => 2, # protocol version
218 secauth => 'on',
219 cluster_name => $clustername,
220 config_version => 0,
221 ip_version => $use_ipv6 ? 'ipv6' : 'ipv4',
222 interface => {
223 0 => {
224 bindnetaddr => $bindnet0_addr,
225 ringnumber => 0,
226 },
227 },
228 },
229 nodelist => {
230 node => {
231 $nodename => {
232 name => $nodename,
233 nodeid => $nodeid,
234 quorum_votes => $votes,
235 ring0_addr => $ring0_addr,
236 },
237 },
238 },
239 quorum => {
240 provider => 'corosync_votequorum',
241 },
242 logging => {
243 to_syslog => 'yes',
244 debug => 'off',
245 },
246 };
247
248 die "Param bindnet1_addr set but ring1_addr not specified!\n"
249 if (defined($param{bindnet1_addr}) && !defined($param{ring1_addr}));
250
251 my $ring1_addr = $param{ring1_addr};
252 my $bindnet1_addr = $param{bindnet1_addr} // $param{ring1_addr};
253
254 if ($bindnet1_addr) {
255 die "ring 1 addresses must be from same IP family as ring 0!\n"
256 if $use_ipv6 != ip_is_ipv6($bindnet1_addr) ||
257 $use_ipv6 != ip_is_ipv6($ring1_addr);
258
259 $conf->{totem}->{interface}->{1} = {
260 bindnetaddr => $bindnet1_addr,
261 ringnumber => 1,
262 };
263 $conf->{totem}->{rrp_mode} = 'passive';
264 $conf->{nodelist}->{node}->{$nodename}->{ring1_addr} = $ring1_addr;
265 }
266
267 return { main => $conf };
268}
269
c393636b
SR
270sub for_all_corosync_addresses {
271 my ($corosync_conf, $ip_version, $func) = @_;
272
273 my $nodelist = nodelist($corosync_conf);
274 return if !defined($nodelist);
275
276 # iterate sorted to make rules deterministic (for change detection)
277 foreach my $node_name (sort keys %$nodelist) {
278 my $node_config = $nodelist->{$node_name};
279 foreach my $node_key (sort keys %$node_config) {
280 if ($node_key =~ /^(ring|link)\d+_addr$/) {
281 my $node_address = $node_config->{$node_key};
282
283 my($ip, $version) = resolve_hostname_like_corosync($node_address, $corosync_conf);
284 next if !defined($ip);
285 next if defined($version) && defined($ip_version) && $version != $ip_version;
286
287 $func->($node_name, $ip, $version, $node_key);
288 }
289 }
290 }
291}
292
293# NOTE: Corosync actually only resolves on startup or config change, but we
294# currently do not have an easy way to synchronize our behaviour to that.
295sub resolve_hostname_like_corosync {
296 my ($hostname, $corosync_conf) = @_;
297
298 my $corosync_strategy = $corosync_conf->{main}->{totem}->{ip_version};
a8db1d36
FG
299 # Corosync 2.x default
300 $corosync_strategy = lc ($corosync_strategy // "ipv4");
c393636b
SR
301
302 my $match_ip_and_version = sub {
303 my ($addr) = @_;
304
305 return undef if !defined($addr);
306
307 if ($addr =~ m/^$IPV4RE$/) {
308 return ($addr, 4);
309 } elsif ($addr =~ m/^$IPV6RE$/) {
310 return ($addr, 6);
311 }
312
313 return undef;
314 };
315
316 my ($resolved_ip, $ip_version) = $match_ip_and_version->($hostname);
317
318 return ($resolved_ip, $ip_version) if defined($resolved_ip);
319
320 my $resolved_ip4;
321 my $resolved_ip6;
322
323 my @resolved_raw;
324 eval { @resolved_raw = PVE::Tools::getaddrinfo_all($hostname); };
325
326 return undef if ($@ || !@resolved_raw);
327
328 foreach my $socket_info (@resolved_raw) {
329 next if !$socket_info->{addr};
330
331 my ($family, undef, $host) = PVE::Tools::unpack_sockaddr_in46($socket_info->{addr});
332
333 if ($family == AF_INET && !defined($resolved_ip4)) {
334 $resolved_ip4 = inet_ntop(AF_INET, $host);
335 } elsif ($family == AF_INET6 && !defined($resolved_ip6)) {
336 $resolved_ip6 = inet_ntop(AF_INET6, $host);
337 }
338
339 if ($corosync_strategy eq "any"
340 && ($family == AF_INET || $family == AF_INET6)) {
341 # "any" means return first one found by getaddrinfo
342 return $match_ip_and_version->($resolved_ip4 // $resolved_ip6);
343 }
344
345 last if defined($resolved_ip4) && defined($resolved_ip6);
346 }
347
348 # corosync_strategy specifies the which IP address family is resolved by
349 # corosync. We need to match that, to ensure we create firewall rules for
350 # the correct one.
351 if ($corosync_strategy eq "ipv4") {
352 $resolved_ip = $resolved_ip4;
353 } elsif ($corosync_strategy eq "ipv6") {
354 $resolved_ip = $resolved_ip6;
355 } elsif ($corosync_strategy eq "any") {
356 # shouldn't get here, but just in case
357 $resolved_ip = $resolved_ip4 // $resolved_ip6;
358 }
359
360 return $match_ip_and_version->($resolved_ip);
361}
362
b6973a89 3631;