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