]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/API2/Cluster.pm
certs: reload postfix to activate new certificate
[pmg-api.git] / src / PMG / API2 / Cluster.pm
1 package PMG::API2::Cluster;
2
3 use strict;
4 use warnings;
5 use Data::Dumper;
6
7 use PVE::SafeSyslog;
8 use PVE::Tools qw(extract_param);
9 use HTTP::Status qw(:constants);
10 use Storable qw(dclone);
11 use PVE::JSONSchema qw(get_standard_option);
12 use PVE::RESTHandler;
13 use PVE::INotify;
14 use PVE::APIClient::LWP;
15
16 use PMG::RESTEnvironment;
17 use PMG::ClusterConfig;
18 use PMG::Cluster;
19 use PMG::DBTools;
20 use PMG::MailQueue;
21
22 use PMG::API2::Nodes;
23
24 use base qw(PVE::RESTHandler);
25
26 sub cluster_join {
27 my ($cinfo, $conn_setup) = @_;
28
29 my $conn = PVE::APIClient::LWP->new(%$conn_setup);
30
31 my $info = PMG::Cluster::read_local_cluster_info();
32
33 my $res = $conn->post("/config/cluster/nodes", $info);
34
35 foreach my $node (@$res) {
36 $cinfo->{ids}->{$node->{cid}} = $node;
37 }
38
39 eval {
40 print STDERR "stop all services accessing the database\n";
41 # stop all services accessing the database
42 PMG::Utils::service_wait_stopped(40, $PMG::Utils::db_service_list);
43
44 print STDERR "save new cluster configuration\n";
45 $cinfo->write();
46
47 PMG::Cluster::update_ssh_keys($cinfo);
48
49 print STDERR "cluster node successfully joined\n";
50
51 $cinfo = PMG::ClusterConfig->new(); # reload
52
53 my $role = $cinfo->{'local'}->{type} // '-';
54 die "local node '$cinfo->{local}->{name}' not part of cluster\n"
55 if $role eq '-';
56
57 die "got unexpected role '$role' for local node '$cinfo->{local}->{name}'\n"
58 if $role ne 'node';
59
60 my $cid = $cinfo->{'local'}->{cid};
61
62 PMG::MailQueue::create_spooldirs($cid);
63
64 PMG::Cluster::sync_config_from_master($cinfo->{master}->{name}, $cinfo->{master}->{ip});
65
66 PMG::DBTools::init_nodedb($cinfo);
67
68 my $cfg = PMG::Config->new();
69 my $ruledb = PMG::RuleDB->new();
70 my $rulecache = PMG::RuleCache->new($ruledb);
71
72 $cfg->rewrite_config($rulecache, 1);
73
74 print STDERR "syncing quarantine data\n";
75 PMG::Cluster::sync_master_quar($cinfo->{master}->{ip}, $cinfo->{master}->{name});
76 print STDERR "syncing quarantine data finished\n";
77 };
78 my $err = $@;
79
80 foreach my $service (reverse @$PMG::Utils::db_service_list) {
81 eval { PVE::Tools::run_command(['systemctl', 'start', $service]); };
82 warn $@ if $@;
83 }
84
85 die $err if $err;
86 }
87
88 __PACKAGE__->register_method({
89 name => 'index',
90 path => '',
91 method => 'GET',
92 description => "Directory index.",
93 permissions => { user => 'all' },
94 parameters => {
95 additionalProperties => 0,
96 properties => {},
97 },
98 returns => {
99 type => 'array',
100 items => {
101 type => "object",
102 properties => {},
103 },
104 links => [ { rel => 'child', href => "{name}" } ],
105 },
106 code => sub {
107 my ($param) = @_;
108
109 my $result = [
110 { name => 'nodes' },
111 { name => 'status' },
112 { name => 'create' },
113 { name => 'join' },
114 { name => 'update-fingerprints' },
115 ];
116
117 return $result;
118 }});
119
120 __PACKAGE__->register_method({
121 name => 'nodes',
122 path => 'nodes',
123 method => 'GET',
124 description => "Cluster node index.",
125 # alway read local file
126 parameters => {
127 additionalProperties => 0,
128 properties => {},
129 },
130 permissions => { check => [ 'admin', 'qmanager', 'audit' ] },
131 returns => {
132 type => 'array',
133 items => {
134 type => "object",
135 properties => {
136 type => { type => 'string' },
137 cid => { type => 'integer' },
138 ip => { type => 'string' },
139 name => { type => 'string' },
140 hostrsapubkey => { type => 'string' },
141 rootrsapubkey => { type => 'string' },
142 fingerprint => { type => 'string' },
143 },
144 },
145 },
146 code => sub {
147 my ($param) = @_;
148
149 my $cinfo = PMG::ClusterConfig->new();
150
151 if (scalar(keys %{$cinfo->{ids}})) {
152 my $role = $cinfo->{local}->{type} // '-';
153 if ($role eq '-') {
154 die "local node '$cinfo->{local}->{name}' not part of cluster\n";
155 }
156 }
157
158 return PVE::RESTHandler::hash_to_array($cinfo->{ids}, 'cid');
159 }});
160
161 __PACKAGE__->register_method({
162 name => 'status',
163 path => 'status',
164 method => 'GET',
165 description => "Cluster node status.",
166 # alway read local file
167 parameters => {
168 additionalProperties => 0,
169 properties => {
170 list_single_node => {
171 description => "List local node if there is no cluster defined. Please note that RSA keys and fingerprint are not valid in that case.",
172 type => 'boolean',
173 optional => 1,
174 default => 0,
175 },
176 },
177 },
178 permissions => { check => [ 'admin', 'qmanager', 'audit' ] },
179 returns => {
180 type => 'array',
181 items => {
182 type => "object",
183 properties => {
184 type => { type => 'string' },
185 cid => { type => 'integer' },
186 ip => { type => 'string' },
187 name => { type => 'string' },
188 hostrsapubkey => { type => 'string' },
189 rootrsapubkey => { type => 'string' },
190 fingerprint => { type => 'string' },
191 },
192 },
193 links => [ { rel => 'child', href => "{cid}" } ],
194 },
195 code => sub {
196 my ($param) = @_;
197
198 my $cinfo = PMG::ClusterConfig->new();
199 my $nodename = PVE::INotify::nodename();
200
201 my $res = [];
202 if (scalar(keys %{$cinfo->{ids}})) {
203 my $role = $cinfo->{local}->{type} // '-';
204 if ($role eq '-') {
205 die "local node '$cinfo->{local}->{name}' not part of cluster\n";
206 }
207 $res = PVE::RESTHandler::hash_to_array($cinfo->{ids}, 'cid');
208
209 } elsif ($param->{list_single_node}) {
210 my $ni = { type => '-' };
211 foreach my $k (qw(ip name cid)) {
212 $ni->{$k} = $cinfo->{local}->{$k};
213 }
214 foreach my $k (qw(hostrsapubkey rootrsapubkey fingerprint)) {
215 $ni->{$k} = '-'; # invalid
216 }
217 $res = [ $ni ];
218 }
219
220 my $rpcenv = PMG::RESTEnvironment->get();
221 my $authuser = $rpcenv->get_user();
222 my $ticket = $rpcenv->get_ticket();
223
224 foreach my $ni (@$res) {
225 my $info;
226 eval {
227 if ($ni->{cid} eq $cinfo->{local}->{cid}) {
228 $info = PMG::API2::NodeInfo->status({ node => $nodename });
229 } else {
230 my $conn = PVE::APIClient::LWP->new(
231 ticket => $ticket,
232 cookie_name => 'PMGAuthCookie',
233 host => $ni->{ip},
234 cached_fingerprints => {
235 $ni->{fingerprint} => 1,
236 });
237
238 $info = $conn->get("/nodes/localhost/status", {});
239 }
240 };
241 if (my $err = $@) {
242 $ni->{conn_error} = "$err"; # convert $err to string
243 next;
244 }
245 foreach my $k (keys %$info) {
246 $ni->{$k} = $info->{$k} if !defined($ni->{$k});
247 }
248 }
249
250 return $res;
251 }});
252
253 my $add_node_schema = PMG::ClusterConfig::Node->createSchema(1);
254 delete $add_node_schema->{properties}->{cid};
255
256 __PACKAGE__->register_method({
257 name => 'add_node',
258 path => 'nodes',
259 method => 'POST',
260 description => "Add an node to the cluster config.",
261 proxyto => 'master',
262 protected => 1,
263 parameters => $add_node_schema,
264 returns => {
265 description => "Returns the resulting node list.",
266 type => 'array',
267 items => {
268 type => "object",
269 properties => {
270 cid => { type => 'integer' },
271 },
272 },
273 },
274 code => sub {
275 my ($param) = @_;
276
277 my $code = sub {
278 my $cinfo = PMG::ClusterConfig->new();
279
280 die "no cluster defined\n" if !scalar(keys %{$cinfo->{ids}});
281
282 my $master = $cinfo->{master} || die "unable to lookup master node\n";
283
284 my $next_cid;
285 foreach my $cid (keys %{$cinfo->{ids}}) {
286 my $d = $cinfo->{ids}->{$cid};
287
288 if ($d->{type} eq 'node' && $d->{ip} eq $param->{ip} && $d->{name} eq $param->{name}) {
289 $next_cid = $cid; # allow overwrite existing node data
290 last;
291 }
292
293 if ($d->{ip} eq $param->{ip}) {
294 die "ip address '$param->{ip}' is already used by existing node $d->{name}\n";
295 }
296
297 if ($d->{name} eq $param->{name}) {
298 die "node with name '$param->{name}' already exists\n";
299 }
300 }
301
302 if (!defined($next_cid)) {
303 $next_cid = ++$master->{maxcid};
304 }
305
306 # create spooldir for new node to prevent problems if it gets
307 # delete from the cluster before being synced initially
308 PMG::MailQueue::create_spooldirs($master->{maxcid});
309
310 my $node = {
311 type => 'node',
312 cid => $master->{maxcid},
313 };
314
315 foreach my $k (qw(ip name hostrsapubkey rootrsapubkey fingerprint)) {
316 $node->{$k} = $param->{$k};
317 }
318
319 $cinfo->{ids}->{$node->{cid}} = $node;
320
321 $cinfo->write();
322
323 PMG::DBTools::update_master_clusterinfo($node->{cid});
324
325 PMG::Cluster::update_ssh_keys($cinfo);
326
327 return PVE::RESTHandler::hash_to_array($cinfo->{ids}, 'cid');
328 };
329
330 return PMG::ClusterConfig::lock_config($code, "add node failed");
331 }});
332
333 __PACKAGE__->register_method({
334 name => 'create',
335 path => 'create',
336 method => 'POST',
337 description => "Create initial cluster config with current node as master.",
338 # alway read local file
339 parameters => {
340 additionalProperties => 0,
341 properties => {},
342 },
343 protected => 1,
344 returns => { type => 'string' },
345 code => sub {
346 my ($param) = @_;
347
348 my $rpcenv = PMG::RESTEnvironment->get();
349 my $authuser = $rpcenv->get_user();
350
351 my $realcmd = sub {
352 my $cinfo = PMG::ClusterConfig->new();
353
354 die "cluster already defined\n" if scalar(keys %{$cinfo->{ids}});
355
356 my $info = PMG::Cluster::read_local_cluster_info();
357
358 my $cid = 1;
359
360 $info->{type} = 'master';
361
362 $info->{maxcid} = $cid,
363
364 $cinfo->{ids}->{$cid} = $info;
365
366 eval {
367 print STDERR "stop all services accessing the database\n";
368 # stop all services accessing the database
369 PMG::Utils::service_wait_stopped(40, $PMG::Utils::db_service_list);
370
371 print STDERR "save new cluster configuration\n";
372 $cinfo->write();
373
374 PMG::DBTools::init_masterdb($cid);
375
376 PMG::MailQueue::create_spooldirs($cid);
377
378 print STDERR "cluster master successfully created\n";
379 };
380 my $err = $@;
381
382 foreach my $service (reverse @$PMG::Utils::db_service_list) {
383 eval { PVE::Tools::run_command(['systemctl', 'start', $service]); };
384 warn $@ if $@;
385 }
386
387 die $err if $err;
388 };
389
390 my $code = sub {
391 return $rpcenv->fork_worker('clustercreate', undef, $authuser, $realcmd);
392 };
393
394 return PMG::ClusterConfig::lock_config($code, "create cluster failed");
395 }});
396
397 __PACKAGE__->register_method({
398 name => 'join',
399 path => 'join',
400 method => 'POST',
401 description => "Join local node to an existing cluster.",
402 # alway read local file
403 protected => 1,
404 parameters => {
405 additionalProperties => 0,
406 properties => {
407 master_ip => {
408 description => "IP address.",
409 type => 'string', format => 'ip',
410 },
411 fingerprint => {
412 description => "SSL certificate fingerprint.",
413 type => 'string',
414 pattern => '^(:?[A-Z0-9][A-Z0-9]:){31}[A-Z0-9][A-Z0-9]$',
415 },
416 password => {
417 description => "Superuser password.",
418 type => 'string',
419 maxLength => 128,
420 },
421 },
422 },
423 returns => { type => 'string' },
424 code => sub {
425 my ($param) = @_;
426
427 my $rpcenv = PMG::RESTEnvironment->get();
428 my $authuser = $rpcenv->get_user();
429
430 my $realcmd = sub {
431 my $cinfo = PMG::ClusterConfig->new();
432
433 die "cluster already defined\n" if scalar(keys %{$cinfo->{ids}});
434
435 my $setup = {
436 username => 'root@pam',
437 password => $param->{password},
438 cookie_name => 'PMGAuthCookie',
439 host => $param->{master_ip},
440 cached_fingerprints => {
441 $param->{fingerprint} => 1,
442 }
443 };
444
445 cluster_join($cinfo, $setup);
446 };
447
448 my $code = sub {
449 return $rpcenv->fork_worker('clusterjoin', undef, $authuser, $realcmd);
450 };
451
452 return PMG::ClusterConfig::lock_config($code, "cluster join failed");
453 }});
454
455 __PACKAGE__->register_method({
456 name => 'update_fingerprints',
457 path => 'update-fingerprints',
458 method => 'POST',
459 description => "Update API certificate fingerprints (by fetching it via ssh).",
460 proxyto => 'master',
461 protected => 1,
462 parameters => {
463 additionalProperties => 0,
464 },
465 returns => { type => 'null' },
466 code => sub {
467 my ($param) = @_;
468
469 my $code = sub {
470 my $cinfo = PMG::ClusterConfig->new();
471
472 die "no cluster defined\n" if !scalar(keys %{$cinfo->{ids}});
473
474 my $localcid = $cinfo->{local}->{cid};
475
476 foreach my $cid (sort keys %{$cinfo->{ids}}) {
477 my $fp;
478 if ($cid == $localcid) {
479 $fp = PMG::Cluster::read_local_ssl_cert_fingerprint();
480 } else {
481 $fp = PMG::Cluster::get_remote_cert_fingerprint($cinfo->{ids}->{$cid});
482 }
483 $cinfo->{ids}->{$cid}->{fingerprint} = $fp;
484 }
485
486 $cinfo->write();
487
488 return;
489 };
490
491 PMG::ClusterConfig::lock_config($code, "update fingerprints failed");
492 }});
493
494 1;