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