]> git.proxmox.com Git - pve-cluster.git/blob - data/PVE/API2/ClusterConfig.pm
api/cluster: create cluster in forked worker
[pve-cluster.git] / data / PVE / API2 / ClusterConfig.pm
1 package PVE::API2::ClusterConfig;
2
3 use strict;
4 use warnings;
5
6 use PVE::Tools;
7 use PVE::SafeSyslog;
8 use PVE::RESTHandler;
9 use PVE::RPCEnvironment;
10 use PVE::JSONSchema qw(get_standard_option);
11 use PVE::Cluster;
12 use PVE::APIClient::LWP;
13 use PVE::Corosync;
14
15 use base qw(PVE::RESTHandler);
16
17 my $clusterconf = "/etc/pve/corosync.conf";
18 my $authfile = "/etc/corosync/authkey";
19
20 __PACKAGE__->register_method({
21 name => 'index',
22 path => '',
23 method => 'GET',
24 description => "Directory index.",
25 permissions => {
26 check => ['perm', '/', [ 'Sys.Audit' ]],
27 },
28 parameters => {
29 additionalProperties => 0,
30 properties => {},
31 },
32 returns => {
33 type => 'array',
34 items => {
35 type => "object",
36 properties => {},
37 },
38 links => [ { rel => 'child', href => "{name}" } ],
39 },
40 code => sub {
41 my ($param) = @_;
42
43 my $result = [
44 { name => 'nodes' },
45 { name => 'totem' },
46 { name => 'join' },
47 ];
48
49 return $result;
50 }});
51
52 __PACKAGE__->register_method ({
53 name => 'create',
54 path => '',
55 method => 'POST',
56 protected => 1,
57 description => "Generate new cluster configuration.",
58 parameters => {
59 additionalProperties => 0,
60 properties => {
61 clustername => {
62 description => "The name of the cluster.",
63 type => 'string', format => 'pve-node',
64 maxLength => 15,
65 },
66 nodeid => {
67 type => 'integer',
68 description => "Node id for this node.",
69 minimum => 1,
70 optional => 1,
71 },
72 votes => {
73 type => 'integer',
74 description => "Number of votes for this node.",
75 minimum => 1,
76 optional => 1,
77 },
78 bindnet0_addr => {
79 type => 'string', format => 'ip',
80 description => "This specifies the network address the corosync ring 0".
81 " executive should bind to and defaults to the local IP address of the node.",
82 optional => 1,
83 },
84 ring0_addr => {
85 type => 'string', format => 'address',
86 description => "Hostname (or IP) of the corosync ring0 address of this node.".
87 " Defaults to the hostname of the node.",
88 optional => 1,
89 },
90 bindnet1_addr => {
91 type => 'string', format => 'ip',
92 description => "This specifies the network address the corosync ring 1".
93 " executive should bind to and is optional.",
94 optional => 1,
95 },
96 ring1_addr => {
97 type => 'string', format => 'address',
98 description => "Hostname (or IP) of the corosync ring1 address, this".
99 " needs an valid bindnet1_addr.",
100 optional => 1,
101 },
102 },
103 },
104 returns => { type => 'string' },
105 code => sub {
106 my ($param) = @_;
107
108 -f $clusterconf && die "cluster config '$clusterconf' already exists\n";
109
110 my $rpcenv = PVE::RPCEnvironment::get();
111 my $authuser = $rpcenv->get_user();
112
113 my $worker = sub {
114 PVE::Cluster::setup_sshd_config(1);
115 PVE::Cluster::setup_rootsshconfig();
116 PVE::Cluster::setup_ssh_keys();
117
118 PVE::Tools::run_command(['/usr/sbin/corosync-keygen', '-lk', $authfile])
119 if !-f $authfile;
120 die "no authentication key available\n" if -f !$authfile;
121
122 my $nodename = PVE::INotify::nodename();
123
124 # get the corosync basis config for the new cluster
125 my $config = PVE::Corosync::create_conf($nodename, %$param);
126
127 print "Writing corosync config to /etc/pve/corosync.conf\n";
128 PVE::Corosync::atomic_write_conf($config);
129
130 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
131 PVE::Cluster::ssh_merge_keys();
132 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
133 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
134
135 print "Restart corosync and cluster filesystem\n";
136 PVE::Tools::run_command('systemctl restart corosync pve-cluster');
137 };
138
139 return $rpcenv->fork_worker('clustercreate', '', $authuser, $worker);
140 }});
141
142 __PACKAGE__->register_method({
143 name => 'nodes',
144 path => 'nodes',
145 method => 'GET',
146 description => "Corosync node list.",
147 permissions => {
148 check => ['perm', '/', [ 'Sys.Audit' ]],
149 },
150 parameters => {
151 additionalProperties => 0,
152 properties => {},
153 },
154 returns => {
155 type => 'array',
156 items => {
157 type => "object",
158 properties => {
159 node => { type => 'string' },
160 },
161 },
162 links => [ { rel => 'child', href => "{node}" } ],
163 },
164 code => sub {
165 my ($param) = @_;
166
167
168 my $conf = PVE::Cluster::cfs_read_file('corosync.conf');
169 my $nodelist = PVE::Corosync::nodelist($conf);
170
171 return PVE::RESTHandler::hash_to_array($nodelist, 'node');
172 }});
173
174 # lock method to ensure local and cluster wide atomicity
175 # if we're a single node cluster just lock locally, we have no other cluster
176 # node which we could contend with, else also acquire a cluster wide lock
177 my $config_change_lock = sub {
178 my ($code) = @_;
179
180 my $local_lock_fn = "/var/lock/pvecm.lock";
181 PVE::Tools::lock_file($local_lock_fn, 10, sub {
182 PVE::Cluster::cfs_update(1);
183 my $members = PVE::Cluster::get_members();
184 if (scalar(keys %$members) > 1) {
185 return PVE::Cluster::cfs_lock_file('corosync.conf', 10, $code);
186 } else {
187 return $code->();
188 }
189 });
190 };
191
192 __PACKAGE__->register_method ({
193 name => 'addnode',
194 path => 'nodes/{node}',
195 method => 'POST',
196 protected => 1,
197 description => "Adds a node to the cluster configuration.",
198 parameters => {
199 additionalProperties => 0,
200 properties => {
201 node => get_standard_option('pve-node'),
202 nodeid => {
203 type => 'integer',
204 description => "Node id for this node.",
205 minimum => 1,
206 optional => 1,
207 },
208 votes => {
209 type => 'integer',
210 description => "Number of votes for this node",
211 minimum => 0,
212 optional => 1,
213 },
214 force => {
215 type => 'boolean',
216 description => "Do not throw error if node already exists.",
217 optional => 1,
218 },
219 ring0_addr => {
220 type => 'string', format => 'address',
221 description => "Hostname (or IP) of the corosync ring0 address of this node.".
222 " Defaults to nodes hostname.",
223 optional => 1,
224 },
225 ring1_addr => {
226 type => 'string', format => 'address',
227 description => "Hostname (or IP) of the corosync ring1 address, this".
228 " needs an valid bindnet1_addr.",
229 optional => 1,
230 },
231 },
232 },
233 returns => {
234 type => "object",
235 properties => {
236 corosync_authkey => {
237 type => 'string',
238 },
239 corosync_conf => {
240 type => 'string',
241 }
242 },
243 },
244 code => sub {
245 my ($param) = @_;
246
247 PVE::Cluster::check_cfs_quorum();
248
249 my $code = sub {
250 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
251 my $nodelist = PVE::Corosync::nodelist($conf);
252 my $totem_cfg = PVE::Corosync::totem_config($conf);
253
254 my $name = $param->{node};
255
256 # ensure we do not reuse an address, that can crash the whole cluster!
257 my $check_duplicate_addr = sub {
258 my $addr = shift;
259 return if !defined($addr);
260
261 while (my ($k, $v) = each %$nodelist) {
262 next if $k eq $name; # allows re-adding a node if force is set
263 if ($v->{ring0_addr} eq $addr || ($v->{ring1_addr} && $v->{ring1_addr} eq $addr)) {
264 die "corosync: address '$addr' already defined by node '$k'\n";
265 }
266 }
267 };
268
269 &$check_duplicate_addr($param->{ring0_addr});
270 &$check_duplicate_addr($param->{ring1_addr});
271
272 $param->{ring0_addr} = $name if !$param->{ring0_addr};
273
274 die "corosync: using 'ring1_addr' parameter needs a configured ring 1 interface!\n"
275 if $param->{ring1_addr} && !defined($totem_cfg->{interface}->{1});
276
277 die "corosync: ring 1 interface configured but 'ring1_addr' parameter not defined!\n"
278 if defined($totem_cfg->{interface}->{1}) && !defined($param->{ring1_addr});
279
280 if (defined(my $res = $nodelist->{$name})) {
281 $param->{nodeid} = $res->{nodeid} if !$param->{nodeid};
282 $param->{votes} = $res->{quorum_votes} if !defined($param->{votes});
283
284 if ($res->{quorum_votes} == $param->{votes} &&
285 $res->{nodeid} == $param->{nodeid} && $param->{force}) {
286 print "forcing overwrite of configured node '$name'\n";
287 } else {
288 die "can't add existing node '$name'\n";
289 }
290 } elsif (!$param->{nodeid}) {
291 my $nodeid = 1;
292
293 while(1) {
294 my $found = 0;
295 foreach my $v (values %$nodelist) {
296 if ($v->{nodeid} eq $nodeid) {
297 $found = 1;
298 $nodeid++;
299 last;
300 }
301 }
302 last if !$found;
303 };
304
305 $param->{nodeid} = $nodeid;
306 }
307
308 $param->{votes} = 1 if !defined($param->{votes});
309
310 PVE::Cluster::gen_local_dirs($name);
311
312 eval { PVE::Cluster::ssh_merge_keys(); };
313 warn $@ if $@;
314
315 $nodelist->{$name} = {
316 ring0_addr => $param->{ring0_addr},
317 nodeid => $param->{nodeid},
318 name => $name,
319 };
320 $nodelist->{$name}->{ring1_addr} = $param->{ring1_addr} if $param->{ring1_addr};
321 $nodelist->{$name}->{quorum_votes} = $param->{votes} if $param->{votes};
322
323 PVE::Cluster::log_msg('notice', 'root@pam', "adding node $name to cluster");
324
325 PVE::Corosync::update_nodelist($conf, $nodelist);
326 };
327
328 $config_change_lock->($code);
329 die $@ if $@;
330
331 my $res = {
332 corosync_authkey => PVE::Tools::file_get_contents($authfile),
333 corosync_conf => PVE::Tools::file_get_contents($clusterconf),
334 };
335
336 return $res;
337 }});
338
339
340 __PACKAGE__->register_method ({
341 name => 'delnode',
342 path => 'nodes/{node}',
343 method => 'DELETE',
344 protected => 1,
345 description => "Removes a node from the cluster configuration.",
346 parameters => {
347 additionalProperties => 0,
348 properties => {
349 node => get_standard_option('pve-node'),
350 },
351 },
352 returns => { type => 'null' },
353 code => sub {
354 my ($param) = @_;
355
356 my $local_node = PVE::INotify::nodename();
357 die "Cannot delete myself from cluster!\n" if $param->{node} eq $local_node;
358
359 PVE::Cluster::check_cfs_quorum();
360
361 my $code = sub {
362 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
363 my $nodelist = PVE::Corosync::nodelist($conf);
364
365 my $node;
366 my $nodeid;
367
368 foreach my $tmp_node (keys %$nodelist) {
369 my $d = $nodelist->{$tmp_node};
370 my $ring0_addr = $d->{ring0_addr};
371 my $ring1_addr = $d->{ring1_addr};
372 if (($tmp_node eq $param->{node}) ||
373 (defined($ring0_addr) && ($ring0_addr eq $param->{node})) ||
374 (defined($ring1_addr) && ($ring1_addr eq $param->{node}))) {
375 $node = $tmp_node;
376 $nodeid = $d->{nodeid};
377 last;
378 }
379 }
380
381 die "Node/IP: $param->{node} is not a known host of the cluster.\n"
382 if !defined($node);
383
384 PVE::Cluster::log_msg('notice', 'root@pam', "deleting node $node from cluster");
385
386 delete $nodelist->{$node};
387
388 PVE::Corosync::update_nodelist($conf, $nodelist);
389
390 PVE::Tools::run_command(['corosync-cfgtool','-k', $nodeid]) if defined($nodeid);
391 };
392
393 $config_change_lock->($code);
394 die $@ if $@;
395
396 return undef;
397 }});
398
399 __PACKAGE__->register_method ({
400 name => 'join',
401 path => 'join',
402 method => 'POST',
403 protected => 1,
404 description => "Joins this node into an existing cluster.",
405 parameters => {
406 additionalProperties => 0,
407 properties => {
408 hostname => {
409 type => 'string',
410 description => "Hostname (or IP) of an existing cluster member."
411 },
412 nodeid => {
413 type => 'integer',
414 description => "Node id for this node.",
415 minimum => 1,
416 optional => 1,
417 },
418 votes => {
419 type => 'integer',
420 description => "Number of votes for this node",
421 minimum => 0,
422 optional => 1,
423 },
424 force => {
425 type => 'boolean',
426 description => "Do not throw error if node already exists.",
427 optional => 1,
428 },
429 ring0_addr => {
430 type => 'string', format => 'address',
431 description => "Hostname (or IP) of the corosync ring0 address of this node.".
432 " Defaults to nodes hostname.",
433 optional => 1,
434 },
435 ring1_addr => {
436 type => 'string', format => 'address',
437 description => "Hostname (or IP) of the corosync ring1 address, this".
438 " needs an valid configured ring 1 interface in the cluster.",
439 optional => 1,
440 },
441 fingerprint => get_standard_option('fingerprint-sha256'),
442 password => {
443 description => "Superuser (root) password of peer node.",
444 type => 'string',
445 maxLength => 128,
446 },
447 },
448 },
449 returns => { type => 'string' },
450 code => sub {
451 my ($param) = @_;
452
453 my $rpcenv = PVE::RPCEnvironment::get();
454 my $authuser = $rpcenv->get_user();
455
456 my $worker = sub {
457 PVE::Cluster::join($param);
458 };
459
460 return $rpcenv->fork_worker('clusterjoin', '', $authuser, $worker);
461 }});
462
463
464 __PACKAGE__->register_method({
465 name => 'totem',
466 path => 'totem',
467 method => 'GET',
468 description => "Get corosync totem protocol settings.",
469 permissions => {
470 check => ['perm', '/', [ 'Sys.Audit' ]],
471 },
472 parameters => {
473 additionalProperties => 0,
474 properties => {},
475 },
476 returns => {
477 type => "object",
478 properties => {},
479 },
480 code => sub {
481 my ($param) = @_;
482
483
484 my $conf = PVE::Cluster::cfs_read_file('corosync.conf');
485
486 my $totem_cfg = $conf->{main}->{totem};
487
488 return $totem_cfg;
489 }});
490
491 1;