]>
git.proxmox.com Git - pve-cluster.git/blob - data/PVE/Cluster/Setup.pm
1 package PVE
::Cluster
::Setup
;
14 use PVE
::APIClient
::LWP
;
23 my $pmxcfs_base_dir = PVE
::Cluster
::base_dir
();
24 my $pmxcfs_auth_dir = PVE
::Cluster
::auth_dir
();
26 # only write output if something fails
31 my $record = sub { $outbuf .= shift . "\n"; };
33 eval { PVE
::Tools
::run_command
($cmd, outfunc
=> $record, errfunc
=> $record) };
41 # Corosync related files
42 my $localclusterdir = "/etc/corosync";
43 my $localclusterconf = "$localclusterdir/corosync.conf";
44 my $authfile = "$localclusterdir/authkey";
45 my $clusterconf = "$pmxcfs_base_dir/corosync.conf";
47 # CA/certificate related files
48 my $pveca_key_fn = "$pmxcfs_auth_dir/pve-root-ca.key";
49 my $pveca_srl_fn = "$pmxcfs_auth_dir/pve-root-ca.srl";
50 my $pveca_cert_fn = "$pmxcfs_base_dir/pve-root-ca.pem";
51 # this is just a secret accessable by the web browser
52 # and is used for CSRF prevention
53 my $pvewww_key_fn = "$pmxcfs_base_dir/pve-www.key";
56 my $ssh_rsa_id_priv = "/root/.ssh/id_rsa";
57 my $ssh_rsa_id = "/root/.ssh/id_rsa.pub";
58 my $ssh_host_rsa_id = "/etc/ssh/ssh_host_rsa_key.pub";
59 my $sshglobalknownhosts = "/etc/ssh/ssh_known_hosts";
60 my $sshknownhosts = "$pmxcfs_auth_dir/known_hosts";
61 my $sshauthkeys = "$pmxcfs_auth_dir/authorized_keys";
62 my $sshd_config_fn = "/etc/ssh/sshd_config";
63 my $rootsshauthkeys = "/root/.ssh/authorized_keys";
64 my $rootsshauthkeysbackup = "${rootsshauthkeys}.org";
65 my $rootsshconfig = "/root/.ssh/config";
67 # ssh related utility functions
70 # remove duplicate keys in $sshauthkeys
71 # ssh-copy-id simply add keys, so the file can grow to large
74 if (-f
$sshauthkeys) {
75 $data = PVE
::Tools
::file_get_contents
($sshauthkeys, 128*1024);
80 if (-f
$rootsshauthkeysbackup) {
82 $data .= PVE
::Tools
::file_get_contents
($rootsshauthkeysbackup, 128*1024);
89 my $pub = PVE
::Tools
::file_get_contents
($ssh_rsa_id);
96 my @lines = split(/\n/, $data);
97 foreach my $line (@lines) {
98 if ($line !~ /^#/ && $line =~ m/(^|\s)ssh-(rsa|dsa)\s+(\S+)\s+\S+$/) {
99 next if $vhash->{$3}++;
101 $newdata .= "$line\n";
104 PVE
::Tools
::file_set_contents
($sshauthkeys, $newdata, 0600);
106 if ($found_backup && -l
$rootsshauthkeys) {
107 # everything went well, so we can remove the backup
108 unlink $rootsshauthkeysbackup;
112 sub setup_sshd_config
{
115 my $conf = PVE
::Tools
::file_get_contents
($sshd_config_fn);
117 return if $conf =~ m/^PermitRootLogin\s+yes\s*$/m;
119 if ($conf !~ s/^#?PermitRootLogin.*$/PermitRootLogin yes/m) {
121 $conf .= "\nPermitRootLogin yes\n";
124 PVE
::Tools
::file_set_contents
($sshd_config_fn, $conf);
126 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'sshd']);
129 sub setup_rootsshconfig
{
131 # create ssh key if it does not exist
132 if (! -f
$ssh_rsa_id) {
134 system ("echo|ssh-keygen -t rsa -N '' -b 2048 -f ${ssh_rsa_id_priv}");
137 # create ssh config if it does not exist
138 if (! -f
$rootsshconfig) {
140 if (my $fh = IO
::File-
>new($rootsshconfig, O_CREAT
|O_WRONLY
|O_EXCL
, 0640)) {
141 # this is the default ciphers list from Debian's OpenSSH package (OpenSSH_7.4p1 Debian-10, OpenSSL 1.0.2k 26 Jan 2017)
142 # changed order to put AES before Chacha20 (most hardware has AESNI)
143 print $fh "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm\@openssh.com,aes256-gcm\@openssh.com,chacha20-poly1305\@openssh.com\n";
151 mkdir $pmxcfs_auth_dir;
155 if (! -f
$sshauthkeys) {
157 if (-f
$rootsshauthkeys) {
158 $old = PVE
::Tools
::file_get_contents
($rootsshauthkeys, 128*1024);
160 if (my $fh = IO
::File-
>new ($sshauthkeys, O_CREAT
|O_WRONLY
|O_EXCL
, 0400)) {
161 PVE
::Tools
::safe_print
($sshauthkeys, $fh, $old) if $old;
167 warn "can't create shared ssh key database '$sshauthkeys'\n"
168 if ! -f
$sshauthkeys;
170 if (-f
$rootsshauthkeys && ! -l
$rootsshauthkeys) {
171 if (!rename($rootsshauthkeys , $rootsshauthkeysbackup)) {
172 warn "rename $rootsshauthkeys failed - $!\n";
176 if (! -l
$rootsshauthkeys) {
177 symlink $sshauthkeys, $rootsshauthkeys;
180 if (! -l
$rootsshauthkeys) {
181 warn "can't create symlink for ssh keys '$rootsshauthkeys' -> '$sshauthkeys'\n";
183 unlink $rootsshauthkeysbackup if $import_ok;
187 sub ssh_unmerge_known_hosts
{
188 return if ! -l
$sshglobalknownhosts;
191 $old = PVE
::Tools
::file_get_contents
($sshknownhosts, 128*1024)
192 if -f
$sshknownhosts;
194 PVE
::Tools
::file_set_contents
($sshglobalknownhosts, $old);
197 sub ssh_merge_known_hosts
{
198 my ($nodename, $ip_address, $createLink) = @_;
200 die "no node name specified" if !$nodename;
201 die "no ip address specified" if !$ip_address;
203 # ssh lowercases hostnames (aliases) before comparision, so we need too
204 $nodename = lc($nodename);
205 $ip_address = lc($ip_address);
207 mkdir $pmxcfs_auth_dir;
209 if (! -f
$sshknownhosts) {
210 if (my $fh = IO
::File-
>new($sshknownhosts, O_CREAT
|O_WRONLY
|O_EXCL
, 0600)) {
215 my $old = PVE
::Tools
::file_get_contents
($sshknownhosts, 128*1024);
219 if ((! -l
$sshglobalknownhosts) && (-f
$sshglobalknownhosts)) {
220 $new = PVE
::Tools
::file_get_contents
($sshglobalknownhosts, 128*1024);
223 my $hostkey = PVE
::Tools
::file_get_contents
($ssh_host_rsa_id);
224 # Note: file sometimes containe emty lines at start, so we use multiline match
225 die "can't parse $ssh_host_rsa_id" if $hostkey !~ m/^(ssh-rsa\s\S+)(\s.*)?$/m;
234 my $merge_line = sub {
235 my ($line, $all) = @_;
237 return if $line =~ m/^\s*$/; # skip empty lines
238 return if $line =~ m/^#/; # skip comments
240 if ($line =~ m/^(\S+)\s(ssh-rsa\s\S+)(\s.*)?$/) {
243 if (!$vhash->{$key}) {
245 if ($key =~ m/\|1\|([^\|\s]+)\|([^\|\s]+)$/) {
246 my $salt = decode_base64
($1);
248 my $hmac = Digest
::HMAC_SHA1-
>new($salt);
249 $hmac->add($nodename);
250 my $hd = $hmac->b64digest . '=';
251 if ($digest eq $hd) {
252 if ($rsakey eq $hostkey) {
258 $hmac = Digest
::HMAC_SHA1-
>new($salt);
259 $hmac->add($ip_address);
260 $hd = $hmac->b64digest . '=';
261 if ($digest eq $hd) {
262 if ($rsakey eq $hostkey) {
269 $key = lc($key); # avoid duplicate entries, ssh compares lowercased
270 if ($key eq $ip_address) {
271 $found_local_ip = 1 if $rsakey eq $hostkey;
272 } elsif ($key eq $nodename) {
273 $found_nodename = 1 if $rsakey eq $hostkey;
283 while ($old && $old =~ s/^((.*?)(\n|$))//) {
285 &$merge_line($line, 1);
288 while ($new && $new =~ s/^((.*?)(\n|$))//) {
293 # add our own key if not already there
294 $data .= "$nodename $hostkey\n" if !$found_nodename;
295 $data .= "$ip_address $hostkey\n" if !$found_local_ip;
297 PVE
::Tools
::file_set_contents
($sshknownhosts, $data);
299 return if !$createLink;
301 unlink $sshglobalknownhosts;
302 symlink $sshknownhosts, $sshglobalknownhosts;
304 warn "can't create symlink for ssh known hosts '$sshglobalknownhosts' -> '$sshknownhosts'\n"
305 if ! -l
$sshglobalknownhosts;
309 # directory and file creation
314 PVE
::Cluster
::check_cfs_is_mounted
();
316 my @required_dirs = (
317 "$pmxcfs_base_dir/priv",
318 "$pmxcfs_base_dir/nodes",
319 "$pmxcfs_base_dir/nodes/$nodename",
320 "$pmxcfs_base_dir/nodes/$nodename/lxc",
321 "$pmxcfs_base_dir/nodes/$nodename/qemu-server",
322 "$pmxcfs_base_dir/nodes/$nodename/openvz",
323 "$pmxcfs_base_dir/nodes/$nodename/priv");
325 foreach my $dir (@required_dirs) {
327 mkdir($dir) || $! == EEXIST
|| die "unable to create directory '$dir' - $!\n";
333 my $authprivkeyfn = "$pmxcfs_auth_dir/authkey.key";
334 my $authpubkeyfn = "$pmxcfs_base_dir/authkey.pub";
336 return if -f
"$authprivkeyfn";
338 PVE
::Cluster
::check_cfs_is_mounted
();
340 PVE
::Cluster
::cfs_lock_authkey
(undef, sub {
341 mkdir $pmxcfs_auth_dir || $! == EEXIST
|| die "unable to create dir '$pmxcfs_auth_dir' - $!\n";
343 run_silent_cmd
(['openssl', 'genrsa', '-out', $authprivkeyfn, '2048']);
345 run_silent_cmd
(['openssl', 'rsa', '-in', $authprivkeyfn, '-pubout', '-out', $authpubkeyfn]);
353 return if -f
$pveca_key_fn;
356 run_silent_cmd
(['openssl', 'genrsa', '-out', $pveca_key_fn, '4096']);
359 die "unable to generate pve ca key:\n$@" if $@;
364 if (-f
$pveca_key_fn && -f
$pveca_cert_fn) {
370 # we try to generate an unique 'subject' to avoid browser problems
371 # (reused serial numbers, ..)
373 UUID
::generate
($uuid);
375 UUID
::unparse
($uuid, $uuid_str);
378 # wrap openssl with faketime to prevent bug #904
379 run_silent_cmd
(['faketime', 'yesterday', 'openssl', 'req', '-batch',
380 '-days', '3650', '-new', '-x509', '-nodes', '-key',
381 $pveca_key_fn, '-out', $pveca_cert_fn, '-subj',
382 "/CN=Proxmox Virtual Environment/OU=$uuid_str/O=PVE Cluster Manager CA/"]);
385 die "generating pve root certificate failed:\n$@" if $@;
390 sub gen_pve_ssl_key
{
393 die "no node name specified" if !$nodename;
395 my $pvessl_key_fn = "$pmxcfs_base_dir/nodes/$nodename/pve-ssl.key";
397 return if -f
$pvessl_key_fn;
400 run_silent_cmd
(['openssl', 'genrsa', '-out', $pvessl_key_fn, '2048']);
403 die "unable to generate pve ssl key for node '$nodename':\n$@" if $@;
406 sub gen_pve_www_key
{
408 return if -f
$pvewww_key_fn;
411 run_silent_cmd
(['openssl', 'genrsa', '-out', $pvewww_key_fn, '2048']);
414 die "unable to generate pve www key:\n$@" if $@;
420 PVE
::Tools
::file_set_contents
($pveca_srl_fn, $serial);
423 sub gen_pve_ssl_cert
{
424 my ($force, $nodename, $ip) = @_;
426 die "no node name specified" if !$nodename;
427 die "no IP specified" if !$ip;
429 my $pvessl_cert_fn = "$pmxcfs_base_dir/nodes/$nodename/pve-ssl.pem";
431 return if !$force && -f
$pvessl_cert_fn;
433 my $names = "IP:127.0.0.1,IP:::1,DNS:localhost";
435 my $rc = PVE
::INotify
::read_file
('resolvconf');
439 my $fqdn = $nodename;
441 $names .= ",DNS:$nodename";
443 if ($rc && $rc->{search
}) {
444 $fqdn = $nodename . "." . $rc->{search
};
445 $names .= ",DNS:$fqdn";
448 my $sslconf = <<__EOD;
449 RANDFILE = /root/.rnd
454 distinguished_name = req_distinguished_name
455 req_extensions = v3_req
457 string_mask = nombstr
459 [ req_distinguished_name ]
460 organizationalUnitName = PVE Cluster Node
461 organizationName = Proxmox Virtual Environment
465 basicConstraints = CA:FALSE
466 extendedKeyUsage = serverAuth
467 subjectAltName = $names
470 my $cfgfn = "/tmp/pvesslconf-$$.tmp";
471 my $fh = IO
::File-
>new ($cfgfn, "w");
475 my $reqfn = "/tmp/pvecertreq-$$.tmp";
478 my $pvessl_key_fn = "$pmxcfs_base_dir/nodes/$nodename/pve-ssl.key";
480 run_silent_cmd
(['openssl', 'req', '-batch', '-new', '-config', $cfgfn,
481 '-key', $pvessl_key_fn, '-out', $reqfn]);
487 die "unable to generate pve certificate request:\n$err";
490 update_serial
("0000000000000000") if ! -f
$pveca_srl_fn;
493 my $cainfo = PVE
::Certificate
::get_certificate_info
($pveca_cert_fn);
494 my $daysleft = int(($cainfo->{notafter
} - time())/(24*60*60));
496 if ($daysleft < 14) {
497 die "CA expires in less than 2 weeks, unable to generate certificate.\n";
500 # let the certificate expire a little sooner that the ca, so subtract 2 days
503 # we want the certificates to only last 2 years, since some browsers
504 # do not accept certificates with very long expiry time
505 if ($daysleft >= 2*365) {
510 # wrap openssl with faketime to prevent bug #904
511 run_silent_cmd
(['faketime', 'yesterday', 'openssl', 'x509', '-req',
512 '-in', $reqfn, '-days', $daysleft, '-out', $pvessl_cert_fn,
513 '-CAkey', $pveca_key_fn, '-CA', $pveca_cert_fn,
514 '-CAserial', $pveca_srl_fn, '-extfile', $cfgfn]);
520 die "unable to generate pve ssl certificate:\n$err";
527 sub gen_pve_node_files
{
528 my ($nodename, $ip, $opt_force) = @_;
530 gen_local_dirs
($nodename);
534 # make sure we have a (cluster wide) secret
535 # for CSRFR prevention
538 # make sure we have a (per node) private key
539 gen_pve_ssl_key
($nodename);
541 # make sure we have a CA
542 my $force = gen_pveca_cert
();
544 $force = 1 if $opt_force;
546 gen_pve_ssl_cert
($force, $nodename, $ip);
549 my $vzdump_cron_dummy = <<__EOD;
550 # cluster wide vzdump cron schedule
551 # Atomatically generated file - do not edit
553 PATH="/usr/sbin:/usr/bin:/sbin:/bin"
557 sub gen_pve_vzdump_symlink
{
559 my $filename = "/etc/pve/vzdump.cron";
561 my $link_fn = "/etc/cron.d/vzdump";
563 if ((-f
$filename) && (! -l
$link_fn)) {
564 rename($link_fn, "/root/etc_cron_vzdump.org"); # make backup if file exists
565 symlink($filename, $link_fn);
569 sub gen_pve_vzdump_files
{
571 my $filename = "/etc/pve/vzdump.cron";
573 PVE
::Tools
::file_set_contents
($filename, $vzdump_cron_dummy)
576 gen_pve_vzdump_symlink
();
581 sub assert_joinable
{
582 my ($local_addr, $link0, $link1, $force) = @_;
585 my $error = sub { $errors .= "* $_[0]\n"; };
588 $error->("authentication key '$authfile' already exists");
591 if (-f
$clusterconf) {
592 $error->("cluster config '$clusterconf' already exists");
595 my $vmlist = PVE
::Cluster
::get_vmlist
();
596 if ($vmlist && $vmlist->{ids
} && scalar(keys %{$vmlist->{ids
}})) {
597 $error->("this host already contains virtual guests");
600 if (PVE
::Tools
::run_command
(['corosync-quorumtool', '-l'], noerr
=> 1, quiet
=> 1) == 0) {
601 $error->("corosync is already running, is this node already in a cluster?!");
604 # check if corosync ring IPs are configured on the current nodes interfaces
606 my $ip = shift // return;
608 if (!PVE
::JSONSchema
::pve_verify_ip
($ip, 1)) {
610 eval { $ip = PVE
::Network
::get_ip_from_hostname
($host); };
612 $error->("$logid: cannot use '$host': $@\n") ;
617 my $cidr = (Net
::IP
::ip_is_ipv6
($ip)) ?
"$ip/128" : "$ip/32";
618 my $configured_ips = PVE
::Network
::get_local_ip_from_cidr
($cidr);
620 $error->("$logid: cannot use IP '$ip', not found on local node!\n")
621 if scalar(@$configured_ips) < 1;
624 $check_ip->($local_addr, 'local node address');
625 $check_ip->($link0->{address
}, 'ring0') if defined($link0);
626 $check_ip->($link1->{address
}, 'ring1') if defined($link1);
629 warn "detected the following error(s):\n$errors";
630 die "Check if node may join a cluster failed!\n" if !$force;
637 my $nodename = PVE
::INotify
::nodename
();
638 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
640 my $link0 = PVE
::Corosync
::parse_corosync_link
($param->{link0
});
641 my $link1 = PVE
::Corosync
::parse_corosync_link
($param->{link1
});
643 # check if we can join with the given parameters and current node state
644 assert_joinable
($local_ip_address, $link0, $link1, $param->{force
});
647 setup_rootsshconfig
();
650 # make sure known_hosts is on local filesystem
651 ssh_unmerge_known_hosts
();
653 my $host = $param->{hostname
};
655 username
=> 'root@pam',
656 password
=> $param->{password
},
657 cookie_name
=> 'PVEAuthCookie',
663 if (my $fp = $param->{fingerprint
}) {
664 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
666 # API schema ensures that we can only get here from CLI handler
667 $conn_args->{manual_verification
} = 1;
670 print "Establishing API connection with host '$host'\n";
672 my $conn = PVE
::APIClient
::LWP-
>new(%$conn_args);
675 # login raises an exception on failure, so if we get here we're good
676 print "Login succeeded.\n";
679 $args->{force
} = $param->{force
} if defined($param->{force
});
680 $args->{nodeid
} = $param->{nodeid
} if $param->{nodeid
};
681 $args->{votes
} = $param->{votes
} if defined($param->{votes
});
682 # just pass the un-parsed string through, or as we've address as the
683 # default_key, we can just pass the fallback directly too
684 $args->{link0
} = $param->{link0
} // $local_ip_address;
685 $args->{link1
} = $param->{link1
} if defined($param->{link1
});
687 print "Request addition of this node\n";
688 my $res = $conn->post("/cluster/config/nodes/$nodename", $args);
690 print "Join request OK, finishing setup locally\n";
692 # added successfuly - now prepare local node
693 finish_join
($nodename, $res->{corosync_conf
}, $res->{corosync_authkey
});
697 my ($nodename, $corosync_conf, $corosync_authkey) = @_;
699 mkdir "$localclusterdir";
700 PVE
::Tools
::file_set_contents
($authfile, $corosync_authkey);
701 PVE
::Tools
::file_set_contents
($localclusterconf, $corosync_conf);
703 print "stopping pve-cluster service\n";
704 my $cmd = ['systemctl', 'stop', 'pve-cluster'];
705 PVE
::Tools
::run_command
($cmd, errmsg
=> "can't stop pve-cluster service");
707 my $dbfile = PVE
::Cluster
::cfs_backup_database
();
710 $cmd = ['systemctl', 'start', 'corosync', 'pve-cluster'];
711 PVE
::Tools
::run_command
($cmd, errmsg
=> "starting pve-cluster failed");
715 while (!PVE
::Cluster
::check_cfs_quorum
(1)) {
717 print "waiting for quorum...";
723 print "OK\n" if !$printqmsg;
725 updatecerts_and_ssh
(1);
727 print "generated new node certificate, restart pveproxy and pvedaemon services\n";
728 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'pvedaemon', 'pveproxy']);
730 print "successfully added node '$nodename' to cluster.\n";
733 sub updatecerts_and_ssh
{
734 my ($force_new_cert, $silent) = @_;
736 my $p = sub { print "$_[0]\n" if !$silent };
738 setup_rootsshconfig
();
740 gen_pve_vzdump_symlink
();
742 if (!PVE
::Cluster
::check_cfs_quorum
(1)) {
743 return undef if $silent;
744 die "no quorum - unable to update files\n";
749 my $nodename = PVE
::INotify
::nodename
();
750 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
752 $p->("(re)generate node files");
753 $p->("generate new node certificate") if $force_new_cert;
754 gen_pve_node_files
($nodename, $local_ip_address, $force_new_cert);
756 $p->("merge authorized SSH keys and known hosts");
758 ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
759 gen_pve_vzdump_files
();