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