X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=data%2FPVE%2FCLI%2Fpvecm.pm;h=3f0ac128c22888801c947bf494edfd71eb33086c;hb=8db9997d46e8807551f7de3a1e9e674330538518;hp=1fbb58d2bbf206e0e69c8680088152e3350e0308;hpb=1d26c2028062bc33f0e4655043b3279f318a2b4b;p=pve-cluster.git diff --git a/data/PVE/CLI/pvecm.pm b/data/PVE/CLI/pvecm.pm index 1fbb58d..3f0ac12 100755 --- a/data/PVE/CLI/pvecm.pm +++ b/data/PVE/CLI/pvecm.pm @@ -3,14 +3,15 @@ package PVE::CLI::pvecm; use strict; use warnings; -use Net::IP; use File::Path; use File::Basename; use PVE::Tools qw(run_command); use PVE::Cluster; use PVE::INotify; -use PVE::JSONSchema; +use PVE::JSONSchema qw(get_standard_option); +use PVE::RPCEnvironment; use PVE::CLIHandler; +use PVE::PTY; use PVE::API2::ClusterConfig; use PVE::Corosync; @@ -21,45 +22,13 @@ $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'], - ['sqlite3', $dbfile], - ['gzip', '-', \ ">${backupdir}/config-${ctime}.sql.gz"], - ]; - - run_command($cmd, 'errmsg' => "cannot 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]; - } +sub setup_environment { + PVE::RPCEnvironment->setup_default_cli_env(); } - __PACKAGE__->register_method ({ name => 'keygen', path => 'keygen', @@ -94,158 +63,6 @@ __PACKAGE__->register_method ({ 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, - }, - bindnet0_addr => { - type => 'string', format => 'ip', - description => "This specifies the network address the corosync ring 0". - " executive should bind to and defaults to the local IP address of the node.", - optional => 1, - }, - ring0_addr => { - type => 'string', format => 'address', - description => "Hostname (or IP) of the corosync ring0 address of this node.". - " Defaults to the hostname of the node.", - optional => 1, - }, - bindnet1_addr => { - type => 'string', format => 'ip', - description => "This specifies the network address the corosync ring 1". - " executive should bind to and is optional.", - optional => 1, - }, - ring1_addr => { - type => 'string', format => 'address', - description => "Hostname (or IP) of the corosync ring1 address, this". - " needs an valid bindnet1_addr.", - optional => 1, - }, - }, - }, - returns => { type => 'null' }, - - code => sub { - my ($param) = @_; - - -f $clusterconf && die "cluster config '$clusterconf' already exists\n"; - - PVE::Cluster::setup_sshd_config(1); - 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); - - $param->{bindnet0_addr} = $local_ip_address - if !defined($param->{bindnet0_addr}); - - $param->{ring0_addr} = $nodename if !defined($param->{ring0_addr}); - - die "Param bindnet1_addr and ring1_addr are dependend, use both or none!\n" - if (defined($param->{bindnet1_addr}) != defined($param->{ring1_addr})); - - my $bind_is_ipv6 = Net::IP::ip_is_ipv6($param->{bindnet0_addr}); - - # use string as here-doc format distracts more - my $interfaces = "interface {\n ringnumber: 0\n" . - " bindnetaddr: $param->{bindnet0_addr}\n }"; - - my $ring_addresses = "ring0_addr: $param->{ring0_addr}" ; - - # allow use of multiple rings (rrp) at cluster creation time - if ($param->{bindnet1_addr}) { - die "IPv6 and IPv4 cannot be mixed, use one or the other!\n" - if Net::IP::ip_is_ipv6($param->{bindnet1_addr}) != $bind_is_ipv6; - - $interfaces .= "\n interface {\n ringnumber: 1\n" . - " bindnetaddr: $param->{bindnet1_addr}\n }\n"; - - $interfaces .= "rrp_mode: passive\n"; # only passive is stable and tested - - $ring_addresses .= "\n ring1_addr: $param->{ring1_addr}"; - } - - # No, corosync cannot deduce this on its own - my $ipversion = $bind_is_ipv6 ? 'ipv6' : 'ipv4'; - - my $config = <<_EOD; -totem { - version: 2 - secauth: on - cluster_name: $clustername - config_version: 1 - ip_version: $ipversion - $interfaces -} - -nodelist { - node { - $ring_addresses - name: $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); - - run_command('systemctl restart pve-cluster'); # restart - - run_command('systemctl restart corosync'); # restart - - return undef; -}}); - __PACKAGE__->register_method ({ name => 'add', path => 'add', @@ -258,12 +75,7 @@ __PACKAGE__->register_method ({ type => 'string', description => "Hostname (or IP) of an existing cluster member." }, - nodeid => { - type => 'integer', - description => "Node id for this node.", - minimum => 1, - optional => 1, - }, + nodeid => get_standard_option('corosync-nodeid'), votes => { type => 'integer', description => "Number of votes for this node", @@ -275,16 +87,14 @@ __PACKAGE__->register_method ({ description => "Do not throw error if node already exists.", optional => 1, }, - ring0_addr => { - type => 'string', format => 'address', - description => "Hostname (or IP) of the corosync ring0 address of this node.". - " Defaults to nodes hostname.", + ring0_addr => get_standard_option('corosync-ring0-addr'), + ring1_addr => get_standard_option('corosync-ring1-addr'), + fingerprint => get_standard_option('fingerprint-sha256', { optional => 1, - }, - ring1_addr => { - type => 'string', format => 'address', - description => "Hostname (or IP) of the corosync ring1 address, this". - " needs an valid configured ring 1 interface in the cluster.", + }), + 'use_ssh' => { + type => 'boolean', + description => "Always use SSH to join, even if peer may do it over API.", optional => 1, }, }, @@ -296,162 +106,86 @@ __PACKAGE__->register_method ({ my $nodename = PVE::INotify::nodename(); - PVE::Cluster::setup_sshd_config(); - PVE::Cluster::setup_rootsshconfig(); - PVE::Cluster::setup_ssh_keys(); - my $host = $param->{hostname}; + my $local_ip_address = PVE::Cluster::remote_node_ip($nodename); - my ($errors, $warnings) = ('', ''); - - my $error = sub { - my ($msg, $suppress) = @_; - - if ($suppress) { - $warnings .= "* $msg\n"; - } else { - $errors .= "* $msg\n"; - } - }; - - if (!$param->{force}) { + PVE::Cluster::assert_joinable($param->{ring0_addr}, $param->{ring1_addr}, $param->{force}); - if (-f $authfile) { - &$error("authentication key '$authfile' already exists", $param->{force}); - } + my $worker = sub { - if (-f $clusterconf) { - &$error("cluster config '$clusterconf' already exists", $param->{force}); - } + if (!$param->{use_ssh}) { + print "Please enter superuser (root) password for '$host':\n"; + my $password = PVE::PTY::read_password("Password for root\@$host: "); - my $vmlist = PVE::Cluster::get_vmlist(); - if ($vmlist && $vmlist->{ids} && scalar(keys %{$vmlist->{ids}})) { - &$error("this host already contains virtual guests", $param->{force}); - } + delete $param->{use_ssh}; + $param->{password} = $password; - if (system("corosync-quorumtool -l >/dev/null 2>&1") == 0) { - &$error("corosync is already running, is this node already in a cluster?!", $param->{force}); - } - } + my $local_cluster_lock = "/var/lock/pvecm.lock"; + PVE::Tools::lock_file($local_cluster_lock, 10, \&PVE::Cluster::join, $param); - # check if corosync ring IPs are configured on the current nodes interfaces - my $check_ip = sub { - my $ip = shift; - if (defined($ip)) { - if (!PVE::JSONSchema::pve_verify_ip($ip, 1)) { - my $host = $ip; - eval { $ip = PVE::Network::get_ip_from_hostname($host); }; - if ($@) { - &$error("cannot use '$host': $@\n") ; - return; + if (my $err = $@) { + if (ref($err) eq 'PVE::APIClient::Exception' && defined($err->{code}) && $err->{code} == 501) { + $err = "Remote side is not able to use API for Cluster join!\n" . + "Pass the 'use_ssh' switch or update the remote side.\n"; } + die $err; } - - my $cidr = (Net::IP::ip_is_ipv6($ip)) ? "$ip/128" : "$ip/32"; - my $configured_ips = PVE::Network::get_local_ip_from_cidr($cidr); - - &$error("cannot use IP '$ip', it must be configured exactly once on local node!\n") - if (scalar(@$configured_ips) != 1); + return; # all OK, the API join endpoint successfully set us up } - }; - - &$check_ip($param->{ring0_addr}); - &$check_ip($param->{ring1_addr}); - - warn "warning, ignore the following errors:\n$warnings" if $warnings; - die "detected the following error(s):\n$errors" if $errors; - - # 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"]; - run_command($cmd, 'outfunc' => sub {}, 'errfunc' => sub {}, - 'errmsg' => "unable to copy ssh ID"); - - $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}); - - push @$cmd, '--ring0_addr', $param->{ring0_addr} if defined($param->{ring0_addr}); - - push @$cmd, '--ring1_addr', $param->{ring1_addr} if defined($param->{ring1_addr}); - - 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"; + # allow fallback to old ssh only join if wished or needed - mkdir "/etc/corosync"; - my $confbase = basename($clusterconf); + PVE::Cluster::setup_sshd_config(); + PVE::Cluster::setup_rootsshconfig(); + PVE::Cluster::setup_ssh_keys(); - $cmd = "cp '$tmpdir/$confbase' '/etc/corosync/$confbase'"; - system($cmd) == 0 || die "can't copy cluster configuration\n"; + # make sure known_hosts is on local filesystem + PVE::Cluster::ssh_unmerge_known_hosts(); - my $keybase = basename($authfile); - system ("cp '$tmpdir/$keybase' '$authfile'") == 0 || - die "can't copy '$tmpdir/$keybase' to '$authfile'\n"; + my $cmd = ['ssh-copy-id', '-i', '/root/.ssh/id_rsa', "root\@$host"]; + run_command($cmd, 'outfunc' => sub {}, 'errfunc' => sub {}, + 'errmsg' => "unable to copy ssh ID"); - print "stopping pve-cluster service\n"; + $cmd = ['ssh', $host, '-o', 'BatchMode=yes', + 'pvecm', 'addnode', $nodename, '--force', 1]; - system("umount $basedir -f >/dev/null 2>&1"); - system("systemctl stop pve-cluster") == 0 || - die "can't stop pve-cluster service\n"; + push @$cmd, '--nodeid', $param->{nodeid} if $param->{nodeid}; + push @$cmd, '--votes', $param->{votes} if defined($param->{votes}); + push @$cmd, '--ring0_addr', $param->{ring0_addr} // $local_ip_address; + push @$cmd, '--ring1_addr', $param->{ring1_addr} if defined($param->{ring1_addr}); - backup_database(); - - unlink $dbfile; - - system("systemctl start pve-cluster") == 0 || - die "starting pve-cluster failed\n"; + if (system (@$cmd) != 0) { + my $cmdtxt = join (' ', @$cmd); + die "unable to add node: command failed ($cmdtxt)\n"; + } - system("systemctl start corosync"); + my $tmpdir = "$libdir/.pvecm_add.tmp.$$"; + mkdir $tmpdir; - # 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; + eval { + print "copy corosync auth key\n"; + $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq', + "[$host]:$authfile $clusterconf", $tmpdir]; - my $local_ip_address = PVE::Cluster::remote_node_ip($nodename); + system(@$cmd) == 0 || die "can't rsync data from host '$host'\n"; - print "generating node certificates\n"; - PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address); + my $corosync_conf = PVE::Tools::file_get_contents("$tmpdir/corosync.conf"); + my $corosync_authkey = PVE::Tools::file_get_contents("$tmpdir/authkey"); - print "merge known_hosts file\n"; - PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1); + PVE::Cluster::finish_join($host, $corosync_conf, $corosync_authkey); + }; + my $err = $@; - print "restart services\n"; - # restart pvedaemon (changed certs) - system("systemctl restart pvedaemon"); - # restart pveproxy (changed certs) - system("systemctl restart pveproxy"); + rmtree $tmpdir; - print "successfully added node '$nodename' to cluster.\n"; + die $err if $err; }; - my $err = $@; - rmtree $tmpdir; + # use a synced worker so we get a nice task log when joining through CLI + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); - die $err if $err; + $rpcenv->fork_worker('clusterjoin', '', $authuser, $worker); return undef; }}); @@ -556,25 +290,7 @@ __PACKAGE__->register_method ({ 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(); + PVE::Cluster::updatecerts_and_ssh($param->@{qw(force silent)}); return undef; }}); @@ -666,7 +382,7 @@ __PACKAGE__->register_method ({ our $cmddef = { keygen => [ __PACKAGE__, 'keygen', ['filename']], - create => [ __PACKAGE__, 'create', ['clustername']], + create => [ 'PVE::API2::ClusterConfig', 'create', ['clustername']], add => [ __PACKAGE__, 'add', ['hostname']], addnode => [ 'PVE::API2::ClusterConfig', 'addnode', ['node']], delnode => [ 'PVE::API2::ClusterConfig', 'delnode', ['node']],