]> git.proxmox.com Git - pve-cluster.git/blame - src/PVE/Cluster/Setup.pm
pvecm: stop merging SSH known hosts by default
[pve-cluster.git] / src / PVE / Cluster / Setup.pm
CommitLineData
c5204e14
FG
1package PVE::Cluster::Setup;
2
3use strict;
4use warnings;
5
6use Digest::HMAC_SHA1;
7use Digest::SHA;
8use IO::File;
9use MIME::Base64;
10use Net::IP;
11use UUID;
12use POSIX qw(EEXIST);
13
14use PVE::APIClient::LWP;
15use PVE::Cluster;
cde60d30 16use PVE::Corosync;
c5204e14
FG
17use PVE::INotify;
18use PVE::JSONSchema;
19use PVE::Network;
20use PVE::Tools;
c2fd094e 21use PVE::Certificate;
c5204e14 22
498e355e 23# relevant for joining or getting joined checks, after that versions in cluster can differ
a755ff54 24use constant JOIN_API_VERSION => 1;
498e355e 25# (APIVER-this) is oldest version a new node in addnode can have and still be accepted
a755ff54
SR
26use constant JOIN_API_AGE_AS_CLUSTER => 1;
27# (APIVER-this) is oldest version a cluster node can have to still try joining
28use constant JOIN_API_AGE_AS_JOINEE => 1;
29
498e355e
TL
30sub assert_we_can_join_cluster_version {
31 my ($version) = @_;
32 my $min_version = JOIN_API_VERSION - JOIN_API_AGE_AS_JOINEE;
33 return if $version >= $min_version;
34 die "error: incompatible join API version on cluster ($version), local node"
35 ." has ". JOIN_API_VERSION ." and supports >= $min_version. Make sure"
36 ."all cluster nodes are up-to-date.\n";
37}
38
39sub assert_node_can_join_our_version {
40 my ($version) = @_;
41 my $min_version = JOIN_API_VERSION - JOIN_API_AGE_AS_CLUSTER;
42 return if $version >= $min_version;
43 die "error: unsupported old API version on joining node ($version), cluster"
44 ." node has ". JOIN_API_VERSION ." and supports >= $min_version. Please"
45 ." upgrade node before joining\n";
46}
47
c5204e14
FG
48my $pmxcfs_base_dir = PVE::Cluster::base_dir();
49my $pmxcfs_auth_dir = PVE::Cluster::auth_dir();
50
51# only write output if something fails
52sub run_silent_cmd {
53 my ($cmd) = @_;
54
55 my $outbuf = '';
56 my $record = sub { $outbuf .= shift . "\n"; };
57
58 eval { PVE::Tools::run_command($cmd, outfunc => $record, errfunc => $record) };
59
60 if (my $err = $@) {
61 print STDERR $outbuf;
62 die $err;
63 }
64}
65
66# Corosync related files
67my $localclusterdir = "/etc/corosync";
68my $localclusterconf = "$localclusterdir/corosync.conf";
69my $authfile = "$localclusterdir/authkey";
70my $clusterconf = "$pmxcfs_base_dir/corosync.conf";
71
72# CA/certificate related files
73my $pveca_key_fn = "$pmxcfs_auth_dir/pve-root-ca.key";
74my $pveca_srl_fn = "$pmxcfs_auth_dir/pve-root-ca.srl";
75my $pveca_cert_fn = "$pmxcfs_base_dir/pve-root-ca.pem";
76# this is just a secret accessable by the web browser
77# and is used for CSRF prevention
78my $pvewww_key_fn = "$pmxcfs_base_dir/pve-www.key";
79
80# ssh related files
c5204e14 81my $ssh_host_rsa_id = "/etc/ssh/ssh_host_rsa_key.pub";
d6dcd53f
TL
82my $ssh_cluster_known_hosts = "$pmxcfs_auth_dir/known_hosts";
83my $ssh_cluster_authorized_keys = "$pmxcfs_auth_dir/authorized_keys";
84my $ssh_system_known_hosts = "/etc/ssh/ssh_known_hosts";
85my $ssh_system_server_config = "/etc/ssh/sshd_config";
86
87my $ssh_root_rsa_key_private = "/root/.ssh/id_rsa";
88my $ssh_root_rsa_key_public = "/root/.ssh/id_rsa.pub";
89my $ssh_root_authorized_keys = "/root/.ssh/authorized_keys";
90my $ssh_root_authorized_keys_backup = "${ssh_root_authorized_keys}.org";
91my $ssh_root_client_config = "/root/.ssh/config";
c5204e14
FG
92
93# ssh related utility functions
94
95sub ssh_merge_keys {
d6dcd53f 96 # remove duplicate keys in $ssh_cluster_authorized_keys
c5204e14
FG
97 # ssh-copy-id simply add keys, so the file can grow to large
98
99 my $data = '';
d6dcd53f 100 if (-f $ssh_cluster_authorized_keys) {
ebcef77f 101 $data = PVE::Tools::file_get_contents($ssh_cluster_authorized_keys);
c5204e14
FG
102 chomp($data);
103 }
104
105 my $found_backup;
d6dcd53f 106 if (-f $ssh_root_authorized_keys_backup) {
c5204e14 107 $data .= "\n";
ebcef77f 108 $data .= PVE::Tools::file_get_contents($ssh_root_authorized_keys_backup);
c5204e14
FG
109 chomp($data);
110 $found_backup = 1;
111 }
112
113 # always add ourself
d6dcd53f
TL
114 if (-f $ssh_root_rsa_key_public) {
115 my $pub = PVE::Tools::file_get_contents($ssh_root_rsa_key_public);
c5204e14
FG
116 chomp($pub);
117 $data .= "\n$pub\n";
118 }
119
120 my $newdata = "";
121 my $vhash = {};
122 my @lines = split(/\n/, $data);
123 foreach my $line (@lines) {
124 if ($line !~ /^#/ && $line =~ m/(^|\s)ssh-(rsa|dsa)\s+(\S+)\s+\S+$/) {
125 next if $vhash->{$3}++;
126 }
127 $newdata .= "$line\n";
128 }
129
d6dcd53f 130 PVE::Tools::file_set_contents($ssh_cluster_authorized_keys, $newdata, 0600);
c5204e14 131
d6dcd53f
TL
132 if ($found_backup && -l $ssh_root_authorized_keys) {
133 # everything went well, so we can remove the backup
134 unlink $ssh_root_authorized_keys_backup;
135 }
c5204e14 136 }
c5204e14
FG
137
138sub setup_sshd_config {
139 my () = @_;
140
d6dcd53f 141 my $conf = PVE::Tools::file_get_contents($ssh_system_server_config);
c5204e14
FG
142
143 return if $conf =~ m/^PermitRootLogin\s+yes\s*$/m;
144
145 if ($conf !~ s/^#?PermitRootLogin.*$/PermitRootLogin yes/m) {
146 chomp $conf;
147 $conf .= "\nPermitRootLogin yes\n";
148 }
149
d6dcd53f 150 PVE::Tools::file_set_contents($ssh_system_server_config, $conf);
c5204e14
FG
151
152 PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'sshd']);
153}
154
155sub setup_rootsshconfig {
156
157 # create ssh key if it does not exist
d6dcd53f 158 if (! -f $ssh_root_rsa_key_public) {
c5204e14 159 mkdir '/root/.ssh/';
81936212 160 system ("echo|ssh-keygen -t rsa -N '' -b 4096 -f ${ssh_root_rsa_key_private}");
c5204e14
FG
161 }
162
163 # create ssh config if it does not exist
d6dcd53f 164 if (! -f $ssh_root_client_config) {
c5204e14 165 mkdir '/root/.ssh';
d6dcd53f 166 if (my $fh = IO::File->new($ssh_root_client_config, O_CREAT|O_WRONLY|O_EXCL, 0640)) {
c5204e14
FG
167 # this is the default ciphers list from Debian's OpenSSH package (OpenSSH_7.4p1 Debian-10, OpenSSL 1.0.2k 26 Jan 2017)
168 # changed order to put AES before Chacha20 (most hardware has AESNI)
169 print $fh "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm\@openssh.com,aes256-gcm\@openssh.com,chacha20-poly1305\@openssh.com\n";
170 close($fh);
171 }
172 }
173}
174
175sub setup_ssh_keys {
176
177 mkdir $pmxcfs_auth_dir;
178
179 my $import_ok;
180
d6dcd53f 181 if (! -f $ssh_cluster_authorized_keys) {
c5204e14 182 my $old;
d6dcd53f 183 if (-f $ssh_root_authorized_keys) {
ebcef77f 184 $old = PVE::Tools::file_get_contents($ssh_root_authorized_keys);
c5204e14 185 }
d6dcd53f
TL
186 if (my $fh = IO::File->new ($ssh_cluster_authorized_keys, O_CREAT|O_WRONLY|O_EXCL, 0400)) {
187 PVE::Tools::safe_print($ssh_cluster_authorized_keys, $fh, $old) if $old;
c5204e14
FG
188 close($fh);
189 $import_ok = 1;
190 }
191 }
192
d6dcd53f
TL
193 warn "can't create shared ssh key database '$ssh_cluster_authorized_keys'\n"
194 if ! -f $ssh_cluster_authorized_keys;
c5204e14 195
d6dcd53f
TL
196 if (-f $ssh_root_authorized_keys && ! -l $ssh_root_authorized_keys) {
197 if (!rename($ssh_root_authorized_keys , $ssh_root_authorized_keys_backup)) {
198 warn "rename $ssh_root_authorized_keys failed - $!\n";
c5204e14
FG
199 }
200 }
201
d6dcd53f
TL
202 if (! -l $ssh_root_authorized_keys) {
203 symlink $ssh_cluster_authorized_keys, $ssh_root_authorized_keys;
c5204e14
FG
204 }
205
d6dcd53f
TL
206 if (! -l $ssh_root_authorized_keys) {
207 warn "can't create symlink for ssh keys '$ssh_root_authorized_keys' -> '$ssh_cluster_authorized_keys'\n";
c5204e14 208 } else {
d6dcd53f 209 unlink $ssh_root_authorized_keys_backup if $import_ok;
c5204e14
FG
210 }
211}
212
213sub ssh_unmerge_known_hosts {
d6dcd53f 214 return if ! -l $ssh_system_known_hosts;
c5204e14
FG
215
216 my $old = '';
ebcef77f 217 $old = PVE::Tools::file_get_contents($ssh_cluster_known_hosts)
d6dcd53f 218 if -f $ssh_cluster_known_hosts;
c5204e14 219
d6dcd53f 220 PVE::Tools::file_set_contents($ssh_system_known_hosts, $old);
c5204e14
FG
221}
222
a904dfe0
FG
223sub ssh_create_node_known_hosts {
224 my ($nodename) = @_;
225
226 my $hostkey = PVE::Tools::file_get_contents($ssh_host_rsa_id);
227 # Note: file sometimes containe empty lines at start, so we use multiline match
228 die "can't parse $ssh_host_rsa_id" if $hostkey !~ m/^(ssh-rsa\s\S+)(\s.*)?$/m;
229 $hostkey = $1;
230
231 my $raw = "$nodename $hostkey";
232 PVE::Tools::file_set_contents("/etc/pve/nodes/$nodename/ssh_known_hosts", $raw);
233
234 # TODO: also setup custom keypair and client config here to disentangle entirely from /root/.ssh?
235}
236
c5204e14
FG
237sub ssh_merge_known_hosts {
238 my ($nodename, $ip_address, $createLink) = @_;
239
240 die "no node name specified" if !$nodename;
241 die "no ip address specified" if !$ip_address;
242
243 # ssh lowercases hostnames (aliases) before comparision, so we need too
244 $nodename = lc($nodename);
245 $ip_address = lc($ip_address);
246
247 mkdir $pmxcfs_auth_dir;
248
d6dcd53f
TL
249 if (! -f $ssh_cluster_known_hosts) {
250 if (my $fh = IO::File->new($ssh_cluster_known_hosts, O_CREAT|O_WRONLY|O_EXCL, 0600)) {
c5204e14
FG
251 close($fh);
252 }
253 }
254
ebcef77f 255 my $old = PVE::Tools::file_get_contents($ssh_cluster_known_hosts);
c5204e14
FG
256
257 my $new = '';
258
d6dcd53f 259 if ((! -l $ssh_system_known_hosts) && (-f $ssh_system_known_hosts)) {
ebcef77f 260 $new = PVE::Tools::file_get_contents($ssh_system_known_hosts);
c5204e14
FG
261 }
262
263 my $hostkey = PVE::Tools::file_get_contents($ssh_host_rsa_id);
264 # Note: file sometimes containe emty lines at start, so we use multiline match
265 die "can't parse $ssh_host_rsa_id" if $hostkey !~ m/^(ssh-rsa\s\S+)(\s.*)?$/m;
266 $hostkey = $1;
267
268 my $data = '';
269 my $vhash = {};
270
271 my $found_nodename;
272 my $found_local_ip;
273
274 my $merge_line = sub {
275 my ($line, $all) = @_;
276
277 return if $line =~ m/^\s*$/; # skip empty lines
278 return if $line =~ m/^#/; # skip comments
279
280 if ($line =~ m/^(\S+)\s(ssh-rsa\s\S+)(\s.*)?$/) {
281 my $key = $1;
282 my $rsakey = $2;
283 if (!$vhash->{$key}) {
284 $vhash->{$key} = 1;
285 if ($key =~ m/\|1\|([^\|\s]+)\|([^\|\s]+)$/) {
286 my $salt = decode_base64($1);
287 my $digest = $2;
288 my $hmac = Digest::HMAC_SHA1->new($salt);
289 $hmac->add($nodename);
290 my $hd = $hmac->b64digest . '=';
291 if ($digest eq $hd) {
292 if ($rsakey eq $hostkey) {
293 $found_nodename = 1;
294 $data .= $line;
295 }
296 return;
297 }
298 $hmac = Digest::HMAC_SHA1->new($salt);
299 $hmac->add($ip_address);
300 $hd = $hmac->b64digest . '=';
301 if ($digest eq $hd) {
302 if ($rsakey eq $hostkey) {
303 $found_local_ip = 1;
304 $data .= $line;
305 }
306 return;
307 }
308 } else {
309 $key = lc($key); # avoid duplicate entries, ssh compares lowercased
310 if ($key eq $ip_address) {
311 $found_local_ip = 1 if $rsakey eq $hostkey;
312 } elsif ($key eq $nodename) {
313 $found_nodename = 1 if $rsakey eq $hostkey;
314 }
315 }
316 $data .= $line;
317 }
318 } elsif ($all) {
319 $data .= $line;
320 }
321 };
322
323 while ($old && $old =~ s/^((.*?)(\n|$))//) {
324 my $line = "$2\n";
325 &$merge_line($line, 1);
326 }
327
328 while ($new && $new =~ s/^((.*?)(\n|$))//) {
329 my $line = "$2\n";
330 &$merge_line($line);
331 }
332
333 # add our own key if not already there
334 $data .= "$nodename $hostkey\n" if !$found_nodename;
335 $data .= "$ip_address $hostkey\n" if !$found_local_ip;
336
d6dcd53f 337 PVE::Tools::file_set_contents($ssh_cluster_known_hosts, $data);
c5204e14
FG
338
339 return if !$createLink;
340
d6dcd53f
TL
341 unlink $ssh_system_known_hosts;
342 symlink $ssh_cluster_known_hosts, $ssh_system_known_hosts;
c5204e14 343
d6dcd53f
TL
344 warn "can't create symlink for ssh known hosts '$ssh_system_known_hosts' -> '$ssh_cluster_known_hosts'\n"
345 if ! -l $ssh_system_known_hosts;
c5204e14
FG
346
347}
348
349# directory and file creation
350
351sub gen_local_dirs {
352 my ($nodename) = @_;
353
354 PVE::Cluster::check_cfs_is_mounted();
355
356 my @required_dirs = (
357 "$pmxcfs_base_dir/priv",
358 "$pmxcfs_base_dir/nodes",
359 "$pmxcfs_base_dir/nodes/$nodename",
360 "$pmxcfs_base_dir/nodes/$nodename/lxc",
361 "$pmxcfs_base_dir/nodes/$nodename/qemu-server",
362 "$pmxcfs_base_dir/nodes/$nodename/openvz",
363 "$pmxcfs_base_dir/nodes/$nodename/priv");
364
365 foreach my $dir (@required_dirs) {
366 if (! -d $dir) {
367 mkdir($dir) || $! == EEXIST || die "unable to create directory '$dir' - $!\n";
368 }
369 }
370}
371
372sub gen_auth_key {
373 my $authprivkeyfn = "$pmxcfs_auth_dir/authkey.key";
374 my $authpubkeyfn = "$pmxcfs_base_dir/authkey.pub";
375
376 return if -f "$authprivkeyfn";
377
378 PVE::Cluster::check_cfs_is_mounted();
379
380 PVE::Cluster::cfs_lock_authkey(undef, sub {
381 mkdir $pmxcfs_auth_dir || $! == EEXIST || die "unable to create dir '$pmxcfs_auth_dir' - $!\n";
382
383 run_silent_cmd(['openssl', 'genrsa', '-out', $authprivkeyfn, '2048']);
384
385 run_silent_cmd(['openssl', 'rsa', '-in', $authprivkeyfn, '-pubout', '-out', $authpubkeyfn]);
386 });
387
388 die "$@\n" if $@;
389}
390
391sub gen_pveca_key {
392
393 return if -f $pveca_key_fn;
394
395 eval {
396 run_silent_cmd(['openssl', 'genrsa', '-out', $pveca_key_fn, '4096']);
397 };
398
399 die "unable to generate pve ca key:\n$@" if $@;
400}
401
402sub gen_pveca_cert {
403
404 if (-f $pveca_key_fn && -f $pveca_cert_fn) {
405 return 0;
406 }
407
408 gen_pveca_key();
409
410 # we try to generate an unique 'subject' to avoid browser problems
411 # (reused serial numbers, ..)
412 my $uuid;
413 UUID::generate($uuid);
414 my $uuid_str;
415 UUID::unparse($uuid, $uuid_str);
416
417 eval {
418 # wrap openssl with faketime to prevent bug #904
419 run_silent_cmd(['faketime', 'yesterday', 'openssl', 'req', '-batch',
420 '-days', '3650', '-new', '-x509', '-nodes', '-key',
421 $pveca_key_fn, '-out', $pveca_cert_fn, '-subj',
422 "/CN=Proxmox Virtual Environment/OU=$uuid_str/O=PVE Cluster Manager CA/"]);
423 };
424
425 die "generating pve root certificate failed:\n$@" if $@;
426
427 return 1;
428}
429
430sub gen_pve_ssl_key {
431 my ($nodename) = @_;
432
433 die "no node name specified" if !$nodename;
434
435 my $pvessl_key_fn = "$pmxcfs_base_dir/nodes/$nodename/pve-ssl.key";
436
437 return if -f $pvessl_key_fn;
438
439 eval {
440 run_silent_cmd(['openssl', 'genrsa', '-out', $pvessl_key_fn, '2048']);
441 };
442
443 die "unable to generate pve ssl key for node '$nodename':\n$@" if $@;
444}
445
446sub gen_pve_www_key {
447
448 return if -f $pvewww_key_fn;
449
450 eval {
451 run_silent_cmd(['openssl', 'genrsa', '-out', $pvewww_key_fn, '2048']);
452 };
453
454 die "unable to generate pve www key:\n$@" if $@;
455}
456
457sub update_serial {
458 my ($serial) = @_;
459
460 PVE::Tools::file_set_contents($pveca_srl_fn, $serial);
461}
462
463sub gen_pve_ssl_cert {
464 my ($force, $nodename, $ip) = @_;
465
466 die "no node name specified" if !$nodename;
467 die "no IP specified" if !$ip;
468
469 my $pvessl_cert_fn = "$pmxcfs_base_dir/nodes/$nodename/pve-ssl.pem";
470
471 return if !$force && -f $pvessl_cert_fn;
472
473 my $names = "IP:127.0.0.1,IP:::1,DNS:localhost";
474
475 my $rc = PVE::INotify::read_file('resolvconf');
476
477 $names .= ",IP:$ip";
478
c5204e14
FG
479 $names .= ",DNS:$nodename";
480
d44f3247 481 my $fqdn = $nodename;
c5204e14 482 if ($rc && $rc->{search}) {
d44f3247 483 $fqdn .= ".$rc->{search}";
c5204e14
FG
484 $names .= ",DNS:$fqdn";
485 }
486
487 my $sslconf = <<__EOD;
488RANDFILE = /root/.rnd
489extensions = v3_req
490
491[ req ]
492default_bits = 2048
493distinguished_name = req_distinguished_name
494req_extensions = v3_req
495prompt = no
496string_mask = nombstr
497
498[ req_distinguished_name ]
499organizationalUnitName = PVE Cluster Node
500organizationName = Proxmox Virtual Environment
501commonName = $fqdn
502
503[ v3_req ]
504basicConstraints = CA:FALSE
505extendedKeyUsage = serverAuth
506subjectAltName = $names
507__EOD
508
509 my $cfgfn = "/tmp/pvesslconf-$$.tmp";
510 my $fh = IO::File->new ($cfgfn, "w");
511 print $fh $sslconf;
512 close ($fh);
513
514 my $reqfn = "/tmp/pvecertreq-$$.tmp";
515 unlink $reqfn;
516
517 my $pvessl_key_fn = "$pmxcfs_base_dir/nodes/$nodename/pve-ssl.key";
518 eval {
d44f3247
TL
519 run_silent_cmd([
520 'openssl', 'req', '-batch', '-new', '-config', $cfgfn, '-key', $pvessl_key_fn, '-out', $reqfn
521 ]);
c5204e14
FG
522 };
523
524 if (my $err = $@) {
525 unlink $reqfn;
526 unlink $cfgfn;
527 die "unable to generate pve certificate request:\n$err";
528 }
529
530 update_serial("0000000000000000") if ! -f $pveca_srl_fn;
531
c2fd094e
DC
532 # get ca expiry
533 my $cainfo = PVE::Certificate::get_certificate_info($pveca_cert_fn);
534 my $daysleft = int(($cainfo->{notafter} - time())/(24*60*60));
535
536 if ($daysleft < 14) {
537 die "CA expires in less than 2 weeks, unable to generate certificate.\n";
538 }
539
540 # let the certificate expire a little sooner that the ca, so subtract 2 days
541 $daysleft -= 2;
542
543 # we want the certificates to only last 2 years, since some browsers
544 # do not accept certificates with very long expiry time
545 if ($daysleft >= 2*365) {
546 $daysleft = 2*365;
547 }
548
c5204e14 549 eval {
d44f3247
TL
550 run_silent_cmd([
551 'faketime', 'yesterday', # NOTE: wrap openssl with faketime to prevent bug #904
552 'openssl', 'x509', '-req', '-in', $reqfn, '-days', $daysleft, '-out', $pvessl_cert_fn,
553 '-CAkey', $pveca_key_fn, '-CA', $pveca_cert_fn, '-CAserial', $pveca_srl_fn, '-extfile', $cfgfn
554 ]);
c5204e14
FG
555 };
556
557 if (my $err = $@) {
558 unlink $reqfn;
559 unlink $cfgfn;
560 die "unable to generate pve ssl certificate:\n$err";
561 }
562
563 unlink $cfgfn;
564 unlink $reqfn;
565}
566
567sub gen_pve_node_files {
568 my ($nodename, $ip, $opt_force) = @_;
569
570 gen_local_dirs($nodename);
571
572 gen_auth_key();
573
574 # make sure we have a (cluster wide) secret
575 # for CSRFR prevention
576 gen_pve_www_key();
577
578 # make sure we have a (per node) private key
579 gen_pve_ssl_key($nodename);
580
581 # make sure we have a CA
582 my $force = gen_pveca_cert();
583
584 $force = 1 if $opt_force;
585
586 gen_pve_ssl_cert($force, $nodename, $ip);
587}
588
589my $vzdump_cron_dummy = <<__EOD;
590# cluster wide vzdump cron schedule
591# Atomatically generated file - do not edit
592
593PATH="/usr/sbin:/usr/bin:/sbin:/bin"
594
595__EOD
596
597sub gen_pve_vzdump_symlink {
598
599 my $filename = "/etc/pve/vzdump.cron";
600
601 my $link_fn = "/etc/cron.d/vzdump";
602
603 if ((-f $filename) && (! -l $link_fn)) {
604 rename($link_fn, "/root/etc_cron_vzdump.org"); # make backup if file exists
605 symlink($filename, $link_fn);
606 }
607}
608
609sub gen_pve_vzdump_files {
610
611 my $filename = "/etc/pve/vzdump.cron";
612
613 PVE::Tools::file_set_contents($filename, $vzdump_cron_dummy)
614 if ! -f $filename;
615
616 gen_pve_vzdump_symlink();
617};
618
619# join helpers
620
621sub assert_joinable {
8ef581e4 622 my ($local_addr, $links, $force) = @_;
c5204e14
FG
623
624 my $errors = '';
625 my $error = sub { $errors .= "* $_[0]\n"; };
626
627 if (-f $authfile) {
628 $error->("authentication key '$authfile' already exists");
629 }
630
631 if (-f $clusterconf) {
632 $error->("cluster config '$clusterconf' already exists");
633 }
634
635 my $vmlist = PVE::Cluster::get_vmlist();
636 if ($vmlist && $vmlist->{ids} && scalar(keys %{$vmlist->{ids}})) {
637 $error->("this host already contains virtual guests");
638 }
639
640 if (PVE::Tools::run_command(['corosync-quorumtool', '-l'], noerr => 1, quiet => 1) == 0) {
641 $error->("corosync is already running, is this node already in a cluster?!");
642 }
643
644 # check if corosync ring IPs are configured on the current nodes interfaces
645 my $check_ip = sub {
646 my $ip = shift // return;
647 my $logid = shift;
648 if (!PVE::JSONSchema::pve_verify_ip($ip, 1)) {
649 my $host = $ip;
650 eval { $ip = PVE::Network::get_ip_from_hostname($host); };
651 if ($@) {
652 $error->("$logid: cannot use '$host': $@\n") ;
653 return;
654 }
655 }
656
657 my $cidr = (Net::IP::ip_is_ipv6($ip)) ? "$ip/128" : "$ip/32";
658 my $configured_ips = PVE::Network::get_local_ip_from_cidr($cidr);
659
5a06080c
TL
660 $error->("$logid: cannot use IP '$ip', not found on local node!\n")
661 if scalar(@$configured_ips) < 1;
c5204e14
FG
662 };
663
664 $check_ip->($local_addr, 'local node address');
8ef581e4
SR
665
666 foreach my $link (keys %$links) {
667 $check_ip->($links->{$link}->{address}, "link$link");
668 }
c5204e14
FG
669
670 if ($errors) {
671 warn "detected the following error(s):\n$errors";
672 die "Check if node may join a cluster failed!\n" if !$force;
cd519df4 673 warn "\nWARNING : detected error but forced to continue!\n\n";
c5204e14
FG
674 }
675}
676
677sub join {
678 my ($param) = @_;
679
680 my $nodename = PVE::INotify::nodename();
681 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
682
8ef581e4 683 my $links = PVE::Corosync::extract_corosync_link_args($param);
c5204e14
FG
684
685 # check if we can join with the given parameters and current node state
8ef581e4 686 assert_joinable($local_ip_address, $links, $param->{force});
c5204e14
FG
687
688 setup_sshd_config();
689 setup_rootsshconfig();
690 setup_ssh_keys();
691
692 # make sure known_hosts is on local filesystem
693 ssh_unmerge_known_hosts();
694
695 my $host = $param->{hostname};
696 my $conn_args = {
697 username => 'root@pam',
698 password => $param->{password},
699 cookie_name => 'PVEAuthCookie',
700 protocol => 'https',
701 host => $host,
702 port => 8006,
703 };
704
705 if (my $fp = $param->{fingerprint}) {
706 $conn_args->{cached_fingerprints} = { uc($fp) => 1 };
707 } else {
708 # API schema ensures that we can only get here from CLI handler
709 $conn_args->{manual_verification} = 1;
710 }
711
712 print "Establishing API connection with host '$host'\n";
713
714 my $conn = PVE::APIClient::LWP->new(%$conn_args);
715 $conn->login();
716
717 # login raises an exception on failure, so if we get here we're good
718 print "Login succeeded.\n";
719
498e355e 720 print "check cluster join API version\n";
a755ff54 721 my $apiver = eval { $conn->get("/cluster/config/apiversion") } // 0;
498e355e 722 assert_we_can_join_cluster_version($apiver);
a755ff54 723
c5204e14
FG
724 my $args = {};
725 $args->{force} = $param->{force} if defined($param->{force});
726 $args->{nodeid} = $param->{nodeid} if $param->{nodeid};
727 $args->{votes} = $param->{votes} if defined($param->{votes});
8ef581e4
SR
728 foreach my $link (keys %$links) {
729 $args->{"link$link"} = PVE::Corosync::print_corosync_link($links->{$link});
730 }
c5204e14 731
88b4cb13
SR
732 # this will be used as fallback if no links are specified
733 if (!%$links) {
a755ff54
SR
734 $args->{link0} = $local_ip_address if $apiver == 0;
735 $args->{new_node_ip} = $local_ip_address if $apiver >= 1;
736
88b4cb13
SR
737 print "No cluster network links passed explicitly, fallback to local node"
738 . " IP '$local_ip_address'\n";
739 }
740
a755ff54
SR
741 if ($apiver >= 1) {
742 $args->{apiversion} = JOIN_API_VERSION;
743 }
744
c5204e14 745 print "Request addition of this node\n";
b2cae1f8
SR
746 my $res = eval { $conn->post("/cluster/config/nodes/$nodename", $args); };
747 if (my $err = $@) {
748 if (ref($err) && $err->isa('PVE::APIClient::Exception')) {
749 # we received additional info about the error, show the user
750 chomp $err->{msg};
da578f5a 751 warn "An error occurred on the cluster node: $err->{msg}\n";
b2cae1f8
SR
752 foreach my $key (sort keys %{$err->{errors}}) {
753 my $symbol = ($key =~ m/^warning/) ? '*' : '!';
754 warn "$symbol $err->{errors}->{$key}\n";
755 }
756
757 die "Cluster join aborted!\n";
758 }
759
760 die $@;
761 }
762
763 if (defined($res->{warnings})) {
764 foreach my $warn (@{$res->{warnings}}) {
765 warn "cluster: $warn\n";
766 }
767 }
c5204e14
FG
768
769 print "Join request OK, finishing setup locally\n";
770
771 # added successfuly - now prepare local node
772 finish_join($nodename, $res->{corosync_conf}, $res->{corosync_authkey});
773}
774
775sub finish_join {
776 my ($nodename, $corosync_conf, $corosync_authkey) = @_;
777
778 mkdir "$localclusterdir";
779 PVE::Tools::file_set_contents($authfile, $corosync_authkey);
780 PVE::Tools::file_set_contents($localclusterconf, $corosync_conf);
781
782 print "stopping pve-cluster service\n";
783 my $cmd = ['systemctl', 'stop', 'pve-cluster'];
784 PVE::Tools::run_command($cmd, errmsg => "can't stop pve-cluster service");
785
786 my $dbfile = PVE::Cluster::cfs_backup_database();
787 unlink $dbfile;
788
789 $cmd = ['systemctl', 'start', 'corosync', 'pve-cluster'];
790 PVE::Tools::run_command($cmd, errmsg => "starting pve-cluster failed");
791
792 # wait for quorum
793 my $printqmsg = 1;
794 while (!PVE::Cluster::check_cfs_quorum(1)) {
795 if ($printqmsg) {
796 print "waiting for quorum...";
797 STDOUT->flush();
798 $printqmsg = 0;
799 }
800 sleep(1);
801 }
802 print "OK\n" if !$printqmsg;
803
9a375348 804 generate_local_files();
c5204e14
FG
805 updatecerts_and_ssh(1);
806
807 print "generated new node certificate, restart pveproxy and pvedaemon services\n";
808 PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pvedaemon', 'pveproxy']);
809
810 print "successfully added node '$nodename' to cluster.\n";
811}
812
9a375348
FE
813sub generate_local_files {
814 setup_rootsshconfig();
815 gen_pve_vzdump_symlink();
816}
817
c5204e14 818sub updatecerts_and_ssh {
b1302e9e 819 my ($force_new_cert, $silent, $unmerge_ssh) = @_;
c5204e14
FG
820
821 my $p = sub { print "$_[0]\n" if !$silent };
822
c5204e14
FG
823 if (!PVE::Cluster::check_cfs_quorum(1)) {
824 return undef if $silent;
825 die "no quorum - unable to update files\n";
826 }
827
828 setup_ssh_keys();
829
830 my $nodename = PVE::INotify::nodename();
831 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
832
833 $p->("(re)generate node files");
834 $p->("generate new node certificate") if $force_new_cert;
835 gen_pve_node_files($nodename, $local_ip_address, $force_new_cert);
836
b1302e9e 837 $p->("merge authorized SSH keys");
c5204e14 838 ssh_merge_keys();
b1302e9e
FG
839 if ($unmerge_ssh) {
840 $p->("unmerge SSH known hosts");
841 ssh_unmerge_known_hosts();
842 }
a904dfe0 843 ssh_create_node_known_hosts($nodename);
c5204e14
FG
844 gen_pve_vzdump_files();
845}
846
8471;