]>
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);
13 use PVE
::RPCEnvironment
;
16 use PVE
::API2
::ClusterConfig
;
19 use base
qw(PVE::CLIHandler);
21 $ENV{HOME
} = '/root'; # for ssh-copy-id
23 my $basedir = "/etc/pve";
24 my $clusterconf = "$basedir/corosync.conf";
25 my $libdir = "/var/lib/pve-cluster";
26 my $authfile = "/etc/corosync/authkey";
29 sub setup_environment
{
30 PVE
::RPCEnvironment-
>setup_default_cli_env();
33 __PACKAGE__-
>register_method ({
37 description
=> "Generate new cryptographic key for corosync.",
39 additionalProperties
=> 0,
43 description
=> "Output file name"
47 returns
=> { type
=> 'null' },
52 my $filename = $param->{filename
};
55 $> == 0 || die "Error: Authorization key must be generated as root user.\n";
56 my $dirname = dirname
($filename);
58 die "key file '$filename' already exists\n" if -e
$filename;
60 File
::Path
::make_path
($dirname) if $dirname;
62 run_command
(['corosync-keygen', '-l', '-k', $filename]);
67 __PACKAGE__-
>register_method ({
71 description
=> "Generate new cluster configuration.",
73 additionalProperties
=> 0,
76 description
=> "The name of the cluster.",
77 type
=> 'string', format
=> 'pve-node',
82 description
=> "Node id for this node.",
88 description
=> "Number of votes for this node.",
93 type
=> 'string', format
=> 'ip',
94 description
=> "This specifies the network address the corosync ring 0".
95 " executive should bind to and defaults to the local IP address of the node.",
99 type
=> 'string', format
=> 'address',
100 description
=> "Hostname (or IP) of the corosync ring0 address of this node.".
101 " Defaults to the hostname of the node.",
105 type
=> 'string', format
=> 'ip',
106 description
=> "This specifies the network address the corosync ring 1".
107 " executive should bind to and is optional.",
111 type
=> 'string', format
=> 'address',
112 description
=> "Hostname (or IP) of the corosync ring1 address, this".
113 " needs an valid bindnet1_addr.",
118 returns
=> { type
=> 'null' },
123 -f
$clusterconf && die "cluster config '$clusterconf' already exists\n";
125 PVE
::Cluster
::setup_sshd_config
(1);
126 PVE
::Cluster
::setup_rootsshconfig
();
127 PVE
::Cluster
::setup_ssh_keys
();
129 -f
$authfile || __PACKAGE__-
>keygen({filename
=> $authfile});
131 -f
$authfile || die "no authentication key available\n";
133 my $clustername = $param->{clustername
};
135 $param->{nodeid
} = 1 if !$param->{nodeid
};
137 $param->{votes
} = 1 if !defined($param->{votes
});
139 my $nodename = PVE
::INotify
::nodename
();
141 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
143 $param->{bindnet0_addr
} = $local_ip_address
144 if !defined($param->{bindnet0_addr
});
146 $param->{ring0_addr
} = $nodename if !defined($param->{ring0_addr
});
148 die "Param bindnet1_addr and ring1_addr are dependend, use both or none!\n"
149 if (defined($param->{bindnet1_addr
}) != defined($param->{ring1_addr
}));
151 my $bind_is_ipv6 = Net
::IP
::ip_is_ipv6
($param->{bindnet0_addr
});
153 # use string as here-doc format distracts more
154 my $interfaces = "interface {\n ringnumber: 0\n" .
155 " bindnetaddr: $param->{bindnet0_addr}\n }";
157 my $ring_addresses = "ring0_addr: $param->{ring0_addr}" ;
159 # allow use of multiple rings (rrp) at cluster creation time
160 if ($param->{bindnet1_addr
}) {
161 die "IPv6 and IPv4 cannot be mixed, use one or the other!\n"
162 if Net
::IP
::ip_is_ipv6
($param->{bindnet1_addr
}) != $bind_is_ipv6;
164 $interfaces .= "\n interface {\n ringnumber: 1\n" .
165 " bindnetaddr: $param->{bindnet1_addr}\n }\n";
167 $interfaces .= "rrp_mode: passive\n"; # only passive is stable and tested
169 $ring_addresses .= "\n ring1_addr: $param->{ring1_addr}";
172 # No, corosync cannot deduce this on its own
173 my $ipversion = $bind_is_ipv6 ?
'ipv6' : 'ipv4';
179 cluster_name: $clustername
181 ip_version: $ipversion
189 nodeid: $param->{nodeid}
190 quorum_votes: $param->{votes}
195 provider: corosync_votequorum
204 PVE
::Tools
::file_set_contents
($clusterconf, $config);
206 PVE
::Cluster
::ssh_merge_keys
();
208 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address);
210 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
212 run_command
('systemctl restart pve-cluster'); # restart
214 run_command
('systemctl restart corosync'); # restart
219 __PACKAGE__-
>register_method ({
223 description
=> "Adds the current node to an existing cluster.",
225 additionalProperties
=> 0,
229 description
=> "Hostname (or IP) of an existing cluster member."
233 description
=> "Node id for this node.",
239 description
=> "Number of votes for this node",
245 description
=> "Do not throw error if node already exists.",
249 type
=> 'string', format
=> 'address',
250 description
=> "Hostname (or IP) of the corosync ring0 address of this node.".
251 " Defaults to nodes hostname.",
255 type
=> 'string', format
=> 'address',
256 description
=> "Hostname (or IP) of the corosync ring1 address, this".
257 " needs an valid configured ring 1 interface in the cluster.",
260 fingerprint
=> PVE
::JSONSchema
::get_standard_option
('fingerprint-sha256', {
265 description
=> "Always use SSH to join, even if peer may do it over API.",
270 returns
=> { type
=> 'null' },
275 my $nodename = PVE
::INotify
::nodename
();
277 my $host = $param->{hostname
};
279 PVE
::Cluster
::assert_joinable
($param->{ring0_addr
}, $param->{ring1_addr
}, $param->{force
});
281 if (!$param->{use_ssh
}) {
282 print "Please enter superuser (root) password for '$host':\n";
283 my $password = PVE
::PTY
::read_password
("Password for root\@$host: ");
285 delete $param->{use_ssh
};
286 $param->{password
} = $password;
288 eval { PVE
::Cluster
::join($param) };
291 if (ref($err) eq 'PVE::APIClient::Exception' && $err->{code
} == 501) {
292 $err = "Remote side is not able to use API for Cluster join!\n" .
293 "Pass the 'use_ssh' switch or update the remote side.\n";
297 return; # all OK, the API join endpoint successfully set us up
300 # allow fallback to old ssh only join if wished or needed
302 PVE
::Cluster
::setup_sshd_config
();
303 PVE
::Cluster
::setup_rootsshconfig
();
304 PVE
::Cluster
::setup_ssh_keys
();
306 # make sure known_hosts is on local filesystem
307 PVE
::Cluster
::ssh_unmerge_known_hosts
();
309 my $cmd = ['ssh-copy-id', '-i', '/root/.ssh/id_rsa', "root\@$host"];
310 run_command
($cmd, 'outfunc' => sub {}, 'errfunc' => sub {},
311 'errmsg' => "unable to copy ssh ID");
313 $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
314 'pvecm', 'addnode', $nodename, '--force', 1];
316 push @$cmd, '--nodeid', $param->{nodeid
} if $param->{nodeid
};
317 push @$cmd, '--votes', $param->{votes
} if defined($param->{votes
});
318 push @$cmd, '--ring0_addr', $param->{ring0_addr
} if defined($param->{ring0_addr
});
319 push @$cmd, '--ring1_addr', $param->{ring1_addr
} if defined($param->{ring1_addr
});
321 if (system (@$cmd) != 0) {
322 my $cmdtxt = join (' ', @$cmd);
323 die "unable to add node: command failed ($cmdtxt)\n";
326 my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
330 print "copy corosync auth key\n";
331 $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
332 "[$host]:$authfile $clusterconf", $tmpdir];
334 system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
336 my $corosync_conf = PVE
::Tools
::file_get_contents
("$tmpdir/corosync.conf");
337 my $corosync_authkey = PVE
::Tools
::file_get_contents
("$tmpdir/authkey");
339 PVE
::Cluster
::finish_join
($host, $corosync_conf, $corosync_authkey);
350 __PACKAGE__-
>register_method ({
354 description
=> "Displays the local view of the cluster status.",
356 additionalProperties
=> 0,
359 returns
=> { type
=> 'null' },
364 PVE
::Corosync
::check_conf_exists
();
366 my $cmd = ['corosync-quorumtool', '-siH'];
370 exit (-1); # should not be reached
373 __PACKAGE__-
>register_method ({
377 description
=> "Displays the local view of the cluster nodes.",
379 additionalProperties
=> 0,
382 returns
=> { type
=> 'null' },
387 PVE
::Corosync
::check_conf_exists
();
389 my $cmd = ['corosync-quorumtool', '-l'];
393 exit (-1); # should not be reached
396 __PACKAGE__-
>register_method ({
400 description
=> "Tells corosync a new value of expected votes.",
402 additionalProperties
=> 0,
406 description
=> "Expected votes",
411 returns
=> { type
=> 'null' },
416 PVE
::Corosync
::check_conf_exists
();
418 my $cmd = ['corosync-quorumtool', '-e', $param->{expected
}];
422 exit (-1); # should not be reached
426 __PACKAGE__-
>register_method ({
427 name
=> 'updatecerts',
428 path
=> 'updatecerts',
430 description
=> "Update node certificates (and generate all needed files/directories).",
432 additionalProperties
=> 0,
435 description
=> "Force generation of new SSL certifate.",
440 description
=> "Ignore errors (i.e. when cluster has no quorum).",
446 returns
=> { type
=> 'null' },
450 PVE
::Cluster
::setup_rootsshconfig
();
452 PVE
::Cluster
::gen_pve_vzdump_symlink
();
454 if (!PVE
::Cluster
::check_cfs_quorum
(1)) {
455 return undef if $param->{silent
};
456 die "no quorum - unable to update files\n";
459 PVE
::Cluster
::setup_ssh_keys
();
461 my $nodename = PVE
::INotify
::nodename
();
463 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
465 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address, $param->{force
});
466 PVE
::Cluster
::ssh_merge_keys
();
467 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address);
468 PVE
::Cluster
::gen_pve_vzdump_files
();
473 __PACKAGE__-
>register_method ({
477 description
=> "Used by VM/CT migration - do not use manually.",
479 additionalProperties
=> 0,
481 get_migration_ip
=> {
484 description
=> 'return the migration IP, if configured',
487 migration_network
=> {
490 description
=> 'the migration network used to detect the local migration IP',
495 description
=> 'Run a command with a tcp socket as standard input.'
496 .' The IP address and port are printed via this'
497 ." command's stdandard output first, each on a separate line.",
500 'extra-args' => PVE
::JSONSchema
::get_standard_option
('extra-args'),
503 returns
=> { type
=> 'null'},
507 if (!PVE
::Cluster
::check_cfs_quorum
(1)) {
512 my $network = $param->{migration_network
};
513 if ($param->{get_migration_ip
}) {
514 die "cannot use --run-command with --get_migration_ip\n"
515 if $param->{'run-command'};
516 if (my $ip = PVE
::Cluster
::get_local_migration_ip
($network)) {
521 # do not keep tunnel open when asked for migration ip
525 if ($param->{'run-command'}) {
526 my $cmd = $param->{'extra-args'};
527 die "missing command\n"
528 if !$cmd || !scalar(@$cmd);
530 # Get an ip address to listen on, and find a free migration port
532 if (defined($network)) {
533 $ip = PVE
::Cluster
::get_local_migration_ip
($network)
534 or die "failed to get migration IP address to listen on\n";
535 $family = PVE
::Tools
::get_host_address_family
($ip);
537 my $nodename = PVE
::INotify
::nodename
();
538 ($ip, $family) = PVE
::Network
::get_ip_from_hostname
($nodename, 0);
540 my $port = PVE
::Tools
::next_migrate_port
($family, $ip);
542 PVE
::Tools
::pipe_socket_to_command
($cmd, $ip, $port);
546 print "tunnel online\n";
549 while (my $line = <STDIN
>) {
551 last if $line =~ m/^quit$/;
559 keygen
=> [ __PACKAGE__
, 'keygen', ['filename']],
560 create
=> [ __PACKAGE__
, 'create', ['clustername']],
561 add
=> [ __PACKAGE__
, 'add', ['hostname']],
562 addnode
=> [ 'PVE::API2::ClusterConfig', 'addnode', ['node']],
563 delnode
=> [ 'PVE::API2::ClusterConfig', 'delnode', ['node']],
564 status
=> [ __PACKAGE__
, 'status' ],
565 nodes
=> [ __PACKAGE__
, 'nodes' ],
566 expected
=> [ __PACKAGE__
, 'expected', ['expected']],
567 updatecerts
=> [ __PACKAGE__
, 'updatecerts', []],
568 mtunnel
=> [ __PACKAGE__
, 'mtunnel', ['extra-args']],