]> git.proxmox.com Git - pve-cluster.git/blob - data/PVE/CLI/pvecm.pm
cleanup calls to external programs
[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 => PVE::JSONSchema::get_standard_option('pve-node'),
396 },
397 },
398 returns => { type => 'null' },
399
400 code => sub {
401 my ($param) = @_;
402
403 PVE::Cluster::check_cfs_quorum();
404
405 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
406
407 my $nodelist = corosync_nodelist($conf);
408
409 my $nd = delete $nodelist->{$param->{node}};
410 die "no such node '$param->{node}'\n" if !$nd;
411
412 corosync_update_nodelist($conf, $nodelist);
413
414 return undef;
415 }});
416
417 __PACKAGE__->register_method ({
418 name => 'add',
419 path => 'add',
420 method => 'PUT',
421 description => "Adds the current node to an existing cluster.",
422 parameters => {
423 additionalProperties => 0,
424 properties => {
425 hostname => {
426 type => 'string',
427 description => "Hostname (or IP) of an existing cluster member."
428 },
429 nodeid => {
430 type => 'integer',
431 description => "Node id for this node.",
432 minimum => 1,
433 optional => 1,
434 },
435 votes => {
436 type => 'integer',
437 description => "Number of votes for this node",
438 minimum => 0,
439 optional => 1,
440 },
441 force => {
442 type => 'boolean',
443 description => "Do not throw error if node already exists.",
444 optional => 1,
445 },
446 ring0_addr => {
447 type => 'string', format => 'address',
448 description => "Hostname (or IP) of the corosync ring0 address of this node.".
449 " Defaults to nodes hostname.",
450 optional => 1,
451 },
452 ring1_addr => {
453 type => 'string', format => 'address',
454 description => "Hostname (or IP) of the corosync ring1 address, this".
455 " needs an valid configured ring 1 interface in the cluster.",
456 optional => 1,
457 },
458 },
459 },
460 returns => { type => 'null' },
461
462 code => sub {
463 my ($param) = @_;
464
465 my $nodename = PVE::INotify::nodename();
466
467 PVE::Cluster::setup_sshd_config();
468 PVE::Cluster::setup_rootsshconfig();
469 PVE::Cluster::setup_ssh_keys();
470
471 my $host = $param->{hostname};
472
473 if (!$param->{force}) {
474
475 if (-f $authfile) {
476 die "authentication key already exists\n";
477 }
478
479 if (-f $clusterconf) {
480 die "cluster config '$clusterconf' already exists\n";
481 }
482
483 my $vmlist = PVE::Cluster::get_vmlist();
484 if ($vmlist && $vmlist->{ids} && scalar(keys %{$vmlist->{ids}})) {
485 die "this host already contains virtual machines - please remove them first\n";
486 }
487
488 if (system("corosync-quorumtool >/dev/null 2>&1") == 0) {
489 die "corosync is already running\n";
490 }
491 }
492
493 # make sure known_hosts is on local filesystem
494 PVE::Cluster::ssh_unmerge_known_hosts();
495
496 my $cmd = ['ssh-copy-id', '-i', '/root/.ssh/id_rsa', "root\@$host"];
497 PVE::Tools::run_command($cmd, 'outfunc' => sub {}, 'errfunc' => sub {},
498 'errmsg' => "unable to copy ssh ID");
499
500 $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
501 'pvecm', 'addnode', $nodename, '--force', 1];
502
503 push @$cmd, '--nodeid', $param->{nodeid} if $param->{nodeid};
504
505 push @$cmd, '--votes', $param->{votes} if defined($param->{votes});
506
507 push @$cmd, '--ring0_addr', $param->{ring0_addr} if defined($param->{ring0_addr});
508
509 push @$cmd, '--ring1_addr', $param->{ring1_addr} if defined($param->{ring1_addr});
510
511 if (system (@$cmd) != 0) {
512 my $cmdtxt = join (' ', @$cmd);
513 die "unable to add node: command failed ($cmdtxt)\n";
514 }
515
516 my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
517 mkdir $tmpdir;
518
519 eval {
520 print "copy corosync auth key\n";
521 $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
522 "[$host]:$authfile $clusterconf", $tmpdir];
523
524 system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
525
526 mkdir "/etc/corosync";
527 my $confbase = basename($clusterconf);
528
529 $cmd = "cp '$tmpdir/$confbase' '/etc/corosync/$confbase'";
530 system($cmd) == 0 || die "can't copy cluster configuration\n";
531
532 my $keybase = basename($authfile);
533 system ("cp '$tmpdir/$keybase' '$authfile'") == 0 ||
534 die "can't copy '$tmpdir/$keybase' to '$authfile'\n";
535
536 print "stopping pve-cluster service\n";
537
538 system("umount $basedir -f >/dev/null 2>&1");
539 system("systemctl stop pve-cluster") == 0 ||
540 die "can't stop pve-cluster service\n";
541
542 backup_database();
543
544 unlink $dbfile;
545
546 system("systemctl start pve-cluster") == 0 ||
547 die "starting pve-cluster failed\n";
548
549 system("systemctl start corosync");
550
551 # wait for quorum
552 my $printqmsg = 1;
553 while (!PVE::Cluster::check_cfs_quorum(1)) {
554 if ($printqmsg) {
555 print "waiting for quorum...";
556 STDOUT->flush();
557 $printqmsg = 0;
558 }
559 sleep(1);
560 }
561 print "OK\n" if !$printqmsg;
562
563 # system("systemctl start clvm");
564
565 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
566
567 print "generating node certificates\n";
568 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
569
570 print "merge known_hosts file\n";
571 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
572
573 print "restart services\n";
574 # restart pvedaemon (changed certs)
575 system("systemctl restart pvedaemon");
576 # restart pveproxy (changed certs)
577 system("systemctl restart pveproxy");
578
579 print "successfully added node '$nodename' to cluster.\n";
580 };
581 my $err = $@;
582
583 rmtree $tmpdir;
584
585 die $err if $err;
586
587 return undef;
588 }});
589
590 __PACKAGE__->register_method ({
591 name => 'status',
592 path => 'status',
593 method => 'GET',
594 description => "Displays the local view of the cluster status.",
595 parameters => {
596 additionalProperties => 0,
597 properties => {},
598 },
599 returns => { type => 'null' },
600
601 code => sub {
602 my ($param) = @_;
603
604 PVE::Cluster::check_corosync_conf_exists();
605
606 my $cmd = ['corosync-quorumtool', '-siH'];
607
608 exec (@$cmd);
609
610 exit (-1); # should not be reached
611 }});
612
613 __PACKAGE__->register_method ({
614 name => 'nodes',
615 path => 'nodes',
616 method => 'GET',
617 description => "Displays the local view of the cluster nodes.",
618 parameters => {
619 additionalProperties => 0,
620 properties => {},
621 },
622 returns => { type => 'null' },
623
624 code => sub {
625 my ($param) = @_;
626
627 PVE::Cluster::check_corosync_conf_exists();
628
629 my $cmd = ['corosync-quorumtool', '-l'];
630
631 exec (@$cmd);
632
633 exit (-1); # should not be reached
634 }});
635
636 __PACKAGE__->register_method ({
637 name => 'expected',
638 path => 'expected',
639 method => 'PUT',
640 description => "Tells corosync a new value of expected votes.",
641 parameters => {
642 additionalProperties => 0,
643 properties => {
644 expected => {
645 type => 'integer',
646 description => "Expected votes",
647 minimum => 1,
648 },
649 },
650 },
651 returns => { type => 'null' },
652
653 code => sub {
654 my ($param) = @_;
655
656 PVE::Cluster::check_corosync_conf_exists();
657
658 my $cmd = ['corosync-quorumtool', '-e', $param->{expected}];
659
660 exec (@$cmd);
661
662 exit (-1); # should not be reached
663
664 }});
665
666 sub corosync_update_nodelist {
667 my ($conf, $nodelist) = @_;
668
669 delete $conf->{digest};
670
671 my $version = PVE::Cluster::corosync_conf_version($conf);
672 PVE::Cluster::corosync_conf_version($conf, undef, $version + 1);
673
674 my $children = [];
675 foreach my $v (values %$nodelist) {
676 next if !($v->{ring0_addr} || $v->{name});
677 my $kv = [];
678 foreach my $k (keys %$v) {
679 push @$kv, { key => $k, value => $v->{$k} };
680 }
681 my $ns = { section => 'node', children => $kv };
682 push @$children, $ns;
683 }
684
685 foreach my $main (@{$conf->{children}}) {
686 next if !defined($main->{section});
687 if ($main->{section} eq 'nodelist') {
688 $main->{children} = $children;
689 last;
690 }
691 }
692
693
694 PVE::Cluster::cfs_write_file("corosync.conf.new", $conf);
695
696 rename("/etc/pve/corosync.conf.new", "/etc/pve/corosync.conf")
697 || die "activate corosync.conf.new failed - $!\n";
698 }
699
700 sub corosync_nodelist {
701 my ($conf) = @_;
702
703 my $nodelist = {};
704
705 foreach my $main (@{$conf->{children}}) {
706 next if !defined($main->{section});
707 if ($main->{section} eq 'nodelist') {
708 foreach my $ne (@{$main->{children}}) {
709 next if !defined($ne->{section}) || ($ne->{section} ne 'node');
710 my $node = { quorum_votes => 1 };
711 my $name;
712 foreach my $child (@{$ne->{children}}) {
713 next if !defined($child->{key});
714 $node->{$child->{key}} = $child->{value};
715 # use 'name' over 'ring0_addr' if set
716 if ($child->{key} eq 'name') {
717 delete $nodelist->{$name} if $name;
718 $name = $child->{value};
719 $nodelist->{$name} = $node;
720 } elsif(!$name && $child->{key} eq 'ring0_addr') {
721 $name = $child->{value};
722 $nodelist->{$name} = $node;
723 }
724 }
725 }
726 }
727 }
728
729 return $nodelist;
730 }
731
732 # get a hash representation of the corosync config totem section
733 sub corosync_totem_config {
734 my ($conf) = @_;
735
736 my $res = {};
737
738 foreach my $main (@{$conf->{children}}) {
739 next if !defined($main->{section}) ||
740 $main->{section} ne 'totem';
741
742 foreach my $e (@{$main->{children}}) {
743
744 if ($e->{section} && $e->{section} eq 'interface') {
745 my $entry = {};
746
747 $res->{interface} = {};
748
749 foreach my $child (@{$e->{children}}) {
750 next if !defined($child->{key});
751 $entry->{$child->{key}} = $child->{value};
752 if($child->{key} eq 'ringnumber') {
753 $res->{interface}->{$child->{value}} = $entry;
754 }
755 }
756
757 } elsif ($e->{key}) {
758 $res->{$e->{key}} = $e->{value};
759 }
760 }
761 }
762
763 return $res;
764 }
765
766 __PACKAGE__->register_method ({
767 name => 'updatecerts',
768 path => 'updatecerts',
769 method => 'PUT',
770 description => "Update node certificates (and generate all needed files/directories).",
771 parameters => {
772 additionalProperties => 0,
773 properties => {
774 force => {
775 description => "Force generation of new SSL certifate.",
776 type => 'boolean',
777 optional => 1,
778 },
779 silent => {
780 description => "Ignore errors (i.e. when cluster has no quorum).",
781 type => 'boolean',
782 optional => 1,
783 },
784 },
785 },
786 returns => { type => 'null' },
787 code => sub {
788 my ($param) = @_;
789
790 PVE::Cluster::setup_rootsshconfig();
791
792 PVE::Cluster::gen_pve_vzdump_symlink();
793
794 if (!PVE::Cluster::check_cfs_quorum(1)) {
795 return undef if $param->{silent};
796 die "no quorum - unable to update files\n";
797 }
798
799 PVE::Cluster::setup_ssh_keys();
800
801 my $nodename = PVE::INotify::nodename();
802
803 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
804
805 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address, $param->{force});
806 PVE::Cluster::ssh_merge_keys();
807 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address);
808 PVE::Cluster::gen_pve_vzdump_files();
809
810 return undef;
811 }});
812
813
814 our $cmddef = {
815 keygen => [ __PACKAGE__, 'keygen', ['filename']],
816 create => [ __PACKAGE__, 'create', ['clustername']],
817 add => [ __PACKAGE__, 'add', ['hostname']],
818 addnode => [ __PACKAGE__, 'addnode', ['node']],
819 delnode => [ __PACKAGE__, 'delnode', ['node']],
820 status => [ __PACKAGE__, 'status' ],
821 nodes => [ __PACKAGE__, 'nodes' ],
822 expected => [ __PACKAGE__, 'expected', ['expected']],
823 updatecerts => [ __PACKAGE__, 'updatecerts', []],
824 };
825
826 1;