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