]>
git.proxmox.com Git - pve-cluster.git/blob - data/PVE/CLI/pvecm.pm
1 package PVE
::CLI
::pvecm
;
9 use PVE
::Tools
qw(run_command);
14 use PVE
::API2
::ClusterConfig
;
17 use base
qw(PVE::CLIHandler);
19 $ENV{HOME
} = '/root'; # for ssh-copy-id
21 my $basedir = "/etc/pve";
22 my $clusterconf = "$basedir/corosync.conf";
23 my $libdir = "/var/lib/pve-cluster";
24 my $backupdir = "/var/lib/pve-cluster/backup";
25 my $dbfile = "$libdir/config.db";
26 my $authfile = "/etc/corosync/authkey";
30 print "backup old database\n";
38 ['gzip', '-', \
">${backupdir}/config-${ctime}.sql.gz"],
41 run_command
($cmd, 'errmsg' => "cannot backup old database\n");
47 foreach my $fn (<$backupdir/config-*.sql
.gz
>) {
48 if ($fn =~ m!/config-(\d+)\.sql.gz$!) {
49 push @bklist, [$fn, $1];
53 @bklist = sort { $b->[1] <=> $a->[1] } @bklist;
55 while (scalar (@bklist) >= $maxfiles) {
57 print "delete old backup '$d->[0]'\n";
63 __PACKAGE__-
>register_method ({
67 description
=> "Generate new cryptographic key for corosync.",
69 additionalProperties
=> 0,
73 description
=> "Output file name"
77 returns
=> { type
=> 'null' },
82 my $filename = $param->{filename
};
85 $> == 0 || die "Error: Authorization key must be generated as root user.\n";
86 my $dirname = dirname
($filename);
88 die "key file '$filename' already exists\n" if -e
$filename;
90 File
::Path
::make_path
($dirname) if $dirname;
92 run_command
(['corosync-keygen', '-l', '-k', $filename]);
97 __PACKAGE__-
>register_method ({
101 description
=> "Generate new cluster configuration.",
103 additionalProperties
=> 0,
106 description
=> "The name of the cluster.",
107 type
=> 'string', format
=> 'pve-node',
112 description
=> "Node id for this node.",
118 description
=> "Number of votes for this node.",
123 type
=> 'string', format
=> 'ip',
124 description
=> "This specifies the network address the corosync ring 0".
125 " executive should bind to and defaults to the local IP address of the node.",
129 type
=> 'string', format
=> 'address',
130 description
=> "Hostname (or IP) of the corosync ring0 address of this node.".
131 " Defaults to the hostname of the node.",
135 type
=> 'string', format
=> 'ip',
136 description
=> "This specifies the network address the corosync ring 1".
137 " executive should bind to and is optional.",
141 type
=> 'string', format
=> 'address',
142 description
=> "Hostname (or IP) of the corosync ring1 address, this".
143 " needs an valid bindnet1_addr.",
148 returns
=> { type
=> 'null' },
153 -f
$clusterconf && die "cluster config '$clusterconf' already exists\n";
155 PVE
::Cluster
::setup_sshd_config
(1);
156 PVE
::Cluster
::setup_rootsshconfig
();
157 PVE
::Cluster
::setup_ssh_keys
();
159 -f
$authfile || __PACKAGE__-
>keygen({filename
=> $authfile});
161 -f
$authfile || die "no authentication key available\n";
163 my $clustername = $param->{clustername
};
165 $param->{nodeid
} = 1 if !$param->{nodeid
};
167 $param->{votes
} = 1 if !defined($param->{votes
});
169 my $nodename = PVE
::INotify
::nodename
();
171 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
173 $param->{bindnet0_addr
} = $local_ip_address
174 if !defined($param->{bindnet0_addr
});
176 $param->{ring0_addr
} = $nodename if !defined($param->{ring0_addr
});
178 die "Param bindnet1_addr and ring1_addr are dependend, use both or none!\n"
179 if (defined($param->{bindnet1_addr
}) != defined($param->{ring1_addr
}));
181 my $bind_is_ipv6 = Net
::IP
::ip_is_ipv6
($param->{bindnet0_addr
});
183 # use string as here-doc format distracts more
184 my $interfaces = "interface {\n ringnumber: 0\n" .
185 " bindnetaddr: $param->{bindnet0_addr}\n }";
187 my $ring_addresses = "ring0_addr: $param->{ring0_addr}" ;
189 # allow use of multiple rings (rrp) at cluster creation time
190 if ($param->{bindnet1_addr
}) {
191 die "IPv6 and IPv4 cannot be mixed, use one or the other!\n"
192 if Net
::IP
::ip_is_ipv6
($param->{bindnet1_addr
}) != $bind_is_ipv6;
194 $interfaces .= "\n interface {\n ringnumber: 1\n" .
195 " bindnetaddr: $param->{bindnet1_addr}\n }\n";
197 $interfaces .= "rrp_mode: passive\n"; # only passive is stable and tested
199 $ring_addresses .= "\n ring1_addr: $param->{ring1_addr}";
202 # No, corosync cannot deduce this on its own
203 my $ipversion = $bind_is_ipv6 ?
'ipv6' : 'ipv4';
209 cluster_name: $clustername
211 ip_version: $ipversion
219 nodeid: $param->{nodeid}
220 quorum_votes: $param->{votes}
225 provider: corosync_votequorum
234 PVE
::Tools
::file_set_contents
($clusterconf, $config);
236 PVE
::Cluster
::ssh_merge_keys
();
238 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address);
240 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
242 run_command
('systemctl restart pve-cluster'); # restart
244 run_command
('systemctl restart corosync'); # restart
249 __PACKAGE__-
>register_method ({
253 description
=> "Adds the current node to an existing cluster.",
255 additionalProperties
=> 0,
259 description
=> "Hostname (or IP) of an existing cluster member."
263 description
=> "Node id for this node.",
269 description
=> "Number of votes for this node",
275 description
=> "Do not throw error if node already exists.",
279 type
=> 'string', format
=> 'address',
280 description
=> "Hostname (or IP) of the corosync ring0 address of this node.".
281 " Defaults to nodes hostname.",
285 type
=> 'string', format
=> 'address',
286 description
=> "Hostname (or IP) of the corosync ring1 address, this".
287 " needs an valid configured ring 1 interface in the cluster.",
292 returns
=> { type
=> 'null' },
297 my $nodename = PVE
::INotify
::nodename
();
299 PVE
::Cluster
::setup_sshd_config
();
300 PVE
::Cluster
::setup_rootsshconfig
();
301 PVE
::Cluster
::setup_ssh_keys
();
303 my $host = $param->{hostname
};
305 my ($errors, $warnings) = ('', '');
308 my ($msg, $suppress) = @_;
311 $warnings .= "* $msg\n";
313 $errors .= "* $msg\n";
317 if (!$param->{force
}) {
320 &$error("authentication key '$authfile' already exists", $param->{force
});
323 if (-f
$clusterconf) {
324 &$error("cluster config '$clusterconf' already exists", $param->{force
});
327 my $vmlist = PVE
::Cluster
::get_vmlist
();
328 if ($vmlist && $vmlist->{ids
} && scalar(keys %{$vmlist->{ids
}})) {
329 &$error("this host already contains virtual guests", $param->{force
});
332 if (system("corosync-quorumtool -l >/dev/null 2>&1") == 0) {
333 &$error("corosync is already running, is this node already in a cluster?!", $param->{force
});
337 # check if corosync ring IPs are configured on the current nodes interfaces
341 if (!PVE
::JSONSchema
::pve_verify_ip
($ip, 1)) {
343 eval { $ip = PVE
::Network
::get_ip_from_hostname
($host); };
345 &$error("cannot use '$host': $@\n") ;
350 my $cidr = (Net
::IP
::ip_is_ipv6
($ip)) ?
"$ip/128" : "$ip/32";
351 my $configured_ips = PVE
::Network
::get_local_ip_from_cidr
($cidr);
353 &$error("cannot use IP '$ip', it must be configured exactly once on local node!\n")
354 if (scalar(@$configured_ips) != 1);
358 &$check_ip($param->{ring0_addr
});
359 &$check_ip($param->{ring1_addr
});
361 warn "warning, ignore the following errors:\n$warnings" if $warnings;
362 die "detected the following error(s):\n$errors" if $errors;
364 # make sure known_hosts is on local filesystem
365 PVE
::Cluster
::ssh_unmerge_known_hosts
();
367 my $cmd = ['ssh-copy-id', '-i', '/root/.ssh/id_rsa', "root\@$host"];
368 run_command
($cmd, 'outfunc' => sub {}, 'errfunc' => sub {},
369 'errmsg' => "unable to copy ssh ID");
371 $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
372 'pvecm', 'addnode', $nodename, '--force', 1];
374 push @$cmd, '--nodeid', $param->{nodeid
} if $param->{nodeid
};
376 push @$cmd, '--votes', $param->{votes
} if defined($param->{votes
});
378 push @$cmd, '--ring0_addr', $param->{ring0_addr
} if defined($param->{ring0_addr
});
380 push @$cmd, '--ring1_addr', $param->{ring1_addr
} if defined($param->{ring1_addr
});
382 if (system (@$cmd) != 0) {
383 my $cmdtxt = join (' ', @$cmd);
384 die "unable to add node: command failed ($cmdtxt)\n";
387 my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
391 print "copy corosync auth key\n";
392 $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
393 "[$host]:$authfile $clusterconf", $tmpdir];
395 system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
397 mkdir "/etc/corosync";
398 my $confbase = basename
($clusterconf);
400 $cmd = "cp '$tmpdir/$confbase' '/etc/corosync/$confbase'";
401 system($cmd) == 0 || die "can't copy cluster configuration\n";
403 my $keybase = basename
($authfile);
404 system ("cp '$tmpdir/$keybase' '$authfile'") == 0 ||
405 die "can't copy '$tmpdir/$keybase' to '$authfile'\n";
407 print "stopping pve-cluster service\n";
409 system("umount $basedir -f >/dev/null 2>&1");
410 system("systemctl stop pve-cluster") == 0 ||
411 die "can't stop pve-cluster service\n";
417 system("systemctl start pve-cluster") == 0 ||
418 die "starting pve-cluster failed\n";
420 system("systemctl start corosync");
424 while (!PVE
::Cluster
::check_cfs_quorum
(1)) {
426 print "waiting for quorum...";
432 print "OK\n" if !$printqmsg;
434 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
436 print "generating node certificates\n";
437 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address);
439 print "merge known_hosts file\n";
440 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
442 print "restart services\n";
443 # restart pvedaemon (changed certs)
444 system("systemctl restart pvedaemon");
445 # restart pveproxy (changed certs)
446 system("systemctl restart pveproxy");
448 print "successfully added node '$nodename' to cluster.\n";
459 __PACKAGE__-
>register_method ({
463 description
=> "Displays the local view of the cluster status.",
465 additionalProperties
=> 0,
468 returns
=> { type
=> 'null' },
473 PVE
::Corosync
::check_conf_exists
();
475 my $cmd = ['corosync-quorumtool', '-siH'];
479 exit (-1); # should not be reached
482 __PACKAGE__-
>register_method ({
486 description
=> "Displays the local view of the cluster nodes.",
488 additionalProperties
=> 0,
491 returns
=> { type
=> 'null' },
496 PVE
::Corosync
::check_conf_exists
();
498 my $cmd = ['corosync-quorumtool', '-l'];
502 exit (-1); # should not be reached
505 __PACKAGE__-
>register_method ({
509 description
=> "Tells corosync a new value of expected votes.",
511 additionalProperties
=> 0,
515 description
=> "Expected votes",
520 returns
=> { type
=> 'null' },
525 PVE
::Corosync
::check_conf_exists
();
527 my $cmd = ['corosync-quorumtool', '-e', $param->{expected
}];
531 exit (-1); # should not be reached
535 __PACKAGE__-
>register_method ({
536 name
=> 'updatecerts',
537 path
=> 'updatecerts',
539 description
=> "Update node certificates (and generate all needed files/directories).",
541 additionalProperties
=> 0,
544 description
=> "Force generation of new SSL certifate.",
549 description
=> "Ignore errors (i.e. when cluster has no quorum).",
555 returns
=> { type
=> 'null' },
559 PVE
::Cluster
::setup_rootsshconfig
();
561 PVE
::Cluster
::gen_pve_vzdump_symlink
();
563 if (!PVE
::Cluster
::check_cfs_quorum
(1)) {
564 return undef if $param->{silent
};
565 die "no quorum - unable to update files\n";
568 PVE
::Cluster
::setup_ssh_keys
();
570 my $nodename = PVE
::INotify
::nodename
();
572 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
574 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address, $param->{force
});
575 PVE
::Cluster
::ssh_merge_keys
();
576 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address);
577 PVE
::Cluster
::gen_pve_vzdump_files
();
582 __PACKAGE__-
>register_method ({
586 description
=> "Used by VM/CT migration - do not use manually.",
588 additionalProperties
=> 0,
590 get_migration_ip
=> {
593 description
=> 'return the migration IP, if configured',
596 migration_network
=> {
599 description
=> 'the migration network used to detect the local migration IP',
604 description
=> 'Run a command with a tcp socket as standard input.'
605 .' The IP address and port are printed via this'
606 ." command's stdandard output first, each on a separate line.",
609 'extra-args' => PVE
::JSONSchema
::get_standard_option
('extra-args'),
612 returns
=> { type
=> 'null'},
616 if (!PVE
::Cluster
::check_cfs_quorum
(1)) {
621 my $network = $param->{migration_network
};
622 if ($param->{get_migration_ip
}) {
623 die "cannot use --run-command with --get_migration_ip\n"
624 if $param->{'run-command'};
625 if (my $ip = PVE
::Cluster
::get_local_migration_ip
($network)) {
630 # do not keep tunnel open when asked for migration ip
634 if ($param->{'run-command'}) {
635 my $cmd = $param->{'extra-args'};
636 die "missing command\n"
637 if !$cmd || !scalar(@$cmd);
639 # Get an ip address to listen on, and find a free migration port
641 if (defined($network)) {
642 $ip = PVE
::Cluster
::get_local_migration_ip
($network)
643 or die "failed to get migration IP address to listen on\n";
644 $family = PVE
::Tools
::get_host_address_family
($ip);
646 my $nodename = PVE
::INotify
::nodename
();
647 ($ip, $family) = PVE
::Network
::get_ip_from_hostname
($nodename, 0);
649 my $port = PVE
::Tools
::next_migrate_port
($family, $ip);
651 PVE
::Tools
::pipe_socket_to_command
($cmd, $ip, $port);
655 print "tunnel online\n";
658 while (my $line = <STDIN
>) {
660 last if $line =~ m/^quit$/;
668 keygen
=> [ __PACKAGE__
, 'keygen', ['filename']],
669 create
=> [ __PACKAGE__
, 'create', ['clustername']],
670 add
=> [ __PACKAGE__
, 'add', ['hostname']],
671 addnode
=> [ 'PVE::API2::ClusterConfig', 'addnode', ['node']],
672 delnode
=> [ 'PVE::API2::ClusterConfig', 'delnode', ['node']],
673 status
=> [ __PACKAGE__
, 'status' ],
674 nodes
=> [ __PACKAGE__
, 'nodes' ],
675 expected
=> [ __PACKAGE__
, 'expected', ['expected']],
676 updatecerts
=> [ __PACKAGE__
, 'updatecerts', []],
677 mtunnel
=> [ __PACKAGE__
, 'mtunnel', ['extra-args']],