]> git.proxmox.com Git - pmg-api.git/blame - PMG/Cluster.pm
PMG/Cluster.pm: improve sync_config_from_master
[pmg-api.git] / PMG / Cluster.pm
CommitLineData
0854fb22
DM
1package PMG::Cluster;
2
3use strict;
4use warnings;
45e68618 5use Data::Dumper;
0854fb22 6use Socket;
cfdf6608 7use File::Path;
45e68618 8
a7c7cad7 9use PVE::SafeSyslog;
0854fb22
DM
10use PVE::Tools;
11use PVE::INotify;
12
a7c7cad7 13use PMG::Config;
9f67f5b3 14use PMG::ClusterConfig;
0854fb22 15
cfdf6608
DM
16our $spooldir = "/var/spool/proxmox";
17
18sub create_needed_dirs {
19 my ($lcid, $cleanup) = @_;
20
21 # if requested, remove any stale date
58072364 22 rmtree("$spooldir/cluster", "$spooldir/virus", "$spooldir/spam") if $cleanup;
d8782874
DM
23
24 mkdir "$spooldir/spam";
25 mkdir "$spooldir/virus";
cfdf6608
DM
26
27 if ($lcid) {
28 mkpath "$spooldir/cluster/$lcid/virus";
29 mkpath "$spooldir/cluster/$lcid/spam";
30 }
31}
32
8737f93a
DM
33sub remote_node_ip {
34 my ($nodename, $noerr) = @_;
35
d8782874 36 my $cinfo = PMG::ClusterConfig->new();
8737f93a 37
45e68618 38 foreach my $entry (values %{$cinfo->{ids}}) {
8737f93a
DM
39 if ($entry->{name} eq $nodename) {
40 my $ip = $entry->{ip};
41 return $ip if !wantarray;
42 my $family = PVE::Tools::get_host_address_family($ip);
43 return ($ip, $family);
44 }
45 }
46
47 # fallback: try to get IP by other means
9f67f5b3 48 return PMG::Utils::lookup_node_ip($nodename, $noerr);
8737f93a
DM
49}
50
d2e43f9e
DM
51sub get_master_node {
52 my ($cinfo) = @_;
53
d8782874 54 $cinfo = PMG::ClusterConfig->new() if !$cinfo;
d2e43f9e
DM
55
56 return $cinfo->{master}->{name} if defined($cinfo->{master});
57
58 return 'localhost';
59}
60
cba17aeb
DM
61sub read_local_ssl_cert_fingerprint {
62 my $cert_path = "/etc/pmg/pmg-api.pem";
0854fb22 63
cba17aeb
DM
64 my $cert;
65 eval {
66 my $bio = Net::SSLeay::BIO_new_file($cert_path, 'r');
67 $cert = Net::SSLeay::PEM_read_bio_X509($bio);
68 Net::SSLeay::BIO_free($bio);
69 };
70 if (my $err = $@) {
71 die "unable to read certificate '$cert_path' - $err\n";
72 }
0854fb22 73
cba17aeb
DM
74 if (!defined($cert)) {
75 die "unable to read certificate '$cert_path' - got empty value\n";
76 }
77
78 my $fp;
79 eval {
80 $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
81 };
82 if (my $err = $@) {
83 die "unable to get fingerprint for '$cert_path' - $err\n";
84 }
0854fb22 85
cba17aeb
DM
86 if (!defined($fp) || $fp eq '') {
87 die "unable to get fingerprint for '$cert_path' - got empty value\n";
88 }
0854fb22 89
cba17aeb
DM
90 return $fp;
91}
0854fb22 92
cba17aeb
DM
93my $hostrsapubkey_fn = '/etc/ssh/ssh_host_rsa_key.pub';
94my $rootrsakey_fn = '/root/.ssh/id_rsa';
95my $rootrsapubkey_fn = '/root/.ssh/id_rsa.pub';
0854fb22 96
cba17aeb
DM
97sub read_local_cluster_info {
98
99 my $res = {};
100
101 my $hostrsapubkey = PVE::Tools::file_read_firstline($hostrsapubkey_fn);
102 $hostrsapubkey =~ s/^.*ssh-rsa\s+//i;
103 $hostrsapubkey =~ s/\s+root\@\S+\s*$//i;
104
105 die "unable to parse ${hostrsapubkey_fn}\n"
106 if $hostrsapubkey !~ m/^[A-Za-z0-9\.\/\+]{200,}$/;
0854fb22
DM
107
108 my $nodename = PVE::INotify::nodename();
109
cba17aeb 110 $res->{name} = $nodename;
0854fb22 111
cba17aeb 112 $res->{ip} = PMG::Utils::lookup_node_ip($nodename);
0854fb22 113
cba17aeb 114 $res->{hostrsapubkey} = $hostrsapubkey;
0854fb22 115
cba17aeb
DM
116 if (! -f $rootrsapubkey_fn) {
117 unlink $rootrsakey_fn;
118 my $cmd = ['ssh-keygen', '-t', 'rsa', '-N', '', '-b', '2048',
119 '-f', $rootrsakey_fn];
120 PVE::Tools::run_command($cmd);
121 }
122
123 my $rootrsapubkey = PVE::Tools::file_read_firstline($rootrsapubkey_fn);
124 $rootrsapubkey =~ s/^.*ssh-rsa\s+//i;
125 $rootrsapubkey =~ s/\s+root\@\S+\s*$//i;
126
127 die "unable to parse ${rootrsapubkey_fn}\n"
128 if $rootrsapubkey !~ m/^[A-Za-z0-9\.\/\+]{200,}$/;
129
130 $res->{rootrsapubkey} = $rootrsapubkey;
131
132 $res->{fingerprint} = read_local_ssl_cert_fingerprint();
133
134 return $res;
135}
136
137# X509 Certificate cache helper
138
139my $cert_cache_nodes = {};
140my $cert_cache_timestamp = time();
141my $cert_cache_fingerprints = {};
0854fb22 142
cba17aeb 143sub update_cert_cache {
0854fb22 144
cba17aeb
DM
145 $cert_cache_timestamp = time();
146
147 $cert_cache_fingerprints = {};
148 $cert_cache_nodes = {};
149
d8782874 150 my $cinfo = PMG::ClusterConfig->new();
cba17aeb
DM
151
152 foreach my $entry (values %{$cinfo->{ids}}) {
153 my $node = $entry->{name};
154 my $fp = $entry->{fingerprint};
155 if ($node && $fp) {
156 $cert_cache_fingerprints->{$fp} = 1;
157 $cert_cache_nodes->{$node} = $fp;
0854fb22
DM
158 }
159 }
160}
161
162# load and cache cert fingerprint once
163sub initialize_cert_cache {
164 my ($node) = @_;
165
cba17aeb 166 update_cert_cache()
0854fb22
DM
167 if defined($node) && !defined($cert_cache_nodes->{$node});
168}
169
170sub check_cert_fingerprint {
171 my ($cert) = @_;
172
173 # clear cache every 30 minutes at least
cba17aeb 174 update_cert_cache() if time() - $cert_cache_timestamp >= 60*30;
0854fb22
DM
175
176 # get fingerprint of server certificate
177 my $fp;
178 eval {
179 $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
180 };
181 return 0 if $@ || !defined($fp) || $fp eq ''; # error
182
183 my $check = sub {
184 for my $expected (keys %$cert_cache_fingerprints) {
185 return 1 if $fp eq $expected;
186 }
187 return 0;
188 };
189
cba17aeb 190 return 1 if $check->();
0854fb22
DM
191
192 # clear cache and retry at most once every minute
193 if (time() - $cert_cache_timestamp >= 60) {
194 syslog ('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache");
195 update_cert_cache();
cba17aeb 196 return $check->();
0854fb22
DM
197 }
198
199 return 0;
200}
201
58072364
DM
202my $sshglobalknownhosts = "/etc/ssh/ssh_known_hosts2";
203my $rootsshauthkeys = "/root/.ssh/authorized_keys";
204my $ssh_rsa_id = "/root/.ssh/id_rsa.pub";
205
206sub update_ssh_keys {
207 my ($cinfo) = @_;
208
209 my $data = '';
210 foreach my $node (values %{$cinfo->{ids}}) {
211 $data .= "$node->{ip} ssh-rsa $node->{hostrsapubkey}\n";
212 $data .= "$node->{name} ssh-rsa $node->{hostrsapubkey}\n";
213 }
214
215 PVE::Tools::file_set_contents($sshglobalknownhosts, $data);
216
217 $data = '';
218
219 # always add ourself
220 if (-f $ssh_rsa_id) {
221 my $pub = PVE::Tools::file_get_contents($ssh_rsa_id);
222 chomp($pub);
223 $data .= "$pub\n";
224 }
225
226 foreach my $node (values %{$cinfo->{ids}}) {
227 $data .= "ssh-rsa $node->{rootrsapubkey} root\@$node->{name}\n";
228 }
229
230 if (-f $rootsshauthkeys) {
a7c7cad7
DM
231 my $mykey = PVE::Tools::file_get_contents($rootsshauthkeys, 128*1024);
232 chomp($mykey);
233 $data .= "$mykey\n";
58072364
DM
234 }
235
236 my $newdata = "";
237 my $vhash = {};
238 my @lines = split(/\n/, $data);
239 foreach my $line (@lines) {
240 if ($line !~ /^#/ && $line =~ m/(^|\s)ssh-(rsa|dsa)\s+(\S+)\s+\S+$/) {
241 next if $vhash->{$3}++;
242 }
243 $newdata .= "$line\n";
244 }
245
246 PVE::Tools::file_set_contents($rootsshauthkeys, $newdata, 0600);
247}
248
a7c7cad7
DM
249my $cfgdir = '/etc/pmg';
250my $syncdir = "$cfgdir/master";
251
252my $cond_commit_synced_file = sub {
253 my ($filename, $dstfn) = @_;
254
255 $dstfn = "$cfgdir/$filename" if !defined($dstfn);
256 my $srcfn = "$syncdir/$filename";
257
258 if (! -f $srcfn) {
259 unlink $dstfn;
260 return;
261 }
262
263 my $new = PVE::Tools::file_get_contents($srcfn, 1024*1024);
264
265 if (-f $dstfn) {
266 my $old = PVE::Tools::file_get_contents($dstfn, 1024*1024);
267 return 0 if $new eq $old;
268 }
269
270 rename($srcfn, $dstfn) ||
271 die "cond_rename_file '$filename' failed - $!\n";
272
273 print STDERR "updated $dstfn\n";
274
275 return 1;
276};
277
58072364
DM
278sub sync_config_from_master {
279 my ($cinfo, $master_ip, $noreload) = @_;
280
281 my $local_ip = $cinfo->{local}->{ip};
282 my $local_name = $cinfo->{local}->{name};
283
284 return if $local_ip eq $master_ip;
285
58072364 286 mkdir $syncdir;
a7c7cad7 287 File::Path::remove_tree($syncdir, {keep_root => 1});
58072364 288
a7c7cad7
DM
289 my $sa_conf_dir = "/etc/mail/spamassassin";
290 my $sa_custom_cf = "custom.cf";
58072364
DM
291
292 my $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
a7c7cad7 293 "${master_ip}:$cfgdir/* ${sa_conf_dir}/${sa_custom_cf}",
58072364 294 "$syncdir/",
a7c7cad7
DM
295 '--exclude', '*~',
296 '--exclude', '*.db',
297 '--exclude', 'pmg-api.pem',
298 '--exclude', 'pmg-tls.pem',
299 ];
58072364
DM
300
301 my $errmsg = "syncing master configuration from '${master_ip}' failed";
302 PVE::Tools::run_command($cmd, errmsg => $errmsg);
a7c7cad7
DM
303
304 # verify that the remote host is cluster master
305 open (my $fh, '<', "$syncdir/cluster.conf") ||
306 die "unable to open synced cluster.conf - $!\n";
307 my $newcinfo = PMG::ClusterConfig::read_cluster_conf('cluster.conf', $fh);
308
309 if (!$newcinfo->{master} || ($newcinfo->{master}->{ip} ne $master_ip)) {
310 die "host '$master_ip' is not cluster master\n";
311 }
312
313 my $role = $newcinfo->{'local'}->{type} // '-';
314 die "local node '$newcinfo->{local}->{name}' not part of cluster\n"
315 if $role eq '-';
316
317 die "local node '$newcinfo->{local}->{name}' is new cluster master\n"
318 if $role eq 'master';
319
320
321 $cond_commit_synced_file->('cluster.conf');
322 $cinfo = $newcinfo;
323
324 my $files = [
325 'pmg-authkey.key',
326 'pmg-authkey.pub',
327 'pmg-csrf.key',
328 'ldap.conf',
329 'user.conf',
330 ];
331
332 foreach my $filename (@$files) {
333 $cond_commit_synced_file->($filename);
334 }
335
336 my $force_restart = {};
337
338 if ($cond_commit_synced_file->($sa_custom_cf, "${sa_conf_dir}/${sa_custom_cf}")) {
339 $force_restart->{spam} = 1;
340 }
341
342 $cond_commit_synced_file->('pmg.conf');
343
344 my $cfg = PMG::Config->new();
345
346 $cfg->rewrite_config(1, $force_restart);
58072364
DM
347}
348
0854fb22 3491;