]> git.proxmox.com Git - pve-cluster.git/blame - data/PVE/CLI/pvecm.pm
use 'name' over 'ring0_addr' and prepare for rrp
[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;
11use Data::Dumper; # fixme: remove
12use PVE::Tools;
13use PVE::Cluster;
14use PVE::INotify;
15use PVE::JSONSchema;
16use PVE::RPCEnvironment;
17use PVE::CLIHandler;
18
19use base qw(PVE::CLIHandler);
20
21$ENV{HOME} = '/root'; # for ssh-copy-id
22
23my $basedir = "/etc/pve";
24my $clusterconf = "$basedir/corosync.conf";
25my $libdir = "/var/lib/pve-cluster";
26my $backupdir = "/var/lib/pve-cluster/backup";
27my $dbfile = "$libdir/config.db";
28my $authfile = "/etc/corosync/authkey";
29
30sub backup_database {
31
32 print "backup old database\n";
33
34 mkdir $backupdir;
35
36 my $ctime = time();
37 my $cmd = "echo '.dump' |";
38 $cmd .= "sqlite3 '$dbfile' |";
39 $cmd .= "gzip - >'${backupdir}/config-${ctime}.sql.gz'";
40
41 system($cmd) == 0 ||
42 die "can't 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 },
125 },
126 returns => { type => 'null' },
127
128 code => sub {
129 my ($param) = @_;
130
131 -f $clusterconf && die "cluster config '$clusterconf' already exists\n";
132
133 PVE::Cluster::setup_sshd_config();
134 PVE::Cluster::setup_rootsshconfig();
135 PVE::Cluster::setup_ssh_keys();
136
137 -f $authfile || __PACKAGE__->keygen({filename => $authfile});
138
139 -f $authfile || die "no authentication key available\n";
140
141 my $clustername = $param->{clustername};
142
143 $param->{nodeid} = 1 if !$param->{nodeid};
144
145 $param->{votes} = 1 if !defined($param->{votes});
146
147 my $nodename = PVE::INotify::nodename();
148
149 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
150
151 # No, corosync cannot deduce this on its own
152 my $ipversion = Net::IP::ip_is_ipv6($local_ip_address) ? 'ipv6' : 'ipv4';
153
154 my $config = <<_EOD;
155totem {
156 version: 2
157 secauth: on
158 cluster_name: $clustername
159 config_version: 1
160 ip_version: $ipversion
161 interface {
162 ringnumber: 0
163 bindnetaddr: $local_ip_address
164 }
165}
166
167nodelist {
168 node {
169 ring0_addr: $nodename
170 nodeid: $param->{nodeid}
171 quorum_votes: $param->{votes}
172 }
173}
174
175quorum {
176 provider: corosync_votequorum
177}
178
179logging {
180 to_syslog: yes
181 debug: off
182}
183_EOD
184;
185 PVE::Tools::file_set_contents($clusterconf, $config);
186
187 PVE::Cluster::ssh_merge_keys();
188
189 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
190
191 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
192
193 PVE::Tools::run_command('systemctl restart pve-cluster'); # restart
194
195 PVE::Tools::run_command('systemctl restart corosync'); # restart
196
197 return undef;
198}});
199
200__PACKAGE__->register_method ({
201 name => 'addnode',
202 path => 'addnode',
203 method => 'PUT',
204 description => "Adds a node to the cluster configuration.",
205 parameters => {
206 additionalProperties => 0,
207 properties => {
208 node => PVE::JSONSchema::get_standard_option('pve-node'),
209 nodeid => {
210 type => 'integer',
211 description => "Node id for this node.",
212 minimum => 1,
213 optional => 1,
214 },
215 votes => {
216 type => 'integer',
217 description => "Number of votes for this node",
218 minimum => 0,
219 optional => 1,
220 },
221 force => {
222 type => 'boolean',
223 description => "Do not throw error if node already exists.",
224 optional => 1,
225 },
226 },
227 },
228 returns => { type => 'null' },
229
230 code => sub {
231 my ($param) = @_;
232
233 PVE::Cluster::check_cfs_quorum();
234
235 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
236
237 my $nodelist = corosync_nodelist($conf);
238
239 my $name = $param->{node};
240
241 if (defined(my $res = $nodelist->{$name})) {
242 $param->{nodeid} = $res->{nodeid} if !$param->{nodeid};
243 $param->{votes} = $res->{quorum_votes} if !defined($param->{votes});
244
245 if ($res->{quorum_votes} == $param->{votes} &&
246 $res->{nodeid} == $param->{nodeid}) {
247 print "node $name already defined\n";
248 if ($param->{force}) {
249 exit (0);
250 } else {
251 exit (-1);
252 }
253 } else {
254 die "can't add existing node\n";
255 }
256 } elsif (!$param->{nodeid}) {
257 my $nodeid = 1;
258
259 while(1) {
260 my $found = 0;
261 foreach my $v (values %$nodelist) {
262 if ($v->{nodeid} eq $nodeid) {
263 $found = 1;
264 $nodeid++;
265 last;
266 }
267 }
268 last if !$found;
269 };
270
271 $param->{nodeid} = $nodeid;
272 }
273
274 $param->{votes} = 1 if !defined($param->{votes});
275
276 PVE::Cluster::gen_local_dirs($name);
277
278 eval { PVE::Cluster::ssh_merge_keys(); };
279 warn $@ if $@;
280
281 $nodelist->{$name} = { ring0_addr => $name, nodeid => $param->{nodeid} };
282 $nodelist->{$name}->{quorum_votes} = $param->{votes} if $param->{votes};
283
284 corosync_update_nodelist($conf, $nodelist);
285
286 exit (0);
287 }});
288
289
290__PACKAGE__->register_method ({
291 name => 'delnode',
292 path => 'delnode',
293 method => 'PUT',
294 description => "Removes a node to the cluster configuration.",
295 parameters => {
296 additionalProperties => 0,
297 properties => {
298 node => PVE::JSONSchema::get_standard_option('pve-node'),
299 },
300 },
301 returns => { type => 'null' },
302
303 code => sub {
304 my ($param) = @_;
305
306 PVE::Cluster::check_cfs_quorum();
307
308 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
309
310 my $nodelist = corosync_nodelist($conf);
311
312 my $nd = delete $nodelist->{$param->{node}};
313 die "no such node '$param->{node}'\n" if !$nd;
314
315 corosync_update_nodelist($conf, $nodelist);
316
317 return undef;
318 }});
319
320__PACKAGE__->register_method ({
321 name => 'add',
322 path => 'add',
323 method => 'PUT',
324 description => "Adds the current node to an existing cluster.",
325 parameters => {
326 additionalProperties => 0,
327 properties => {
328 hostname => {
329 type => 'string',
330 description => "Hostname (or IP) of an existing cluster member."
331 },
332 nodeid => {
333 type => 'integer',
334 description => "Node id for this node.",
335 minimum => 1,
336 optional => 1,
337 },
338 votes => {
339 type => 'integer',
340 description => "Number of votes for this node",
341 minimum => 0,
342 optional => 1,
343 },
344 force => {
345 type => 'boolean',
346 description => "Do not throw error if node already exists.",
347 optional => 1,
348 },
349 },
350 },
351 returns => { type => 'null' },
352
353 code => sub {
354 my ($param) = @_;
355
356 my $nodename = PVE::INotify::nodename();
357
358 PVE::Cluster::setup_sshd_config();
359 PVE::Cluster::setup_rootsshconfig();
360 PVE::Cluster::setup_ssh_keys();
361
362 my $host = $param->{hostname};
363
364 if (!$param->{force}) {
365
366 if (-f $authfile) {
367 die "authentication key already exists\n";
368 }
369
370 if (-f $clusterconf) {
371 die "cluster config '$clusterconf' already exists\n";
372 }
373
374 my $vmlist = PVE::Cluster::get_vmlist();
375 if ($vmlist && $vmlist->{ids} && scalar(keys %{$vmlist->{ids}})) {
376 die "this host already contains virtual machines - please remove them first\n";
377 }
378
379 if (system("corosync-quorumtool >/dev/null 2>&1") == 0) {
380 die "corosync is already running\n";
381 }
382 }
383
384 # make sure known_hosts is on local filesystem
385 PVE::Cluster::ssh_unmerge_known_hosts();
386
387 my $cmd = "ssh-copy-id -i /root/.ssh/id_rsa 'root\@$host' >/dev/null 2>&1";
388 system ($cmd) == 0 ||
389 die "unable to copy ssh ID\n";
390
391 $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
392 'pvecm', 'addnode', $nodename, '--force', 1];
393
394 push @$cmd, '--nodeid', $param->{nodeid} if $param->{nodeid};
395
396 push @$cmd, '--votes', $param->{votes} if defined($param->{votes});
397
398 if (system (@$cmd) != 0) {
399 my $cmdtxt = join (' ', @$cmd);
400 die "unable to add node: command failed ($cmdtxt)\n";
401 }
402
403 my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
404 mkdir $tmpdir;
405
406 eval {
407 print "copy corosync auth key\n";
408 $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
409 "[$host]:$authfile $clusterconf", $tmpdir];
410
411 system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
412
413 mkdir "/etc/corosync";
414 my $confbase = basename($clusterconf);
415
416 $cmd = "cp '$tmpdir/$confbase' '/etc/corosync/$confbase'";
417 system($cmd) == 0 || die "can't copy cluster configuration\n";
418
419 my $keybase = basename($authfile);
420 system ("cp '$tmpdir/$keybase' '$authfile'") == 0 ||
421 die "can't copy '$tmpdir/$keybase' to '$authfile'\n";
422
423 print "stopping pve-cluster service\n";
424
425 system("umount $basedir -f >/dev/null 2>&1");
426 system("systemctl stop pve-cluster") == 0 ||
427 die "can't stop pve-cluster service\n";
428
429 backup_database();
430
431 unlink $dbfile;
432
433 system("systemctl start pve-cluster") == 0 ||
434 die "starting pve-cluster failed\n";
435
436 system("systemctl start corosync");
437
438 # wait for quorum
439 my $printqmsg = 1;
440 while (!PVE::Cluster::check_cfs_quorum(1)) {
441 if ($printqmsg) {
442 print "waiting for quorum...";
443 STDOUT->flush();
444 $printqmsg = 0;
445 }
446 sleep(1);
447 }
448 print "OK\n" if !$printqmsg;
449
450 # system("systemctl start clvm");
451
452 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
453
454 print "generating node certificates\n";
455 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
456
457 print "merge known_hosts file\n";
458 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
459
460 print "restart services\n";
461 # restart pvedaemon (changed certs)
462 system("systemctl restart pvedaemon");
463 # restart pveproxy (changed certs)
464 system("systemctl restart pveproxy");
465
466 print "successfully added node '$nodename' to cluster.\n";
467 };
468 my $err = $@;
469
470 rmtree $tmpdir;
471
472 die $err if $err;
473
474 return undef;
475 }});
476
477__PACKAGE__->register_method ({
478 name => 'status',
479 path => 'status',
480 method => 'GET',
481 description => "Displays the local view of the cluster status.",
482 parameters => {
483 additionalProperties => 0,
484 properties => {},
485 },
486 returns => { type => 'null' },
487
488 code => sub {
489 my ($param) = @_;
490
491 my $cmd = ['corosync-quorumtool', '-siH'];
492
493 exec (@$cmd);
494
495 exit (-1); # should not be reached
496 }});
497
498__PACKAGE__->register_method ({
499 name => 'nodes',
500 path => 'nodes',
501 method => 'GET',
502 description => "Displays the local view of the cluster nodes.",
503 parameters => {
504 additionalProperties => 0,
505 properties => {},
506 },
507 returns => { type => 'null' },
508
509 code => sub {
510 my ($param) = @_;
511
512 my $cmd = ['corosync-quorumtool', '-l'];
513
514 exec (@$cmd);
515
516 exit (-1); # should not be reached
517 }});
518
519__PACKAGE__->register_method ({
520 name => 'expected',
521 path => 'expected',
522 method => 'PUT',
523 description => "Tells corosync a new value of expected votes.",
524 parameters => {
525 additionalProperties => 0,
526 properties => {
527 expected => {
528 type => 'integer',
529 description => "Expected votes",
530 minimum => 1,
531 },
532 },
533 },
534 returns => { type => 'null' },
535
536 code => sub {
537 my ($param) = @_;
538
539 my $cmd = ['corosync-quorumtool', '-e', $param->{expected}];
540
541 exec (@$cmd);
542
543 exit (-1); # should not be reached
544
545 }});
546
547sub corosync_update_nodelist {
548 my ($conf, $nodelist) = @_;
549
550 delete $conf->{digest};
551
552 my $version = PVE::Cluster::corosync_conf_version($conf);
553 PVE::Cluster::corosync_conf_version($conf, undef, $version + 1);
554
555 my $children = [];
556 foreach my $v (values %$nodelist) {
baf39b62 557 next if !($v->{ring0_addr} || $v->{name});
13d44dc5
DM
558 my $kv = [];
559 foreach my $k (keys %$v) {
560 push @$kv, { key => $k, value => $v->{$k} };
561 }
562 my $ns = { section => 'node', children => $kv };
563 push @$children, $ns;
564 }
565
566 foreach my $main (@{$conf->{children}}) {
567 next if !defined($main->{section});
568 if ($main->{section} eq 'nodelist') {
569 $main->{children} = $children;
570 last;
571 }
572 }
573
574
575 PVE::Cluster::cfs_write_file("corosync.conf.new", $conf);
576
577 rename("/etc/pve/corosync.conf.new", "/etc/pve/corosync.conf")
578 || die "activate corosync.conf.new failed - $!\n";
579}
580
581sub corosync_nodelist {
582 my ($conf) = @_;
13d44dc5
DM
583
584 my $nodelist = {};
baf39b62 585
13d44dc5
DM
586 foreach my $main (@{$conf->{children}}) {
587 next if !defined($main->{section});
588 if ($main->{section} eq 'nodelist') {
589 foreach my $ne (@{$main->{children}}) {
590 next if !defined($ne->{section}) || ($ne->{section} ne 'node');
591 my $node = { quorum_votes => 1 };
baf39b62 592 my $name;
13d44dc5
DM
593 foreach my $child (@{$ne->{children}}) {
594 next if !defined($child->{key});
595 $node->{$child->{key}} = $child->{value};
baf39b62
TL
596 # use 'name' over 'ring0_addr' if set
597 if ($child->{key} eq 'name') {
598 delete $nodelist->{$name} if $name;
599 $name = $child->{value};
600 $nodelist->{$name} = $node;
601 } elsif(!$name && $child->{key} eq 'ring0_addr') {
602 $name = $child->{value};
603 $nodelist->{$name} = $node;
13d44dc5
DM
604 }
605 }
606 }
607 }
608 }
609
610 return $nodelist;
611}
612
baf39b62
TL
613# get a hash representation of the corosync config totem section
614sub corosync_totem_config {
615 my ($conf) = @_;
616
617 my $res = {};
618
619 foreach my $main (@{$conf->{children}}) {
620 next if !defined($main->{section}) ||
621 $main->{section} ne 'totem';
622
623 foreach my $e (@{$main->{children}}) {
624
625 if ($e->{section} && $e->{section} eq 'interface') {
626 my $entry = {};
627
628 $res->{interface} = {};
629
630 foreach my $child (@{$e->{children}}) {
631 next if !defined($child->{key});
632 $entry->{$child->{key}} = $child->{value};
633 if($child->{key} eq 'ringnumber') {
634 $res->{interface}->{$child->{value}} = $entry;
635 }
636 }
637
638 } elsif ($e->{key}) {
639 $res->{$e->{key}} = $e->{value};
640 }
641 }
642 }
643
644 return $res;
645}
646
13d44dc5
DM
647__PACKAGE__->register_method ({
648 name => 'updatecerts',
649 path => 'updatecerts',
650 method => 'PUT',
651 description => "Update node certificates (and generate all needed files/directories).",
652 parameters => {
653 additionalProperties => 0,
654 properties => {
655 force => {
656 description => "Force generation of new SSL certifate.",
657 type => 'boolean',
658 optional => 1,
659 },
660 silent => {
661 description => "Ignore errors (i.e. when cluster has no quorum).",
662 type => 'boolean',
663 optional => 1,
664 },
665 },
666 },
667 returns => { type => 'null' },
668 code => sub {
669 my ($param) = @_;
670
671 PVE::Cluster::setup_rootsshconfig();
672
673 PVE::Cluster::gen_pve_vzdump_symlink();
674
675 if (!PVE::Cluster::check_cfs_quorum(1)) {
676 return undef if $param->{silent};
677 die "no quorum - unable to update files\n";
678 }
679
680 PVE::Cluster::setup_ssh_keys();
681
682 my $nodename = PVE::INotify::nodename();
683
684 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
685
686 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address, $param->{force});
687 PVE::Cluster::ssh_merge_keys();
688 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address);
689 PVE::Cluster::gen_pve_vzdump_files();
690
691 return undef;
692 }});
693
694
695our $cmddef = {
696 keygen => [ __PACKAGE__, 'keygen', ['filename']],
697 create => [ __PACKAGE__, 'create', ['clustername']],
698 add => [ __PACKAGE__, 'add', ['hostname']],
699 addnode => [ __PACKAGE__, 'addnode', ['node']],
700 delnode => [ __PACKAGE__, 'delnode', ['node']],
701 status => [ __PACKAGE__, 'status' ],
702 nodes => [ __PACKAGE__, 'nodes' ],
703 expected => [ __PACKAGE__, 'expected', ['expected']],
704 updatecerts => [ __PACKAGE__, 'updatecerts', []],
705};
706
7071;
708
709__END__
710
711=head1 NAME
712
713pvecm - Proxmox VE cluster manager toolkit
714
715=head1 SYNOPSIS
716
717=include synopsis
718
719=head1 DESCRIPTION
720
721pvecm is a program to manage the cluster configuration. It can be used
722to create a new cluster, join nodes to a cluster, leave the cluster,
723get status information and do various other cluster related tasks.
724
725=include pve_copyright
726
727