]> git.proxmox.com Git - pve-cluster.git/blob - data/PVE/pvecm
remove cman, and use corosync.conf instead of cluster.conf
[pve-cluster.git] / data / PVE / pvecm
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5 use Getopt::Long;
6 use Socket;
7 use IO::File;
8 use File::Path;
9 use File::Basename;
10 use Data::Dumper; # fixme: remove
11 use PVE::Tools;
12 use PVE::Cluster;
13 use PVE::INotify;
14 use PVE::JSONSchema;
15 use PVE::CLIHandler;
16
17 use base qw(PVE::CLIHandler);
18
19 $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
20 $ENV{LC_ALL} = 'C';
21 $ENV{HOME} = '/root'; # for ssh-copy-id
22
23 die "please run as root\n" if $> != 0;
24
25 my $nodename = PVE::INotify::nodename();
26 # trigger check that we have resolvable name
27 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
28
29 my $basedir = "/etc/pve";
30 my $clusterconf = "$basedir/corosync.conf";
31 my $libdir = "/var/lib/pve-cluster";
32 my $backupdir = "/var/lib/pve-cluster/backup";
33 my $dbfile = "$libdir/config.db";
34 my $authfile = "/etc/corosync/authkey";
35
36
37 sub backup_database {
38
39 print "backup old database\n";
40
41 mkdir $backupdir;
42
43 my $ctime = time();
44 my $cmd = "echo '.dump' |";
45 $cmd .= "sqlite3 '$dbfile' |";
46 $cmd .= "gzip - >'${backupdir}/config-${ctime}.sql.gz'";
47
48 system($cmd) == 0 ||
49 die "can't backup old database: $!\n";
50
51 # purge older backup
52 my $maxfiles = 10;
53
54 my @bklist = ();
55 foreach my $fn (<$backupdir/config-*.sql.gz>) {
56 if ($fn =~ m!/config-(\d+)\.sql.gz$!) {
57 push @bklist, [$fn, $1];
58 }
59 }
60
61 @bklist = sort { $b->[1] <=> $a->[1] } @bklist;
62
63 while (scalar (@bklist) >= $maxfiles) {
64 my $d = pop @bklist;
65 print "delete old backup '$d->[0]'\n";
66 unlink $d->[0];
67 }
68 }
69
70 __PACKAGE__->register_method ({
71 name => 'keygen',
72 path => 'keygen',
73 method => 'PUT',
74 description => "Generate new cryptographic key for corosync.",
75 parameters => {
76 additionalProperties => 0,
77 properties => {
78 filename => {
79 type => 'string',
80 description => "Output file name"
81 }
82 },
83 },
84 returns => { type => 'null' },
85
86 code => sub {
87 my ($param) = @_;
88
89 my $filename = $param->{filename};
90
91 # test EUID
92 $> == 0 || die "Error: Authorization key must be generated as root user.\n";
93 my $dirname = dirname($filename);
94 my $basename = basename($filename);
95
96 die "key file '$filename' already exists\n" if -e $filename;
97
98 File::Path::make_path($dirname) if $dirname;
99
100 my $cmd = ['corosync-keygen', '-l', '-k', $filename];
101 PVE::Tools::run_command($cmd);
102
103 return undef;
104 }});
105
106 __PACKAGE__->register_method ({
107 name => 'create',
108 path => 'create',
109 method => 'PUT',
110 description => "Generate new cluster configuration.",
111 parameters => {
112 additionalProperties => 0,
113 properties => {
114 clustername => {
115 description => "The name of the cluster.",
116 type => 'string', format => 'pve-node',
117 maxLength => 15,
118 },
119 nodeid => {
120 type => 'integer',
121 description => "Node id for this node.",
122 minimum => 1,
123 optional => 1,
124 },
125 votes => {
126 type => 'integer',
127 description => "Number of votes for this node.",
128 minimum => 1,
129 optional => 1,
130 },
131 },
132 },
133 returns => { type => 'null' },
134
135 code => sub {
136 my ($param) = @_;
137
138 -f $clusterconf && die "cluster config '$clusterconf' already exists\n";
139
140 PVE::Cluster::setup_rootsshconfig();
141 PVE::Cluster::setup_ssh_keys();
142
143 -f $authfile || __PACKAGE__->keygen({filename => $authfile});
144
145 -f $authfile || die "no authentication key available\n";
146
147 my $clustername = $param->{clustername};
148
149 $param->{nodeid} = 1 if !$param->{nodeid};
150
151 $param->{votes} = 1 if !defined($param->{votes});
152
153 my $config = <<_EOD;
154 totem {
155 version: 2
156 secauth: on
157 cluster_name: $clustername
158 config_version: 1
159 }
160
161 nodelist {
162 node {
163 ring0_addr: $nodename
164 nodeid: $param->{nodeid}
165 quorum_votes: $param->{votes}
166 }
167 }
168
169 quorum {
170 provider: corosync_votequorum
171 }
172
173 logging {
174 to_syslog: yes
175 debug: off
176 }
177 _EOD
178 ;
179 PVE::Tools::file_set_contents($clusterconf, $config);
180
181 PVE::Cluster::ssh_merge_keys();
182
183 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
184
185 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
186
187 PVE::Tools::run_command('service pve-cluster restart'); # restart
188
189 PVE::Tools::run_command('service corosync restart'); # restart
190
191 return undef;
192 }});
193
194 __PACKAGE__->register_method ({
195 name => 'addnode',
196 path => 'addnode',
197 method => 'PUT',
198 description => "Adds a node to the cluster configuration.",
199 parameters => {
200 additionalProperties => 0,
201 properties => {
202 node => PVE::JSONSchema::get_standard_option('pve-node'),
203 nodeid => {
204 type => 'integer',
205 description => "Node id for this node.",
206 minimum => 1,
207 optional => 1,
208 },
209 votes => {
210 type => 'integer',
211 description => "Number of votes for this node",
212 minimum => 0,
213 optional => 1,
214 },
215 force => {
216 type => 'boolean',
217 description => "Do not throw error if node already exists.",
218 optional => 1,
219 },
220 },
221 },
222 returns => { type => 'null' },
223
224 code => sub {
225 my ($param) = @_;
226
227 PVE::Cluster::check_cfs_quorum();
228
229 my $lst = lsnode();
230
231 my $name = $param->{node};
232
233 if (defined(my $res = $lst->{$name})) {
234 $param->{nodeid} = $res->{nodeid} if !$param->{nodeid};
235 $param->{votes} = $res->{votes} if !defined($param->{votes});
236
237 if ($res->{votes} == $param->{votes} &&
238 $res->{nodeid} == $param->{nodeid}) {
239 print "node $name already defined\n";
240 if ($param->{force}) {
241 exit (0);
242 } else {
243 exit (-1);
244 }
245 }
246 } elsif (!$param->{nodeid}) {
247 my $nodeid = 1;
248
249 while(1) {
250 my $found = 0;
251 foreach my $v (values %$lst) {
252 if ($v->{nodeid} eq $nodeid) {
253 $found = 1;
254 $nodeid++;
255 last;
256 }
257 }
258 last if !$found;
259 };
260
261 $param->{nodeid} = $nodeid;
262 }
263
264 $param->{votes} = 1 if !defined($param->{votes});
265
266 PVE::Cluster::gen_local_dirs($name);
267
268 eval { PVE::Cluster::ssh_merge_keys(); };
269 warn $@ if $@;
270
271 my $cmd = ['ccs_tool', 'addnode', '-c', $clusterconf];
272
273 # NOTE: cman does not like votes="0"
274 if ($param->{votes}) {
275 push @$cmd, '-v', $param->{votes};
276 }
277
278 push @$cmd, '-n', $param->{nodeid}, $name;
279
280 system(@$cmd) == 0 || exit(-1);
281
282 exit (0);
283 }});
284
285
286 __PACKAGE__->register_method ({
287 name => 'delnode',
288 path => 'delnode',
289 method => 'PUT',
290 description => "Removes a node to the cluster configuration.",
291 parameters => {
292 additionalProperties => 0,
293 properties => {
294 node => PVE::JSONSchema::get_standard_option('pve-node'),
295 },
296 },
297 returns => { type => 'null' },
298
299 code => sub {
300 my ($param) = @_;
301
302 PVE::Cluster::check_cfs_quorum();
303
304 my $cmd = ['ccs_tool', 'delnode', '-c', $clusterconf, $param->{node}];
305
306 exec (@$cmd);
307
308 exit (-1); # should not be reached
309 }});
310
311 __PACKAGE__->register_method ({
312 name => 'add',
313 path => 'add',
314 method => 'PUT',
315 description => "Adds the current node to an existing cluster.",
316 parameters => {
317 additionalProperties => 0,
318 properties => {
319 hostname => {
320 type => 'string',
321 description => "Hostname (or IP) of an existing cluster member."
322 },
323 nodeid => {
324 type => 'integer',
325 description => "Node id for this node.",
326 minimum => 1,
327 optional => 1,
328 },
329 votes => {
330 type => 'integer',
331 description => "Number of votes for this node",
332 minimum => 0,
333 optional => 1,
334 },
335 force => {
336 type => 'boolean',
337 description => "Do not throw error if node already exists.",
338 optional => 1,
339 },
340 },
341 },
342 returns => { type => 'null' },
343
344 code => sub {
345 my ($param) = @_;
346
347 PVE::Cluster::setup_rootsshconfig();
348 PVE::Cluster::setup_ssh_keys();
349
350 my $host = $param->{hostname};
351
352 if (!$param->{force}) {
353
354 if (-f $authfile) {
355 die "authentication key already exists\n";
356 }
357
358 if (-f $clusterconf) {
359 die "cluster config '$clusterconf' already exists\n";
360 }
361
362 my $vmlist = PVE::Cluster::get_vmlist();
363 if ($vmlist && $vmlist->{ids} && scalar(keys %{$vmlist->{ids}})) {
364 die "this host already contains virtual machines - please remove the first\n";
365 }
366
367 if (system("corosync-quorumtool >/dev/null 2>&1") == 0) {
368 die "corosync is already running\n";
369 }
370 }
371
372 # make sure known_hosts is on local filesystem
373 PVE::Cluster::ssh_unmerge_known_hosts();
374
375 my $cmd = "ssh-copy-id -i /root/.ssh/id_rsa 'root\@$host' >/dev/null 2>&1";
376 system ($cmd) == 0 ||
377 die "unable to copy ssh ID\n";
378
379 $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
380 'pvecm', 'addnode', $nodename, '--force', 1];
381
382 push @$cmd, '--nodeid', $param->{nodeid} if $param->{nodeid};
383
384 push @$cmd, '--votes', $param->{votes} if defined($param->{votes});
385
386 if (system (@$cmd) != 0) {
387 my $cmdtxt = join (' ', @$cmd);
388 die "unable to add node: command failed ($cmdtxt)\n";
389 }
390
391 my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
392 mkdir $tmpdir;
393
394 eval {
395 print "copy corosync auth key\n";
396 $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
397 "$host:$authfile $clusterconf", $tmpdir];
398
399 system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
400
401 mkdir "/etc/cluster";
402 my $confbase = basename($clusterconf);
403
404 $cmd = "cp '$tmpdir/$confbase' '/etc/cluster/$confbase'";
405 system($cmd) == 0 || die "can't copy cluster configuration\n";
406
407 my $keybase = basename($authfile);
408 system ("cp '$tmpdir/$keybase' '$authfile'") == 0 ||
409 die "can't copy '$tmpdir/$keybase' to '$authfile'\n";
410
411 print "stopping pve-cluster service\n";
412
413 system("umount $basedir -f >/dev/null 2>&1");
414 system("/etc/init.d/pve-cluster stop") == 0 ||
415 die "can't stop pve-cluster service\n";
416
417 backup_database();
418
419 unlink $dbfile;
420
421 system("/etc/init.d/pve-cluster start") == 0 ||
422 die "starting pve-cluster failed\n";
423
424 system("/etc/init.d/cman start");
425
426 # wait for quorum
427 my $printqmsg = 1;
428 while (!PVE::Cluster::check_cfs_quorum(1)) {
429 if ($printqmsg) {
430 print "waiting for quorum...";
431 STDOUT->flush();
432 $printqmsg = 0;
433 }
434 sleep(1);
435 }
436 print "OK\n" if !$printqmsg;
437
438 system("/etc/init.d/clvm start");
439
440 print "generating node certificates\n";
441 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
442
443 print "merge known_hosts file\n";
444 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
445
446 print "restart services\n";
447 # restart pvedaemon (changed certs)
448 system("/etc/init.d/pvedaemon restart");
449 # restart pveproxy (changed certs)
450 system("/etc/init.d/pveproxy restart");
451
452 print "successfully added node '$nodename' to cluster.\n";
453 };
454 my $err = $@;
455
456 rmtree $tmpdir;
457
458 die $err if $err;
459
460 return undef;
461 }});
462
463 __PACKAGE__->register_method ({
464 name => 'status',
465 path => 'status',
466 method => 'GET',
467 description => "Displays the local view of the cluster status.",
468 parameters => {
469 additionalProperties => 0,
470 properties => {},
471 },
472 returns => { type => 'null' },
473
474 code => sub {
475 my ($param) = @_;
476
477 my $cmd = ['corosync-quorumtool', '-siH'];
478
479 exec (@$cmd);
480
481 exit (-1); # should not be reached
482 }});
483
484 __PACKAGE__->register_method ({
485 name => 'nodes',
486 path => 'nodes',
487 method => 'GET',
488 description => "Displays the local view of the cluster nodes.",
489 parameters => {
490 additionalProperties => 0,
491 properties => {},
492 },
493 returns => { type => 'null' },
494
495 code => sub {
496 my ($param) = @_;
497
498 my $cmd = ['corosync-quorumtool', '-l'];
499
500 exec (@$cmd);
501
502 exit (-1); # should not be reached
503 }});
504
505 __PACKAGE__->register_method ({
506 name => 'expected',
507 path => 'expected',
508 method => 'PUT',
509 description => "Tells CMAN a new value of expected votes.",
510 parameters => {
511 additionalProperties => 0,
512 properties => {
513 expected => {
514 type => 'integer',
515 description => "Expected votes",
516 minimum => 1,
517 },
518 },
519 },
520 returns => { type => 'null' },
521
522 code => sub {
523 my ($param) = @_;
524
525 my $cmd = ['cman_tool', 'expected', '-e', $param->{expected}];
526
527 exec (@$cmd);
528
529 exit (-1); # should not be reached
530
531 }});
532
533 sub lsnode {
534
535 my $res = {};
536
537 my $cmd = ['ccs_tool', 'lsnode', '-c', $clusterconf];
538
539 my $scan = 0;
540 my $parser = sub {
541 my $line = shift;
542
543 if ($line =~ m/^Nodename\s+Votes\s+Nodeid\s/i) {
544 $scan = 1;
545 return;
546 }
547 return if !$scan;
548 if ($line =~ m/^(\S+)\s+(\d+)\s+(\d+)\s/) {
549 $res->{$1} = {
550 name => $1,
551 votes => $2,
552 nodeid => $3 };
553 }
554 };
555
556 PVE::Tools::run_command($cmd, outfunc => $parser);
557 return $res;
558 }
559
560 __PACKAGE__->register_method ({
561 name => 'updatecerts',
562 path => 'updatecerts',
563 method => 'PUT',
564 description => "Update node certificates (and generate all needed files/directories).",
565 parameters => {
566 additionalProperties => 0,
567 properties => {
568 force => {
569 description => "Force generation of new SSL certifate.",
570 type => 'boolean',
571 optional => 1,
572 },
573 silent => {
574 description => "Ignore errors (i.e. when cluster has no quorum).",
575 type => 'boolean',
576 optional => 1,
577 },
578 },
579 },
580 returns => { type => 'null' },
581 code => sub {
582 my ($param) = @_;
583
584 PVE::Cluster::setup_rootsshconfig();
585
586 PVE::Cluster::gen_pve_vzdump_symlink();
587
588 if (!PVE::Cluster::check_cfs_quorum(1)) {
589 return undef if $param->{silent};
590 die "no quorum - unable to update files\n";
591 }
592
593 PVE::Cluster::setup_ssh_keys();
594
595 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address, $param->{force});
596 PVE::Cluster::ssh_merge_keys();
597 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address);
598 PVE::Cluster::gen_pve_vzdump_files();
599
600 return undef;
601 }});
602
603
604 my $cmddef = {
605 keygen => [ __PACKAGE__, 'keygen', ['filename']],
606 create => [ __PACKAGE__, 'create', ['clustername']],
607 add => [ __PACKAGE__, 'add', ['hostname']],
608 addnode => [ __PACKAGE__, 'addnode', ['node']],
609 delnode => [ __PACKAGE__, 'delnode', ['node']],
610 status => [ __PACKAGE__, 'status' ],
611 nodes => [ __PACKAGE__, 'nodes' ],
612 expected => [ __PACKAGE__, 'expected', ['expected']],
613 updatecerts => [ __PACKAGE__, 'updatecerts', []],
614 };
615
616 my $cmd = shift;
617
618 if ($cmd && $cmd ne 'printmanpod' && $cmd ne 'verifyapi') {
619 PVE::Cluster::check_cfs_is_mounted();
620 PVE::Cluster::cfs_update();
621 }
622
623 PVE::CLIHandler::handle_cmd($cmddef, "pvecm", $cmd, \@ARGV, undef, $0);
624
625 exit 0;
626
627 __END__
628
629 =head1 NAME
630
631 pvecm - Proxmox VE cluster manager toolkit
632
633 =head1 SYNOPSIS
634
635 =include synopsis
636
637 =head1 DESCRIPTION
638
639 pvecm is a program to manage the cluster configuration. It can be used
640 to create a new cluster, join nodes to a cluster, leave the cluster,
641 get status information and do various other cluster related tasks.
642
643 =include pve_copyright
644
645