]> git.proxmox.com Git - pve-cluster.git/commitdiff
convert pvecm into a PVE::CLI class
authorDietmar Maurer <dietmar@proxmox.com>
Fri, 25 Sep 2015 10:05:18 +0000 (12:05 +0200)
committerDietmar Maurer <dietmar@proxmox.com>
Fri, 25 Sep 2015 10:07:51 +0000 (12:07 +0200)
data/PVE/CLI/pvecm.pm [new file with mode: 0755]
data/PVE/Makefile.am
data/PVE/pvecm

diff --git a/data/PVE/CLI/pvecm.pm b/data/PVE/CLI/pvecm.pm
new file mode 100755 (executable)
index 0000000..c9ebffc
--- /dev/null
@@ -0,0 +1,688 @@
+package PVE::CLI::pvecm;
+
+use strict;
+use warnings;
+use Getopt::Long;
+use Socket;
+use IO::File;
+use Net::IP;
+use File::Path;
+use File::Basename;
+use Data::Dumper; # fixme: remove 
+use PVE::Tools;
+use PVE::Cluster;
+use PVE::INotify;
+use PVE::JSONSchema;
+use PVE::RPCEnvironment;
+use PVE::CLIHandler;
+
+use base qw(PVE::CLIHandler);
+
+$ENV{HOME} = '/root'; # for ssh-copy-id
+
+my $basedir = "/etc/pve";
+my $clusterconf = "$basedir/corosync.conf";
+my $libdir = "/var/lib/pve-cluster";
+my $backupdir = "/var/lib/pve-cluster/backup";
+my $dbfile = "$libdir/config.db";
+my $authfile = "/etc/corosync/authkey";
+
+sub backup_database {
+
+    print "backup old database\n";
+
+    mkdir $backupdir;
+    
+    my $ctime = time();
+    my $cmd = "echo '.dump' |";
+    $cmd .= "sqlite3 '$dbfile' |";
+    $cmd .= "gzip - >'${backupdir}/config-${ctime}.sql.gz'";
+
+    system($cmd) == 0 ||
+       die "can't backup old database: $!\n";
+
+    # purge older backup
+    my $maxfiles = 10;
+
+    my @bklist = ();
+    foreach my $fn (<$backupdir/config-*.sql.gz>) {
+       if ($fn =~ m!/config-(\d+)\.sql.gz$!) {
+           push @bklist, [$fn, $1];
+       }
+    }
+       
+    @bklist = sort { $b->[1] <=> $a->[1] } @bklist;
+
+    while (scalar (@bklist) >= $maxfiles) {
+       my $d = pop @bklist;
+       print "delete old backup '$d->[0]'\n";
+       unlink $d->[0];
+    }
+}
+
+__PACKAGE__->register_method ({
+    name => 'keygen', 
+    path => 'keygen',
+    method => 'PUT',
+    description => "Generate new cryptographic key for corosync.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           filename => {
+               type => 'string',
+               description => "Output file name"
+           }
+       },
+    },
+    returns => { type => 'null' },
+    
+    code => sub {
+       my ($param) = @_;
+
+       my $filename = $param->{filename};
+
+       # test EUID
+       $> == 0 || die "Error: Authorization key must be generated as root user.\n";
+       my $dirname = dirname($filename);
+       my $basename = basename($filename);
+
+       die "key file '$filename' already exists\n" if -e $filename;
+
+       File::Path::make_path($dirname) if $dirname;
+
+       my $cmd = ['corosync-keygen', '-l', '-k', $filename];
+       PVE::Tools::run_command($cmd);   
+
+       return undef;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'create', 
+    path => 'create',
+    method => 'PUT',
+    description => "Generate new cluster configuration.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           clustername => {
+               description => "The name of the cluster.",
+               type => 'string', format => 'pve-node',
+               maxLength => 15,
+           },
+           nodeid => {
+               type => 'integer',
+               description => "Node id for this node.",
+               minimum => 1,
+               optional => 1,
+           },
+           votes => {
+               type => 'integer',
+               description => "Number of votes for this node.",
+               minimum => 1,
+               optional => 1,
+           },
+       }, 
+    },
+    returns => { type => 'null' },
+    
+    code => sub {
+       my ($param) = @_;
+
+       -f $clusterconf && die "cluster config '$clusterconf' already exists\n";
+
+       PVE::Cluster::setup_sshd_config();
+       PVE::Cluster::setup_rootsshconfig();
+       PVE::Cluster::setup_ssh_keys();
+
+       -f $authfile || __PACKAGE__->keygen({filename => $authfile});
+
+       -f $authfile || die "no authentication key available\n";
+
+       my $clustername = $param->{clustername};
+
+       $param->{nodeid} = 1 if !$param->{nodeid};
+
+       $param->{votes} = 1 if !defined($param->{votes});
+
+       my $nodename = PVE::INotify::nodename();
+       
+       my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
+
+       # No, corosync cannot deduce this on its own
+       my $ipversion = Net::IP::ip_is_ipv6($local_ip_address) ? 'ipv6' : 'ipv4';
+
+       my $config = <<_EOD;
+totem {
+  version: 2
+  secauth: on
+  cluster_name: $clustername
+  config_version: 1
+  ip_version: $ipversion
+  interface {
+    ringnumber: 0
+    bindnetaddr: $local_ip_address
+  }
+}
+
+nodelist {
+  node {
+    ring0_addr: $nodename
+    nodeid: $param->{nodeid}
+    quorum_votes: $param->{votes}
+  }
+}
+       
+quorum {
+  provider: corosync_votequorum
+}
+
+logging {
+  to_syslog: yes
+  debug: off
+}
+_EOD
+;      
+       PVE::Tools::file_set_contents($clusterconf, $config);
+
+       PVE::Cluster::ssh_merge_keys();
+
+       PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
+
+       PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
+
+       PVE::Tools::run_command('systemctl restart pve-cluster'); # restart
+
+       PVE::Tools::run_command('systemctl restart corosync'); # restart
+       
+       return undef;
+}});
+
+__PACKAGE__->register_method ({
+    name => 'addnode', 
+    path => 'addnode',
+    method => 'PUT',
+    description => "Adds a node to the cluster configuration.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => PVE::JSONSchema::get_standard_option('pve-node'),
+           nodeid => {
+               type => 'integer',
+               description => "Node id for this node.",
+               minimum => 1,
+               optional => 1,
+           },
+           votes => {
+               type => 'integer',
+               description => "Number of votes for this node",
+               minimum => 0,
+               optional => 1,
+           },
+           force => {
+               type => 'boolean',
+               description => "Do not throw error if node already exists.",
+               optional => 1,
+           },
+       },
+    },
+    returns => { type => 'null' },
+    
+    code => sub {
+       my ($param) = @_;
+
+       PVE::Cluster::check_cfs_quorum();
+
+       my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
+
+       my $nodelist = corosync_nodelist($conf);
+
+       my $name = $param->{node};
+
+       if (defined(my $res = $nodelist->{$name})) {
+           $param->{nodeid} = $res->{nodeid} if !$param->{nodeid};
+           $param->{votes} = $res->{quorum_votes} if !defined($param->{votes});
+
+           if ($res->{quorum_votes} == $param->{votes} &&
+               $res->{nodeid} == $param->{nodeid}) {
+               print "node $name already defined\n";
+               if ($param->{force}) {
+                   exit (0);
+               } else {
+                   exit (-1);
+               }
+           } else {
+               die "can't add existing node\n";
+           }
+       } elsif (!$param->{nodeid}) {
+           my $nodeid = 1;
+           
+           while(1) {
+               my $found = 0; 
+               foreach my $v (values %$nodelist) {
+                   if ($v->{nodeid} eq $nodeid) {
+                       $found = 1;
+                       $nodeid++;
+                       last;
+                   }
+               }
+               last if !$found;
+           };
+
+           $param->{nodeid} = $nodeid;
+       }
+
+       $param->{votes} = 1 if !defined($param->{votes});
+
+       PVE::Cluster::gen_local_dirs($name);
+
+       eval {  PVE::Cluster::ssh_merge_keys(); };
+       warn $@ if $@;
+
+       $nodelist->{$name} = { ring0_addr => $name, nodeid => $param->{nodeid} };
+       $nodelist->{$name}->{quorum_votes} = $param->{votes} if $param->{votes};
+       
+       corosync_update_nodelist($conf, $nodelist);
+       
+       exit (0);
+    }});
+
+
+__PACKAGE__->register_method ({
+    name => 'delnode', 
+    path => 'delnode',
+    method => 'PUT',
+    description => "Removes a node to the cluster configuration.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => PVE::JSONSchema::get_standard_option('pve-node'),
+       },
+    },
+    returns => { type => 'null' },
+    
+    code => sub {
+       my ($param) = @_;
+
+       PVE::Cluster::check_cfs_quorum();
+
+       my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
+
+       my $nodelist = corosync_nodelist($conf);
+
+       my $nd = delete $nodelist->{$param->{node}};
+       die "no such node '$param->{node}'\n" if !$nd;
+       
+       corosync_update_nodelist($conf, $nodelist);
+
+       return undef;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'add', 
+    path => 'add',
+    method => 'PUT',
+    description => "Adds the current node to an existing cluster.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           hostname => {
+               type => 'string',
+               description => "Hostname (or IP) of an existing cluster member."
+           },
+           nodeid => {
+               type => 'integer',
+               description => "Node id for this node.",
+               minimum => 1,
+               optional => 1,
+           },
+           votes => {
+               type => 'integer',
+               description => "Number of votes for this node",
+               minimum => 0,
+               optional => 1,
+           },
+           force => {
+               type => 'boolean',
+               description => "Do not throw error if node already exists.",
+               optional => 1,
+           },
+       },
+    },
+    returns => { type => 'null' },
+    
+    code => sub {
+       my ($param) = @_;
+
+       my $nodename = PVE::INotify::nodename();
+
+       PVE::Cluster::setup_sshd_config();
+       PVE::Cluster::setup_rootsshconfig();
+       PVE::Cluster::setup_ssh_keys();
+
+       my $host = $param->{hostname};
+
+       if (!$param->{force}) {
+           
+           if (-f $authfile) {
+               die "authentication key already exists\n";
+           }
+
+           if (-f $clusterconf)  {
+               die "cluster config '$clusterconf' already exists\n";
+           }
+
+           my $vmlist = PVE::Cluster::get_vmlist();
+           if ($vmlist && $vmlist->{ids} && scalar(keys %{$vmlist->{ids}})) {
+               die "this host already contains virtual machines - please remove them first\n";
+           }
+
+           if (system("corosync-quorumtool >/dev/null 2>&1") == 0) {
+               die "corosync is already running\n";
+           }
+       }
+
+       # make sure known_hosts is on local filesystem
+       PVE::Cluster::ssh_unmerge_known_hosts();
+
+       my $cmd = "ssh-copy-id -i /root/.ssh/id_rsa 'root\@$host' >/dev/null 2>&1";
+       system ($cmd) == 0 ||
+           die "unable to copy ssh ID\n";
+
+       $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
+               'pvecm', 'addnode', $nodename, '--force', 1];
+
+       push @$cmd, '--nodeid', $param->{nodeid} if $param->{nodeid};
+
+       push @$cmd, '--votes', $param->{votes} if defined($param->{votes});
+
+       if (system (@$cmd) != 0) {
+           my $cmdtxt = join (' ', @$cmd);
+           die "unable to add node: command failed ($cmdtxt)\n";
+       }
+
+       my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
+       mkdir $tmpdir;
+
+       eval {
+           print "copy corosync auth key\n";
+           $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq', 
+                   "[$host]:$authfile $clusterconf", $tmpdir];
+
+           system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
+
+           mkdir "/etc/corosync";
+           my $confbase = basename($clusterconf);
+
+           $cmd = "cp '$tmpdir/$confbase' '/etc/corosync/$confbase'";
+           system($cmd) == 0 || die "can't copy cluster configuration\n";
+
+           my $keybase = basename($authfile);
+           system ("cp '$tmpdir/$keybase' '$authfile'") == 0 ||
+               die "can't copy '$tmpdir/$keybase' to '$authfile'\n";
+
+           print "stopping pve-cluster service\n";
+
+           system("umount $basedir -f >/dev/null 2>&1");
+           system("systemctl stop pve-cluster") == 0 ||
+               die "can't stop pve-cluster service\n";
+
+           backup_database();
+
+           unlink $dbfile;
+
+           system("systemctl start pve-cluster") == 0 ||
+               die "starting pve-cluster failed\n";
+
+           system("systemctl start corosync");
+
+           # wait for quorum
+           my $printqmsg = 1;
+           while (!PVE::Cluster::check_cfs_quorum(1)) {
+               if ($printqmsg) {
+                   print "waiting for quorum...";
+                   STDOUT->flush();
+                   $printqmsg = 0;
+               }
+               sleep(1);
+           }
+           print "OK\n" if !$printqmsg;
+
+           # system("systemctl start clvm");
+
+           my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
+
+           print "generating node certificates\n";
+           PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address); 
+
+           print "merge known_hosts file\n";
+           PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
+
+           print "restart services\n";
+           # restart pvedaemon (changed certs)
+           system("systemctl restart pvedaemon");
+           # restart pveproxy (changed certs)
+           system("systemctl restart pveproxy");
+
+           print "successfully added node '$nodename' to cluster.\n";
+       };
+       my $err = $@;
+
+       rmtree $tmpdir;
+
+       die $err if $err;
+
+       return undef;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'status', 
+    path => 'status',
+    method => 'GET',
+    description => "Displays the local view of the cluster status.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {},
+    },
+    returns => { type => 'null' },
+    
+    code => sub {
+       my ($param) = @_;
+
+       my $cmd = ['corosync-quorumtool', '-siH'];
+
+       exec (@$cmd);
+
+       exit (-1); # should not be reached
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'nodes', 
+    path => 'nodes',
+    method => 'GET',
+    description => "Displays the local view of the cluster nodes.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {},
+    },
+    returns => { type => 'null' },
+    
+    code => sub {
+       my ($param) = @_;
+
+       my $cmd = ['corosync-quorumtool', '-l'];
+
+       exec (@$cmd);
+
+       exit (-1); # should not be reached
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'expected', 
+    path => 'expected',
+    method => 'PUT',
+    description => "Tells corosync a new value of expected votes.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           expected => {
+               type => 'integer',
+               description => "Expected votes",
+               minimum => 1,
+           },
+       },
+    },
+    returns => { type => 'null' },
+    
+    code => sub {
+       my ($param) = @_;
+
+       my $cmd = ['corosync-quorumtool', '-e', $param->{expected}];
+
+       exec (@$cmd);
+
+       exit (-1); # should not be reached
+
+    }});
+
+sub corosync_update_nodelist {
+    my ($conf, $nodelist) = @_;
+
+    delete $conf->{digest};
+    
+    my $version = PVE::Cluster::corosync_conf_version($conf);
+    PVE::Cluster::corosync_conf_version($conf, undef, $version + 1);
+
+    my $children = [];
+    foreach my $v (values %$nodelist) {
+       next if !$v->{ring0_addr};
+       my $kv = [];
+       foreach my $k (keys %$v) {
+           push @$kv, { key => $k, value => $v->{$k} };
+       } 
+       my $ns = { section => 'node', children => $kv };
+       push @$children, $ns;
+    }
+    
+    foreach my $main (@{$conf->{children}}) {
+       next if !defined($main->{section});
+       if ($main->{section} eq 'nodelist') {
+           $main->{children} = $children;
+           last;
+       }
+    }
+
+    
+    PVE::Cluster::cfs_write_file("corosync.conf.new", $conf);
+    
+    rename("/etc/pve/corosync.conf.new", "/etc/pve/corosync.conf")
+       || die "activate  corosync.conf.new failed - $!\n";
+}
+
+sub corosync_nodelist {
+    my ($conf) = @_;
+    
+    my $res = {};
+
+    my $nodelist = {};
+    
+    foreach my $main (@{$conf->{children}}) {
+       next if !defined($main->{section});
+       if ($main->{section} eq 'nodelist') {
+           foreach my $ne (@{$main->{children}}) {
+               next if !defined($ne->{section}) || ($ne->{section} ne 'node');
+               my $node = { quorum_votes => 1 };
+               foreach my $child (@{$ne->{children}}) {
+                   next if !defined($child->{key});
+                   $node->{$child->{key}} = $child->{value};
+                   if ($child->{key} eq 'ring0_addr') {
+                       $nodelist->{$child->{value}} = $node;
+                   }
+               }
+           }
+       }
+    }   
+
+    return $nodelist;
+}
+
+__PACKAGE__->register_method ({
+    name => 'updatecerts', 
+    path => 'updatecerts',
+    method => 'PUT',
+    description => "Update node certificates (and generate all needed files/directories).",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           force => {
+               description => "Force generation of new SSL certifate.",
+               type => 'boolean',
+               optional => 1,
+           },
+           silent => {
+               description => "Ignore errors (i.e. when cluster has no quorum).",
+               type => 'boolean',
+               optional => 1,
+           },
+       },
+    },
+    returns => { type => 'null' },
+    code => sub {
+       my ($param) = @_;
+
+       PVE::Cluster::setup_rootsshconfig();
+
+       PVE::Cluster::gen_pve_vzdump_symlink();
+
+       if (!PVE::Cluster::check_cfs_quorum(1)) {
+           return undef if $param->{silent};
+           die "no quorum - unable to update files\n";
+       }
+
+       PVE::Cluster::setup_ssh_keys();
+
+       my $nodename = PVE::INotify::nodename();
+
+       my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
+
+       PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address, $param->{force});
+       PVE::Cluster::ssh_merge_keys();
+       PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address);
+       PVE::Cluster::gen_pve_vzdump_files();
+
+       return undef;
+    }});
+
+
+our $cmddef = {
+    keygen => [ __PACKAGE__, 'keygen', ['filename']],
+    create => [ __PACKAGE__, 'create', ['clustername']],
+    add => [ __PACKAGE__, 'add', ['hostname']],
+    addnode => [ __PACKAGE__, 'addnode', ['node']],
+    delnode => [ __PACKAGE__, 'delnode', ['node']],
+    status => [ __PACKAGE__, 'status' ],
+    nodes => [ __PACKAGE__, 'nodes' ],
+    expected => [ __PACKAGE__, 'expected', ['expected']],
+    updatecerts => [ __PACKAGE__, 'updatecerts', []],
+};
+
+1;
+
+__END__
+
+=head1 NAME
+
+pvecm - Proxmox VE cluster manager toolkit
+
+=head1 SYNOPSIS
+
+=include synopsis
+
+=head1 DESCRIPTION
+
+pvecm is a program to manage the cluster configuration. It can be used
+to create a new cluster, join nodes to a cluster, leave the cluster,
+get status information and do various other cluster related tasks.
+
+=include pve_copyright
+
+
index 7b9c76f21c33ccc559ec880c8bf846e1f61fa3a1..8160c658dbd235468790e1d782f37ace6317fb84 100644 (file)
@@ -17,7 +17,9 @@ perlbindir = $(PERL_INSTALLARCHLIB)/auto/PVE/IPCC
        cat $<|pod2man -n $* -s 5 -r ${VERSION} -c "Proxmox Documentation"|gzip -c9 >$@
 
 %.1.pod: %
-       perl -I.. ./$* printmanpod > $@
+       perl -I.. -T -e "use PVE::CLI::$*; PVE::CLI::$*->generate_pod_manpage();" >$@.tmp
+       mv $@.tmp $@
+
 
 datacenter.cfg.5.pod: Cluster.pm gen-datacenter-pod.pl
        perl -I.. ./gen-datacenter-pod.pl > $@
@@ -37,7 +39,10 @@ IPCC_so_SOURCES = IPCC.c ppport.h
 pvelib_DATA = IPCC.pm Cluster.pm
 pvelibdir = $(PERL_VENDORLIB)/PVE
 
+cliclass_DATA = CLI/pvecm.pm
+cliclassdir = $(PERL_VENDORLIB)/PVE/CLI
+
 install-exec-hook:
-       perl -I.. ./pvecm verifyapi
+       perl -I.. -T -e "use PVE::CLI::pvecm; PVE::CLI::pvecm->verify_api();"
 
 CLEANFILES = IPCC.c *~ *.1.gz *.5.gz *.pod
index 2cd13e5bfddc1d8d7579cd72bb4fe0d55c556e74..35f3ff778594e205129b0225e087b15056148bcf 100755 (executable)
 
 use strict;
 use warnings;
-use Getopt::Long;
-use Socket;
-use IO::File;
-use Net::IP;
-use File::Path;
-use File::Basename;
-use Data::Dumper; # fixme: remove 
-use PVE::Tools;
-use PVE::Cluster;
-use PVE::INotify;
-use PVE::JSONSchema;
-use PVE::CLIHandler;
-
-use base qw(PVE::CLIHandler);
-
-$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
-$ENV{LC_ALL} = 'C';
-$ENV{HOME} = '/root'; # for ssh-copy-id
-
-my ($local_ip_address, $nodename);
-
-if (defined($ARGV[0]) && $ARGV[0] ne 'printmanpod' && $ARGV[0] ne 'verifyapi') {
-    die "please run as root\n" if $> != 0;
-
-    $nodename = PVE::INotify::nodename();
-    # trigger check that we have resolvable name
-    $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
-}
-
-my $basedir = "/etc/pve";
-my $clusterconf = "$basedir/corosync.conf";
-my $libdir = "/var/lib/pve-cluster";
-my $backupdir = "/var/lib/pve-cluster/backup";
-my $dbfile = "$libdir/config.db";
-my $authfile = "/etc/corosync/authkey";
-
-
-sub backup_database {
-
-    print "backup old database\n";
-
-    mkdir $backupdir;
-    
-    my $ctime = time();
-    my $cmd = "echo '.dump' |";
-    $cmd .= "sqlite3 '$dbfile' |";
-    $cmd .= "gzip - >'${backupdir}/config-${ctime}.sql.gz'";
-
-    system($cmd) == 0 ||
-       die "can't backup old database: $!\n";
-
-    # purge older backup
-    my $maxfiles = 10;
-
-    my @bklist = ();
-    foreach my $fn (<$backupdir/config-*.sql.gz>) {
-       if ($fn =~ m!/config-(\d+)\.sql.gz$!) {
-           push @bklist, [$fn, $1];
-       }
-    }
-       
-    @bklist = sort { $b->[1] <=> $a->[1] } @bklist;
-
-    while (scalar (@bklist) >= $maxfiles) {
-       my $d = pop @bklist;
-       print "delete old backup '$d->[0]'\n";
-       unlink $d->[0];
-    }
-}
-
-__PACKAGE__->register_method ({
-    name => 'keygen', 
-    path => 'keygen',
-    method => 'PUT',
-    description => "Generate new cryptographic key for corosync.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           filename => {
-               type => 'string',
-               description => "Output file name"
-           }
-       },
-    },
-    returns => { type => 'null' },
-    
-    code => sub {
-       my ($param) = @_;
-
-       my $filename = $param->{filename};
-
-       # test EUID
-       $> == 0 || die "Error: Authorization key must be generated as root user.\n";
-       my $dirname = dirname($filename);
-       my $basename = basename($filename);
-
-       die "key file '$filename' already exists\n" if -e $filename;
-
-       File::Path::make_path($dirname) if $dirname;
-
-       my $cmd = ['corosync-keygen', '-l', '-k', $filename];
-       PVE::Tools::run_command($cmd);   
-
-       return undef;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'create', 
-    path => 'create',
-    method => 'PUT',
-    description => "Generate new cluster configuration.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           clustername => {
-               description => "The name of the cluster.",
-               type => 'string', format => 'pve-node',
-               maxLength => 15,
-           },
-           nodeid => {
-               type => 'integer',
-               description => "Node id for this node.",
-               minimum => 1,
-               optional => 1,
-           },
-           votes => {
-               type => 'integer',
-               description => "Number of votes for this node.",
-               minimum => 1,
-               optional => 1,
-           },
-       }, 
-    },
-    returns => { type => 'null' },
-    
-    code => sub {
-       my ($param) = @_;
-
-       -f $clusterconf && die "cluster config '$clusterconf' already exists\n";
-
-       PVE::Cluster::setup_sshd_config();
-       PVE::Cluster::setup_rootsshconfig();
-       PVE::Cluster::setup_ssh_keys();
-
-       -f $authfile || __PACKAGE__->keygen({filename => $authfile});
-
-       -f $authfile || die "no authentication key available\n";
-
-       my $clustername = $param->{clustername};
-
-       $param->{nodeid} = 1 if !$param->{nodeid};
-
-       $param->{votes} = 1 if !defined($param->{votes});
-
-       # No, corosync cannot deduce this on its own
-       my $ipversion = Net::IP::ip_is_ipv6($local_ip_address) ? 'ipv6' : 'ipv4';
-
-       my $config = <<_EOD;
-totem {
-  version: 2
-  secauth: on
-  cluster_name: $clustername
-  config_version: 1
-  ip_version: $ipversion
-  interface {
-    ringnumber: 0
-    bindnetaddr: $local_ip_address
-  }
-}
-
-nodelist {
-  node {
-    ring0_addr: $nodename
-    nodeid: $param->{nodeid}
-    quorum_votes: $param->{votes}
-  }
-}
-       
-quorum {
-  provider: corosync_votequorum
-}
-
-logging {
-  to_syslog: yes
-  debug: off
-}
-_EOD
-;      
-       PVE::Tools::file_set_contents($clusterconf, $config);
-
-       PVE::Cluster::ssh_merge_keys();
-
-       PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
-
-       PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
-
-       PVE::Tools::run_command('systemctl restart pve-cluster'); # restart
-
-       PVE::Tools::run_command('systemctl restart corosync'); # restart
-       
-       return undef;
-}});
-
-__PACKAGE__->register_method ({
-    name => 'addnode', 
-    path => 'addnode',
-    method => 'PUT',
-    description => "Adds a node to the cluster configuration.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => PVE::JSONSchema::get_standard_option('pve-node'),
-           nodeid => {
-               type => 'integer',
-               description => "Node id for this node.",
-               minimum => 1,
-               optional => 1,
-           },
-           votes => {
-               type => 'integer',
-               description => "Number of votes for this node",
-               minimum => 0,
-               optional => 1,
-           },
-           force => {
-               type => 'boolean',
-               description => "Do not throw error if node already exists.",
-               optional => 1,
-           },
-       },
-    },
-    returns => { type => 'null' },
-    
-    code => sub {
-       my ($param) = @_;
-
-       PVE::Cluster::check_cfs_quorum();
-
-       my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
-
-       my $nodelist = corosync_nodelist($conf);
-
-       my $name = $param->{node};
-
-       if (defined(my $res = $nodelist->{$name})) {
-           $param->{nodeid} = $res->{nodeid} if !$param->{nodeid};
-           $param->{votes} = $res->{quorum_votes} if !defined($param->{votes});
-
-           if ($res->{quorum_votes} == $param->{votes} &&
-               $res->{nodeid} == $param->{nodeid}) {
-               print "node $name already defined\n";
-               if ($param->{force}) {
-                   exit (0);
-               } else {
-                   exit (-1);
-               }
-           } else {
-               die "can't add existing node\n";
-           }
-       } elsif (!$param->{nodeid}) {
-           my $nodeid = 1;
-           
-           while(1) {
-               my $found = 0; 
-               foreach my $v (values %$nodelist) {
-                   if ($v->{nodeid} eq $nodeid) {
-                       $found = 1;
-                       $nodeid++;
-                       last;
-                   }
-               }
-               last if !$found;
-           };
-
-           $param->{nodeid} = $nodeid;
-       }
-
-       $param->{votes} = 1 if !defined($param->{votes});
-
-       PVE::Cluster::gen_local_dirs($name);
-
-       eval {  PVE::Cluster::ssh_merge_keys(); };
-       warn $@ if $@;
-
-       $nodelist->{$name} = { ring0_addr => $name, nodeid => $param->{nodeid} };
-       $nodelist->{$name}->{quorum_votes} = $param->{votes} if $param->{votes};
-       
-       corosync_update_nodelist($conf, $nodelist);
-       
-       exit (0);
-    }});
-
-
-__PACKAGE__->register_method ({
-    name => 'delnode', 
-    path => 'delnode',
-    method => 'PUT',
-    description => "Removes a node to the cluster configuration.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           node => PVE::JSONSchema::get_standard_option('pve-node'),
-       },
-    },
-    returns => { type => 'null' },
-    
-    code => sub {
-       my ($param) = @_;
-
-       PVE::Cluster::check_cfs_quorum();
-
-       my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
-
-       my $nodelist = corosync_nodelist($conf);
-
-       my $nd = delete $nodelist->{$param->{node}};
-       die "no such node '$param->{node}'\n" if !$nd;
-       
-       corosync_update_nodelist($conf, $nodelist);
-
-       return undef;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'add', 
-    path => 'add',
-    method => 'PUT',
-    description => "Adds the current node to an existing cluster.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           hostname => {
-               type => 'string',
-               description => "Hostname (or IP) of an existing cluster member."
-           },
-           nodeid => {
-               type => 'integer',
-               description => "Node id for this node.",
-               minimum => 1,
-               optional => 1,
-           },
-           votes => {
-               type => 'integer',
-               description => "Number of votes for this node",
-               minimum => 0,
-               optional => 1,
-           },
-           force => {
-               type => 'boolean',
-               description => "Do not throw error if node already exists.",
-               optional => 1,
-           },
-       },
-    },
-    returns => { type => 'null' },
-    
-    code => sub {
-       my ($param) = @_;
-
-       PVE::Cluster::setup_sshd_config();
-       PVE::Cluster::setup_rootsshconfig();
-       PVE::Cluster::setup_ssh_keys();
-
-       my $host = $param->{hostname};
-
-       if (!$param->{force}) {
-           
-           if (-f $authfile) {
-               die "authentication key already exists\n";
-           }
-
-           if (-f $clusterconf)  {
-               die "cluster config '$clusterconf' already exists\n";
-           }
-
-           my $vmlist = PVE::Cluster::get_vmlist();
-           if ($vmlist && $vmlist->{ids} && scalar(keys %{$vmlist->{ids}})) {
-               die "this host already contains virtual machines - please remove them first\n";
-           }
-
-           if (system("corosync-quorumtool >/dev/null 2>&1") == 0) {
-               die "corosync is already running\n";
-           }
-       }
-
-       # make sure known_hosts is on local filesystem
-       PVE::Cluster::ssh_unmerge_known_hosts();
-
-       my $cmd = "ssh-copy-id -i /root/.ssh/id_rsa 'root\@$host' >/dev/null 2>&1";
-       system ($cmd) == 0 ||
-           die "unable to copy ssh ID\n";
 
-       $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
-               'pvecm', 'addnode', $nodename, '--force', 1];
-
-       push @$cmd, '--nodeid', $param->{nodeid} if $param->{nodeid};
-
-       push @$cmd, '--votes', $param->{votes} if defined($param->{votes});
-
-       if (system (@$cmd) != 0) {
-           my $cmdtxt = join (' ', @$cmd);
-           die "unable to add node: command failed ($cmdtxt)\n";
-       }
-
-       my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
-       mkdir $tmpdir;
-
-       eval {
-           print "copy corosync auth key\n";
-           $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq', 
-                   "[$host]:$authfile $clusterconf", $tmpdir];
-
-           system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
-
-           mkdir "/etc/corosync";
-           my $confbase = basename($clusterconf);
-
-           $cmd = "cp '$tmpdir/$confbase' '/etc/corosync/$confbase'";
-           system($cmd) == 0 || die "can't copy cluster configuration\n";
-
-           my $keybase = basename($authfile);
-           system ("cp '$tmpdir/$keybase' '$authfile'") == 0 ||
-               die "can't copy '$tmpdir/$keybase' to '$authfile'\n";
-
-           print "stopping pve-cluster service\n";
-
-           system("umount $basedir -f >/dev/null 2>&1");
-           system("systemctl stop pve-cluster") == 0 ||
-               die "can't stop pve-cluster service\n";
-
-           backup_database();
-
-           unlink $dbfile;
-
-           system("systemctl start pve-cluster") == 0 ||
-               die "starting pve-cluster failed\n";
-
-           system("systemctl start corosync");
-
-           # wait for quorum
-           my $printqmsg = 1;
-           while (!PVE::Cluster::check_cfs_quorum(1)) {
-               if ($printqmsg) {
-                   print "waiting for quorum...";
-                   STDOUT->flush();
-                   $printqmsg = 0;
-               }
-               sleep(1);
-           }
-           print "OK\n" if !$printqmsg;
-
-           # system("systemctl start clvm");
-
-           print "generating node certificates\n";
-           PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address); 
-
-           print "merge known_hosts file\n";
-           PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
-
-           print "restart services\n";
-           # restart pvedaemon (changed certs)
-           system("systemctl restart pvedaemon");
-           # restart pveproxy (changed certs)
-           system("systemctl restart pveproxy");
-
-           print "successfully added node '$nodename' to cluster.\n";
-       };
-       my $err = $@;
-
-       rmtree $tmpdir;
-
-       die $err if $err;
-
-       return undef;
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'status', 
-    path => 'status',
-    method => 'GET',
-    description => "Displays the local view of the cluster status.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {},
-    },
-    returns => { type => 'null' },
-    
-    code => sub {
-       my ($param) = @_;
-
-       my $cmd = ['corosync-quorumtool', '-siH'];
-
-       exec (@$cmd);
-
-       exit (-1); # should not be reached
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'nodes', 
-    path => 'nodes',
-    method => 'GET',
-    description => "Displays the local view of the cluster nodes.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {},
-    },
-    returns => { type => 'null' },
-    
-    code => sub {
-       my ($param) = @_;
-
-       my $cmd = ['corosync-quorumtool', '-l'];
-
-       exec (@$cmd);
-
-       exit (-1); # should not be reached
-    }});
-
-__PACKAGE__->register_method ({
-    name => 'expected', 
-    path => 'expected',
-    method => 'PUT',
-    description => "Tells corosync a new value of expected votes.",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           expected => {
-               type => 'integer',
-               description => "Expected votes",
-               minimum => 1,
-           },
-       },
-    },
-    returns => { type => 'null' },
-    
-    code => sub {
-       my ($param) = @_;
-
-       my $cmd = ['corosync-quorumtool', '-e', $param->{expected}];
-
-       exec (@$cmd);
-
-       exit (-1); # should not be reached
-
-    }});
-
-sub corosync_update_nodelist {
-    my ($conf, $nodelist) = @_;
-
-    delete $conf->{digest};
-    
-    my $version = PVE::Cluster::corosync_conf_version($conf);
-    PVE::Cluster::corosync_conf_version($conf, undef, $version + 1);
-
-    my $children = [];
-    foreach my $v (values %$nodelist) {
-       next if !$v->{ring0_addr};
-       my $kv = [];
-       foreach my $k (keys %$v) {
-           push @$kv, { key => $k, value => $v->{$k} };
-       } 
-       my $ns = { section => 'node', children => $kv };
-       push @$children, $ns;
-    }
-    
-    foreach my $main (@{$conf->{children}}) {
-       next if !defined($main->{section});
-       if ($main->{section} eq 'nodelist') {
-           $main->{children} = $children;
-           last;
-       }
-    }
-
-    
-    PVE::Cluster::cfs_write_file("corosync.conf.new", $conf);
-    
-    rename("/etc/pve/corosync.conf.new", "/etc/pve/corosync.conf")
-       || die "activate  corosync.conf.new failed - $!\n";
-}
-
-sub corosync_nodelist {
-    my ($conf) = @_;
-    
-    my $res = {};
-
-    my $nodelist = {};
-    
-    foreach my $main (@{$conf->{children}}) {
-       next if !defined($main->{section});
-       if ($main->{section} eq 'nodelist') {
-           foreach my $ne (@{$main->{children}}) {
-               next if !defined($ne->{section}) || ($ne->{section} ne 'node');
-               my $node = { quorum_votes => 1 };
-               foreach my $child (@{$ne->{children}}) {
-                   next if !defined($child->{key});
-                   $node->{$child->{key}} = $child->{value};
-                   if ($child->{key} eq 'ring0_addr') {
-                       $nodelist->{$child->{value}} = $node;
-                   }
-               }
-           }
-       }
-    }   
-
-    return $nodelist;
-}
-
-__PACKAGE__->register_method ({
-    name => 'updatecerts', 
-    path => 'updatecerts',
-    method => 'PUT',
-    description => "Update node certificates (and generate all needed files/directories).",
-    parameters => {
-       additionalProperties => 0,
-       properties => {
-           force => {
-               description => "Force generation of new SSL certifate.",
-               type => 'boolean',
-               optional => 1,
-           },
-           silent => {
-               description => "Ignore errors (i.e. when cluster has no quorum).",
-               type => 'boolean',
-               optional => 1,
-           },
-       },
-    },
-    returns => { type => 'null' },
-    code => sub {
-       my ($param) = @_;
-
-       PVE::Cluster::setup_rootsshconfig();
-
-       PVE::Cluster::gen_pve_vzdump_symlink();
-
-       if (!PVE::Cluster::check_cfs_quorum(1)) {
-           return undef if $param->{silent};
-           die "no quorum - unable to update files\n";
-       }
-
-       PVE::Cluster::setup_ssh_keys();
-
-       PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address, $param->{force});
-       PVE::Cluster::ssh_merge_keys();
-       PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address);
-       PVE::Cluster::gen_pve_vzdump_files();
-
-       return undef;
-    }});
-
-
-my $cmddef = {
-    keygen => [ __PACKAGE__, 'keygen', ['filename']],
-    create => [ __PACKAGE__, 'create', ['clustername']],
-    add => [ __PACKAGE__, 'add', ['hostname']],
-    addnode => [ __PACKAGE__, 'addnode', ['node']],
-    delnode => [ __PACKAGE__, 'delnode', ['node']],
-    status => [ __PACKAGE__, 'status' ],
-    nodes => [ __PACKAGE__, 'nodes' ],
-    expected => [ __PACKAGE__, 'expected', ['expected']],
-    updatecerts => [ __PACKAGE__, 'updatecerts', []],
-};
+use PVE::INotify;
+use PVE::Cluster;
+use PVE::CLI::pvecm;
 
-my $cmd = shift;
+my $prepare = sub {
+    my $nodename = PVE::INotify::nodename();
 
-if ($cmd && $cmd ne 'printmanpod' && $cmd ne 'verifyapi') {
     PVE::Cluster::check_cfs_is_mounted();
     PVE::Cluster::cfs_update();
-}
-
-PVE::CLIHandler::handle_cmd($cmddef, "pvecm", $cmd, \@ARGV, undef, $0);
-
-exit 0;
-
-__END__
-
-=head1 NAME
-
-pvecm - Proxmox VE cluster manager toolkit
-
-=head1 SYNOPSIS
-
-=include synopsis
-
-=head1 DESCRIPTION
-
-pvecm is a program to manage the cluster configuration. It can be used
-to create a new cluster, join nodes to a cluster, leave the cluster,
-get status information and do various other cluster related tasks.
-
-=include pve_copyright
 
+    # trigger check that we have resolvable name
+    my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
+};
 
+PVE::CLI::pvecm->run_cli(undef, undef, $prepare);