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