]> git.proxmox.com Git - pve-cluster.git/blob - data/PVE/CLI/pvecm.pm
25273327f53cfc40a14c2b3efff01187720870a4
[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' => "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 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 rrp_mode => {
137 type => 'string',
138 enum => ['none', 'active', 'passive'],
139 description => "This specifies the mode of redundant ring, which" .
140 " may be none, active or passive. Using multiple interfaces".
141 " only allows 'active' or 'passive'.",
142 default => 'none',
143 optional => 1,
144 },
145 bindnet1_addr => {
146 type => 'string', format => 'ip',
147 description => "This specifies the network address the corosync ring 1".
148 " executive should bind to and is optional.",
149 optional => 1,
150 },
151 ring1_addr => {
152 type => 'string', format => 'address',
153 description => "Hostname (or IP) of the corosync ring1 address, this".
154 " needs an valid bindnet1_addr.",
155 optional => 1,
156 },
157 },
158 },
159 returns => { type => 'null' },
160
161 code => sub {
162 my ($param) = @_;
163
164 -f $clusterconf && die "cluster config '$clusterconf' already exists\n";
165
166 PVE::Cluster::setup_sshd_config();
167 PVE::Cluster::setup_rootsshconfig();
168 PVE::Cluster::setup_ssh_keys();
169
170 -f $authfile || __PACKAGE__->keygen({filename => $authfile});
171
172 -f $authfile || die "no authentication key available\n";
173
174 my $clustername = $param->{clustername};
175
176 $param->{nodeid} = 1 if !$param->{nodeid};
177
178 $param->{votes} = 1 if !defined($param->{votes});
179
180 my $nodename = PVE::INotify::nodename();
181
182 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
183
184 $param->{bindnet0_addr} = $local_ip_address
185 if !defined($param->{bindnet0_addr});
186
187 $param->{ring0_addr} = $nodename if !defined($param->{ring0_addr});
188
189 die "Param bindnet1_addr and ring1_addr are dependend, use both or none!\n"
190 if (defined($param->{bindnet1_addr}) != defined($param->{ring1_addr}));
191
192 my $bind_is_ipv6 = Net::IP::ip_is_ipv6($param->{bindnet0_addr});
193
194 # use string as here-doc format distracts more
195 my $interfaces = "interface {\n ringnumber: 0\n" .
196 " bindnetaddr: $param->{bindnet0_addr}\n }";
197
198 my $ring_addresses = "ring0_addr: $param->{ring0_addr}" ;
199
200 # allow use of multiple rings (rrp) at cluster creation time
201 if ($param->{bindnet1_addr}) {
202 die "IPv6 and IPv4 cannot be mixed, use one or the other!\n"
203 if Net::IP::ip_is_ipv6($param->{bindnet1_addr}) != $bind_is_ipv6;
204
205 die "rrp_mode 'none' is not allowed when using multiple interfaces,".
206 " use 'active' or 'passive'!\n"
207 if !$param->{rrp_mode} || $param->{rrp_mode} eq 'none';
208
209 $interfaces .= "\n interface {\n ringnumber: 1\n" .
210 " bindnetaddr: $param->{bindnet1_addr}\n }\n";
211
212 $ring_addresses .= "\n ring1_addr: $param->{ring1_addr}";
213
214 } elsif($param->{rrp_mode} && $param->{rrp_mode} ne 'none') {
215
216 warn "rrp_mode '$param->{rrp_mode}' useless when using only one".
217 " ring, using 'none' instead";
218 # corosync defaults to none if only one interface is configured
219 $param->{rrp_mode} = undef;
220
221 }
222
223 $interfaces = "rrp_mode: $param->{rrp_mode}\n " . $interfaces
224 if $param->{rrp_mode};
225
226 # No, corosync cannot deduce this on its own
227 my $ipversion = $bind_is_ipv6 ? 'ipv6' : 'ipv4';
228
229 my $config = <<_EOD;
230 totem {
231 version: 2
232 secauth: on
233 cluster_name: $clustername
234 config_version: 1
235 ip_version: $ipversion
236 $interfaces
237 }
238
239 nodelist {
240 node {
241 $ring_addresses
242 name: $nodename
243 nodeid: $param->{nodeid}
244 quorum_votes: $param->{votes}
245 }
246 }
247
248 quorum {
249 provider: corosync_votequorum
250 }
251
252 logging {
253 to_syslog: yes
254 debug: off
255 }
256 _EOD
257 ;
258 PVE::Tools::file_set_contents($clusterconf, $config);
259
260 PVE::Cluster::ssh_merge_keys();
261
262 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
263
264 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
265
266 PVE::Tools::run_command('systemctl restart pve-cluster'); # restart
267
268 PVE::Tools::run_command('systemctl restart corosync'); # restart
269
270 return undef;
271 }});
272
273 __PACKAGE__->register_method ({
274 name => 'addnode',
275 path => 'addnode',
276 method => 'PUT',
277 description => "Adds a node to the cluster configuration.",
278 parameters => {
279 additionalProperties => 0,
280 properties => {
281 node => PVE::JSONSchema::get_standard_option('pve-node'),
282 nodeid => {
283 type => 'integer',
284 description => "Node id for this node.",
285 minimum => 1,
286 optional => 1,
287 },
288 votes => {
289 type => 'integer',
290 description => "Number of votes for this node",
291 minimum => 0,
292 optional => 1,
293 },
294 force => {
295 type => 'boolean',
296 description => "Do not throw error if node already exists.",
297 optional => 1,
298 },
299 ring0_addr => {
300 type => 'string', format => 'address',
301 description => "Hostname (or IP) of the corosync ring0 address of this node.".
302 " Defaults to nodes hostname.",
303 optional => 1,
304 },
305 ring1_addr => {
306 type => 'string', format => 'address',
307 description => "Hostname (or IP) of the corosync ring1 address, this".
308 " needs an valid bindnet1_addr.",
309 optional => 1,
310 },
311 },
312 },
313 returns => { type => 'null' },
314
315 code => sub {
316 my ($param) = @_;
317
318 PVE::Cluster::check_cfs_quorum();
319
320 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
321
322 my $nodelist = corosync_nodelist($conf);
323
324 my $totem_cfg = corosync_totem_config($conf);
325
326 my $name = $param->{node};
327
328 $param->{ring0_addr} = $name if !$param->{ring0_addr};
329
330 die " ring1_addr needs a configured ring 1 interface!\n"
331 if $param->{ring1_addr} && !defined($totem_cfg->{interface}->{1});
332
333 if (defined(my $res = $nodelist->{$name})) {
334 $param->{nodeid} = $res->{nodeid} if !$param->{nodeid};
335 $param->{votes} = $res->{quorum_votes} if !defined($param->{votes});
336
337 if ($res->{quorum_votes} == $param->{votes} &&
338 $res->{nodeid} == $param->{nodeid}) {
339 print "node $name already defined\n";
340 if ($param->{force}) {
341 exit (0);
342 } else {
343 exit (-1);
344 }
345 } else {
346 die "can't add existing node\n";
347 }
348 } elsif (!$param->{nodeid}) {
349 my $nodeid = 1;
350
351 while(1) {
352 my $found = 0;
353 foreach my $v (values %$nodelist) {
354 if ($v->{nodeid} eq $nodeid) {
355 $found = 1;
356 $nodeid++;
357 last;
358 }
359 }
360 last if !$found;
361 };
362
363 $param->{nodeid} = $nodeid;
364 }
365
366 $param->{votes} = 1 if !defined($param->{votes});
367
368 PVE::Cluster::gen_local_dirs($name);
369
370 eval { PVE::Cluster::ssh_merge_keys(); };
371 warn $@ if $@;
372
373 $nodelist->{$name} = {
374 ring0_addr => $param->{ring0_addr},
375 nodeid => $param->{nodeid},
376 name => $name,
377 };
378 $nodelist->{$name}->{ring1_addr} = $param->{ring1_addr} if $param->{ring1_addr};
379 $nodelist->{$name}->{quorum_votes} = $param->{votes} if $param->{votes};
380
381 corosync_update_nodelist($conf, $nodelist);
382
383 exit (0);
384 }});
385
386
387 __PACKAGE__->register_method ({
388 name => 'delnode',
389 path => 'delnode',
390 method => 'PUT',
391 description => "Removes a node to the cluster configuration.",
392 parameters => {
393 additionalProperties => 0,
394 properties => {
395 node => {
396 type => 'string',
397 description => "Hostname or IP of the corosync ring0 address of this node.",
398 },
399 },
400 },
401 returns => { type => 'null' },
402
403 code => sub {
404 my ($param) = @_;
405
406 PVE::Cluster::check_cfs_quorum();
407
408 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
409
410 my $nodelist = corosync_nodelist($conf);
411
412 my $node;
413
414 foreach my $tmp_node (keys %$nodelist) {
415 my $ring0_addr = $nodelist->{$tmp_node}->{ring0_addr};
416 my $ring1_addr = $nodelist->{$tmp_node}->{ring1_addr};
417 if (($tmp_node eq $param->{node}) ||
418 (defined($ring0_addr) && ($ring0_addr eq $param->{node})) ||
419 (defined($ring1_addr) && ($ring1_addr eq $param->{node}))) {
420 $node = $tmp_node;
421 last;
422 }
423 }
424
425 die "Node/IP: $param->{node} is not a known host of the cluster.\n"
426 if !defined($node);
427
428 delete $nodelist->{$node};
429
430 corosync_update_nodelist($conf, $nodelist);
431
432 return undef;
433 }});
434
435 __PACKAGE__->register_method ({
436 name => 'add',
437 path => 'add',
438 method => 'PUT',
439 description => "Adds the current node to an existing cluster.",
440 parameters => {
441 additionalProperties => 0,
442 properties => {
443 hostname => {
444 type => 'string',
445 description => "Hostname (or IP) of an existing cluster member."
446 },
447 nodeid => {
448 type => 'integer',
449 description => "Node id for this node.",
450 minimum => 1,
451 optional => 1,
452 },
453 votes => {
454 type => 'integer',
455 description => "Number of votes for this node",
456 minimum => 0,
457 optional => 1,
458 },
459 force => {
460 type => 'boolean',
461 description => "Do not throw error if node already exists.",
462 optional => 1,
463 },
464 ring0_addr => {
465 type => 'string', format => 'address',
466 description => "Hostname (or IP) of the corosync ring0 address of this node.".
467 " Defaults to nodes hostname.",
468 optional => 1,
469 },
470 ring1_addr => {
471 type => 'string', format => 'address',
472 description => "Hostname (or IP) of the corosync ring1 address, this".
473 " needs an valid configured ring 1 interface in the cluster.",
474 optional => 1,
475 },
476 },
477 },
478 returns => { type => 'null' },
479
480 code => sub {
481 my ($param) = @_;
482
483 my $nodename = PVE::INotify::nodename();
484
485 PVE::Cluster::setup_sshd_config();
486 PVE::Cluster::setup_rootsshconfig();
487 PVE::Cluster::setup_ssh_keys();
488
489 my $host = $param->{hostname};
490
491 if (!$param->{force}) {
492
493 if (-f $authfile) {
494 die "authentication key already exists\n";
495 }
496
497 if (-f $clusterconf) {
498 die "cluster config '$clusterconf' already exists\n";
499 }
500
501 my $vmlist = PVE::Cluster::get_vmlist();
502 if ($vmlist && $vmlist->{ids} && scalar(keys %{$vmlist->{ids}})) {
503 die "this host already contains virtual machines - please remove them first\n";
504 }
505
506 if (system("corosync-quorumtool >/dev/null 2>&1") == 0) {
507 die "corosync is already running\n";
508 }
509 }
510
511 # make sure known_hosts is on local filesystem
512 PVE::Cluster::ssh_unmerge_known_hosts();
513
514 my $cmd = ['ssh-copy-id', '-i', '/root/.ssh/id_rsa', "root\@$host"];
515 PVE::Tools::run_command($cmd, 'outfunc' => sub {}, 'errfunc' => sub {},
516 'errmsg' => "unable to copy ssh ID");
517
518 $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
519 'pvecm', 'addnode', $nodename, '--force', 1];
520
521 push @$cmd, '--nodeid', $param->{nodeid} if $param->{nodeid};
522
523 push @$cmd, '--votes', $param->{votes} if defined($param->{votes});
524
525 push @$cmd, '--ring0_addr', $param->{ring0_addr} if defined($param->{ring0_addr});
526
527 push @$cmd, '--ring1_addr', $param->{ring1_addr} if defined($param->{ring1_addr});
528
529 if (system (@$cmd) != 0) {
530 my $cmdtxt = join (' ', @$cmd);
531 die "unable to add node: command failed ($cmdtxt)\n";
532 }
533
534 my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
535 mkdir $tmpdir;
536
537 eval {
538 print "copy corosync auth key\n";
539 $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
540 "[$host]:$authfile $clusterconf", $tmpdir];
541
542 system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
543
544 mkdir "/etc/corosync";
545 my $confbase = basename($clusterconf);
546
547 $cmd = "cp '$tmpdir/$confbase' '/etc/corosync/$confbase'";
548 system($cmd) == 0 || die "can't copy cluster configuration\n";
549
550 my $keybase = basename($authfile);
551 system ("cp '$tmpdir/$keybase' '$authfile'") == 0 ||
552 die "can't copy '$tmpdir/$keybase' to '$authfile'\n";
553
554 print "stopping pve-cluster service\n";
555
556 system("umount $basedir -f >/dev/null 2>&1");
557 system("systemctl stop pve-cluster") == 0 ||
558 die "can't stop pve-cluster service\n";
559
560 backup_database();
561
562 unlink $dbfile;
563
564 system("systemctl start pve-cluster") == 0 ||
565 die "starting pve-cluster failed\n";
566
567 system("systemctl start corosync");
568
569 # wait for quorum
570 my $printqmsg = 1;
571 while (!PVE::Cluster::check_cfs_quorum(1)) {
572 if ($printqmsg) {
573 print "waiting for quorum...";
574 STDOUT->flush();
575 $printqmsg = 0;
576 }
577 sleep(1);
578 }
579 print "OK\n" if !$printqmsg;
580
581 # system("systemctl start clvm");
582
583 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
584
585 print "generating node certificates\n";
586 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
587
588 print "merge known_hosts file\n";
589 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
590
591 print "restart services\n";
592 # restart pvedaemon (changed certs)
593 system("systemctl restart pvedaemon");
594 # restart pveproxy (changed certs)
595 system("systemctl restart pveproxy");
596
597 print "successfully added node '$nodename' to cluster.\n";
598 };
599 my $err = $@;
600
601 rmtree $tmpdir;
602
603 die $err if $err;
604
605 return undef;
606 }});
607
608 __PACKAGE__->register_method ({
609 name => 'status',
610 path => 'status',
611 method => 'GET',
612 description => "Displays the local view of the cluster status.",
613 parameters => {
614 additionalProperties => 0,
615 properties => {},
616 },
617 returns => { type => 'null' },
618
619 code => sub {
620 my ($param) = @_;
621
622 PVE::Cluster::check_corosync_conf_exists();
623
624 my $cmd = ['corosync-quorumtool', '-siH'];
625
626 exec (@$cmd);
627
628 exit (-1); # should not be reached
629 }});
630
631 __PACKAGE__->register_method ({
632 name => 'nodes',
633 path => 'nodes',
634 method => 'GET',
635 description => "Displays the local view of the cluster nodes.",
636 parameters => {
637 additionalProperties => 0,
638 properties => {},
639 },
640 returns => { type => 'null' },
641
642 code => sub {
643 my ($param) = @_;
644
645 PVE::Cluster::check_corosync_conf_exists();
646
647 my $cmd = ['corosync-quorumtool', '-l'];
648
649 exec (@$cmd);
650
651 exit (-1); # should not be reached
652 }});
653
654 __PACKAGE__->register_method ({
655 name => 'expected',
656 path => 'expected',
657 method => 'PUT',
658 description => "Tells corosync a new value of expected votes.",
659 parameters => {
660 additionalProperties => 0,
661 properties => {
662 expected => {
663 type => 'integer',
664 description => "Expected votes",
665 minimum => 1,
666 },
667 },
668 },
669 returns => { type => 'null' },
670
671 code => sub {
672 my ($param) = @_;
673
674 PVE::Cluster::check_corosync_conf_exists();
675
676 my $cmd = ['corosync-quorumtool', '-e', $param->{expected}];
677
678 exec (@$cmd);
679
680 exit (-1); # should not be reached
681
682 }});
683
684 sub corosync_update_nodelist {
685 my ($conf, $nodelist) = @_;
686
687 delete $conf->{digest};
688
689 my $version = PVE::Cluster::corosync_conf_version($conf);
690 PVE::Cluster::corosync_conf_version($conf, undef, $version + 1);
691
692 my $children = [];
693 foreach my $v (values %$nodelist) {
694 next if !($v->{ring0_addr} || $v->{name});
695 my $kv = [];
696 foreach my $k (keys %$v) {
697 push @$kv, { key => $k, value => $v->{$k} };
698 }
699 my $ns = { section => 'node', children => $kv };
700 push @$children, $ns;
701 }
702
703 foreach my $main (@{$conf->{children}}) {
704 next if !defined($main->{section});
705 if ($main->{section} eq 'nodelist') {
706 $main->{children} = $children;
707 last;
708 }
709 }
710
711
712 PVE::Cluster::cfs_write_file("corosync.conf.new", $conf);
713
714 rename("/etc/pve/corosync.conf.new", "/etc/pve/corosync.conf")
715 || die "activate corosync.conf.new failed - $!\n";
716 }
717
718 sub corosync_nodelist {
719 my ($conf) = @_;
720
721 my $nodelist = {};
722
723 foreach my $main (@{$conf->{children}}) {
724 next if !defined($main->{section});
725 if ($main->{section} eq 'nodelist') {
726 foreach my $ne (@{$main->{children}}) {
727 next if !defined($ne->{section}) || ($ne->{section} ne 'node');
728 my $node = { quorum_votes => 1 };
729 my $name;
730 foreach my $child (@{$ne->{children}}) {
731 next if !defined($child->{key});
732 $node->{$child->{key}} = $child->{value};
733 # use 'name' over 'ring0_addr' if set
734 if ($child->{key} eq 'name') {
735 delete $nodelist->{$name} if $name;
736 $name = $child->{value};
737 $nodelist->{$name} = $node;
738 } elsif(!$name && $child->{key} eq 'ring0_addr') {
739 $name = $child->{value};
740 $nodelist->{$name} = $node;
741 }
742 }
743 }
744 }
745 }
746
747 return $nodelist;
748 }
749
750 # get a hash representation of the corosync config totem section
751 sub corosync_totem_config {
752 my ($conf) = @_;
753
754 my $res = {};
755
756 foreach my $main (@{$conf->{children}}) {
757 next if !defined($main->{section}) ||
758 $main->{section} ne 'totem';
759
760 foreach my $e (@{$main->{children}}) {
761
762 if ($e->{section} && $e->{section} eq 'interface') {
763 my $entry = {};
764
765 $res->{interface} = {};
766
767 foreach my $child (@{$e->{children}}) {
768 next if !defined($child->{key});
769 $entry->{$child->{key}} = $child->{value};
770 if($child->{key} eq 'ringnumber') {
771 $res->{interface}->{$child->{value}} = $entry;
772 }
773 }
774
775 } elsif ($e->{key}) {
776 $res->{$e->{key}} = $e->{value};
777 }
778 }
779 }
780
781 return $res;
782 }
783
784 __PACKAGE__->register_method ({
785 name => 'updatecerts',
786 path => 'updatecerts',
787 method => 'PUT',
788 description => "Update node certificates (and generate all needed files/directories).",
789 parameters => {
790 additionalProperties => 0,
791 properties => {
792 force => {
793 description => "Force generation of new SSL certifate.",
794 type => 'boolean',
795 optional => 1,
796 },
797 silent => {
798 description => "Ignore errors (i.e. when cluster has no quorum).",
799 type => 'boolean',
800 optional => 1,
801 },
802 },
803 },
804 returns => { type => 'null' },
805 code => sub {
806 my ($param) = @_;
807
808 PVE::Cluster::setup_rootsshconfig();
809
810 PVE::Cluster::gen_pve_vzdump_symlink();
811
812 if (!PVE::Cluster::check_cfs_quorum(1)) {
813 return undef if $param->{silent};
814 die "no quorum - unable to update files\n";
815 }
816
817 PVE::Cluster::setup_ssh_keys();
818
819 my $nodename = PVE::INotify::nodename();
820
821 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
822
823 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address, $param->{force});
824 PVE::Cluster::ssh_merge_keys();
825 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address);
826 PVE::Cluster::gen_pve_vzdump_files();
827
828 return undef;
829 }});
830
831
832 our $cmddef = {
833 keygen => [ __PACKAGE__, 'keygen', ['filename']],
834 create => [ __PACKAGE__, 'create', ['clustername']],
835 add => [ __PACKAGE__, 'add', ['hostname']],
836 addnode => [ __PACKAGE__, 'addnode', ['node']],
837 delnode => [ __PACKAGE__, 'delnode', ['node']],
838 status => [ __PACKAGE__, 'status' ],
839 nodes => [ __PACKAGE__, 'nodes' ],
840 expected => [ __PACKAGE__, 'expected', ['expected']],
841 updatecerts => [ __PACKAGE__, 'updatecerts', []],
842 };
843
844 1;