]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/API2/Cluster.pm
ceda100993e722a7741cdd1cffd1add8b886662b
[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 ];
115
116 return $result;
117 }});
118
119 __PACKAGE__->register_method({
120 name => 'nodes',
121 path => 'nodes',
122 method => 'GET',
123 description => "Cluster node index.",
124 # alway read local file
125 parameters => {
126 additionalProperties => 0,
127 properties => {},
128 },
129 permissions => { check => [ 'admin', 'qmanager', 'audit' ] },
130 returns => {
131 type => 'array',
132 items => {
133 type => "object",
134 properties => {
135 type => { type => 'string' },
136 cid => { type => 'integer' },
137 ip => { type => 'string' },
138 name => { type => 'string' },
139 hostrsapubkey => { type => 'string' },
140 rootrsapubkey => { type => 'string' },
141 fingerprint => { type => 'string' },
142 },
143 },
144 },
145 code => sub {
146 my ($param) = @_;
147
148 my $cinfo = PMG::ClusterConfig->new();
149
150 if (scalar(keys %{$cinfo->{ids}})) {
151 my $role = $cinfo->{local}->{type} // '-';
152 if ($role eq '-') {
153 die "local node '$cinfo->{local}->{name}' not part of cluster\n";
154 }
155 }
156
157 return PVE::RESTHandler::hash_to_array($cinfo->{ids}, 'cid');
158 }});
159
160 __PACKAGE__->register_method({
161 name => 'status',
162 path => 'status',
163 method => 'GET',
164 description => "Cluster node status.",
165 # alway read local file
166 parameters => {
167 additionalProperties => 0,
168 properties => {
169 list_single_node => {
170 description => "List local node if there is no cluster defined. Please note that RSA keys and fingerprint are not valid in that case.",
171 type => 'boolean',
172 optional => 1,
173 default => 0,
174 },
175 },
176 },
177 permissions => { check => [ 'admin', 'qmanager', 'audit' ] },
178 returns => {
179 type => 'array',
180 items => {
181 type => "object",
182 properties => {
183 type => { type => 'string' },
184 cid => { type => 'integer' },
185 ip => { type => 'string' },
186 name => { type => 'string' },
187 hostrsapubkey => { type => 'string' },
188 rootrsapubkey => { type => 'string' },
189 fingerprint => { type => 'string' },
190 },
191 },
192 links => [ { rel => 'child', href => "{cid}" } ],
193 },
194 code => sub {
195 my ($param) = @_;
196
197 my $cinfo = PMG::ClusterConfig->new();
198 my $nodename = PVE::INotify::nodename();
199
200 my $res = [];
201 if (scalar(keys %{$cinfo->{ids}})) {
202 my $role = $cinfo->{local}->{type} // '-';
203 if ($role eq '-') {
204 die "local node '$cinfo->{local}->{name}' not part of cluster\n";
205 }
206 $res = PVE::RESTHandler::hash_to_array($cinfo->{ids}, 'cid');
207
208 } elsif ($param->{list_single_node}) {
209 my $ni = { type => '-' };
210 foreach my $k (qw(ip name cid)) {
211 $ni->{$k} = $cinfo->{local}->{$k};
212 }
213 foreach my $k (qw(hostrsapubkey rootrsapubkey fingerprint)) {
214 $ni->{$k} = '-'; # invalid
215 }
216 $res = [ $ni ];
217 }
218
219 my $rpcenv = PMG::RESTEnvironment->get();
220 my $authuser = $rpcenv->get_user();
221 my $ticket = $rpcenv->get_ticket();
222
223 foreach my $ni (@$res) {
224 my $info;
225 eval {
226 if ($ni->{cid} eq $cinfo->{local}->{cid}) {
227 $info = PMG::API2::NodeInfo->status({ node => $nodename });
228 } else {
229 my $conn = PVE::APIClient::LWP->new(
230 ticket => $ticket,
231 cookie_name => 'PMGAuthCookie',
232 host => $ni->{ip},
233 cached_fingerprints => {
234 $ni->{fingerprint} => 1,
235 });
236
237 $info = $conn->get("/nodes/localhost/status", {});
238 }
239 };
240 if (my $err = $@) {
241 $ni->{conn_error} = "$err"; # convert $err to string
242 next;
243 }
244 foreach my $k (keys %$info) {
245 $ni->{$k} = $info->{$k} if !defined($ni->{$k});
246 }
247 }
248
249 return $res;
250 }});
251
252 my $add_node_schema = PMG::ClusterConfig::Node->createSchema(1);
253 delete $add_node_schema->{properties}->{cid};
254
255 __PACKAGE__->register_method({
256 name => 'add_node',
257 path => 'nodes',
258 method => 'POST',
259 description => "Add an node to the cluster config.",
260 proxyto => 'master',
261 protected => 1,
262 parameters => $add_node_schema,
263 returns => {
264 description => "Returns the resulting node list.",
265 type => 'array',
266 items => {
267 type => "object",
268 properties => {
269 cid => { type => 'integer' },
270 },
271 },
272 },
273 code => sub {
274 my ($param) = @_;
275
276 my $code = sub {
277 my $cinfo = PMG::ClusterConfig->new();
278
279 die "no cluster defined\n" if !scalar(keys %{$cinfo->{ids}});
280
281 my $master = $cinfo->{master} || die "unable to lookup master node\n";
282
283 my $next_cid;
284 foreach my $cid (keys %{$cinfo->{ids}}) {
285 my $d = $cinfo->{ids}->{$cid};
286
287 if ($d->{type} eq 'node' && $d->{ip} eq $param->{ip} && $d->{name} eq $param->{name}) {
288 $next_cid = $cid; # allow overwrite existing node data
289 last;
290 }
291
292 if ($d->{ip} eq $param->{ip}) {
293 die "ip address '$param->{ip}' is already used by existing node $d->{name}\n";
294 }
295
296 if ($d->{name} eq $param->{name}) {
297 die "node with name '$param->{name}' already exists\n";
298 }
299 }
300
301 if (!defined($next_cid)) {
302 $next_cid = ++$master->{maxcid};
303 }
304
305 my $node = {
306 type => 'node',
307 cid => $master->{maxcid},
308 };
309
310 foreach my $k (qw(ip name hostrsapubkey rootrsapubkey fingerprint)) {
311 $node->{$k} = $param->{$k};
312 }
313
314 $cinfo->{ids}->{$node->{cid}} = $node;
315
316 $cinfo->write();
317
318 PMG::DBTools::update_master_clusterinfo($node->{cid});
319
320 PMG::Cluster::update_ssh_keys($cinfo);
321
322 return PVE::RESTHandler::hash_to_array($cinfo->{ids}, 'cid');
323 };
324
325 return PMG::ClusterConfig::lock_config($code, "add node failed");
326 }});
327
328 __PACKAGE__->register_method({
329 name => 'create',
330 path => 'create',
331 method => 'POST',
332 description => "Create initial cluster config with current node as master.",
333 # alway read local file
334 parameters => {
335 additionalProperties => 0,
336 properties => {},
337 },
338 protected => 1,
339 returns => { type => 'string' },
340 code => sub {
341 my ($param) = @_;
342
343 my $rpcenv = PMG::RESTEnvironment->get();
344 my $authuser = $rpcenv->get_user();
345
346 my $realcmd = sub {
347 my $cinfo = PMG::ClusterConfig->new();
348
349 die "cluster already defined\n" if scalar(keys %{$cinfo->{ids}});
350
351 my $info = PMG::Cluster::read_local_cluster_info();
352
353 my $cid = 1;
354
355 $info->{type} = 'master';
356
357 $info->{maxcid} = $cid,
358
359 $cinfo->{ids}->{$cid} = $info;
360
361 eval {
362 print STDERR "stop all services accessing the database\n";
363 # stop all services accessing the database
364 PMG::Utils::service_wait_stopped(40, $PMG::Utils::db_service_list);
365
366 print STDERR "save new cluster configuration\n";
367 $cinfo->write();
368
369 PMG::DBTools::init_masterdb($cid);
370
371 PMG::MailQueue::create_spooldirs($cid);
372
373 print STDERR "cluster master successfully created\n";
374 };
375 my $err = $@;
376
377 foreach my $service (reverse @$PMG::Utils::db_service_list) {
378 eval { PVE::Tools::run_command(['systemctl', 'start', $service]); };
379 warn $@ if $@;
380 }
381
382 die $err if $err;
383 };
384
385 my $code = sub {
386 return $rpcenv->fork_worker('clustercreate', undef, $authuser, $realcmd);
387 };
388
389 return PMG::ClusterConfig::lock_config($code, "create cluster failed");
390 }});
391
392 __PACKAGE__->register_method({
393 name => 'join',
394 path => 'join',
395 method => 'POST',
396 description => "Join local node to an existing cluster.",
397 # alway read local file
398 protected => 1,
399 parameters => {
400 additionalProperties => 0,
401 properties => {
402 master_ip => {
403 description => "IP address.",
404 type => 'string', format => 'ip',
405 },
406 fingerprint => {
407 description => "SSL certificate fingerprint.",
408 type => 'string',
409 pattern => '^(:?[A-Z0-9][A-Z0-9]:){31}[A-Z0-9][A-Z0-9]$',
410 },
411 password => {
412 description => "Superuser password.",
413 type => 'string',
414 maxLength => 128,
415 },
416 },
417 },
418 returns => { type => 'string' },
419 code => sub {
420 my ($param) = @_;
421
422 my $rpcenv = PMG::RESTEnvironment->get();
423 my $authuser = $rpcenv->get_user();
424
425 my $realcmd = sub {
426 my $cinfo = PMG::ClusterConfig->new();
427
428 die "cluster already defined\n" if scalar(keys %{$cinfo->{ids}});
429
430 my $setup = {
431 username => 'root@pam',
432 password => $param->{password},
433 cookie_name => 'PMGAuthCookie',
434 host => $param->{master_ip},
435 cached_fingerprints => {
436 $param->{fingerprint} => 1,
437 }
438 };
439
440 cluster_join($cinfo, $setup);
441 };
442
443 my $code = sub {
444 return $rpcenv->fork_worker('clusterjoin', undef, $authuser, $realcmd);
445 };
446
447 return PMG::ClusterConfig::lock_config($code, "cluster join failed");
448 }});
449
450
451 1;