5 use POSIX
qw(EEXIST ENOENT);
8 use Storable
qw(dclone);
12 use Digest
::HMAC_SHA1
;
14 use PVE
::Tools
qw(run_command);
20 use PVE
::Cluster
::IPCConst
;
33 use Data
::Dumper
; # fixme: remove
35 # x509 certificate utils
37 my $basedir = "/etc/pve";
38 my $authdir = "$basedir/priv";
39 my $lockdir = "/etc/pve/priv/lock";
41 # cfs and corosync files
42 my $dbfile = "/var/lib/pve-cluster/config.db";
43 my $dbbackupdir = "/var/lib/pve-cluster/backup";
44 my $localclusterdir = "/etc/corosync";
45 my $localclusterconf = "$localclusterdir/corosync.conf";
46 my $authfile = "$localclusterdir/authkey";
47 my $clusterconf = "$basedir/corosync.conf";
49 my $authprivkeyfn = "$authdir/authkey.key";
50 my $authpubkeyfn = "$basedir/authkey.pub";
51 my $pveca_key_fn = "$authdir/pve-root-ca.key";
52 my $pveca_srl_fn = "$authdir/pve-root-ca.srl";
53 my $pveca_cert_fn = "$basedir/pve-root-ca.pem";
54 # this is just a secret accessable by the web browser
55 # and is used for CSRF prevention
56 my $pvewww_key_fn = "$basedir/pve-www.key";
59 my $ssh_rsa_id_priv = "/root/.ssh/id_rsa";
60 my $ssh_rsa_id = "/root/.ssh/id_rsa.pub";
61 my $ssh_host_rsa_id = "/etc/ssh/ssh_host_rsa_key.pub";
62 my $sshglobalknownhosts = "/etc/ssh/ssh_known_hosts";
63 my $sshknownhosts = "/etc/pve/priv/known_hosts";
64 my $sshauthkeys = "/etc/pve/priv/authorized_keys";
65 my $sshd_config_fn = "/etc/ssh/sshd_config";
66 my $rootsshauthkeys = "/root/.ssh/authorized_keys";
67 my $rootsshauthkeysbackup = "${rootsshauthkeys}.org";
68 my $rootsshconfig = "/root/.ssh/config";
70 # this is just a readonly copy, the relevant one is in status.c from pmxcfs
71 # observed files are the one we can get directly through IPCC, they are cached
72 # using a computed version and only those can be used by the cfs_*_file methods
76 'datacenter.cfg' => 1,
77 'replication.cfg' => 1,
79 'corosync.conf.new' => 1,
82 'priv/shadow.cfg' => 1,
86 'ha/crm_commands' => 1,
87 'ha/manager_status' => 1,
88 'ha/resources.cfg' => 1,
95 # only write output if something fails
100 my $record = sub { $outbuf .= shift . "\n"; };
102 eval { run_command
($cmd, outfunc
=> $record, errfunc
=> $record) };
105 print STDERR
$outbuf;
110 sub check_cfs_quorum
{
113 # note: -w filename always return 1 for root, so wee need
114 # to use File::lstat here
115 my $st = File
::stat::lstat("$basedir/local");
116 my $quorate = ($st && (($st->mode & 0200) != 0));
118 die "cluster not ready - no quorum?\n" if !$quorate && !$noerr;
123 sub check_cfs_is_mounted
{
126 my $res = -l
"$basedir/local";
128 die "pve configuration filesystem not mounted\n"
137 check_cfs_is_mounted
();
139 my @required_dirs = (
142 "$basedir/nodes/$nodename",
143 "$basedir/nodes/$nodename/lxc",
144 "$basedir/nodes/$nodename/qemu-server",
145 "$basedir/nodes/$nodename/openvz",
146 "$basedir/nodes/$nodename/priv");
148 foreach my $dir (@required_dirs) {
150 mkdir($dir) || $! == EEXIST
|| die "unable to create directory '$dir' - $!\n";
157 return if -f
"$authprivkeyfn";
159 check_cfs_is_mounted
();
161 cfs_lock_authkey
(undef, sub {
162 mkdir $authdir || $! == EEXIST
|| die "unable to create dir '$authdir' - $!\n";
164 run_silent_cmd
(['openssl', 'genrsa', '-out', $authprivkeyfn, '2048']);
166 run_silent_cmd
(['openssl', 'rsa', '-in', $authprivkeyfn, '-pubout', '-out', $authpubkeyfn]);
174 return if -f
$pveca_key_fn;
177 run_silent_cmd
(['openssl', 'genrsa', '-out', $pveca_key_fn, '4096']);
180 die "unable to generate pve ca key:\n$@" if $@;
185 if (-f
$pveca_key_fn && -f
$pveca_cert_fn) {
191 # we try to generate an unique 'subject' to avoid browser problems
192 # (reused serial numbers, ..)
194 UUID
::generate
($uuid);
196 UUID
::unparse
($uuid, $uuid_str);
199 # wrap openssl with faketime to prevent bug #904
200 run_silent_cmd
(['faketime', 'yesterday', 'openssl', 'req', '-batch',
201 '-days', '3650', '-new', '-x509', '-nodes', '-key',
202 $pveca_key_fn, '-out', $pveca_cert_fn, '-subj',
203 "/CN=Proxmox Virtual Environment/OU=$uuid_str/O=PVE Cluster Manager CA/"]);
206 die "generating pve root certificate failed:\n$@" if $@;
211 sub gen_pve_ssl_key
{
214 die "no node name specified" if !$nodename;
216 my $pvessl_key_fn = "$basedir/nodes/$nodename/pve-ssl.key";
218 return if -f
$pvessl_key_fn;
221 run_silent_cmd
(['openssl', 'genrsa', '-out', $pvessl_key_fn, '2048']);
224 die "unable to generate pve ssl key for node '$nodename':\n$@" if $@;
227 sub gen_pve_www_key
{
229 return if -f
$pvewww_key_fn;
232 run_silent_cmd
(['openssl', 'genrsa', '-out', $pvewww_key_fn, '2048']);
235 die "unable to generate pve www key:\n$@" if $@;
241 PVE
::Tools
::file_set_contents
($pveca_srl_fn, $serial);
244 sub gen_pve_ssl_cert
{
245 my ($force, $nodename, $ip) = @_;
247 die "no node name specified" if !$nodename;
248 die "no IP specified" if !$ip;
250 my $pvessl_cert_fn = "$basedir/nodes/$nodename/pve-ssl.pem";
252 return if !$force && -f
$pvessl_cert_fn;
254 my $names = "IP:127.0.0.1,IP:::1,DNS:localhost";
256 my $rc = PVE
::INotify
::read_file
('resolvconf');
260 my $fqdn = $nodename;
262 $names .= ",DNS:$nodename";
264 if ($rc && $rc->{search
}) {
265 $fqdn = $nodename . "." . $rc->{search
};
266 $names .= ",DNS:$fqdn";
269 my $sslconf = <<__EOD;
270 RANDFILE = /root/.rnd
275 distinguished_name = req_distinguished_name
276 req_extensions = v3_req
278 string_mask = nombstr
280 [ req_distinguished_name ]
281 organizationalUnitName = PVE Cluster Node
282 organizationName = Proxmox Virtual Environment
286 basicConstraints = CA:FALSE
287 extendedKeyUsage = serverAuth
288 subjectAltName = $names
291 my $cfgfn = "/tmp/pvesslconf-$$.tmp";
292 my $fh = IO
::File-
>new ($cfgfn, "w");
296 my $reqfn = "/tmp/pvecertreq-$$.tmp";
299 my $pvessl_key_fn = "$basedir/nodes/$nodename/pve-ssl.key";
301 run_silent_cmd
(['openssl', 'req', '-batch', '-new', '-config', $cfgfn,
302 '-key', $pvessl_key_fn, '-out', $reqfn]);
308 die "unable to generate pve certificate request:\n$err";
311 update_serial
("0000000000000000") if ! -f
$pveca_srl_fn;
314 # wrap openssl with faketime to prevent bug #904
315 run_silent_cmd
(['faketime', 'yesterday', 'openssl', 'x509', '-req',
316 '-in', $reqfn, '-days', '3650', '-out', $pvessl_cert_fn,
317 '-CAkey', $pveca_key_fn, '-CA', $pveca_cert_fn,
318 '-CAserial', $pveca_srl_fn, '-extfile', $cfgfn]);
324 die "unable to generate pve ssl certificate:\n$err";
331 sub gen_pve_node_files
{
332 my ($nodename, $ip, $opt_force) = @_;
334 gen_local_dirs
($nodename);
338 # make sure we have a (cluster wide) secret
339 # for CSRFR prevention
342 # make sure we have a (per node) private key
343 gen_pve_ssl_key
($nodename);
345 # make sure we have a CA
346 my $force = gen_pveca_cert
();
348 $force = 1 if $opt_force;
350 gen_pve_ssl_cert
($force, $nodename, $ip);
353 my $vzdump_cron_dummy = <<__EOD;
354 # cluster wide vzdump cron schedule
355 # Atomatically generated file - do not edit
357 PATH="/usr/sbin:/usr/bin:/sbin:/bin"
361 sub gen_pve_vzdump_symlink
{
363 my $filename = "/etc/pve/vzdump.cron";
365 my $link_fn = "/etc/cron.d/vzdump";
367 if ((-f
$filename) && (! -l
$link_fn)) {
368 rename($link_fn, "/root/etc_cron_vzdump.org"); # make backup if file exists
369 symlink($filename, $link_fn);
373 sub gen_pve_vzdump_files
{
375 my $filename = "/etc/pve/vzdump.cron";
377 PVE
::Tools
::file_set_contents
($filename, $vzdump_cron_dummy)
380 gen_pve_vzdump_symlink
();
387 my $ipcc_send_rec = sub {
388 my ($msgid, $data) = @_;
390 my $res = PVE
::IPCC
::ipcc_send_rec
($msgid, $data);
392 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
397 my $ipcc_send_rec_json = sub {
398 my ($msgid, $data) = @_;
400 my $res = PVE
::IPCC
::ipcc_send_rec
($msgid, $data);
402 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
404 return decode_json
($res);
407 my $ipcc_get_config = sub {
410 my $bindata = pack "Z*", $path;
411 my $res = PVE
::IPCC
::ipcc_send_rec
(CFS_IPC_GET_CONFIG
, $bindata);
412 if (!defined($res)) {
414 return undef if $! == ENOENT
;
423 my $ipcc_get_status = sub {
424 my ($name, $nodename) = @_;
426 my $bindata = pack "Z[256]Z[256]", $name, ($nodename || "");
427 return PVE
::IPCC
::ipcc_send_rec
(CFS_IPC_GET_STATUS
, $bindata);
430 my $ipcc_update_status = sub {
431 my ($name, $data) = @_;
433 my $raw = ref($data) ? encode_json
($data) : $data;
435 my $bindata = pack "Z[256]Z*", $name, $raw;
437 return &$ipcc_send_rec(CFS_IPC_SET_STATUS
, $bindata);
441 my ($priority, $ident, $tag, $msg) = @_;
443 my $bindata = pack "CCCZ*Z*Z*", $priority, bytes
::length($ident) + 1,
444 bytes
::length($tag) + 1, $ident, $tag, $msg;
446 return &$ipcc_send_rec(CFS_IPC_LOG_CLUSTER_MSG
, $bindata);
449 my $ipcc_get_cluster_log = sub {
450 my ($user, $max) = @_;
452 $max = 0 if !defined($max);
454 my $bindata = pack "VVVVZ*", $max, 0, 0, 0, ($user || "");
455 return &$ipcc_send_rec(CFS_IPC_GET_CLUSTER_LOG
, $bindata);
463 my $res = &$ipcc_send_rec_json(CFS_IPC_GET_FS_VERSION
);
464 #warn "GOT1: " . Dumper($res);
465 die "no starttime\n" if !$res->{starttime
};
467 if (!$res->{starttime
} || !$versions->{starttime
} ||
468 $res->{starttime
} != $versions->{starttime
}) {
469 #print "detected changed starttime\n";
488 if (!$clinfo->{version
} || $clinfo->{version
} != $versions->{clinfo
}) {
489 #warn "detected new clinfo\n";
490 $clinfo = &$ipcc_send_rec_json(CFS_IPC_GET_CLUSTER_INFO
);
501 if (!$vmlist->{version
} || $vmlist->{version
} != $versions->{vmlist
}) {
502 #warn "detected new vmlist1\n";
503 $vmlist = &$ipcc_send_rec_json(CFS_IPC_GET_GUEST_LIST
);
523 return $clinfo->{nodelist
};
527 my $nodelist = $clinfo->{nodelist
};
529 my $nodename = PVE
::INotify
::nodename
();
531 if (!$nodelist || !$nodelist->{$nodename}) {
532 return [ $nodename ];
535 return [ keys %$nodelist ];
538 # $data must be a chronological descending ordered array of tasks
539 sub broadcast_tasklist
{
542 # the serialized list may not get bigger than 32kb (CFS_MAX_STATUS_SIZE
543 # from pmxcfs) - drop older items until we satisfy this constraint
544 my $size = length(encode_json
($data));
545 while ($size >= (32 * 1024)) {
547 $size = length(encode_json
($data));
551 &$ipcc_update_status("tasklist", $data);
557 my $tasklistcache = {};
562 my $kvstore = $versions->{kvstore
} || {};
564 my $nodelist = get_nodelist
();
567 foreach my $node (@$nodelist) {
568 next if $nodename && ($nodename ne $node);
570 my $ver = $kvstore->{$node}->{tasklist
} if $kvstore->{$node};
571 my $cd = $tasklistcache->{$node};
572 if (!$cd || !$ver || !$cd->{version
} ||
573 ($cd->{version
} != $ver)) {
574 my $raw = &$ipcc_get_status("tasklist", $node) || '[]';
575 my $data = decode_json
($raw);
577 $cd = $tasklistcache->{$node} = {
581 } elsif ($cd && $cd->{data
}) {
582 push @$res, @{$cd->{data
}};
586 syslog
('err', $err) if $err;
593 my ($rrdid, $data) = @_;
596 &$ipcc_update_status("rrd/$rrdid", $data);
603 my $last_rrd_dump = 0;
604 my $last_rrd_data = "";
610 my $diff = $ctime - $last_rrd_dump;
612 return $last_rrd_data;
617 $raw = &$ipcc_send_rec(CFS_IPC_GET_RRD_DUMP
);
629 while ($raw =~ s/^(.*)\n//) {
630 my ($key, @ela) = split(/:/, $1);
632 next if !(scalar(@ela) > 1);
633 $res->{$key} = [ map { $_ eq 'U' ?
undef : $_ } @ela ];
637 $last_rrd_dump = $ctime;
638 $last_rrd_data = $res;
643 sub create_rrd_data
{
644 my ($rrdname, $timeframe, $cf) = @_;
646 my $rrddir = "/var/lib/rrdcached/db";
648 my $rrd = "$rrddir/$rrdname";
652 day
=> [ 60*30, 70 ],
653 week
=> [ 60*180, 70 ],
654 month
=> [ 60*720, 70 ],
655 year
=> [ 60*10080, 70 ],
658 my ($reso, $count) = @{$setup->{$timeframe}};
659 my $ctime = $reso*int(time()/$reso);
660 my $req_start = $ctime - $reso*$count;
662 $cf = "AVERAGE" if !$cf;
670 my $socket = "/var/run/rrdcached.sock";
671 push @args, "--daemon" => "unix:$socket" if -S
$socket;
673 my ($start, $step, $names, $data) = RRDs
::fetch
($rrd, $cf, @args);
675 my $err = RRDs
::error
;
676 die "RRD error: $err\n" if $err;
678 die "got wrong time resolution ($step != $reso)\n"
682 my $fields = scalar(@$names);
683 for my $line (@$data) {
684 my $entry = { 'time' => $start };
686 for (my $i = 0; $i < $fields; $i++) {
687 my $name = $names->[$i];
688 if (defined(my $val = $line->[$i])) {
689 $entry->{$name} = $val;
691 # leave empty fields undefined
692 # maybe make this configurable?
701 sub create_rrd_graph
{
702 my ($rrdname, $timeframe, $ds, $cf) = @_;
704 # Using RRD graph is clumsy - maybe it
705 # is better to simply fetch the data, and do all display
706 # related things with javascript (new extjs html5 graph library).
708 my $rrddir = "/var/lib/rrdcached/db";
710 my $rrd = "$rrddir/$rrdname";
712 my @ids = PVE
::Tools
::split_list
($ds);
714 my $ds_txt = join('_', @ids);
716 my $filename = "${rrd}_${ds_txt}.png";
720 day
=> [ 60*30, 70 ],
721 week
=> [ 60*180, 70 ],
722 month
=> [ 60*720, 70 ],
723 year
=> [ 60*10080, 70 ],
726 my ($reso, $count) = @{$setup->{$timeframe}};
729 "--imgformat" => "PNG",
733 "--start" => - $reso*$count,
735 "--lower-limit" => 0,
738 my $socket = "/var/run/rrdcached.sock";
739 push @args, "--daemon" => "unix:$socket" if -S
$socket;
741 my @coldef = ('#00ddff', '#ff0000');
743 $cf = "AVERAGE" if !$cf;
746 foreach my $id (@ids) {
747 my $col = $coldef[$i++] || die "fixme: no color definition";
748 push @args, "DEF:${id}=$rrd:${id}:$cf";
750 if ($id eq 'cpu' || $id eq 'iowait') {
751 push @args, "CDEF:${id}_per=${id},100,*";
752 $dataid = "${id}_per";
754 push @args, "LINE2:${dataid}${col}:${id}";
757 push @args, '--full-size-mode';
759 # we do not really store data into the file
760 my $res = RRDs
::graphv
('-', @args);
762 my $err = RRDs
::error
;
763 die "RRD error: $err\n" if $err;
765 return { filename
=> $filename, image
=> $res->{image
} };
768 # a fast way to read files (avoid fuse overhead)
772 return &$ipcc_get_config($path);
775 sub get_cluster_log
{
776 my ($user, $max) = @_;
778 return &$ipcc_get_cluster_log($user, $max);
783 sub cfs_register_file
{
784 my ($filename, $parser, $writer) = @_;
786 $observed->{$filename} || die "unknown file '$filename'";
788 die "file '$filename' already registered" if $file_info->{$filename};
790 $file_info->{$filename} = {
796 my $ccache_read = sub {
797 my ($filename, $parser, $version) = @_;
799 $ccache->{$filename} = {} if !$ccache->{$filename};
801 my $ci = $ccache->{$filename};
803 if (!$ci->{version
} || !$version || $ci->{version
} != $version) {
804 # we always call the parser, even when the file does not exists
805 # (in that case $data is undef)
806 my $data = get_config
($filename);
807 $ci->{data
} = &$parser("/etc/pve/$filename", $data);
808 $ci->{version
} = $version;
811 my $res = ref($ci->{data
}) ? dclone
($ci->{data
}) : $ci->{data
};
816 sub cfs_file_version
{
821 if ($filename =~ m!^nodes/[^/]+/(openvz|lxc|qemu-server)/(\d+)\.conf$!) {
822 my ($type, $vmid) = ($1, $2);
823 if ($vmlist && $vmlist->{ids
} && $vmlist->{ids
}->{$vmid}) {
824 $version = $vmlist->{ids
}->{$vmid}->{version
};
826 $infotag = "/$type/";
828 $infotag = $filename;
829 $version = $versions->{$filename};
832 my $info = $file_info->{$infotag} ||
833 die "unknown file type '$filename'\n";
835 return wantarray ?
($version, $info) : $version;
841 my ($version, $info) = cfs_file_version
($filename);
842 my $parser = $info->{parser
};
844 return &$ccache_read($filename, $parser, $version);
848 my ($filename, $data) = @_;
850 my ($version, $info) = cfs_file_version
($filename);
852 my $writer = $info->{writer
} || die "no writer defined";
854 my $fsname = "/etc/pve/$filename";
856 my $raw = &$writer($fsname, $data);
858 if (my $ci = $ccache->{$filename}) {
859 $ci->{version
} = undef;
862 PVE
::Tools
::file_set_contents
($fsname, $raw);
866 my ($lockid, $timeout, $code, @param) = @_;
868 my $prev_alarm = alarm(0); # suspend outer alarm early
873 # this timeout is for acquire the lock
874 $timeout = 10 if !$timeout;
876 my $filename = "$lockdir/$lockid";
883 die "pve cluster filesystem not online.\n";
886 my $timeout_err = sub { die "got lock request timeout\n"; };
887 local $SIG{ALRM
} = $timeout_err;
891 $got_lock = mkdir($filename);
892 $timeout = alarm(0) - 1; # we'll sleep for 1s, see down below
896 $timeout_err->() if $timeout <= 0;
898 print STDERR
"trying to acquire cfs lock '$lockid' ...\n";
899 utime (0, 0, $filename); # cfs unlock request
903 # fixed command timeout: cfs locks have a timeout of 120
904 # using 60 gives us another 60 seconds to abort the task
905 local $SIG{ALRM
} = sub { die "got lock timeout - aborting command\n"; };
908 cfs_update
(); # make sure we read latest versions inside code()
910 $res = &$code(@param);
917 $err = "no quorum!\n" if !$got_lock && !check_cfs_quorum
(1);
919 rmdir $filename if $got_lock; # if we held the lock always unlock again
924 $@ = "error with cfs lock '$lockid': $err";
934 my ($filename, $timeout, $code, @param) = @_;
936 my $info = $observed->{$filename} || die "unknown file '$filename'";
938 my $lockid = "file-$filename";
939 $lockid =~ s/[.\/]/_
/g
;
941 &$cfs_lock($lockid, $timeout, $code, @param);
944 sub cfs_lock_storage
{
945 my ($storeid, $timeout, $code, @param) = @_;
947 my $lockid = "storage-$storeid";
949 &$cfs_lock($lockid, $timeout, $code, @param);
952 sub cfs_lock_domain
{
953 my ($domainname, $timeout, $code, @param) = @_;
955 my $lockid = "domain-$domainname";
957 &$cfs_lock($lockid, $timeout, $code, @param);
961 my ($account, $timeout, $code, @param) = @_;
963 my $lockid = "acme-$account";
965 &$cfs_lock($lockid, $timeout, $code, @param);
968 sub cfs_lock_authkey
{
969 my ($timeout, $code, @param) = @_;
971 $cfs_lock->('authkey', $timeout, $code, @param);
989 my ($priority, $ident, $msg) = @_;
991 if (my $tmp = $log_levels->{$priority}) {
995 die "need numeric log priority" if $priority !~ /^\d+$/;
997 my $tag = PVE
::SafeSyslog
::tag
();
999 $msg = "empty message" if !$msg;
1001 $ident = "" if !$ident;
1002 $ident = encode
("ascii", $ident,
1003 sub { sprintf "\\u%04x", shift });
1005 my $ascii = encode
("ascii", $msg, sub { sprintf "\\u%04x", shift });
1008 syslog
($priority, "<%s> %s", $ident, $ascii);
1010 syslog
($priority, "%s", $ascii);
1013 eval { &$ipcc_log($priority, $ident, $tag, $ascii); };
1015 syslog
("err", "writing cluster log failed: $@") if $@;
1018 sub check_vmid_unused
{
1019 my ($vmid, $noerr) = @_;
1021 my $vmlist = get_vmlist
();
1023 my $d = $vmlist->{ids
}->{$vmid};
1024 return 1 if !defined($d);
1026 return undef if $noerr;
1028 my $vmtypestr = $d->{type
} eq 'qemu' ?
'VM' : 'CT';
1029 die "$vmtypestr $vmid already exists on node '$d->{node}'\n";
1032 sub check_node_exists
{
1033 my ($nodename, $noerr) = @_;
1035 my $nodelist = $clinfo->{nodelist
};
1036 return 1 if $nodelist && $nodelist->{$nodename};
1038 return undef if $noerr;
1040 die "no such cluster node '$nodename'\n";
1043 # this is also used to get the IP of the local node
1044 sub remote_node_ip
{
1045 my ($nodename, $noerr) = @_;
1047 my $nodelist = $clinfo->{nodelist
};
1048 if ($nodelist && $nodelist->{$nodename}) {
1049 if (my $ip = $nodelist->{$nodename}->{ip
}) {
1050 return $ip if !wantarray;
1051 my $family = $nodelist->{$nodename}->{address_family
};
1053 $nodelist->{$nodename}->{address_family
} =
1055 PVE
::Tools
::get_host_address_family
($ip);
1057 return wantarray ?
($ip, $family) : $ip;
1061 # fallback: try to get IP by other means
1062 return PVE
::Network
::get_ip_from_hostname
($nodename, $noerr);
1065 sub get_local_migration_ip
{
1066 my ($migration_network, $noerr) = @_;
1068 my $cidr = $migration_network;
1070 if (!defined($cidr)) {
1071 my $dc_conf = cfs_read_file
('datacenter.cfg');
1072 $cidr = $dc_conf->{migration
}->{network
}
1073 if defined($dc_conf->{migration
}->{network
});
1076 if (defined($cidr)) {
1077 my $ips = PVE
::Network
::get_local_ip_from_cidr
($cidr);
1079 die "could not get migration ip: no IP address configured on local " .
1080 "node for network '$cidr'\n" if !$noerr && (scalar(@$ips) == 0);
1082 die "could not get migration ip: multiple IP address configured for " .
1083 "network '$cidr'\n" if !$noerr && (scalar(@$ips) > 1);
1091 # ssh related utility functions
1093 sub ssh_merge_keys
{
1094 # remove duplicate keys in $sshauthkeys
1095 # ssh-copy-id simply add keys, so the file can grow to large
1098 if (-f
$sshauthkeys) {
1099 $data = PVE
::Tools
::file_get_contents
($sshauthkeys, 128*1024);
1104 if (-f
$rootsshauthkeysbackup) {
1106 $data .= PVE
::Tools
::file_get_contents
($rootsshauthkeysbackup, 128*1024);
1111 # always add ourself
1112 if (-f
$ssh_rsa_id) {
1113 my $pub = PVE
::Tools
::file_get_contents
($ssh_rsa_id);
1115 $data .= "\n$pub\n";
1120 my @lines = split(/\n/, $data);
1121 foreach my $line (@lines) {
1122 if ($line !~ /^#/ && $line =~ m/(^|\s)ssh-(rsa|dsa)\s+(\S+)\s+\S+$/) {
1123 next if $vhash->{$3}++;
1125 $newdata .= "$line\n";
1128 PVE
::Tools
::file_set_contents
($sshauthkeys, $newdata, 0600);
1130 if ($found_backup && -l
$rootsshauthkeys) {
1131 # everything went well, so we can remove the backup
1132 unlink $rootsshauthkeysbackup;
1136 sub setup_sshd_config
{
1139 my $conf = PVE
::Tools
::file_get_contents
($sshd_config_fn);
1141 return if $conf =~ m/^PermitRootLogin\s+yes\s*$/m;
1143 if ($conf !~ s/^#?PermitRootLogin.*$/PermitRootLogin yes/m) {
1145 $conf .= "\nPermitRootLogin yes\n";
1148 PVE
::Tools
::file_set_contents
($sshd_config_fn, $conf);
1150 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'sshd']);
1153 sub setup_rootsshconfig
{
1155 # create ssh key if it does not exist
1156 if (! -f
$ssh_rsa_id) {
1157 mkdir '/root/.ssh/';
1158 system ("echo|ssh-keygen -t rsa -N '' -b 2048 -f ${ssh_rsa_id_priv}");
1161 # create ssh config if it does not exist
1162 if (! -f
$rootsshconfig) {
1164 if (my $fh = IO
::File-
>new($rootsshconfig, O_CREAT
|O_WRONLY
|O_EXCL
, 0640)) {
1165 # this is the default ciphers list from Debian's OpenSSH package (OpenSSH_7.4p1 Debian-10, OpenSSL 1.0.2k 26 Jan 2017)
1166 # changed order to put AES before Chacha20 (most hardware has AESNI)
1167 print $fh "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm\@openssh.com,aes256-gcm\@openssh.com,chacha20-poly1305\@openssh.com\n";
1173 sub setup_ssh_keys
{
1179 if (! -f
$sshauthkeys) {
1181 if (-f
$rootsshauthkeys) {
1182 $old = PVE
::Tools
::file_get_contents
($rootsshauthkeys, 128*1024);
1184 if (my $fh = IO
::File-
>new ($sshauthkeys, O_CREAT
|O_WRONLY
|O_EXCL
, 0400)) {
1185 PVE
::Tools
::safe_print
($sshauthkeys, $fh, $old) if $old;
1191 warn "can't create shared ssh key database '$sshauthkeys'\n"
1192 if ! -f
$sshauthkeys;
1194 if (-f
$rootsshauthkeys && ! -l
$rootsshauthkeys) {
1195 if (!rename($rootsshauthkeys , $rootsshauthkeysbackup)) {
1196 warn "rename $rootsshauthkeys failed - $!\n";
1200 if (! -l
$rootsshauthkeys) {
1201 symlink $sshauthkeys, $rootsshauthkeys;
1204 if (! -l
$rootsshauthkeys) {
1205 warn "can't create symlink for ssh keys '$rootsshauthkeys' -> '$sshauthkeys'\n";
1207 unlink $rootsshauthkeysbackup if $import_ok;
1211 sub ssh_unmerge_known_hosts
{
1212 return if ! -l
$sshglobalknownhosts;
1215 $old = PVE
::Tools
::file_get_contents
($sshknownhosts, 128*1024)
1216 if -f
$sshknownhosts;
1218 PVE
::Tools
::file_set_contents
($sshglobalknownhosts, $old);
1221 sub ssh_merge_known_hosts
{
1222 my ($nodename, $ip_address, $createLink) = @_;
1224 die "no node name specified" if !$nodename;
1225 die "no ip address specified" if !$ip_address;
1227 # ssh lowercases hostnames (aliases) before comparision, so we need too
1228 $nodename = lc($nodename);
1229 $ip_address = lc($ip_address);
1233 if (! -f
$sshknownhosts) {
1234 if (my $fh = IO
::File-
>new($sshknownhosts, O_CREAT
|O_WRONLY
|O_EXCL
, 0600)) {
1239 my $old = PVE
::Tools
::file_get_contents
($sshknownhosts, 128*1024);
1243 if ((! -l
$sshglobalknownhosts) && (-f
$sshglobalknownhosts)) {
1244 $new = PVE
::Tools
::file_get_contents
($sshglobalknownhosts, 128*1024);
1247 my $hostkey = PVE
::Tools
::file_get_contents
($ssh_host_rsa_id);
1248 # Note: file sometimes containe emty lines at start, so we use multiline match
1249 die "can't parse $ssh_host_rsa_id" if $hostkey !~ m/^(ssh-rsa\s\S+)(\s.*)?$/m;
1258 my $merge_line = sub {
1259 my ($line, $all) = @_;
1261 return if $line =~ m/^\s*$/; # skip empty lines
1262 return if $line =~ m/^#/; # skip comments
1264 if ($line =~ m/^(\S+)\s(ssh-rsa\s\S+)(\s.*)?$/) {
1267 if (!$vhash->{$key}) {
1269 if ($key =~ m/\|1\|([^\|\s]+)\|([^\|\s]+)$/) {
1270 my $salt = decode_base64
($1);
1272 my $hmac = Digest
::HMAC_SHA1-
>new($salt);
1273 $hmac->add($nodename);
1274 my $hd = $hmac->b64digest . '=';
1275 if ($digest eq $hd) {
1276 if ($rsakey eq $hostkey) {
1277 $found_nodename = 1;
1282 $hmac = Digest
::HMAC_SHA1-
>new($salt);
1283 $hmac->add($ip_address);
1284 $hd = $hmac->b64digest . '=';
1285 if ($digest eq $hd) {
1286 if ($rsakey eq $hostkey) {
1287 $found_local_ip = 1;
1293 $key = lc($key); # avoid duplicate entries, ssh compares lowercased
1294 if ($key eq $ip_address) {
1295 $found_local_ip = 1 if $rsakey eq $hostkey;
1296 } elsif ($key eq $nodename) {
1297 $found_nodename = 1 if $rsakey eq $hostkey;
1307 while ($old && $old =~ s/^((.*?)(\n|$))//) {
1309 &$merge_line($line, 1);
1312 while ($new && $new =~ s/^((.*?)(\n|$))//) {
1314 &$merge_line($line);
1317 # add our own key if not already there
1318 $data .= "$nodename $hostkey\n" if !$found_nodename;
1319 $data .= "$ip_address $hostkey\n" if !$found_local_ip;
1321 PVE
::Tools
::file_set_contents
($sshknownhosts, $data);
1323 return if !$createLink;
1325 unlink $sshglobalknownhosts;
1326 symlink $sshknownhosts, $sshglobalknownhosts;
1328 warn "can't create symlink for ssh known hosts '$sshglobalknownhosts' -> '$sshknownhosts'\n"
1329 if ! -l
$sshglobalknownhosts;
1333 my $migration_format = {
1337 enum
=> ['secure', 'insecure'],
1338 description
=> "Migration traffic is encrypted using an SSH tunnel by " .
1339 "default. On secure, completely private networks this can be " .
1340 "disabled to increase performance.",
1341 default => 'secure',
1345 type
=> 'string', format
=> 'CIDR',
1346 format_description
=> 'CIDR',
1347 description
=> "CIDR of the (sub) network that is used for migration."
1352 shutdown_policy
=> {
1354 enum
=> ['freeze', 'failover', 'conditional'],
1355 description
=> "The policy for HA services on node shutdown. 'freeze' disables auto-recovery, 'failover' ensures recovery, 'conditional' recovers on poweroff and freezes on reboot. Running HA Services will always get stopped first on shutdown.",
1356 verbose_description
=> "Describes the policy for handling HA services on poweroff or reboot of a node. Freeze will always freeze services which are still located on the node on shutdown, those services won't be recovered by the HA manager. Failover will not mark the services as frozen and thus the services will get recovered to other nodes, if the shutdown node does not come up again quickly (< 1min). 'conditional' chooses automatically depending on the type of shutdown, i.e., on a reboot the service will be frozen but on a poweroff the service will stay as is, and thus get recovered after about 2 minutes.",
1357 default => 'conditional',
1361 PVE
::JSONSchema
::register_format
('mac-prefix', \
&pve_verify_mac_prefix
);
1362 sub pve_verify_mac_prefix
{
1363 my ($mac_prefix, $noerr) = @_;
1365 if ($mac_prefix !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){0,2}:?$/i) {
1366 return undef if $noerr;
1367 die "value is not a valid unicast MAC address prefix\n";
1372 my $datacenter_schema = {
1374 additionalProperties
=> 0,
1379 description
=> "Default keybord layout for vnc server.",
1380 enum
=> PVE
::Tools
::kvmkeymaplist
(),
1385 description
=> "Default GUI language.",
1411 description
=> "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
1412 pattern
=> "http://.*",
1414 migration_unsecure
=> {
1417 description
=> "Migration is secure using SSH tunnel by default. " .
1418 "For secure private networks you can disable it to speed up " .
1419 "migration. Deprecated, use the 'migration' property instead!",
1423 type
=> 'string', format
=> $migration_format,
1424 description
=> "For cluster wide migration settings.",
1429 description
=> "Select the default Console viewer. You can either use the builtin java applet (VNC; deprecated and maps to html5), an external virt-viewer comtatible application (SPICE), an HTML5 based vnc viewer (noVNC), or an HTML5 based console client (xtermjs). If the selected viewer is not available (e.g. SPICE not activated for the VM), the fallback is noVNC.",
1430 enum
=> ['applet', 'vv', 'html5', 'xtermjs'],
1435 format
=> 'email-opt',
1436 description
=> "Specify email address to send notification from (default is root@\$hostname)",
1442 description
=> "Defines how many workers (per node) are maximal started ".
1443 " on actions like 'stopall VMs' or task from the ha-manager.",
1448 default => 'watchdog',
1449 enum
=> [ 'watchdog', 'hardware', 'both' ],
1450 description
=> "Set the fencing mode of the HA cluster. Hardware mode " .
1451 "needs a valid configuration of fence devices in /etc/pve/ha/fence.cfg." .
1452 " With both all two modes are used." .
1453 "\n\nWARNING: 'hardware' and 'both' are EXPERIMENTAL & WIP",
1457 type
=> 'string', format
=> $ha_format,
1458 description
=> "Cluster wide HA settings.",
1463 format
=> 'mac-prefix',
1464 description
=> 'Prefix for autogenerated MAC addresses.',
1466 bwlimit
=> PVE
::JSONSchema
::get_standard_option
('bwlimit'),
1470 # make schema accessible from outside (for documentation)
1471 sub get_datacenter_schema
{ return $datacenter_schema };
1473 sub parse_datacenter_config
{
1474 my ($filename, $raw) = @_;
1476 my $res = PVE
::JSONSchema
::parse_config
($datacenter_schema, $filename, $raw // '');
1478 if (my $migration = $res->{migration
}) {
1479 $res->{migration
} = PVE
::JSONSchema
::parse_property_string
($migration_format, $migration);
1482 if (my $ha = $res->{ha
}) {
1483 $res->{ha
} = PVE
::JSONSchema
::parse_property_string
($ha_format, $ha);
1486 # for backwards compatibility only, new migration property has precedence
1487 if (defined($res->{migration_unsecure
})) {
1488 if (defined($res->{migration
}->{type
})) {
1489 warn "deprecated setting 'migration_unsecure' and new 'migration: type' " .
1490 "set at same time! Ignore 'migration_unsecure'\n";
1492 $res->{migration
}->{type
} = ($res->{migration_unsecure
}) ?
'insecure' : 'secure';
1496 # for backwards compatibility only, applet maps to html5
1497 if (defined($res->{console
}) && $res->{console
} eq 'applet') {
1498 $res->{console
} = 'html5';
1504 sub write_datacenter_config
{
1505 my ($filename, $cfg) = @_;
1507 # map deprecated setting to new one
1508 if (defined($cfg->{migration_unsecure
}) && !defined($cfg->{migration
})) {
1509 my $migration_unsecure = delete $cfg->{migration_unsecure
};
1510 $cfg->{migration
}->{type
} = ($migration_unsecure) ?
'insecure' : 'secure';
1513 # map deprecated applet setting to html5
1514 if (defined($cfg->{console
}) && $cfg->{console
} eq 'applet') {
1515 $cfg->{console
} = 'html5';
1518 if (my $migration = $cfg->{migration
}) {
1519 $cfg->{migration
} = PVE
::JSONSchema
::print_property_string
($migration, $migration_format);
1522 if (my $ha = $cfg->{ha
}) {
1523 $cfg->{ha
} = PVE
::JSONSchema
::print_property_string
($ha, $ha_format);
1526 return PVE
::JSONSchema
::dump_config
($datacenter_schema, $filename, $cfg);
1529 cfs_register_file
('datacenter.cfg',
1530 \
&parse_datacenter_config
,
1531 \
&write_datacenter_config
);
1533 # X509 Certificate cache helper
1535 my $cert_cache_nodes = {};
1536 my $cert_cache_timestamp = time();
1537 my $cert_cache_fingerprints = {};
1539 sub update_cert_cache
{
1540 my ($update_node, $clear) = @_;
1542 syslog
('info', "Clearing outdated entries from certificate cache")
1545 $cert_cache_timestamp = time() if !defined($update_node);
1547 my $node_list = defined($update_node) ?
1548 [ $update_node ] : [ keys %$cert_cache_nodes ];
1550 foreach my $node (@$node_list) {
1551 my $clear_old = sub {
1552 if (my $old_fp = $cert_cache_nodes->{$node}) {
1553 # distrust old fingerprint
1554 delete $cert_cache_fingerprints->{$old_fp};
1555 # ensure reload on next proxied request
1556 delete $cert_cache_nodes->{$node};
1560 my $fp = eval { get_node_fingerprint
($node) };
1563 &$clear_old() if $clear;
1567 my $old_fp = $cert_cache_nodes->{$node};
1568 $cert_cache_fingerprints->{$fp} = 1;
1569 $cert_cache_nodes->{$node} = $fp;
1571 if (defined($old_fp) && $fp ne $old_fp) {
1572 delete $cert_cache_fingerprints->{$old_fp};
1577 # load and cache cert fingerprint once
1578 sub initialize_cert_cache
{
1581 update_cert_cache
($node)
1582 if defined($node) && !defined($cert_cache_nodes->{$node});
1585 sub read_ssl_cert_fingerprint
{
1586 my ($cert_path) = @_;
1588 my $bio = Net
::SSLeay
::BIO_new_file
($cert_path, 'r')
1589 or die "unable to read '$cert_path' - $!\n";
1591 my $cert = Net
::SSLeay
::PEM_read_bio_X509
($bio);
1592 Net
::SSLeay
::BIO_free
($bio);
1594 die "unable to read certificate from '$cert_path'\n" if !$cert;
1596 my $fp = Net
::SSLeay
::X509_get_fingerprint
($cert, 'sha256');
1597 Net
::SSLeay
::X509_free
($cert);
1599 die "unable to get fingerprint for '$cert_path' - got empty value\n"
1600 if !defined($fp) || $fp eq '';
1605 sub get_node_fingerprint
{
1608 my $cert_path = "/etc/pve/nodes/$node/pve-ssl.pem";
1609 my $custom_cert_path = "/etc/pve/nodes/$node/pveproxy-ssl.pem";
1611 $cert_path = $custom_cert_path if -f
$custom_cert_path;
1613 return read_ssl_cert_fingerprint
($cert_path);
1617 sub check_cert_fingerprint
{
1620 # clear cache every 30 minutes at least
1621 update_cert_cache
(undef, 1) if time() - $cert_cache_timestamp >= 60*30;
1623 # get fingerprint of server certificate
1624 my $fp = Net
::SSLeay
::X509_get_fingerprint
($cert, 'sha256');
1625 return 0 if !defined($fp) || $fp eq ''; # error
1628 for my $expected (keys %$cert_cache_fingerprints) {
1629 return 1 if $fp eq $expected;
1634 return 1 if &$check();
1636 # clear cache and retry at most once every minute
1637 if (time() - $cert_cache_timestamp >= 60) {
1638 syslog
('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache");
1639 update_cert_cache
();
1646 # bash completion helpers
1648 sub complete_next_vmid
{
1650 my $vmlist = get_vmlist
() || {};
1651 my $idlist = $vmlist->{ids
} || {};
1653 for (my $i = 100; $i < 10000; $i++) {
1654 return [$i] if !defined($idlist->{$i});
1662 my $vmlist = get_vmlist
();
1663 my $ids = $vmlist->{ids
} || {};
1665 return [ keys %$ids ];
1668 sub complete_local_vmid
{
1670 my $vmlist = get_vmlist
();
1671 my $ids = $vmlist->{ids
} || {};
1673 my $nodename = PVE
::INotify
::nodename
();
1676 foreach my $vmid (keys %$ids) {
1677 my $d = $ids->{$vmid};
1678 next if !$d->{node
} || $d->{node
} ne $nodename;
1685 sub complete_migration_target
{
1689 my $nodename = PVE
::INotify
::nodename
();
1691 my $nodelist = get_nodelist
();
1692 foreach my $node (@$nodelist) {
1693 next if $node eq $nodename;
1701 my ($node, $network_cidr) = @_;
1704 if (defined($network_cidr)) {
1705 # Use mtunnel via to get the remote node's ip inside $network_cidr.
1706 # This goes over the regular network (iow. uses get_ssh_info() with
1707 # $network_cidr undefined.
1708 # FIXME: Use the REST API client for this after creating an API entry
1709 # for get_migration_ip.
1710 my $default_remote = get_ssh_info
($node, undef);
1711 my $default_ssh = ssh_info_to_command
($default_remote);
1712 my $cmd =[@$default_ssh, 'pvecm', 'mtunnel',
1713 '-migration_network', $network_cidr,
1716 PVE
::Tools
::run_command
($cmd, outfunc
=> sub {
1719 die "internal error: unexpected output from mtunnel\n"
1721 if ($line =~ /^ip: '(.*)'$/) {
1724 die "internal error: bad output from mtunnel\n"
1728 die "failed to get ip for node '$node' in network '$network_cidr'\n"
1731 $ip = remote_node_ip
($node);
1737 network
=> $network_cidr,
1741 sub ssh_info_to_command_base
{
1742 my ($info, @extra_options) = @_;
1746 '-o', 'BatchMode=yes',
1747 '-o', 'HostKeyAlias='.$info->{name
},
1752 sub ssh_info_to_command
{
1753 my ($info, @extra_options) = @_;
1754 my $cmd = ssh_info_to_command_base
($info, @extra_options);
1755 push @$cmd, "root\@$info->{ip}";
1759 sub assert_joinable
{
1760 my ($ring0_addr, $ring1_addr, $force) = @_;
1763 my $error = sub { $errors .= "* $_[0]\n"; };
1766 $error->("authentication key '$authfile' already exists");
1769 if (-f
$clusterconf) {
1770 $error->("cluster config '$clusterconf' already exists");
1773 my $vmlist = get_vmlist
();
1774 if ($vmlist && $vmlist->{ids
} && scalar(keys %{$vmlist->{ids
}})) {
1775 $error->("this host already contains virtual guests");
1778 if (run_command
(['corosync-quorumtool', '-l'], noerr
=> 1, quiet
=> 1) == 0) {
1779 $error->("corosync is already running, is this node already in a cluster?!");
1782 # check if corosync ring IPs are configured on the current nodes interfaces
1783 my $check_ip = sub {
1784 my $ip = shift // return;
1785 if (!PVE
::JSONSchema
::pve_verify_ip
($ip, 1)) {
1787 eval { $ip = PVE
::Network
::get_ip_from_hostname
($host); };
1789 $error->("cannot use '$host': $@\n") ;
1794 my $cidr = (Net
::IP
::ip_is_ipv6
($ip)) ?
"$ip/128" : "$ip/32";
1795 my $configured_ips = PVE
::Network
::get_local_ip_from_cidr
($cidr);
1797 $error->("cannot use IP '$ip', it must be configured exactly once on local node!\n")
1798 if (scalar(@$configured_ips) != 1);
1801 $check_ip->($ring0_addr);
1802 $check_ip->($ring1_addr);
1805 warn "detected the following error(s):\n$errors";
1806 die "Check if node may join a cluster failed!\n" if !$force;
1810 # NOTE: filesystem must be offline here, no DB changes allowed
1811 my $backup_cfs_database = sub {
1817 my $backup_fn = "$dbbackupdir/config-$ctime.sql.gz";
1819 print "backup old database to '$backup_fn'\n";
1821 my $cmd = [ ['sqlite3', $dbfile, '.dump'], ['gzip', '-', \
">${backup_fn}"] ];
1822 run_command
($cmd, 'errmsg' => "cannot backup old database\n");
1824 my $maxfiles = 10; # purge older backup
1825 my $backups = [ sort { $b cmp $a } <$dbbackupdir/config-*.sql
.gz
> ];
1827 if ((my $count = scalar(@$backups)) > $maxfiles) {
1828 foreach my $f (@$backups[$maxfiles..$count-1]) {
1829 next if $f !~ m/^(\S+)$/; # untaint
1830 print "delete old backup '$1'\n";
1839 my $nodename = PVE
::INotify
::nodename
();
1841 setup_sshd_config
();
1842 setup_rootsshconfig
();
1845 # check if we can join with the given parameters and current node state
1846 my ($ring0_addr, $ring1_addr) = $param->@{'ring0_addr', 'ring1_addr'};
1847 assert_joinable
($ring0_addr, $ring1_addr, $param->{force
});
1849 # make sure known_hosts is on local filesystem
1850 ssh_unmerge_known_hosts
();
1852 my $host = $param->{hostname
};
1853 my $local_ip_address = remote_node_ip
($nodename);
1856 username
=> 'root@pam',
1857 password
=> $param->{password
},
1858 cookie_name
=> 'PVEAuthCookie',
1859 protocol
=> 'https',
1864 if (my $fp = $param->{fingerprint
}) {
1865 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
1867 # API schema ensures that we can only get here from CLI handler
1868 $conn_args->{manual_verification
} = 1;
1871 print "Establishing API connection with host '$host'\n";
1873 my $conn = PVE
::APIClient
::LWP-
>new(%$conn_args);
1876 # login raises an exception on failure, so if we get here we're good
1877 print "Login succeeded.\n";
1880 $args->{force
} = $param->{force
} if defined($param->{force
});
1881 $args->{nodeid
} = $param->{nodeid
} if $param->{nodeid
};
1882 $args->{votes
} = $param->{votes
} if defined($param->{votes
});
1883 $args->{ring0_addr
} = $ring0_addr // $local_ip_address;
1884 $args->{ring1_addr
} = $ring1_addr if defined($ring1_addr);
1886 print "Request addition of this node\n";
1887 my $res = $conn->post("/cluster/config/nodes/$nodename", $args);
1889 print "Join request OK, finishing setup locally\n";
1891 # added successfuly - now prepare local node
1892 finish_join
($nodename, $res->{corosync_conf
}, $res->{corosync_authkey
});
1896 my ($nodename, $corosync_conf, $corosync_authkey) = @_;
1898 mkdir "$localclusterdir";
1899 PVE
::Tools
::file_set_contents
($authfile, $corosync_authkey);
1900 PVE
::Tools
::file_set_contents
($localclusterconf, $corosync_conf);
1902 print "stopping pve-cluster service\n";
1903 my $cmd = ['systemctl', 'stop', 'pve-cluster'];
1904 run_command
($cmd, errmsg
=> "can't stop pve-cluster service");
1906 $backup_cfs_database->($dbfile);
1909 $cmd = ['systemctl', 'start', 'corosync', 'pve-cluster'];
1910 run_command
($cmd, errmsg
=> "starting pve-cluster failed");
1914 while (!check_cfs_quorum
(1)) {
1916 print "waiting for quorum...";
1922 print "OK\n" if !$printqmsg;
1924 updatecerts_and_ssh
(1);
1926 print "generated new node certificate, restart pveproxy and pvedaemon services\n";
1927 run_command
(['systemctl', 'reload-or-restart', 'pvedaemon', 'pveproxy']);
1929 print "successfully added node '$nodename' to cluster.\n";
1932 sub updatecerts_and_ssh
{
1933 my ($force_new_cert, $silent) = @_;
1935 my $p = sub { print "$_[0]\n" if !$silent };
1937 setup_rootsshconfig
();
1939 gen_pve_vzdump_symlink
();
1941 if (!check_cfs_quorum
(1)) {
1942 return undef if $silent;
1943 die "no quorum - unable to update files\n";
1948 my $nodename = PVE
::INotify
::nodename
();
1949 my $local_ip_address = remote_node_ip
($nodename);
1951 $p->("(re)generate node files");
1952 $p->("generate new node certificate") if $force_new_cert;
1953 gen_pve_node_files
($nodename, $local_ip_address, $force_new_cert);
1955 $p->("merge authorized SSH keys and known hosts");
1957 ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
1958 gen_pve_vzdump_files
();