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