]> git.proxmox.com Git - pve-cluster.git/blame - data/PVE/CLI/pvecm.pm
pvecm add: use API by default to join cluster
[pve-cluster.git] / data / PVE / CLI / pvecm.pm
CommitLineData
13d44dc5
DM
1package PVE::CLI::pvecm;
2
3use strict;
4use warnings;
95e7bcac 5
13d44dc5
DM
6use Net::IP;
7use File::Path;
8use File::Basename;
294f76c4 9use PVE::Tools qw(run_command);
13d44dc5
DM
10use PVE::Cluster;
11use PVE::INotify;
12use PVE::JSONSchema;
03b23bd0 13use PVE::RPCEnvironment;
13d44dc5 14use PVE::CLIHandler;
03b23bd0 15use PVE::PTY;
1d26c202 16use PVE::API2::ClusterConfig;
b6973a89 17use PVE::Corosync;
13d44dc5
DM
18
19use base qw(PVE::CLIHandler);
20
21$ENV{HOME} = '/root'; # for ssh-copy-id
22
23my $basedir = "/etc/pve";
24my $clusterconf = "$basedir/corosync.conf";
25my $libdir = "/var/lib/pve-cluster";
13d44dc5
DM
26my $authfile = "/etc/corosync/authkey";
27
9db3f0c0 28
03b23bd0
TL
29sub setup_environment {
30 PVE::RPCEnvironment->setup_default_cli_env();
31}
32
13d44dc5 33__PACKAGE__->register_method ({
c53b111f 34 name => 'keygen',
13d44dc5
DM
35 path => 'keygen',
36 method => 'PUT',
37 description => "Generate new cryptographic key for corosync.",
38 parameters => {
39 additionalProperties => 0,
40 properties => {
41 filename => {
42 type => 'string',
43 description => "Output file name"
44 }
45 },
46 },
47 returns => { type => 'null' },
c53b111f 48
13d44dc5
DM
49 code => sub {
50 my ($param) = @_;
51
52 my $filename = $param->{filename};
53
54 # test EUID
55 $> == 0 || die "Error: Authorization key must be generated as root user.\n";
56 my $dirname = dirname($filename);
13d44dc5
DM
57
58 die "key file '$filename' already exists\n" if -e $filename;
59
60 File::Path::make_path($dirname) if $dirname;
61
294f76c4 62 run_command(['corosync-keygen', '-l', '-k', $filename]);
13d44dc5
DM
63
64 return undef;
65 }});
66
67__PACKAGE__->register_method ({
c53b111f 68 name => 'create',
13d44dc5
DM
69 path => 'create',
70 method => 'PUT',
71 description => "Generate new cluster configuration.",
72 parameters => {
73 additionalProperties => 0,
74 properties => {
75 clustername => {
76 description => "The name of the cluster.",
77 type => 'string', format => 'pve-node',
78 maxLength => 15,
79 },
80 nodeid => {
81 type => 'integer',
82 description => "Node id for this node.",
83 minimum => 1,
84 optional => 1,
85 },
86 votes => {
87 type => 'integer',
88 description => "Number of votes for this node.",
89 minimum => 1,
90 optional => 1,
91 },
14d0000a
TL
92 bindnet0_addr => {
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.",
96 optional => 1,
97 },
98 ring0_addr => {
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.",
102 optional => 1,
103 },
14d0000a
TL
104 bindnet1_addr => {
105 type => 'string', format => 'ip',
106 description => "This specifies the network address the corosync ring 1".
107 " executive should bind to and is optional.",
108 optional => 1,
109 },
110 ring1_addr => {
111 type => 'string', format => 'address',
112 description => "Hostname (or IP) of the corosync ring1 address, this".
113 " needs an valid bindnet1_addr.",
114 optional => 1,
115 },
116 },
13d44dc5
DM
117 },
118 returns => { type => 'null' },
c53b111f 119
13d44dc5
DM
120 code => sub {
121 my ($param) = @_;
122
123 -f $clusterconf && die "cluster config '$clusterconf' already exists\n";
124
6c0e95b3 125 PVE::Cluster::setup_sshd_config(1);
13d44dc5
DM
126 PVE::Cluster::setup_rootsshconfig();
127 PVE::Cluster::setup_ssh_keys();
128
129 -f $authfile || __PACKAGE__->keygen({filename => $authfile});
130
131 -f $authfile || die "no authentication key available\n";
132
133 my $clustername = $param->{clustername};
134
135 $param->{nodeid} = 1 if !$param->{nodeid};
136
137 $param->{votes} = 1 if !defined($param->{votes});
138
139 my $nodename = PVE::INotify::nodename();
c53b111f 140
13d44dc5
DM
141 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
142
14d0000a
TL
143 $param->{bindnet0_addr} = $local_ip_address
144 if !defined($param->{bindnet0_addr});
145
146 $param->{ring0_addr} = $nodename if !defined($param->{ring0_addr});
147
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}));
150
151 my $bind_is_ipv6 = Net::IP::ip_is_ipv6($param->{bindnet0_addr});
152
153 # use string as here-doc format distracts more
154 my $interfaces = "interface {\n ringnumber: 0\n" .
155 " bindnetaddr: $param->{bindnet0_addr}\n }";
156
157 my $ring_addresses = "ring0_addr: $param->{ring0_addr}" ;
158
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;
163
14d0000a
TL
164 $interfaces .= "\n interface {\n ringnumber: 1\n" .
165 " bindnetaddr: $param->{bindnet1_addr}\n }\n";
166
606a8904
TL
167 $interfaces .= "rrp_mode: passive\n"; # only passive is stable and tested
168
14d0000a 169 $ring_addresses .= "\n ring1_addr: $param->{ring1_addr}";
14d0000a
TL
170 }
171
13d44dc5 172 # No, corosync cannot deduce this on its own
14d0000a 173 my $ipversion = $bind_is_ipv6 ? 'ipv6' : 'ipv4';
13d44dc5
DM
174
175 my $config = <<_EOD;
176totem {
177 version: 2
178 secauth: on
179 cluster_name: $clustername
180 config_version: 1
181 ip_version: $ipversion
14d0000a 182 $interfaces
13d44dc5
DM
183}
184
185nodelist {
186 node {
14d0000a
TL
187 $ring_addresses
188 name: $nodename
13d44dc5
DM
189 nodeid: $param->{nodeid}
190 quorum_votes: $param->{votes}
191 }
192}
14d0000a 193
13d44dc5
DM
194quorum {
195 provider: corosync_votequorum
196}
197
198logging {
199 to_syslog: yes
200 debug: off
201}
202_EOD
14d0000a 203;
13d44dc5
DM
204 PVE::Tools::file_set_contents($clusterconf, $config);
205
206 PVE::Cluster::ssh_merge_keys();
207
208 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
209
210 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
211
294f76c4 212 run_command('systemctl restart pve-cluster'); # restart
13d44dc5 213
294f76c4 214 run_command('systemctl restart corosync'); # restart
c53b111f 215
13d44dc5
DM
216 return undef;
217}});
218
13d44dc5 219__PACKAGE__->register_method ({
c53b111f 220 name => 'add',
13d44dc5
DM
221 path => 'add',
222 method => 'PUT',
223 description => "Adds the current node to an existing cluster.",
224 parameters => {
225 additionalProperties => 0,
226 properties => {
227 hostname => {
228 type => 'string',
229 description => "Hostname (or IP) of an existing cluster member."
230 },
231 nodeid => {
232 type => 'integer',
233 description => "Node id for this node.",
234 minimum => 1,
235 optional => 1,
236 },
237 votes => {
238 type => 'integer',
239 description => "Number of votes for this node",
240 minimum => 0,
241 optional => 1,
242 },
243 force => {
244 type => 'boolean',
245 description => "Do not throw error if node already exists.",
246 optional => 1,
247 },
14d0000a
TL
248 ring0_addr => {
249 type => 'string', format => 'address',
250 description => "Hostname (or IP) of the corosync ring0 address of this node.".
251 " Defaults to nodes hostname.",
252 optional => 1,
253 },
254 ring1_addr => {
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.",
258 optional => 1,
259 },
03b23bd0
TL
260 fingerprint => PVE::JSONSchema::get_standard_option('fingerprint-sha256', {
261 optional => 1,
262 }),
263 'use_ssh' => {
264 type => 'boolean',
265 description => "Always use SSH to join, even if peer may do it over API.",
266 optional => 1,
267 },
13d44dc5
DM
268 },
269 },
270 returns => { type => 'null' },
c53b111f 271
13d44dc5
DM
272 code => sub {
273 my ($param) = @_;
274
275 my $nodename = PVE::INotify::nodename();
276
03b23bd0 277 my $host = $param->{hostname};
13d44dc5 278
68491de3 279 PVE::Cluster::assert_joinable($param->{ring0_addr}, $param->{ring1_addr}, $param->{force});
f566b424 280
03b23bd0
TL
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: ");
284
285 delete $param->{use_ssh};
286 $param->{password} = $password;
287
288 eval { PVE::Cluster::join($param) };
289
290 if (my $err = $@) {
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";
294 }
295 die $err;
296 }
297 return; # all OK, the API join endpoint successfully set us up
298 }
299
300 # allow fallback to old ssh only join if wished or needed
301
302 PVE::Cluster::setup_sshd_config();
303 PVE::Cluster::setup_rootsshconfig();
304 PVE::Cluster::setup_ssh_keys();
5a630d8f 305
13d44dc5
DM
306 # make sure known_hosts is on local filesystem
307 PVE::Cluster::ssh_unmerge_known_hosts();
308
de4b4155 309 my $cmd = ['ssh-copy-id', '-i', '/root/.ssh/id_rsa', "root\@$host"];
294f76c4 310 run_command($cmd, 'outfunc' => sub {}, 'errfunc' => sub {},
de4b4155 311 'errmsg' => "unable to copy ssh ID");
13d44dc5
DM
312
313 $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
314 'pvecm', 'addnode', $nodename, '--force', 1];
315
316 push @$cmd, '--nodeid', $param->{nodeid} if $param->{nodeid};
13d44dc5 317 push @$cmd, '--votes', $param->{votes} if defined($param->{votes});
14d0000a 318 push @$cmd, '--ring0_addr', $param->{ring0_addr} if defined($param->{ring0_addr});
14d0000a
TL
319 push @$cmd, '--ring1_addr', $param->{ring1_addr} if defined($param->{ring1_addr});
320
13d44dc5
DM
321 if (system (@$cmd) != 0) {
322 my $cmdtxt = join (' ', @$cmd);
323 die "unable to add node: command failed ($cmdtxt)\n";
324 }
325
326 my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
327 mkdir $tmpdir;
328
329 eval {
330 print "copy corosync auth key\n";
c53b111f 331 $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
13d44dc5
DM
332 "[$host]:$authfile $clusterconf", $tmpdir];
333
334 system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
335
e02bdaae
TL
336 my $corosync_conf = PVE::Tools::file_get_contents("$tmpdir/corosync.conf");
337 my $corosync_authkey = PVE::Tools::file_get_contents("$tmpdir/authkey");
13d44dc5 338
e02bdaae 339 PVE::Cluster::finish_join($host, $corosync_conf, $corosync_authkey);
13d44dc5
DM
340 };
341 my $err = $@;
342
343 rmtree $tmpdir;
344
345 die $err if $err;
346
347 return undef;
348 }});
349
350__PACKAGE__->register_method ({
c53b111f 351 name => 'status',
13d44dc5
DM
352 path => 'status',
353 method => 'GET',
354 description => "Displays the local view of the cluster status.",
355 parameters => {
356 additionalProperties => 0,
357 properties => {},
358 },
359 returns => { type => 'null' },
c53b111f 360
13d44dc5
DM
361 code => sub {
362 my ($param) = @_;
363
b6973a89 364 PVE::Corosync::check_conf_exists();
eb51b829 365
13d44dc5
DM
366 my $cmd = ['corosync-quorumtool', '-siH'];
367
368 exec (@$cmd);
369
370 exit (-1); # should not be reached
371 }});
372
373__PACKAGE__->register_method ({
c53b111f 374 name => 'nodes',
13d44dc5
DM
375 path => 'nodes',
376 method => 'GET',
377 description => "Displays the local view of the cluster nodes.",
378 parameters => {
379 additionalProperties => 0,
380 properties => {},
381 },
382 returns => { type => 'null' },
c53b111f 383
13d44dc5
DM
384 code => sub {
385 my ($param) = @_;
386
b6973a89 387 PVE::Corosync::check_conf_exists();
eb51b829 388
13d44dc5
DM
389 my $cmd = ['corosync-quorumtool', '-l'];
390
391 exec (@$cmd);
392
393 exit (-1); # should not be reached
394 }});
395
396__PACKAGE__->register_method ({
c53b111f 397 name => 'expected',
13d44dc5
DM
398 path => 'expected',
399 method => 'PUT',
400 description => "Tells corosync a new value of expected votes.",
401 parameters => {
402 additionalProperties => 0,
403 properties => {
404 expected => {
405 type => 'integer',
406 description => "Expected votes",
407 minimum => 1,
408 },
409 },
410 },
411 returns => { type => 'null' },
c53b111f 412
13d44dc5
DM
413 code => sub {
414 my ($param) = @_;
415
b6973a89 416 PVE::Corosync::check_conf_exists();
eb51b829 417
13d44dc5
DM
418 my $cmd = ['corosync-quorumtool', '-e', $param->{expected}];
419
420 exec (@$cmd);
421
422 exit (-1); # should not be reached
423
424 }});
425
13d44dc5 426__PACKAGE__->register_method ({
c53b111f 427 name => 'updatecerts',
13d44dc5
DM
428 path => 'updatecerts',
429 method => 'PUT',
430 description => "Update node certificates (and generate all needed files/directories).",
431 parameters => {
432 additionalProperties => 0,
433 properties => {
434 force => {
435 description => "Force generation of new SSL certifate.",
436 type => 'boolean',
437 optional => 1,
438 },
439 silent => {
440 description => "Ignore errors (i.e. when cluster has no quorum).",
441 type => 'boolean',
442 optional => 1,
443 },
444 },
445 },
446 returns => { type => 'null' },
447 code => sub {
448 my ($param) = @_;
449
450 PVE::Cluster::setup_rootsshconfig();
451
452 PVE::Cluster::gen_pve_vzdump_symlink();
453
454 if (!PVE::Cluster::check_cfs_quorum(1)) {
455 return undef if $param->{silent};
456 die "no quorum - unable to update files\n";
457 }
458
459 PVE::Cluster::setup_ssh_keys();
460
461 my $nodename = PVE::INotify::nodename();
462
463 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
464
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();
469
470 return undef;
471 }});
472
3a966e22
TL
473__PACKAGE__->register_method ({
474 name => 'mtunnel',
475 path => 'mtunnel',
476 method => 'POST',
477 description => "Used by VM/CT migration - do not use manually.",
478 parameters => {
479 additionalProperties => 0,
f83d8153
TL
480 properties => {
481 get_migration_ip => {
482 type => 'boolean',
483 default => 0,
484 description => 'return the migration IP, if configured',
485 optional => 1,
486 },
487 migration_network => {
488 type => 'string',
489 format => 'CIDR',
490 description => 'the migration network used to detect the local migration IP',
491 optional => 1,
492 },
d56a1ff3
WB
493 'run-command' => {
494 type => 'boolean',
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.",
498 optional => 1,
499 },
500 'extra-args' => PVE::JSONSchema::get_standard_option('extra-args'),
f83d8153 501 },
3a966e22
TL
502 },
503 returns => { type => 'null'},
504 code => sub {
505 my ($param) = @_;
506
507 if (!PVE::Cluster::check_cfs_quorum(1)) {
508 print "no quorum\n";
509 return undef;
510 }
511
d56a1ff3 512 my $network = $param->{migration_network};
f83d8153 513 if ($param->{get_migration_ip}) {
d56a1ff3
WB
514 die "cannot use --run-command with --get_migration_ip\n"
515 if $param->{'run-command'};
43f4c5f6 516 if (my $ip = PVE::Cluster::get_local_migration_ip($network)) {
f83d8153
TL
517 print "ip: '$ip'\n";
518 } else {
43f4c5f6 519 print "no ip\n";
f83d8153
TL
520 }
521 # do not keep tunnel open when asked for migration ip
522 return undef;
523 }
524
d56a1ff3
WB
525 if ($param->{'run-command'}) {
526 my $cmd = $param->{'extra-args'};
527 die "missing command\n"
528 if !$cmd || !scalar(@$cmd);
529
530 # Get an ip address to listen on, and find a free migration port
531 my ($ip, $family);
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";
b69a6e70 535 $family = PVE::Tools::get_host_address_family($ip);
d56a1ff3
WB
536 } else {
537 my $nodename = PVE::INotify::nodename();
538 ($ip, $family) = PVE::Network::get_ip_from_hostname($nodename, 0);
539 }
540 my $port = PVE::Tools::next_migrate_port($family, $ip);
541
48178573 542 PVE::Tools::pipe_socket_to_command($cmd, $ip, $port);
d56a1ff3
WB
543 return undef;
544 }
545
3a966e22
TL
546 print "tunnel online\n";
547 *STDOUT->flush();
548
3bec79e5 549 while (my $line = <STDIN>) {
3a966e22
TL
550 chomp $line;
551 last if $line =~ m/^quit$/;
552 }
553
554 return undef;
555 }});
556
13d44dc5
DM
557
558our $cmddef = {
559 keygen => [ __PACKAGE__, 'keygen', ['filename']],
560 create => [ __PACKAGE__, 'create', ['clustername']],
561 add => [ __PACKAGE__, 'add', ['hostname']],
1d26c202
TL
562 addnode => [ 'PVE::API2::ClusterConfig', 'addnode', ['node']],
563 delnode => [ 'PVE::API2::ClusterConfig', 'delnode', ['node']],
13d44dc5
DM
564 status => [ __PACKAGE__, 'status' ],
565 nodes => [ __PACKAGE__, 'nodes' ],
566 expected => [ __PACKAGE__, 'expected', ['expected']],
567 updatecerts => [ __PACKAGE__, 'updatecerts', []],
d56a1ff3 568 mtunnel => [ __PACKAGE__, 'mtunnel', ['extra-args']],
13d44dc5
DM
569};
570
5711;