]> git.proxmox.com Git - pve-cluster.git/blob - data/PVE/Corosync.pm
corosync: config: write: die if we get a undefined value
[pve-cluster.git] / data / PVE / Corosync.pm
1 package PVE::Corosync;
2
3 use strict;
4 use warnings;
5
6 use Digest::SHA;
7 use Clone 'clone';
8 use Net::IP qw(ip_is_ipv6);
9
10 use PVE::Cluster;
11
12 my $basedir = "/etc/pve";
13
14 my $conf_array_sections = {
15 node => 1,
16 interface => 1,
17 };
18
19 # a very simply parser ...
20 sub parse_conf {
21 my ($filename, $raw) = @_;
22
23 return {} if !$raw;
24
25 my $digest = Digest::SHA::sha1_hex(defined($raw) ? $raw : '');
26
27 $raw =~ s/#.*$//mg;
28 $raw =~ s/\r?\n/ /g;
29 $raw =~ s/\s+/ /g;
30 $raw =~ s/^\s+//;
31 $raw =~ s/\s*$//;
32
33 my @tokens = split(/\s/, $raw);
34
35 my $conf = { 'main' => {} };
36
37 my $stack = [];
38 my $section = $conf->{main};
39
40 while (defined(my $token = shift @tokens)) {
41 my $nexttok = $tokens[0];
42
43 if ($nexttok && ($nexttok eq '{')) {
44 shift @tokens; # skip '{'
45 my $new_section = {};
46 if ($conf_array_sections->{$token}) {
47 $section->{$token} = [] if !defined($section->{$token});
48 push @{$section->{$token}}, $new_section;
49 } elsif (!defined($section->{$token})) {
50 $section->{$token} = $new_section;
51 } else {
52 die "section '$token' already exists and not marked as array!\n";
53 }
54 push @$stack, $section;
55 $section = $new_section;
56 next;
57 }
58
59 if ($token eq '}') {
60 $section = pop @$stack;
61 die "parse error - uncexpected '}'\n" if !$section;
62 next;
63 }
64
65 my $key = $token;
66 die "missing ':' after key '$key'\n" if ! ($key =~ s/:$//);
67
68 die "parse error - no value for '$key'\n" if !defined($nexttok);
69 my $value = shift @tokens;
70
71 $section->{$key} = $value;
72 }
73
74 # make working with the config way easier
75 my ($totem, $nodelist) = $conf->{main}->@{"totem", "nodelist"};
76 $nodelist->{node} = { map { $_->{name} // $_->{ring0_addr} => $_ } @{$nodelist->{node}} };
77 $totem->{interface} = { map { $_->{ringnumber} => $_ } @{$totem->{interface}} };
78
79 $conf->{digest} = $digest;
80
81 return $conf;
82 }
83
84 my $dump_section;
85 $dump_section = sub {
86 my ($section, $prefix) = @_;
87
88 my $raw = '';
89
90 foreach my $k (sort keys %$section) {
91 my $v = $section->{$k};
92 if (ref($v) eq 'HASH') {
93 $raw .= $prefix . "$k {\n";
94 $raw .= &$dump_section($v, "$prefix ");
95 $raw .= $prefix . "}\n";
96 $raw .= "\n" if !$prefix; # add extra newline at 1st level only
97 } elsif (ref($v) eq 'ARRAY') {
98 foreach my $child (@$v) {
99 $raw .= $prefix . "$k {\n";
100 $raw .= &$dump_section($child, "$prefix ");
101 $raw .= $prefix . "}\n";
102 }
103 } elsif (!ref($v)) {
104 die "got undefined value for key '$k'!\n" if !defined($v);
105 $raw .= $prefix . "$k: $v\n";
106 } else {
107 die "unexpected reference in config hash: $k => ". ref($v) ."\n";
108 }
109 }
110
111 return $raw;
112 };
113
114 sub write_conf {
115 my ($filename, $conf) = @_;
116
117 my $c = clone($conf->{main}) // die "no main section";
118
119 # retransform back for easier dumping
120 my $hash_to_array = sub {
121 my ($hash) = @_;
122 return [ $hash->@{sort keys %$hash} ];
123 };
124
125 $c->{nodelist}->{node} = &$hash_to_array($c->{nodelist}->{node});
126 $c->{totem}->{interface} = &$hash_to_array($c->{totem}->{interface});
127
128 my $raw = &$dump_section($c, '');
129
130 return $raw;
131 }
132
133 # read only - use atomic_write_conf method to write
134 PVE::Cluster::cfs_register_file('corosync.conf', \&parse_conf);
135 # this is read/write
136 PVE::Cluster::cfs_register_file('corosync.conf.new', \&parse_conf,
137 \&write_conf);
138
139 sub check_conf_exists {
140 my ($silent) = @_;
141
142 $silent = $silent // 0;
143
144 my $exists = -f "$basedir/corosync.conf";
145
146 warn "Corosync config '$basedir/corosync.conf' does not exist - is this node part of a cluster?\n"
147 if !$silent && !$exists;
148
149 return $exists;
150 }
151
152 sub update_nodelist {
153 my ($conf, $nodelist) = @_;
154
155 $conf->{main}->{nodelist}->{node} = $nodelist;
156
157 atomic_write_conf($conf);
158 }
159
160 sub nodelist {
161 my ($conf) = @_;
162 return clone($conf->{main}->{nodelist}->{node});
163 }
164
165 sub totem_config {
166 my ($conf) = @_;
167 return clone($conf->{main}->{totem});
168 }
169
170 # caller must hold corosync.conf cfs lock if used in read-modify-write cycle
171 sub atomic_write_conf {
172 my ($conf, $no_increase_version) = @_;
173
174 if (!$no_increase_version) {
175 die "invalid corosync config: unable to read config version\n"
176 if !defined($conf->{main}->{totem}->{config_version});
177 $conf->{main}->{totem}->{config_version}++;
178 }
179
180 PVE::Cluster::cfs_write_file("corosync.conf.new", $conf);
181
182 rename("/etc/pve/corosync.conf.new", "/etc/pve/corosync.conf")
183 || die "activating corosync.conf.new failed - $!\n";
184 }
185
186 # for creating a new cluster with the current node
187 # params are those from the API/CLI cluster create call
188 sub create_conf {
189 my ($nodename, %param) = @_;
190
191 my $clustername = $param{clustername};
192 my $nodeid = $param{nodeid} || 1;
193 my $votes = $param{votes} || 1;
194
195 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
196 my $ring0_addr = $param{ring0_addr} // $local_ip_address;
197 my $bindnet0_addr = $param{bindnet0_addr} // $ring0_addr;
198
199 my $use_ipv6 = ip_is_ipv6($ring0_addr);
200 die "ring 0 addresses must be from same IP family!\n"
201 if $use_ipv6 != ip_is_ipv6($bindnet0_addr);
202
203 my $conf = {
204 totem => {
205 version => 2, # protocol version
206 secauth => 'on',
207 cluster_name => $clustername,
208 config_version => 0,
209 ip_version => $use_ipv6 ? 'ipv6' : 'ipv4',
210 interface => {
211 0 => {
212 bindnetaddr => $bindnet0_addr,
213 ringnumber => 0,
214 },
215 },
216 },
217 nodelist => {
218 node => {
219 $nodename => {
220 name => $nodename,
221 nodeid => $nodeid,
222 quorum_votes => $votes,
223 ring0_addr => $ring0_addr,
224 },
225 },
226 },
227 quorum => {
228 provider => 'corosync_votequorum',
229 },
230 logging => {
231 to_syslog => 'yes',
232 debug => 'off',
233 },
234 };
235
236 die "Param bindnet1_addr set but ring1_addr not specified!\n"
237 if (defined($param{bindnet1_addr}) && !defined($param{ring1_addr}));
238
239 my $ring1_addr = $param{ring1_addr};
240 my $bindnet1_addr = $param{bindnet1_addr} // $param{ring1_addr};
241
242 if ($bindnet1_addr) {
243 die "ring 1 addresses must be from same IP family as ring 0!\n"
244 if $use_ipv6 != ip_is_ipv6($bindnet1_addr) ||
245 $use_ipv6 != ip_is_ipv6($ring1_addr);
246
247 $conf->{totem}->{interface}->{1} = {
248 bindnetaddr => $bindnet1_addr,
249 ringnumber => 1,
250 };
251 $conf->{totem}->{rrp_mode} = 'passive';
252 $conf->{nodelist}->{node}->{$nodename}->{ring1_addr} = $ring1_addr;
253 }
254
255 return { main => $conf };
256 }
257
258 1;