]> git.proxmox.com Git - pmg-api.git/blame - PMG/Cluster.pm
fully implement config read/write API
[pmg-api.git] / PMG / Cluster.pm
CommitLineData
0854fb22
DM
1package PMG::Cluster;
2
3use strict;
4use warnings;
5
6use Socket;
7use PVE::Tools;
8use PVE::INotify;
9
10# this is also used to get the IP of the local node
8737f93a 11sub lookup_node_ip {
0854fb22
DM
12 my ($nodename, $noerr) = @_;
13
0854fb22
DM
14 my ($family, $packed_ip);
15
16 eval {
17 my @res = PVE::Tools::getaddrinfo_all($nodename);
18 $family = $res[0]->{family};
19 $packed_ip = (PVE::Tools::unpack_sockaddr_in46($res[0]->{addr}))[2];
20 };
21
22 if ($@) {
23 die "hostname lookup failed:\n$@" if !$noerr;
24 return undef;
25 }
26
27 my $ip = Socket::inet_ntop($family, $packed_ip);
28 if ($ip =~ m/^127\.|^::1$/) {
29 die "hostname lookup failed - got local IP address ($nodename = $ip)\n" if !$noerr;
30 return undef;
31 }
32
33 return wantarray ? ($ip, $family) : $ip;
34}
35
8737f93a
DM
36sub remote_node_ip {
37 my ($nodename, $noerr) = @_;
38
39 my $cinfo = PVE::INotify::read_file("cluster.conf");
40
41 foreach my $entry (@{$cinfo->{nodes}}) {
42 if ($entry->{name} eq $nodename) {
43 my $ip = $entry->{ip};
44 return $ip if !wantarray;
45 my $family = PVE::Tools::get_host_address_family($ip);
46 return ($ip, $family);
47 }
48 }
49
50 # fallback: try to get IP by other means
51 return lookup_node_ip($nodename, $noerr);
52}
53
0854fb22
DM
54# X509 Certificate cache helper
55
56my $cert_cache_nodes = {};
57my $cert_cache_timestamp = time();
58my $cert_cache_fingerprints = {};
59
60sub update_cert_cache {
61 my ($update_node, $clear) = @_;
62
63 syslog('info', "Clearing outdated entries from certificate cache")
64 if $clear;
65
66 $cert_cache_timestamp = time() if !defined($update_node);
67
68 my $node_list = defined($update_node) ?
69 [ $update_node ] : [ keys %$cert_cache_nodes ];
70
71 my $clear_node = sub {
72 my ($node) = @_;
73 if (my $old_fp = $cert_cache_nodes->{$node}) {
74 # distrust old fingerprint
75 delete $cert_cache_fingerprints->{$old_fp};
76 # ensure reload on next proxied request
77 delete $cert_cache_nodes->{$node};
78 }
79 };
80
81 my $nodename = PVE::INotify::nodename();
82
83 foreach my $node (@$node_list) {
84
85 if ($node ne $nodename) {
86 &$clear_node($node) if $clear;
87 next;
88 }
89
90 my $cert_path = "/etc/proxmox/pmg-api.pem";
91
92 my $cert;
93 eval {
94 my $bio = Net::SSLeay::BIO_new_file($cert_path, 'r');
95 $cert = Net::SSLeay::PEM_read_bio_X509($bio);
96 Net::SSLeay::BIO_free($bio);
97 };
98 my $err = $@;
99 if ($err || !defined($cert)) {
100 &$clear_node($node) if $clear;
101 next;
102 }
103
104 my $fp;
105 eval {
106 $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
107 };
108 $err = $@;
109 if ($err || !defined($fp) || $fp eq '') {
110 &$clear_node($node) if $clear;
111 next;
112 }
113
114 my $old_fp = $cert_cache_nodes->{$node};
115 $cert_cache_fingerprints->{$fp} = 1;
116 $cert_cache_nodes->{$node} = $fp;
117
118 if (defined($old_fp) && $fp ne $old_fp) {
119 delete $cert_cache_fingerprints->{$old_fp};
120 }
121 }
122}
123
124# load and cache cert fingerprint once
125sub initialize_cert_cache {
126 my ($node) = @_;
127
128 update_cert_cache($node)
129 if defined($node) && !defined($cert_cache_nodes->{$node});
130}
131
132sub check_cert_fingerprint {
133 my ($cert) = @_;
134
135 # clear cache every 30 minutes at least
136 update_cert_cache(undef, 1) if time() - $cert_cache_timestamp >= 60*30;
137
138 # get fingerprint of server certificate
139 my $fp;
140 eval {
141 $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
142 };
143 return 0 if $@ || !defined($fp) || $fp eq ''; # error
144
145 my $check = sub {
146 for my $expected (keys %$cert_cache_fingerprints) {
147 return 1 if $fp eq $expected;
148 }
149 return 0;
150 };
151
152 return 1 if &$check();
153
154 # clear cache and retry at most once every minute
155 if (time() - $cert_cache_timestamp >= 60) {
156 syslog ('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache");
157 update_cert_cache();
158 return &$check();
159 }
160
161 return 0;
162}
163
539271ed
DM
164sub read_cluster_conf {
165 my ($filename, $fh) = @_;
166
167 my $localname = PVE::INotify::nodename();
8737f93a 168 my $localip = lookup_node_ip($localname);
539271ed
DM
169
170 my $level = 0;
171 my $maxcid = 0;
172
173 my $cinfo;
174
175 $cinfo->{nodes} = [];
176 $cinfo->{remnodes} = [];
177
178 $cinfo->{local} = {
179 role => '-',
180 cid => 0,
181 ip => $localip,
182 name => $localname,
183 configport => 83,
184 dbport => 5432,
185 };
186
8737f93a 187 # fixme: add test is local node is part of node list
539271ed
DM
188 if (defined($fh)) {
189
190 $cinfo->{exists} = 1; # cluster configuratin file exists and is readable
191
192 while (defined(my $line = <$fh>)) {
193 chomp $line;
194
195 next if $line =~ m/^\s*$/; # skip empty lines
196
197 if ($line =~ m/^maxcid\s+(\d+)\s*$/i) {
198 $maxcid = $1 > $maxcid ? $1 : $maxcid;
199 next;
200 }
201
202 if ($line =~ m/^(master|node)\s+(\d+)\s+\{\s*$/i) {
203 $level++;
204 my ($t, $cid) = (lc($1), $2);
205
206 $maxcid = $cid > $maxcid ? $cid : $maxcid;
207
208 my $res = {
209 role => $t eq 'master' ? 'M' : 'N',
210 cid => $cid
211 };
212
213 while (defined($line = <$fh>)) {
214 chomp $line;
215 next if $line =~ m/^\s*$/; # skip empty lines
216 if ($line =~ m/^\}\s*$/) {
217 $level--;
218 last;
219 }
220
221 if ($line =~ m/^\s*(\S+)\s*:\s*(\S+)\s*$/) {
222 my ($n, $v) = (lc $1, $2);
223
224 # fixme: do syntax checks
225 if ($n eq 'ip') {
226 $res->{$n} = $v;
227 } elsif ($n eq 'name') {
228 $res->{$n} = $v;
229 } elsif ($n eq 'hostrsapubkey') {
230 $res->{$n} = $v;
231 } elsif ($n eq 'rootrsapubkey') {
232 $res->{$n} = $v;
233 } else {
234 die "syntax error in configuration file\n";
235 }
236 } else {
237 die "syntax error in configuration file\n";
238 }
239 }
240
241 die "missing ip address for node '$cid'\n" if !$res->{ip};
242 die "missing name for node '$cid'\n" if !$res->{name};
243 #die "missing host RSA key for node '$cid'\n" if !$res->{hostrsapubkey};
244 #die "missing user RSA key for node '$cid'\n" if !$res->{rootrsapubkey};
245
246 push @{$cinfo->{nodes}}, $res;
247
248 if ($res->{role} eq 'M') {
249 $cinfo->{master} = $res;
250 }
251
8737f93a 252 if ($res->{ip} eq $localname) {
539271ed
DM
253 $cinfo->{local} = $res;
254 }
255 } else {
256 die "syntax error in configuration file\n";
257 }
258 }
259 }
260
261 die "syntax error in configuration file\n" if $level;
262
263 $cinfo->{maxcid} = $maxcid;
264
265 my @cidlist = ();
266 foreach my $ni (@{$cinfo->{nodes}}) {
267 next if $cinfo->{local}->{cid} == $ni->{cid}; # skip local CID
268 push @cidlist, $ni->{cid};
269 }
270
271 my $ind = 0;
272 my $portid = {};
273 foreach my $cid (sort @cidlist) {
274 $portid->{$cid} = $ind;
275 $ind++;
276 }
277
278 foreach my $ni (@{$cinfo->{nodes}}) {
279 # fixme: do we still need those ports?
280 $ni->{configport} = $ni->{cid} == $cinfo->{local}->{cid} ? 83 : 50000 + $portid->{$ni->{cid}};
281 $ni->{dbport} = $ni->{cid} == $cinfo->{local}->{cid} ? 5432 : 50100 + $portid->{$ni->{cid}};
282 }
283
284 foreach my $ni (@{$cinfo->{nodes}}) {
285 next if $ni->{cid} == $cinfo->{local}->{cid};
286 push @{$cinfo->{remnodes}}, $ni->{cid};
287 }
288
289 return $cinfo;
290}
291
292sub write_cluster_conf {
293 my ($filename, $fh, $cinfo) = @_;
294
295 my $raw = "maxcid $cinfo->{maxcid}\n\n";
296
297 foreach my $ni (@{$cinfo->{nodes}}) {
298
299 if ($ni->{role} eq 'M') {
300 $raw .= "master $ni->{cid} {\n";
301 $raw .= " IP: $ni->{ip}\n";
302 $raw .= " NAME: $ni->{name}\n";
303 $raw .= " HOSTRSAPUBKEY: $ni->{hostrsapubkey}\n";
304 $raw .= " ROOTRSAPUBKEY: $ni->{rootrsapubkey}\n";
305 $raw .= "}\n\n";
306 } elsif ($ni->{role} eq 'N') {
307 $raw .= "node $ni->{cid} {\n";
308 $raw .= " IP: $ni->{ip}\n";
309 $raw .= " NAME: $ni->{name}\n";
310 $raw .= " HOSTRSAPUBKEY: $ni->{hostrsapubkey}\n";
311 $raw .= " ROOTRSAPUBKEY: $ni->{rootrsapubkey}\n";
312 $raw .= "}\n\n";
313 }
314 }
315
316 PVE::Tools::safe_print($filename, $fh, $raw);
317}
318
319PVE::INotify::register_file('cluster.conf', "/etc/proxmox/cluster.conf",
320 \&read_cluster_conf,
321 \&write_cluster_conf,
322 undef,
323 always_call_parser => 1);
324
0854fb22 3251;