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