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