]>
Commit | Line | Data |
---|---|---|
b6973a89 TL |
1 | package PVE::Corosync; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use Digest::SHA; | |
496de919 | 7 | use Clone 'clone'; |
c393636b | 8 | use Socket qw(AF_INET AF_INET6 inet_ntop); |
b01bc84d | 9 | use Net::IP qw(ip_is_ipv6); |
b6973a89 TL |
10 | |
11 | use PVE::Cluster; | |
c393636b SR |
12 | use PVE::Tools; |
13 | use PVE::Tools qw($IPV4RE $IPV6RE); | |
b6973a89 TL |
14 | |
15 | my $basedir = "/etc/pve"; | |
16 | ||
496de919 TL |
17 | my $conf_array_sections = { |
18 | node => 1, | |
19 | interface => 1, | |
20 | }; | |
21 | ||
b6973a89 TL |
22 | # a very simply parser ... |
23 | sub 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 | ||
96 | my $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 | ||
126 | sub 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 |
146 | PVE::Cluster::cfs_register_file('corosync.conf', \&parse_conf); |
147 | # this is read/write | |
148 | PVE::Cluster::cfs_register_file('corosync.conf.new', \&parse_conf, | |
149 | \&write_conf); | |
150 | ||
151 | sub 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 | ||
164 | sub 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 | ||
172 | sub nodelist { | |
173 | my ($conf) = @_; | |
e7ecad20 | 174 | return clone($conf->{main}->{nodelist}->{node}); |
b6973a89 TL |
175 | } |
176 | ||
b6973a89 TL |
177 | sub 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 |
183 | sub 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 | |
200 | sub 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 |
270 | sub 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. | |
295 | sub 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 | 363 | 1; |