15 use PMG
::ClusterConfig
;
18 use PVE
::APIClient
::LWP
;
20 our $spooldir = "/var/spool/proxmox";
22 sub create_needed_dirs
{
23 my ($lcid, $cleanup) = @_;
25 # if requested, remove any stale date
26 File
::Path
::remove_tree
("$spooldir/cluster", "$spooldir/virus", "$spooldir/spam") if $cleanup;
28 mkdir "$spooldir/spam";
29 mkdir "$spooldir/virus";
32 mkpath
"$spooldir/cluster/$lcid/virus";
33 mkpath
"$spooldir/cluster/$lcid/spam";
38 my ($nodename, $noerr) = @_;
40 my $cinfo = PMG
::ClusterConfig-
>new();
42 foreach my $entry (values %{$cinfo->{ids
}}) {
43 if ($entry->{name
} eq $nodename) {
44 my $ip = $entry->{ip
};
45 return $ip if !wantarray;
46 my $family = PVE
::Tools
::get_host_address_family
($ip);
47 return ($ip, $family);
51 # fallback: try to get IP by other means
52 return PMG
::Utils
::lookup_node_ip
($nodename, $noerr);
58 $cinfo = PMG
::ClusterConfig-
>new() if !$cinfo;
60 return $cinfo->{master
}->{name
} if defined($cinfo->{master
});
65 sub read_local_ssl_cert_fingerprint
{
66 my $cert_path = "/etc/pmg/pmg-api.pem";
70 my $bio = Net
::SSLeay
::BIO_new_file
($cert_path, 'r');
71 $cert = Net
::SSLeay
::PEM_read_bio_X509
($bio);
72 Net
::SSLeay
::BIO_free
($bio);
75 die "unable to read certificate '$cert_path' - $err\n";
78 if (!defined($cert)) {
79 die "unable to read certificate '$cert_path' - got empty value\n";
84 $fp = Net
::SSLeay
::X509_get_fingerprint
($cert, 'sha256');
87 die "unable to get fingerprint for '$cert_path' - $err\n";
90 if (!defined($fp) || $fp eq '') {
91 die "unable to get fingerprint for '$cert_path' - got empty value\n";
97 my $hostrsapubkey_fn = '/etc/ssh/ssh_host_rsa_key.pub';
98 my $rootrsakey_fn = '/root/.ssh/id_rsa';
99 my $rootrsapubkey_fn = '/root/.ssh/id_rsa.pub';
101 sub read_local_cluster_info
{
105 my $hostrsapubkey = PVE
::Tools
::file_read_firstline
($hostrsapubkey_fn);
106 $hostrsapubkey =~ s/^.*ssh-rsa\s+//i;
107 $hostrsapubkey =~ s/\s+root\@\S+\s*$//i;
109 die "unable to parse ${hostrsapubkey_fn}\n"
110 if $hostrsapubkey !~ m/^[A-Za-z0-9\.\/\
+]{200,}$/;
112 my $nodename = PVE
::INotify
::nodename
();
114 $res->{name
} = $nodename;
116 $res->{ip
} = PMG
::Utils
::lookup_node_ip
($nodename);
118 $res->{hostrsapubkey
} = $hostrsapubkey;
120 if (! -f
$rootrsapubkey_fn) {
121 unlink $rootrsakey_fn;
122 my $cmd = ['ssh-keygen', '-t', 'rsa', '-N', '', '-b', '2048',
123 '-f', $rootrsakey_fn];
124 PMG
::Utils
::run_silent_cmd
($cmd);
127 my $rootrsapubkey = PVE
::Tools
::file_read_firstline
($rootrsapubkey_fn);
128 $rootrsapubkey =~ s/^.*ssh-rsa\s+//i;
129 $rootrsapubkey =~ s/\s+root\@\S+\s*$//i;
131 die "unable to parse ${rootrsapubkey_fn}\n"
132 if $rootrsapubkey !~ m/^[A-Za-z0-9\.\/\
+]{200,}$/;
134 $res->{rootrsapubkey
} = $rootrsapubkey;
136 $res->{fingerprint
} = read_local_ssl_cert_fingerprint
();
141 # X509 Certificate cache helper
143 my $cert_cache_nodes = {};
144 my $cert_cache_timestamp = time();
145 my $cert_cache_fingerprints = {};
147 sub update_cert_cache
{
149 $cert_cache_timestamp = time();
151 $cert_cache_fingerprints = {};
152 $cert_cache_nodes = {};
154 my $cinfo = PMG
::ClusterConfig-
>new();
156 foreach my $entry (values %{$cinfo->{ids
}}) {
157 my $node = $entry->{name
};
158 my $fp = $entry->{fingerprint
};
160 $cert_cache_fingerprints->{$fp} = 1;
161 $cert_cache_nodes->{$node} = $fp;
166 # load and cache cert fingerprint once
167 sub initialize_cert_cache
{
171 if defined($node) && !defined($cert_cache_nodes->{$node});
174 sub check_cert_fingerprint
{
177 # clear cache every 30 minutes at least
178 update_cert_cache
() if time() - $cert_cache_timestamp >= 60*30;
180 # get fingerprint of server certificate
183 $fp = Net
::SSLeay
::X509_get_fingerprint
($cert, 'sha256');
185 return 0 if $@ || !defined($fp) || $fp eq ''; # error
188 for my $expected (keys %$cert_cache_fingerprints) {
189 return 1 if $fp eq $expected;
194 return 1 if $check->();
196 # clear cache and retry at most once every minute
197 if (time() - $cert_cache_timestamp >= 60) {
198 syslog
('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache");
206 my $sshglobalknownhosts = "/etc/ssh/ssh_known_hosts2";
207 my $rootsshauthkeys = "/root/.ssh/authorized_keys";
208 my $ssh_rsa_id = "/root/.ssh/id_rsa.pub";
210 sub update_ssh_keys
{
214 foreach my $node (values %{$cinfo->{ids
}}) {
215 $data .= "$node->{ip} ssh-rsa $node->{hostrsapubkey}\n";
216 $data .= "$node->{name} ssh-rsa $node->{hostrsapubkey}\n";
219 PVE
::Tools
::file_set_contents
($sshglobalknownhosts, $data);
224 if (-f
$ssh_rsa_id) {
225 my $pub = PVE
::Tools
::file_get_contents
($ssh_rsa_id);
230 foreach my $node (values %{$cinfo->{ids
}}) {
231 $data .= "ssh-rsa $node->{rootrsapubkey} root\@$node->{name}\n";
234 if (-f
$rootsshauthkeys) {
235 my $mykey = PVE
::Tools
::file_get_contents
($rootsshauthkeys, 128*1024);
242 my @lines = split(/\n/, $data);
243 foreach my $line (@lines) {
244 if ($line !~ /^#/ && $line =~ m/(^|\s)ssh-(rsa|dsa)\s+(\S+)\s+\S+$/) {
245 next if $vhash->{$3}++;
247 $newdata .= "$line\n";
250 PVE
::Tools
::file_set_contents
($rootsshauthkeys, $newdata, 0600);
253 my $cfgdir = '/etc/pmg';
254 my $syncdir = "$cfgdir/master";
256 my $cond_commit_synced_file = sub {
257 my ($filename, $dstfn) = @_;
259 $dstfn = "$cfgdir/$filename" if !defined($dstfn);
260 my $srcfn = "$syncdir/$filename";
267 my $new = PVE
::Tools
::file_get_contents
($srcfn, 1024*1024);
270 my $old = PVE
::Tools
::file_get_contents
($dstfn, 1024*1024);
271 return 0 if $new eq $old;
274 rename($srcfn, $dstfn) ||
275 die "cond_rename_file '$filename' failed - $!\n";
277 print STDERR
"updated $dstfn\n";
282 my $rsync_command = sub {
283 my ($host_key_alias, @args) = @_;
285 my $ssh_cmd = '--rsh=ssh -l root -o BatchMode=yes';
286 $ssh_cmd .= " -o HostKeyAlias=${host_key_alias}" if $host_key_alias;
288 my $cmd = ['rsync', $ssh_cmd, '-q', @args];
293 sub sync_quarantine_files
{
294 my ($host_ip, $host_name, $flistname) = @_;
296 my $cmd = $rsync_command->(
297 $host_name, '--timeout', '10', "${host_ip}:$spooldir", $spooldir,
298 '--files-from', $flistname);
300 Proxmox
::Utils
::run_command
($cmd);
304 my ($host_ip, $host_name, $rcid) = @_;
306 mkdir "$spooldir/cluster/";
307 my $syncdir = "$spooldir/cluster/$rcid";
310 my $cmd = $rsync_command->(
311 $host_name, '-aq', '--timeout', '10', "${host_ip}:$syncdir/", $syncdir);
313 foreach my $incl (('spam/', 'spam/*', 'spam/*/*', 'virus/', 'virus/*', 'virus/*/*')) {
314 push @$cmd, '--include', $incl;
317 push @$cmd, '--exclude', '*';
319 PVE
::Tools
::run_command
($cmd);
322 sub sync_master_quar
{
323 my ($host_ip, $host_name) = @_;
325 my $syncdir = "$spooldir/cluster/";
328 my $cmd = $rsync_command->(
329 $host_name, '-aq', '--timeout', '10', "${host_ip}:$syncdir", $syncdir);
331 PVE
::Tools
::run_command
($cmd);
334 sub sync_config_from_master
{
335 my ($cinfo, $master_name, $master_ip, $noreload) = @_;
337 my $local_ip = $cinfo->{local}->{ip
};
338 my $local_name = $cinfo->{local}->{name
};
340 if ($local_ip eq $master_ip) {
341 print STDERR
"local node is master - nothing to do\n";
346 File
::Path
::remove_tree
($syncdir, {keep_root
=> 1});
348 my $sa_conf_dir = "/etc/mail/spamassassin";
349 my $sa_custom_cf = "custom.cf";
351 my $cmd = $rsync_command->(
352 $master_name, '-lpgoq',
353 "${master_ip}:$cfgdir/* ${sa_conf_dir}/${sa_custom_cf}",
357 '--exclude', 'pmg-api.pem',
358 '--exclude', 'pmg-tls.pem',
361 my $errmsg = "syncing master configuration from '${master_ip}' failed";
362 PVE
::Tools
::run_command
($cmd, errmsg
=> $errmsg);
364 # verify that the remote host is cluster master
365 open (my $fh, '<', "$syncdir/cluster.conf") ||
366 die "unable to open synced cluster.conf - $!\n";
367 my $newcinfo = PMG
::ClusterConfig
::read_cluster_conf
('cluster.conf', $fh);
369 if (!$newcinfo->{master
} || ($newcinfo->{master
}->{ip
} ne $master_ip)) {
370 die "host '$master_ip' is not cluster master\n";
373 my $role = $newcinfo->{'local'}->{type
} // '-';
374 die "local node '$newcinfo->{local}->{name}' not part of cluster\n"
377 die "local node '$newcinfo->{local}->{name}' is new cluster master\n"
378 if $role eq 'master';
381 $cond_commit_synced_file->('cluster.conf');
392 foreach my $filename (@$files) {
393 $cond_commit_synced_file->($filename);
396 my $force_restart = {};
398 if ($cond_commit_synced_file->($sa_custom_cf, "${sa_conf_dir}/${sa_custom_cf}")) {
399 $force_restart->{spam
} = 1;
402 $cond_commit_synced_file->('pmg.conf');
404 my $cfg = PMG
::Config-
>new();
406 $cfg->rewrite_config(1, $force_restart);
409 sub sync_ruledb_from_master
{
410 my ($ldb, $rdb, $ni, $ticket) = @_;
412 my $ruledb = PMG
::RuleDB-
>new($ldb);
413 my $rulecache = PMG
::RuleCache-
>new($ruledb);
415 my $conn = PVE
::APIClient
::LWP-
>new(
417 cookie_name
=> 'PMGAuthCookie',
419 cached_fingerprints
=> {
420 $ni->{fingerprint
} => 1,
423 my $digest = $conn->get("/config/ruledb/digest", {});
425 return if $digest eq $rulecache->{digest
}; # no changes
427 syslog
('info', "detected rule database changes - starting sync from '$ni->{ip}'");
432 $ldb->do("DELETE FROM Rule");
433 $ldb->do("DELETE FROM RuleGroup");
434 $ldb->do("DELETE FROM ObjectGroup");
435 $ldb->do("DELETE FROM Object");
436 $ldb->do("DELETE FROM Attribut");
441 # read a consistent snapshot
442 $rdb->do("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
444 PMG
::DBTools
::copy_table
($ldb, $rdb, "Rule");
445 PMG
::DBTools
::copy_table
($ldb, $rdb, "RuleGroup");
446 PMG
::DBTools
::copy_table
($ldb, $rdb, "ObjectGroup");
447 PMG
::DBTools
::copy_table
($ldb, $rdb, "Object", 'value');
448 PMG
::DBTools
::copy_table
($ldb, $rdb, "Attribut", 'value');
451 $rdb->rollback; # end transaction
457 $ldb->do("SELECT setval('rule_id_seq', max(id)+1) FROM Rule");
458 $ldb->do("SELECT setval('object_id_seq', max(id)+1) FROM Object");
459 $ldb->do("SELECT setval('objectgroup_id_seq', max(id)+1) FROM ObjectGroup");
468 syslog
('info', "finished rule database sync from host '$ni->{ip}'");