]> git.proxmox.com Git - pve-cluster.git/blob - data/PVE/CLI/pvecm.pm
pvecm addnode: error out on interactive call
[pve-cluster.git] / data / PVE / CLI / pvecm.pm
1 package PVE::CLI::pvecm;
2
3 use strict;
4 use warnings;
5 use Getopt::Long;
6 use Socket;
7 use IO::File;
8 use Net::IP;
9 use File::Path;
10 use File::Basename;
11 use Data::Dumper; # fixme: remove
12 use PVE::Tools;
13 use PVE::Cluster;
14 use PVE::INotify;
15 use PVE::JSONSchema;
16 use PVE::CLIHandler;
17
18 use base qw(PVE::CLIHandler);
19
20 $ENV{HOME} = '/root'; # for ssh-copy-id
21
22 my $basedir = "/etc/pve";
23 my $clusterconf = "$basedir/corosync.conf";
24 my $libdir = "/var/lib/pve-cluster";
25 my $backupdir = "/var/lib/pve-cluster/backup";
26 my $dbfile = "$libdir/config.db";
27 my $authfile = "/etc/corosync/authkey";
28
29 sub backup_database {
30
31 print "backup old database\n";
32
33 mkdir $backupdir;
34
35 my $ctime = time();
36 my $cmd = [
37 ['echo', '.dump'],
38 ['sqlite3', $dbfile],
39 ['gzip', '-', \ ">${backupdir}/config-${ctime}.sql.gz"],
40 ];
41
42 PVE::Tools::run_command($cmd, 'errmsg' => "cannot backup old database\n");
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 }
53
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 ({
64 name => 'keygen',
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' },
78
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];
94 PVE::Tools::run_command($cmd);
95
96 return undef;
97 }});
98
99 __PACKAGE__->register_method ({
100 name => 'create',
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 },
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 },
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 },
149 },
150 returns => { type => 'null' },
151
152 code => sub {
153 my ($param) = @_;
154
155 -f $clusterconf && die "cluster config '$clusterconf' already exists\n";
156
157 PVE::Cluster::setup_sshd_config(1);
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();
172
173 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
174
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
196 $interfaces .= "\n interface {\n ringnumber: 1\n" .
197 " bindnetaddr: $param->{bindnet1_addr}\n }\n";
198
199 $interfaces .= "rrp_mode: passive\n"; # only passive is stable and tested
200
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
212 # No, corosync cannot deduce this on its own
213 my $ipversion = $bind_is_ipv6 ? 'ipv6' : 'ipv4';
214
215 my $config = <<_EOD;
216 totem {
217 version: 2
218 secauth: on
219 cluster_name: $clustername
220 config_version: 1
221 ip_version: $ipversion
222 $interfaces
223 }
224
225 nodelist {
226 node {
227 $ring_addresses
228 name: $nodename
229 nodeid: $param->{nodeid}
230 quorum_votes: $param->{votes}
231 }
232 }
233
234 quorum {
235 provider: corosync_votequorum
236 }
237
238 logging {
239 to_syslog: yes
240 debug: off
241 }
242 _EOD
243 ;
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
255
256 return undef;
257 }});
258
259 __PACKAGE__->register_method ({
260 name => 'addnode',
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 },
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 },
297 },
298 },
299 returns => { type => 'null' },
300
301 code => sub {
302 my ($param) = @_;
303
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
309 PVE::Cluster::check_cfs_quorum();
310
311 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
312
313 my $nodelist = PVE::Cluster::corosync_nodelist($conf);
314
315 my $totem_cfg = PVE::Cluster::corosync_totem_config($conf);
316
317 my $name = $param->{node};
318
319 $param->{ring0_addr} = $name if !$param->{ring0_addr};
320
321 die " ring1_addr needs a configured ring 1 interface!\n"
322 if $param->{ring1_addr} && !defined($totem_cfg->{interface}->{1});
323
324 if (defined(my $res = $nodelist->{$name})) {
325 $param->{nodeid} = $res->{nodeid} if !$param->{nodeid};
326 $param->{votes} = $res->{quorum_votes} if !defined($param->{votes});
327
328 if ($res->{quorum_votes} == $param->{votes} &&
329 $res->{nodeid} == $param->{nodeid}) {
330 print "node $name already defined\n";
331 if ($param->{force}) {
332 exit (0);
333 } else {
334 exit (-1);
335 }
336 } else {
337 die "can't add existing node\n";
338 }
339 } elsif (!$param->{nodeid}) {
340 my $nodeid = 1;
341
342 while(1) {
343 my $found = 0;
344 foreach my $v (values %$nodelist) {
345 if ($v->{nodeid} eq $nodeid) {
346 $found = 1;
347 $nodeid++;
348 last;
349 }
350 }
351 last if !$found;
352 };
353
354 $param->{nodeid} = $nodeid;
355 }
356
357 $param->{votes} = 1 if !defined($param->{votes});
358
359 PVE::Cluster::gen_local_dirs($name);
360
361 eval { PVE::Cluster::ssh_merge_keys(); };
362 warn $@ if $@;
363
364 $nodelist->{$name} = {
365 ring0_addr => $param->{ring0_addr},
366 nodeid => $param->{nodeid},
367 name => $name,
368 };
369 $nodelist->{$name}->{ring1_addr} = $param->{ring1_addr} if $param->{ring1_addr};
370 $nodelist->{$name}->{quorum_votes} = $param->{votes} if $param->{votes};
371
372 PVE::Cluster::corosync_update_nodelist($conf, $nodelist);
373
374 exit (0);
375 }});
376
377
378 __PACKAGE__->register_method ({
379 name => 'delnode',
380 path => 'delnode',
381 method => 'PUT',
382 description => "Removes a node to the cluster configuration.",
383 parameters => {
384 additionalProperties => 0,
385 properties => {
386 node => {
387 type => 'string',
388 description => "Hostname or IP of the corosync ring0 address of this node.",
389 },
390 },
391 },
392 returns => { type => 'null' },
393
394 code => sub {
395 my ($param) = @_;
396
397 PVE::Cluster::check_cfs_quorum();
398
399 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
400
401 my $nodelist = PVE::Cluster::corosync_nodelist($conf);
402
403 my $node;
404 my $nodeid;
405
406 foreach my $tmp_node (keys %$nodelist) {
407 my $d = $nodelist->{$tmp_node};
408 my $ring0_addr = $d->{ring0_addr};
409 my $ring1_addr = $d->{ring1_addr};
410 if (($tmp_node eq $param->{node}) ||
411 (defined($ring0_addr) && ($ring0_addr eq $param->{node})) ||
412 (defined($ring1_addr) && ($ring1_addr eq $param->{node}))) {
413 $node = $tmp_node;
414 $nodeid = $d->{nodeid};
415 last;
416 }
417 }
418
419 die "Node/IP: $param->{node} is not a known host of the cluster.\n"
420 if !defined($node);
421
422 delete $nodelist->{$node};
423
424 PVE::Cluster::corosync_update_nodelist($conf, $nodelist);
425
426 PVE::Tools::run_command(['corosync-cfgtool','-k', $nodeid])
427 if defined($nodeid);
428
429 return undef;
430 }});
431
432 __PACKAGE__->register_method ({
433 name => 'add',
434 path => 'add',
435 method => 'PUT',
436 description => "Adds the current node to an existing cluster.",
437 parameters => {
438 additionalProperties => 0,
439 properties => {
440 hostname => {
441 type => 'string',
442 description => "Hostname (or IP) of an existing cluster member."
443 },
444 nodeid => {
445 type => 'integer',
446 description => "Node id for this node.",
447 minimum => 1,
448 optional => 1,
449 },
450 votes => {
451 type => 'integer',
452 description => "Number of votes for this node",
453 minimum => 0,
454 optional => 1,
455 },
456 force => {
457 type => 'boolean',
458 description => "Do not throw error if node already exists.",
459 optional => 1,
460 },
461 ring0_addr => {
462 type => 'string', format => 'address',
463 description => "Hostname (or IP) of the corosync ring0 address of this node.".
464 " Defaults to nodes hostname.",
465 optional => 1,
466 },
467 ring1_addr => {
468 type => 'string', format => 'address',
469 description => "Hostname (or IP) of the corosync ring1 address, this".
470 " needs an valid configured ring 1 interface in the cluster.",
471 optional => 1,
472 },
473 },
474 },
475 returns => { type => 'null' },
476
477 code => sub {
478 my ($param) = @_;
479
480 my $nodename = PVE::INotify::nodename();
481
482 PVE::Cluster::setup_sshd_config(1);
483 PVE::Cluster::setup_rootsshconfig();
484 PVE::Cluster::setup_ssh_keys();
485
486 my $host = $param->{hostname};
487
488 if (!$param->{force}) {
489
490 if (-f $authfile) {
491 die "authentication key already exists\n";
492 }
493
494 if (-f $clusterconf) {
495 die "cluster config '$clusterconf' already exists\n";
496 }
497
498 my $vmlist = PVE::Cluster::get_vmlist();
499 if ($vmlist && $vmlist->{ids} && scalar(keys %{$vmlist->{ids}})) {
500 die "this host already contains virtual machines - please remove them first\n";
501 }
502
503 if (system("corosync-quorumtool -l >/dev/null 2>&1") == 0) {
504 die "corosync is already running\n";
505 }
506 }
507
508 # make sure known_hosts is on local filesystem
509 PVE::Cluster::ssh_unmerge_known_hosts();
510
511 my $cmd = ['ssh-copy-id', '-i', '/root/.ssh/id_rsa', "root\@$host"];
512 PVE::Tools::run_command($cmd, 'outfunc' => sub {}, 'errfunc' => sub {},
513 'errmsg' => "unable to copy ssh ID");
514
515 $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
516 'pvecm', 'addnode', $nodename, '--force', 1];
517
518 push @$cmd, '--nodeid', $param->{nodeid} if $param->{nodeid};
519
520 push @$cmd, '--votes', $param->{votes} if defined($param->{votes});
521
522 push @$cmd, '--ring0_addr', $param->{ring0_addr} if defined($param->{ring0_addr});
523
524 push @$cmd, '--ring1_addr', $param->{ring1_addr} if defined($param->{ring1_addr});
525
526 if (system (@$cmd) != 0) {
527 my $cmdtxt = join (' ', @$cmd);
528 die "unable to add node: command failed ($cmdtxt)\n";
529 }
530
531 my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
532 mkdir $tmpdir;
533
534 eval {
535 print "copy corosync auth key\n";
536 $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
537 "[$host]:$authfile $clusterconf", $tmpdir];
538
539 system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
540
541 mkdir "/etc/corosync";
542 my $confbase = basename($clusterconf);
543
544 $cmd = "cp '$tmpdir/$confbase' '/etc/corosync/$confbase'";
545 system($cmd) == 0 || die "can't copy cluster configuration\n";
546
547 my $keybase = basename($authfile);
548 system ("cp '$tmpdir/$keybase' '$authfile'") == 0 ||
549 die "can't copy '$tmpdir/$keybase' to '$authfile'\n";
550
551 print "stopping pve-cluster service\n";
552
553 system("umount $basedir -f >/dev/null 2>&1");
554 system("systemctl stop pve-cluster") == 0 ||
555 die "can't stop pve-cluster service\n";
556
557 backup_database();
558
559 unlink $dbfile;
560
561 system("systemctl start pve-cluster") == 0 ||
562 die "starting pve-cluster failed\n";
563
564 system("systemctl start corosync");
565
566 # wait for quorum
567 my $printqmsg = 1;
568 while (!PVE::Cluster::check_cfs_quorum(1)) {
569 if ($printqmsg) {
570 print "waiting for quorum...";
571 STDOUT->flush();
572 $printqmsg = 0;
573 }
574 sleep(1);
575 }
576 print "OK\n" if !$printqmsg;
577
578 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
579
580 print "generating node certificates\n";
581 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
582
583 print "merge known_hosts file\n";
584 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
585
586 print "restart services\n";
587 # restart pvedaemon (changed certs)
588 system("systemctl restart pvedaemon");
589 # restart pveproxy (changed certs)
590 system("systemctl restart pveproxy");
591
592 print "successfully added node '$nodename' to cluster.\n";
593 };
594 my $err = $@;
595
596 rmtree $tmpdir;
597
598 die $err if $err;
599
600 return undef;
601 }});
602
603 __PACKAGE__->register_method ({
604 name => 'status',
605 path => 'status',
606 method => 'GET',
607 description => "Displays the local view of the cluster status.",
608 parameters => {
609 additionalProperties => 0,
610 properties => {},
611 },
612 returns => { type => 'null' },
613
614 code => sub {
615 my ($param) = @_;
616
617 PVE::Cluster::check_corosync_conf_exists();
618
619 my $cmd = ['corosync-quorumtool', '-siH'];
620
621 exec (@$cmd);
622
623 exit (-1); # should not be reached
624 }});
625
626 __PACKAGE__->register_method ({
627 name => 'nodes',
628 path => 'nodes',
629 method => 'GET',
630 description => "Displays the local view of the cluster nodes.",
631 parameters => {
632 additionalProperties => 0,
633 properties => {},
634 },
635 returns => { type => 'null' },
636
637 code => sub {
638 my ($param) = @_;
639
640 PVE::Cluster::check_corosync_conf_exists();
641
642 my $cmd = ['corosync-quorumtool', '-l'];
643
644 exec (@$cmd);
645
646 exit (-1); # should not be reached
647 }});
648
649 __PACKAGE__->register_method ({
650 name => 'expected',
651 path => 'expected',
652 method => 'PUT',
653 description => "Tells corosync a new value of expected votes.",
654 parameters => {
655 additionalProperties => 0,
656 properties => {
657 expected => {
658 type => 'integer',
659 description => "Expected votes",
660 minimum => 1,
661 },
662 },
663 },
664 returns => { type => 'null' },
665
666 code => sub {
667 my ($param) = @_;
668
669 PVE::Cluster::check_corosync_conf_exists();
670
671 my $cmd = ['corosync-quorumtool', '-e', $param->{expected}];
672
673 exec (@$cmd);
674
675 exit (-1); # should not be reached
676
677 }});
678
679 __PACKAGE__->register_method ({
680 name => 'updatecerts',
681 path => 'updatecerts',
682 method => 'PUT',
683 description => "Update node certificates (and generate all needed files/directories).",
684 parameters => {
685 additionalProperties => 0,
686 properties => {
687 force => {
688 description => "Force generation of new SSL certifate.",
689 type => 'boolean',
690 optional => 1,
691 },
692 silent => {
693 description => "Ignore errors (i.e. when cluster has no quorum).",
694 type => 'boolean',
695 optional => 1,
696 },
697 },
698 },
699 returns => { type => 'null' },
700 code => sub {
701 my ($param) = @_;
702
703 PVE::Cluster::setup_sshd_config(0);
704 PVE::Cluster::setup_rootsshconfig();
705
706 PVE::Cluster::gen_pve_vzdump_symlink();
707
708 if (!PVE::Cluster::check_cfs_quorum(1)) {
709 return undef if $param->{silent};
710 die "no quorum - unable to update files\n";
711 }
712
713 PVE::Cluster::setup_ssh_keys();
714
715 my $nodename = PVE::INotify::nodename();
716
717 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
718
719 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address, $param->{force});
720 PVE::Cluster::ssh_merge_keys();
721 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address);
722 PVE::Cluster::gen_pve_vzdump_files();
723
724 return undef;
725 }});
726
727 __PACKAGE__->register_method ({
728 name => 'mtunnel',
729 path => 'mtunnel',
730 method => 'POST',
731 description => "Used by VM/CT migration - do not use manually.",
732 parameters => {
733 additionalProperties => 0,
734 properties => {
735 get_migration_ip => {
736 type => 'boolean',
737 default => 0,
738 description => 'return the migration IP, if configured',
739 optional => 1,
740 },
741 migration_network => {
742 type => 'string',
743 format => 'CIDR',
744 description => 'the migration network used to detect the local migration IP',
745 optional => 1,
746 },
747 },
748 },
749 returns => { type => 'null'},
750 code => sub {
751 my ($param) = @_;
752
753 if (!PVE::Cluster::check_cfs_quorum(1)) {
754 print "no quorum\n";
755 return undef;
756 }
757
758 if ($param->{get_migration_ip}) {
759 my $network = $param->{migration_network};
760 if (my $ip = PVE::Cluster::get_local_migration_ip($network)) {
761 print "ip: '$ip'\n";
762 } else {
763 print "no ip\n";
764 }
765 # do not keep tunnel open when asked for migration ip
766 return undef;
767 }
768
769 print "tunnel online\n";
770 *STDOUT->flush();
771
772 while (my $line = <>) {
773 chomp $line;
774 last if $line =~ m/^quit$/;
775 }
776
777 return undef;
778 }});
779
780
781 our $cmddef = {
782 keygen => [ __PACKAGE__, 'keygen', ['filename']],
783 create => [ __PACKAGE__, 'create', ['clustername']],
784 add => [ __PACKAGE__, 'add', ['hostname']],
785 addnode => [ __PACKAGE__, 'addnode', ['node']],
786 delnode => [ __PACKAGE__, 'delnode', ['node']],
787 status => [ __PACKAGE__, 'status' ],
788 nodes => [ __PACKAGE__, 'nodes' ],
789 expected => [ __PACKAGE__, 'expected', ['expected']],
790 updatecerts => [ __PACKAGE__, 'updatecerts', []],
791 mtunnel => [ __PACKAGE__, 'mtunnel', []],
792 };
793
794 1;