]> git.proxmox.com Git - pmg-api.git/blob - PMG/Cluster.pm
implement cluster join - first version
[pmg-api.git] / PMG / Cluster.pm
1 package PMG::Cluster;
2
3 use strict;
4 use warnings;
5 use Data::Dumper;
6 use Socket;
7
8 use PVE::Tools;
9 use PVE::INotify;
10
11 use PMG::ClusterConfig;
12
13 sub remote_node_ip {
14 my ($nodename, $noerr) = @_;
15
16 my $cinfo = PVE::INotify::read_file("cluster.conf");
17
18 foreach my $entry (values %{$cinfo->{ids}}) {
19 if ($entry->{name} eq $nodename) {
20 my $ip = $entry->{ip};
21 return $ip if !wantarray;
22 my $family = PVE::Tools::get_host_address_family($ip);
23 return ($ip, $family);
24 }
25 }
26
27 # fallback: try to get IP by other means
28 return PMG::Utils::lookup_node_ip($nodename, $noerr);
29 }
30
31 sub get_master_node {
32 my ($cinfo) = @_;
33
34 $cinfo = PVE::INotify::read_file("cluster.conf") if !$cinfo;
35
36 return $cinfo->{master}->{name} if defined($cinfo->{master});
37
38 return 'localhost';
39 }
40
41 sub read_local_ssl_cert_fingerprint {
42 my $cert_path = "/etc/pmg/pmg-api.pem";
43
44 my $cert;
45 eval {
46 my $bio = Net::SSLeay::BIO_new_file($cert_path, 'r');
47 $cert = Net::SSLeay::PEM_read_bio_X509($bio);
48 Net::SSLeay::BIO_free($bio);
49 };
50 if (my $err = $@) {
51 die "unable to read certificate '$cert_path' - $err\n";
52 }
53
54 if (!defined($cert)) {
55 die "unable to read certificate '$cert_path' - got empty value\n";
56 }
57
58 my $fp;
59 eval {
60 $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
61 };
62 if (my $err = $@) {
63 die "unable to get fingerprint for '$cert_path' - $err\n";
64 }
65
66 if (!defined($fp) || $fp eq '') {
67 die "unable to get fingerprint for '$cert_path' - got empty value\n";
68 }
69
70 return $fp;
71 }
72
73 my $hostrsapubkey_fn = '/etc/ssh/ssh_host_rsa_key.pub';
74 my $rootrsakey_fn = '/root/.ssh/id_rsa';
75 my $rootrsapubkey_fn = '/root/.ssh/id_rsa.pub';
76
77 sub read_local_cluster_info {
78
79 my $res = {};
80
81 my $hostrsapubkey = PVE::Tools::file_read_firstline($hostrsapubkey_fn);
82 $hostrsapubkey =~ s/^.*ssh-rsa\s+//i;
83 $hostrsapubkey =~ s/\s+root\@\S+\s*$//i;
84
85 die "unable to parse ${hostrsapubkey_fn}\n"
86 if $hostrsapubkey !~ m/^[A-Za-z0-9\.\/\+]{200,}$/;
87
88 my $nodename = PVE::INotify::nodename();
89
90 $res->{name} = $nodename;
91
92 $res->{ip} = PMG::Utils::lookup_node_ip($nodename);
93
94 $res->{hostrsapubkey} = $hostrsapubkey;
95
96 if (! -f $rootrsapubkey_fn) {
97 unlink $rootrsakey_fn;
98 my $cmd = ['ssh-keygen', '-t', 'rsa', '-N', '', '-b', '2048',
99 '-f', $rootrsakey_fn];
100 PVE::Tools::run_command($cmd);
101 }
102
103 my $rootrsapubkey = PVE::Tools::file_read_firstline($rootrsapubkey_fn);
104 $rootrsapubkey =~ s/^.*ssh-rsa\s+//i;
105 $rootrsapubkey =~ s/\s+root\@\S+\s*$//i;
106
107 die "unable to parse ${rootrsapubkey_fn}\n"
108 if $rootrsapubkey !~ m/^[A-Za-z0-9\.\/\+]{200,}$/;
109
110 $res->{rootrsapubkey} = $rootrsapubkey;
111
112 $res->{fingerprint} = read_local_ssl_cert_fingerprint();
113
114 return $res;
115 }
116
117 # X509 Certificate cache helper
118
119 my $cert_cache_nodes = {};
120 my $cert_cache_timestamp = time();
121 my $cert_cache_fingerprints = {};
122
123 sub update_cert_cache {
124
125 $cert_cache_timestamp = time();
126
127 $cert_cache_fingerprints = {};
128 $cert_cache_nodes = {};
129
130 my $cinfo = PVE::INotify::read_file("cluster.conf");
131
132 foreach my $entry (values %{$cinfo->{ids}}) {
133 my $node = $entry->{name};
134 my $fp = $entry->{fingerprint};
135 if ($node && $fp) {
136 $cert_cache_fingerprints->{$fp} = 1;
137 $cert_cache_nodes->{$node} = $fp;
138 }
139 }
140 }
141
142 # load and cache cert fingerprint once
143 sub initialize_cert_cache {
144 my ($node) = @_;
145
146 update_cert_cache()
147 if defined($node) && !defined($cert_cache_nodes->{$node});
148 }
149
150 sub check_cert_fingerprint {
151 my ($cert) = @_;
152
153 # clear cache every 30 minutes at least
154 update_cert_cache() if time() - $cert_cache_timestamp >= 60*30;
155
156 # get fingerprint of server certificate
157 my $fp;
158 eval {
159 $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
160 };
161 return 0 if $@ || !defined($fp) || $fp eq ''; # error
162
163 my $check = sub {
164 for my $expected (keys %$cert_cache_fingerprints) {
165 return 1 if $fp eq $expected;
166 }
167 return 0;
168 };
169
170 return 1 if $check->();
171
172 # clear cache and retry at most once every minute
173 if (time() - $cert_cache_timestamp >= 60) {
174 syslog ('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache");
175 update_cert_cache();
176 return $check->();
177 }
178
179 return 0;
180 }
181
182 1;