]> git.proxmox.com Git - pve-cluster.git/blame - data/PVE/CLI/pvecm.pm
pvecm: lock corosync config on addition and deletion
[pve-cluster.git] / data / PVE / CLI / pvecm.pm
CommitLineData
13d44dc5
DM
1package PVE::CLI::pvecm;
2
3use strict;
4use warnings;
5use Getopt::Long;
6use Socket;
7use IO::File;
8use Net::IP;
9use File::Path;
10use File::Basename;
c53b111f 11use Data::Dumper; # fixme: remove
13d44dc5
DM
12use PVE::Tools;
13use PVE::Cluster;
14use PVE::INotify;
15use PVE::JSONSchema;
13d44dc5
DM
16use PVE::CLIHandler;
17
18use base qw(PVE::CLIHandler);
19
20$ENV{HOME} = '/root'; # for ssh-copy-id
21
22my $basedir = "/etc/pve";
23my $clusterconf = "$basedir/corosync.conf";
24my $libdir = "/var/lib/pve-cluster";
25my $backupdir = "/var/lib/pve-cluster/backup";
26my $dbfile = "$libdir/config.db";
27my $authfile = "/etc/corosync/authkey";
28
29sub backup_database {
30
31 print "backup old database\n";
32
33 mkdir $backupdir;
c53b111f 34
13d44dc5 35 my $ctime = time();
de4b4155
FG
36 my $cmd = [
37 ['echo', '.dump'],
38 ['sqlite3', $dbfile],
7eee3d64 39 ['gzip', '-', \ ">${backupdir}/config-${ctime}.sql.gz"],
de4b4155 40 ];
13d44dc5 41
0c17b64f 42 PVE::Tools::run_command($cmd, 'errmsg' => "cannot backup old database\n");
13d44dc5
DM
43
44 # purge older backup
45 my $maxfiles = 10;
46
47 my @bklist = ();
48 foreach my $fn (<$backupdir/config-*.sql.gz>) {
49 if ($fn =~ m!/config-(\d+)\.sql.gz$!) {
50 push @bklist, [$fn, $1];
51 }
52 }
c53b111f 53
13d44dc5
DM
54 @bklist = sort { $b->[1] <=> $a->[1] } @bklist;
55
56 while (scalar (@bklist) >= $maxfiles) {
57 my $d = pop @bklist;
58 print "delete old backup '$d->[0]'\n";
59 unlink $d->[0];
60 }
61}
62
63__PACKAGE__->register_method ({
c53b111f 64 name => 'keygen',
13d44dc5
DM
65 path => 'keygen',
66 method => 'PUT',
67 description => "Generate new cryptographic key for corosync.",
68 parameters => {
69 additionalProperties => 0,
70 properties => {
71 filename => {
72 type => 'string',
73 description => "Output file name"
74 }
75 },
76 },
77 returns => { type => 'null' },
c53b111f 78
13d44dc5
DM
79 code => sub {
80 my ($param) = @_;
81
82 my $filename = $param->{filename};
83
84 # test EUID
85 $> == 0 || die "Error: Authorization key must be generated as root user.\n";
86 my $dirname = dirname($filename);
87 my $basename = basename($filename);
88
89 die "key file '$filename' already exists\n" if -e $filename;
90
91 File::Path::make_path($dirname) if $dirname;
92
93 my $cmd = ['corosync-keygen', '-l', '-k', $filename];
c53b111f 94 PVE::Tools::run_command($cmd);
13d44dc5
DM
95
96 return undef;
97 }});
98
99__PACKAGE__->register_method ({
c53b111f 100 name => 'create',
13d44dc5
DM
101 path => 'create',
102 method => 'PUT',
103 description => "Generate new cluster configuration.",
104 parameters => {
105 additionalProperties => 0,
106 properties => {
107 clustername => {
108 description => "The name of the cluster.",
109 type => 'string', format => 'pve-node',
110 maxLength => 15,
111 },
112 nodeid => {
113 type => 'integer',
114 description => "Node id for this node.",
115 minimum => 1,
116 optional => 1,
117 },
118 votes => {
119 type => 'integer',
120 description => "Number of votes for this node.",
121 minimum => 1,
122 optional => 1,
123 },
14d0000a
TL
124 bindnet0_addr => {
125 type => 'string', format => 'ip',
126 description => "This specifies the network address the corosync ring 0".
127 " executive should bind to and defaults to the local IP address of the node.",
128 optional => 1,
129 },
130 ring0_addr => {
131 type => 'string', format => 'address',
132 description => "Hostname (or IP) of the corosync ring0 address of this node.".
133 " Defaults to the hostname of the node.",
134 optional => 1,
135 },
14d0000a
TL
136 bindnet1_addr => {
137 type => 'string', format => 'ip',
138 description => "This specifies the network address the corosync ring 1".
139 " executive should bind to and is optional.",
140 optional => 1,
141 },
142 ring1_addr => {
143 type => 'string', format => 'address',
144 description => "Hostname (or IP) of the corosync ring1 address, this".
145 " needs an valid bindnet1_addr.",
146 optional => 1,
147 },
148 },
13d44dc5
DM
149 },
150 returns => { type => 'null' },
c53b111f 151
13d44dc5
DM
152 code => sub {
153 my ($param) = @_;
154
155 -f $clusterconf && die "cluster config '$clusterconf' already exists\n";
156
6c0e95b3 157 PVE::Cluster::setup_sshd_config(1);
13d44dc5
DM
158 PVE::Cluster::setup_rootsshconfig();
159 PVE::Cluster::setup_ssh_keys();
160
161 -f $authfile || __PACKAGE__->keygen({filename => $authfile});
162
163 -f $authfile || die "no authentication key available\n";
164
165 my $clustername = $param->{clustername};
166
167 $param->{nodeid} = 1 if !$param->{nodeid};
168
169 $param->{votes} = 1 if !defined($param->{votes});
170
171 my $nodename = PVE::INotify::nodename();
c53b111f 172
13d44dc5
DM
173 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
174
14d0000a
TL
175 $param->{bindnet0_addr} = $local_ip_address
176 if !defined($param->{bindnet0_addr});
177
178 $param->{ring0_addr} = $nodename if !defined($param->{ring0_addr});
179
180 die "Param bindnet1_addr and ring1_addr are dependend, use both or none!\n"
181 if (defined($param->{bindnet1_addr}) != defined($param->{ring1_addr}));
182
183 my $bind_is_ipv6 = Net::IP::ip_is_ipv6($param->{bindnet0_addr});
184
185 # use string as here-doc format distracts more
186 my $interfaces = "interface {\n ringnumber: 0\n" .
187 " bindnetaddr: $param->{bindnet0_addr}\n }";
188
189 my $ring_addresses = "ring0_addr: $param->{ring0_addr}" ;
190
191 # allow use of multiple rings (rrp) at cluster creation time
192 if ($param->{bindnet1_addr}) {
193 die "IPv6 and IPv4 cannot be mixed, use one or the other!\n"
194 if Net::IP::ip_is_ipv6($param->{bindnet1_addr}) != $bind_is_ipv6;
195
14d0000a
TL
196 $interfaces .= "\n interface {\n ringnumber: 1\n" .
197 " bindnetaddr: $param->{bindnet1_addr}\n }\n";
198
606a8904
TL
199 $interfaces .= "rrp_mode: passive\n"; # only passive is stable and tested
200
14d0000a
TL
201 $ring_addresses .= "\n ring1_addr: $param->{ring1_addr}";
202
203 } elsif($param->{rrp_mode} && $param->{rrp_mode} ne 'none') {
204
205 warn "rrp_mode '$param->{rrp_mode}' useless when using only one".
206 " ring, using 'none' instead";
207 # corosync defaults to none if only one interface is configured
208 $param->{rrp_mode} = undef;
209
210 }
211
13d44dc5 212 # No, corosync cannot deduce this on its own
14d0000a 213 my $ipversion = $bind_is_ipv6 ? 'ipv6' : 'ipv4';
13d44dc5
DM
214
215 my $config = <<_EOD;
216totem {
217 version: 2
218 secauth: on
219 cluster_name: $clustername
220 config_version: 1
221 ip_version: $ipversion
14d0000a 222 $interfaces
13d44dc5
DM
223}
224
225nodelist {
226 node {
14d0000a
TL
227 $ring_addresses
228 name: $nodename
13d44dc5
DM
229 nodeid: $param->{nodeid}
230 quorum_votes: $param->{votes}
231 }
232}
14d0000a 233
13d44dc5
DM
234quorum {
235 provider: corosync_votequorum
236}
237
238logging {
239 to_syslog: yes
240 debug: off
241}
242_EOD
14d0000a 243;
13d44dc5
DM
244 PVE::Tools::file_set_contents($clusterconf, $config);
245
246 PVE::Cluster::ssh_merge_keys();
247
248 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
249
250 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
251
252 PVE::Tools::run_command('systemctl restart pve-cluster'); # restart
253
254 PVE::Tools::run_command('systemctl restart corosync'); # restart
c53b111f 255
13d44dc5
DM
256 return undef;
257}});
258
259__PACKAGE__->register_method ({
c53b111f 260 name => 'addnode',
13d44dc5
DM
261 path => 'addnode',
262 method => 'PUT',
263 description => "Adds a node to the cluster configuration.",
264 parameters => {
265 additionalProperties => 0,
266 properties => {
267 node => PVE::JSONSchema::get_standard_option('pve-node'),
268 nodeid => {
269 type => 'integer',
270 description => "Node id for this node.",
271 minimum => 1,
272 optional => 1,
273 },
274 votes => {
275 type => 'integer',
276 description => "Number of votes for this node",
277 minimum => 0,
278 optional => 1,
279 },
280 force => {
281 type => 'boolean',
282 description => "Do not throw error if node already exists.",
283 optional => 1,
284 },
14d0000a
TL
285 ring0_addr => {
286 type => 'string', format => 'address',
287 description => "Hostname (or IP) of the corosync ring0 address of this node.".
288 " Defaults to nodes hostname.",
289 optional => 1,
290 },
291 ring1_addr => {
292 type => 'string', format => 'address',
293 description => "Hostname (or IP) of the corosync ring1 address, this".
294 " needs an valid bindnet1_addr.",
295 optional => 1,
296 },
13d44dc5
DM
297 },
298 },
299 returns => { type => 'null' },
c53b111f 300
13d44dc5
DM
301 code => sub {
302 my ($param) = @_;
303
e73b85c0
TL
304 if (!$param->{force} && (-t STDIN || -t STDOUT)) {
305 die "error: `addnode` should not get called interactively!\nUse ".
306 "`pvecm add <cluster-node>` to add a node to a cluster!\n";
307 }
308
13d44dc5
DM
309 PVE::Cluster::check_cfs_quorum();
310
eaf03102
TL
311 my $code = sub {
312 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
13d44dc5 313
eaf03102 314 my $nodelist = PVE::Cluster::corosync_nodelist($conf);
13d44dc5 315
eaf03102 316 my $totem_cfg = PVE::Cluster::corosync_totem_config($conf);
14d0000a 317
eaf03102 318 my $name = $param->{node};
13d44dc5 319
eaf03102
TL
320 # ensure we do not reuse an address, that can crash the whole cluster!
321 my $check_duplicate_addr = sub {
322 my $addr = shift;
323 return if !defined($addr);
77a1c918 324
eaf03102
TL
325 while (my ($k, $v) = each %$nodelist) {
326 next if $k eq $name; # allows re-adding a node if force is set
327 if ($v->{ring0_addr} eq $addr || ($v->{ring1_addr} && $v->{ring1_addr} eq $addr)) {
328 die "corosync: address '$addr' already defined by node '$k'\n";
329 }
77a1c918 330 }
eaf03102 331 };
77a1c918 332
eaf03102
TL
333 &$check_duplicate_addr($param->{ring0_addr});
334 &$check_duplicate_addr($param->{ring1_addr});
77a1c918 335
eaf03102 336 $param->{ring0_addr} = $name if !$param->{ring0_addr};
14d0000a 337
eaf03102
TL
338 die "corosync: using 'ring1_addr' parameter needs a configured ring 1 interface!\n"
339 if $param->{ring1_addr} && !defined($totem_cfg->{interface}->{1});
14d0000a 340
eaf03102
TL
341 die "corosync: ring 1 interface configured but 'ring1_addr' parameter not defined!\n"
342 if defined($totem_cfg->{interface}->{1}) && !defined($param->{ring1_addr});
bb8583d0 343
eaf03102
TL
344 if (defined(my $res = $nodelist->{$name})) {
345 $param->{nodeid} = $res->{nodeid} if !$param->{nodeid};
346 $param->{votes} = $res->{quorum_votes} if !defined($param->{votes});
13d44dc5 347
eaf03102
TL
348 if ($res->{quorum_votes} == $param->{votes} &&
349 $res->{nodeid} == $param->{nodeid}) {
350 print "node $name already defined\n";
351 if ($param->{force}) {
352 exit (0);
353 } else {
354 exit (-1);
355 }
13d44dc5 356 } else {
eaf03102 357 die "can't add existing node\n";
13d44dc5 358 }
eaf03102
TL
359 } elsif (!$param->{nodeid}) {
360 my $nodeid = 1;
361
362 while(1) {
363 my $found = 0;
364 foreach my $v (values %$nodelist) {
365 if ($v->{nodeid} eq $nodeid) {
366 $found = 1;
367 $nodeid++;
368 last;
369 }
13d44dc5 370 }
eaf03102
TL
371 last if !$found;
372 };
13d44dc5 373
eaf03102
TL
374 $param->{nodeid} = $nodeid;
375 }
13d44dc5 376
eaf03102 377 $param->{votes} = 1 if !defined($param->{votes});
13d44dc5 378
eaf03102 379 PVE::Cluster::gen_local_dirs($name);
13d44dc5 380
eaf03102
TL
381 eval { PVE::Cluster::ssh_merge_keys(); };
382 warn $@ if $@;
383
384 $nodelist->{$name} = {
385 ring0_addr => $param->{ring0_addr},
386 nodeid => $param->{nodeid},
387 name => $name,
388 };
389 $nodelist->{$name}->{ring1_addr} = $param->{ring1_addr} if $param->{ring1_addr};
390 $nodelist->{$name}->{quorum_votes} = $param->{votes} if $param->{votes};
13d44dc5 391
eaf03102 392 PVE::Cluster::corosync_update_nodelist($conf, $nodelist);
14d0000a 393 };
c53b111f 394
eaf03102
TL
395 PVE::Cluster::cfs_lock_file('corosync.conf', 10, $code);
396 die $@ if $@;
c53b111f 397
13d44dc5
DM
398 exit (0);
399 }});
400
401
402__PACKAGE__->register_method ({
c53b111f 403 name => 'delnode',
13d44dc5
DM
404 path => 'delnode',
405 method => 'PUT',
406 description => "Removes a node to the cluster configuration.",
407 parameters => {
408 additionalProperties => 0,
409 properties => {
7aed8248
WL
410 node => {
411 type => 'string',
412 description => "Hostname or IP of the corosync ring0 address of this node.",
413 },
13d44dc5
DM
414 },
415 },
416 returns => { type => 'null' },
c53b111f 417
13d44dc5
DM
418 code => sub {
419 my ($param) = @_;
420
421 PVE::Cluster::check_cfs_quorum();
422
eaf03102
TL
423 my $code = sub {
424 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
13d44dc5 425
eaf03102 426 my $nodelist = PVE::Cluster::corosync_nodelist($conf);
13d44dc5 427
eaf03102
TL
428 my $node;
429 my $nodeid;
7aed8248 430
eaf03102
TL
431 foreach my $tmp_node (keys %$nodelist) {
432 my $d = $nodelist->{$tmp_node};
433 my $ring0_addr = $d->{ring0_addr};
434 my $ring1_addr = $d->{ring1_addr};
435 if (($tmp_node eq $param->{node}) ||
436 (defined($ring0_addr) && ($ring0_addr eq $param->{node})) ||
437 (defined($ring1_addr) && ($ring1_addr eq $param->{node}))) {
438 $node = $tmp_node;
439 $nodeid = $d->{nodeid};
440 last;
441 }
7aed8248 442 }
7aed8248 443
eaf03102 444 die "Node/IP: $param->{node} is not a known host of the cluster.\n"
7aed8248
WL
445 if !defined($node);
446
eaf03102
TL
447 my $our_nodename = PVE::INotify::nodename();
448 die "Cannot delete myself from cluster!\n" if $node eq $our_nodename;
0436a312 449
eaf03102 450 delete $nodelist->{$node};
7aed8248 451
eaf03102
TL
452 PVE::Cluster::corosync_update_nodelist($conf, $nodelist);
453
454 PVE::Tools::run_command(['corosync-cfgtool','-k', $nodeid])
455 if defined($nodeid);
456 };
13d44dc5 457
eaf03102
TL
458 PVE::Cluster::cfs_lock_file('corosync.conf', 10, $code);
459 die $@ if $@;
d09373b4 460
13d44dc5
DM
461 return undef;
462 }});
463
464__PACKAGE__->register_method ({
c53b111f 465 name => 'add',
13d44dc5
DM
466 path => 'add',
467 method => 'PUT',
468 description => "Adds the current node to an existing cluster.",
469 parameters => {
470 additionalProperties => 0,
471 properties => {
472 hostname => {
473 type => 'string',
474 description => "Hostname (or IP) of an existing cluster member."
475 },
476 nodeid => {
477 type => 'integer',
478 description => "Node id for this node.",
479 minimum => 1,
480 optional => 1,
481 },
482 votes => {
483 type => 'integer',
484 description => "Number of votes for this node",
485 minimum => 0,
486 optional => 1,
487 },
488 force => {
489 type => 'boolean',
490 description => "Do not throw error if node already exists.",
491 optional => 1,
492 },
14d0000a
TL
493 ring0_addr => {
494 type => 'string', format => 'address',
495 description => "Hostname (or IP) of the corosync ring0 address of this node.".
496 " Defaults to nodes hostname.",
497 optional => 1,
498 },
499 ring1_addr => {
500 type => 'string', format => 'address',
501 description => "Hostname (or IP) of the corosync ring1 address, this".
502 " needs an valid configured ring 1 interface in the cluster.",
503 optional => 1,
504 },
13d44dc5
DM
505 },
506 },
507 returns => { type => 'null' },
c53b111f 508
13d44dc5
DM
509 code => sub {
510 my ($param) = @_;
511
512 my $nodename = PVE::INotify::nodename();
513
6c0e95b3 514 PVE::Cluster::setup_sshd_config(1);
13d44dc5
DM
515 PVE::Cluster::setup_rootsshconfig();
516 PVE::Cluster::setup_ssh_keys();
517
518 my $host = $param->{hostname};
519
5a630d8f
TL
520 my ($errors, $warnings) = ('', '');
521
522 my $error = sub {
523 my ($msg, $suppress) = @_;
524
525 if ($suppress) {
526 $warnings .= "* $msg\n";
527 } else {
528 $errors .= "* $msg\n";
529 }
530 };
531
13d44dc5 532 if (!$param->{force}) {
c53b111f 533
13d44dc5 534 if (-f $authfile) {
5a630d8f 535 &$error("authentication key '$authfile' already exists", $param->{force});
13d44dc5
DM
536 }
537
538 if (-f $clusterconf) {
5a630d8f 539 &$error("cluster config '$clusterconf' already exists", $param->{force});
13d44dc5
DM
540 }
541
542 my $vmlist = PVE::Cluster::get_vmlist();
543 if ($vmlist && $vmlist->{ids} && scalar(keys %{$vmlist->{ids}})) {
5a630d8f 544 &$error("this host already contains virtual guests", $param->{force});
13d44dc5
DM
545 }
546
cecd0323 547 if (system("corosync-quorumtool -l >/dev/null 2>&1") == 0) {
5a630d8f 548 &$error("corosync is already running, is this node already in a cluster?!", $param->{force});
13d44dc5
DM
549 }
550 }
551
f566b424
TL
552 # check if corosync ring IPs are configured on the current nodes interfaces
553 my $check_ip = sub {
554 my $ip = shift;
555 if (defined($ip)) {
9d69040c
TL
556 if (!PVE::JSONSchema::pve_verify_ip($ip, 1)) {
557 my $host = $ip;
558 eval { $ip = PVE::Network::get_ip_from_hostname($host); };
559 if ($@) {
560 &$error("cannot use '$host': $@\n") ;
561 return;
562 }
563 }
564
f566b424
TL
565 my $cidr = (Net::IP::ip_is_ipv6($ip)) ? "$ip/128" : "$ip/32";
566 my $configured_ips = PVE::Network::get_local_ip_from_cidr($cidr);
567
568 &$error("cannot use IP '$ip', it must be configured exactly once on local node!\n")
569 if (scalar(@$configured_ips) != 1);
570 }
571 };
572
573 &$check_ip($param->{ring0_addr});
574 &$check_ip($param->{ring1_addr});
575
5a630d8f
TL
576 warn "warning, ignore the following errors:\n$warnings" if $warnings;
577 die "detected the following error(s):\n$errors" if $errors;
578
13d44dc5
DM
579 # make sure known_hosts is on local filesystem
580 PVE::Cluster::ssh_unmerge_known_hosts();
581
de4b4155
FG
582 my $cmd = ['ssh-copy-id', '-i', '/root/.ssh/id_rsa', "root\@$host"];
583 PVE::Tools::run_command($cmd, 'outfunc' => sub {}, 'errfunc' => sub {},
584 'errmsg' => "unable to copy ssh ID");
13d44dc5
DM
585
586 $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
587 'pvecm', 'addnode', $nodename, '--force', 1];
588
589 push @$cmd, '--nodeid', $param->{nodeid} if $param->{nodeid};
590
591 push @$cmd, '--votes', $param->{votes} if defined($param->{votes});
592
14d0000a
TL
593 push @$cmd, '--ring0_addr', $param->{ring0_addr} if defined($param->{ring0_addr});
594
595 push @$cmd, '--ring1_addr', $param->{ring1_addr} if defined($param->{ring1_addr});
596
13d44dc5
DM
597 if (system (@$cmd) != 0) {
598 my $cmdtxt = join (' ', @$cmd);
599 die "unable to add node: command failed ($cmdtxt)\n";
600 }
601
602 my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
603 mkdir $tmpdir;
604
605 eval {
606 print "copy corosync auth key\n";
c53b111f 607 $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
13d44dc5
DM
608 "[$host]:$authfile $clusterconf", $tmpdir];
609
610 system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
611
612 mkdir "/etc/corosync";
613 my $confbase = basename($clusterconf);
614
615 $cmd = "cp '$tmpdir/$confbase' '/etc/corosync/$confbase'";
616 system($cmd) == 0 || die "can't copy cluster configuration\n";
617
618 my $keybase = basename($authfile);
619 system ("cp '$tmpdir/$keybase' '$authfile'") == 0 ||
620 die "can't copy '$tmpdir/$keybase' to '$authfile'\n";
621
622 print "stopping pve-cluster service\n";
623
624 system("umount $basedir -f >/dev/null 2>&1");
625 system("systemctl stop pve-cluster") == 0 ||
626 die "can't stop pve-cluster service\n";
627
628 backup_database();
629
630 unlink $dbfile;
631
632 system("systemctl start pve-cluster") == 0 ||
633 die "starting pve-cluster failed\n";
634
635 system("systemctl start corosync");
636
637 # wait for quorum
638 my $printqmsg = 1;
639 while (!PVE::Cluster::check_cfs_quorum(1)) {
640 if ($printqmsg) {
641 print "waiting for quorum...";
642 STDOUT->flush();
643 $printqmsg = 0;
644 }
645 sleep(1);
646 }
647 print "OK\n" if !$printqmsg;
648
13d44dc5
DM
649 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
650
651 print "generating node certificates\n";
c53b111f 652 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
13d44dc5
DM
653
654 print "merge known_hosts file\n";
655 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
656
657 print "restart services\n";
658 # restart pvedaemon (changed certs)
659 system("systemctl restart pvedaemon");
660 # restart pveproxy (changed certs)
661 system("systemctl restart pveproxy");
662
663 print "successfully added node '$nodename' to cluster.\n";
664 };
665 my $err = $@;
666
667 rmtree $tmpdir;
668
669 die $err if $err;
670
671 return undef;
672 }});
673
674__PACKAGE__->register_method ({
c53b111f 675 name => 'status',
13d44dc5
DM
676 path => 'status',
677 method => 'GET',
678 description => "Displays the local view of the cluster status.",
679 parameters => {
680 additionalProperties => 0,
681 properties => {},
682 },
683 returns => { type => 'null' },
c53b111f 684
13d44dc5
DM
685 code => sub {
686 my ($param) = @_;
687
eb51b829
FG
688 PVE::Cluster::check_corosync_conf_exists();
689
13d44dc5
DM
690 my $cmd = ['corosync-quorumtool', '-siH'];
691
692 exec (@$cmd);
693
694 exit (-1); # should not be reached
695 }});
696
697__PACKAGE__->register_method ({
c53b111f 698 name => 'nodes',
13d44dc5
DM
699 path => 'nodes',
700 method => 'GET',
701 description => "Displays the local view of the cluster nodes.",
702 parameters => {
703 additionalProperties => 0,
704 properties => {},
705 },
706 returns => { type => 'null' },
c53b111f 707
13d44dc5
DM
708 code => sub {
709 my ($param) = @_;
710
eb51b829
FG
711 PVE::Cluster::check_corosync_conf_exists();
712
13d44dc5
DM
713 my $cmd = ['corosync-quorumtool', '-l'];
714
715 exec (@$cmd);
716
717 exit (-1); # should not be reached
718 }});
719
720__PACKAGE__->register_method ({
c53b111f 721 name => 'expected',
13d44dc5
DM
722 path => 'expected',
723 method => 'PUT',
724 description => "Tells corosync a new value of expected votes.",
725 parameters => {
726 additionalProperties => 0,
727 properties => {
728 expected => {
729 type => 'integer',
730 description => "Expected votes",
731 minimum => 1,
732 },
733 },
734 },
735 returns => { type => 'null' },
c53b111f 736
13d44dc5
DM
737 code => sub {
738 my ($param) = @_;
739
eb51b829
FG
740 PVE::Cluster::check_corosync_conf_exists();
741
13d44dc5
DM
742 my $cmd = ['corosync-quorumtool', '-e', $param->{expected}];
743
744 exec (@$cmd);
745
746 exit (-1); # should not be reached
747
748 }});
749
13d44dc5 750__PACKAGE__->register_method ({
c53b111f 751 name => 'updatecerts',
13d44dc5
DM
752 path => 'updatecerts',
753 method => 'PUT',
754 description => "Update node certificates (and generate all needed files/directories).",
755 parameters => {
756 additionalProperties => 0,
757 properties => {
758 force => {
759 description => "Force generation of new SSL certifate.",
760 type => 'boolean',
761 optional => 1,
762 },
763 silent => {
764 description => "Ignore errors (i.e. when cluster has no quorum).",
765 type => 'boolean',
766 optional => 1,
767 },
768 },
769 },
770 returns => { type => 'null' },
771 code => sub {
772 my ($param) = @_;
773
6c0e95b3 774 PVE::Cluster::setup_sshd_config(0);
13d44dc5
DM
775 PVE::Cluster::setup_rootsshconfig();
776
777 PVE::Cluster::gen_pve_vzdump_symlink();
778
779 if (!PVE::Cluster::check_cfs_quorum(1)) {
780 return undef if $param->{silent};
781 die "no quorum - unable to update files\n";
782 }
783
784 PVE::Cluster::setup_ssh_keys();
785
786 my $nodename = PVE::INotify::nodename();
787
788 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
789
790 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address, $param->{force});
791 PVE::Cluster::ssh_merge_keys();
792 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address);
793 PVE::Cluster::gen_pve_vzdump_files();
794
795 return undef;
796 }});
797
3a966e22
TL
798__PACKAGE__->register_method ({
799 name => 'mtunnel',
800 path => 'mtunnel',
801 method => 'POST',
802 description => "Used by VM/CT migration - do not use manually.",
803 parameters => {
804 additionalProperties => 0,
f83d8153
TL
805 properties => {
806 get_migration_ip => {
807 type => 'boolean',
808 default => 0,
809 description => 'return the migration IP, if configured',
810 optional => 1,
811 },
812 migration_network => {
813 type => 'string',
814 format => 'CIDR',
815 description => 'the migration network used to detect the local migration IP',
816 optional => 1,
817 },
818 },
3a966e22
TL
819 },
820 returns => { type => 'null'},
821 code => sub {
822 my ($param) = @_;
823
824 if (!PVE::Cluster::check_cfs_quorum(1)) {
825 print "no quorum\n";
826 return undef;
827 }
828
f83d8153
TL
829 if ($param->{get_migration_ip}) {
830 my $network = $param->{migration_network};
43f4c5f6 831 if (my $ip = PVE::Cluster::get_local_migration_ip($network)) {
f83d8153
TL
832 print "ip: '$ip'\n";
833 } else {
43f4c5f6 834 print "no ip\n";
f83d8153
TL
835 }
836 # do not keep tunnel open when asked for migration ip
837 return undef;
838 }
839
3a966e22
TL
840 print "tunnel online\n";
841 *STDOUT->flush();
842
843 while (my $line = <>) {
844 chomp $line;
845 last if $line =~ m/^quit$/;
846 }
847
848 return undef;
849 }});
850
13d44dc5
DM
851
852our $cmddef = {
853 keygen => [ __PACKAGE__, 'keygen', ['filename']],
854 create => [ __PACKAGE__, 'create', ['clustername']],
855 add => [ __PACKAGE__, 'add', ['hostname']],
856 addnode => [ __PACKAGE__, 'addnode', ['node']],
857 delnode => [ __PACKAGE__, 'delnode', ['node']],
858 status => [ __PACKAGE__, 'status' ],
859 nodes => [ __PACKAGE__, 'nodes' ],
860 expected => [ __PACKAGE__, 'expected', ['expected']],
861 updatecerts => [ __PACKAGE__, 'updatecerts', []],
3a966e22 862 mtunnel => [ __PACKAGE__, 'mtunnel', []],
13d44dc5
DM
863};
864
8651;