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,
87 'ha/crm_commands' => 1,
88 'ha/manager_status' => 1,
89 'ha/resources.cfg' => 1,
96 # only write output if something fails
101 my $record = sub { $outbuf .= shift . "\n"; };
103 eval { run_command
($cmd, outfunc
=> $record, errfunc
=> $record) };
106 print STDERR
$outbuf;
111 sub check_cfs_quorum
{
114 # note: -w filename always return 1 for root, so wee need
115 # to use File::lstat here
116 my $st = File
::stat::lstat("$basedir/local");
117 my $quorate = ($st && (($st->mode & 0200) != 0));
119 die "cluster not ready - no quorum?\n" if !$quorate && !$noerr;
124 sub check_cfs_is_mounted
{
127 my $res = -l
"$basedir/local";
129 die "pve configuration filesystem not mounted\n"
138 check_cfs_is_mounted
();
140 my @required_dirs = (
143 "$basedir/nodes/$nodename",
144 "$basedir/nodes/$nodename/lxc",
145 "$basedir/nodes/$nodename/qemu-server",
146 "$basedir/nodes/$nodename/openvz",
147 "$basedir/nodes/$nodename/priv");
149 foreach my $dir (@required_dirs) {
151 mkdir($dir) || $! == EEXIST
|| die "unable to create directory '$dir' - $!\n";
158 return if -f
"$authprivkeyfn";
160 check_cfs_is_mounted
();
162 cfs_lock_authkey
(undef, sub {
163 mkdir $authdir || $! == EEXIST
|| die "unable to create dir '$authdir' - $!\n";
165 run_silent_cmd
(['openssl', 'genrsa', '-out', $authprivkeyfn, '2048']);
167 run_silent_cmd
(['openssl', 'rsa', '-in', $authprivkeyfn, '-pubout', '-out', $authpubkeyfn]);
175 return if -f
$pveca_key_fn;
178 run_silent_cmd
(['openssl', 'genrsa', '-out', $pveca_key_fn, '4096']);
181 die "unable to generate pve ca key:\n$@" if $@;
186 if (-f
$pveca_key_fn && -f
$pveca_cert_fn) {
192 # we try to generate an unique 'subject' to avoid browser problems
193 # (reused serial numbers, ..)
195 UUID
::generate
($uuid);
197 UUID
::unparse
($uuid, $uuid_str);
200 # wrap openssl with faketime to prevent bug #904
201 run_silent_cmd
(['faketime', 'yesterday', 'openssl', 'req', '-batch',
202 '-days', '3650', '-new', '-x509', '-nodes', '-key',
203 $pveca_key_fn, '-out', $pveca_cert_fn, '-subj',
204 "/CN=Proxmox Virtual Environment/OU=$uuid_str/O=PVE Cluster Manager CA/"]);
207 die "generating pve root certificate failed:\n$@" if $@;
212 sub gen_pve_ssl_key
{
215 die "no node name specified" if !$nodename;
217 my $pvessl_key_fn = "$basedir/nodes/$nodename/pve-ssl.key";
219 return if -f
$pvessl_key_fn;
222 run_silent_cmd
(['openssl', 'genrsa', '-out', $pvessl_key_fn, '2048']);
225 die "unable to generate pve ssl key for node '$nodename':\n$@" if $@;
228 sub gen_pve_www_key
{
230 return if -f
$pvewww_key_fn;
233 run_silent_cmd
(['openssl', 'genrsa', '-out', $pvewww_key_fn, '2048']);
236 die "unable to generate pve www key:\n$@" if $@;
242 PVE
::Tools
::file_set_contents
($pveca_srl_fn, $serial);
245 sub gen_pve_ssl_cert
{
246 my ($force, $nodename, $ip) = @_;
248 die "no node name specified" if !$nodename;
249 die "no IP specified" if !$ip;
251 my $pvessl_cert_fn = "$basedir/nodes/$nodename/pve-ssl.pem";
253 return if !$force && -f
$pvessl_cert_fn;
255 my $names = "IP:127.0.0.1,IP:::1,DNS:localhost";
257 my $rc = PVE
::INotify
::read_file
('resolvconf');
261 my $fqdn = $nodename;
263 $names .= ",DNS:$nodename";
265 if ($rc && $rc->{search
}) {
266 $fqdn = $nodename . "." . $rc->{search
};
267 $names .= ",DNS:$fqdn";
270 my $sslconf = <<__EOD;
271 RANDFILE = /root/.rnd
276 distinguished_name = req_distinguished_name
277 req_extensions = v3_req
279 string_mask = nombstr
281 [ req_distinguished_name ]
282 organizationalUnitName = PVE Cluster Node
283 organizationName = Proxmox Virtual Environment
287 basicConstraints = CA:FALSE
288 extendedKeyUsage = serverAuth
289 subjectAltName = $names
292 my $cfgfn = "/tmp/pvesslconf-$$.tmp";
293 my $fh = IO
::File-
>new ($cfgfn, "w");
297 my $reqfn = "/tmp/pvecertreq-$$.tmp";
300 my $pvessl_key_fn = "$basedir/nodes/$nodename/pve-ssl.key";
302 run_silent_cmd
(['openssl', 'req', '-batch', '-new', '-config', $cfgfn,
303 '-key', $pvessl_key_fn, '-out', $reqfn]);
309 die "unable to generate pve certificate request:\n$err";
312 update_serial
("0000000000000000") if ! -f
$pveca_srl_fn;
315 # wrap openssl with faketime to prevent bug #904
316 run_silent_cmd
(['faketime', 'yesterday', 'openssl', 'x509', '-req',
317 '-in', $reqfn, '-days', '3650', '-out', $pvessl_cert_fn,
318 '-CAkey', $pveca_key_fn, '-CA', $pveca_cert_fn,
319 '-CAserial', $pveca_srl_fn, '-extfile', $cfgfn]);
325 die "unable to generate pve ssl certificate:\n$err";
332 sub gen_pve_node_files
{
333 my ($nodename, $ip, $opt_force) = @_;
335 gen_local_dirs
($nodename);
339 # make sure we have a (cluster wide) secret
340 # for CSRFR prevention
343 # make sure we have a (per node) private key
344 gen_pve_ssl_key
($nodename);
346 # make sure we have a CA
347 my $force = gen_pveca_cert
();
349 $force = 1 if $opt_force;
351 gen_pve_ssl_cert
($force, $nodename, $ip);
354 my $vzdump_cron_dummy = <<__EOD;
355 # cluster wide vzdump cron schedule
356 # Atomatically generated file - do not edit
358 PATH="/usr/sbin:/usr/bin:/sbin:/bin"
362 sub gen_pve_vzdump_symlink
{
364 my $filename = "/etc/pve/vzdump.cron";
366 my $link_fn = "/etc/cron.d/vzdump";
368 if ((-f
$filename) && (! -l
$link_fn)) {
369 rename($link_fn, "/root/etc_cron_vzdump.org"); # make backup if file exists
370 symlink($filename, $link_fn);
374 sub gen_pve_vzdump_files
{
376 my $filename = "/etc/pve/vzdump.cron";
378 PVE
::Tools
::file_set_contents
($filename, $vzdump_cron_dummy)
381 gen_pve_vzdump_symlink
();
388 my $ipcc_send_rec = sub {
389 my ($msgid, $data) = @_;
391 my $res = PVE
::IPCC
::ipcc_send_rec
($msgid, $data);
393 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
398 my $ipcc_send_rec_json = sub {
399 my ($msgid, $data) = @_;
401 my $res = PVE
::IPCC
::ipcc_send_rec
($msgid, $data);
403 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
405 return decode_json
($res);
408 my $ipcc_get_config = sub {
411 my $bindata = pack "Z*", $path;
412 my $res = PVE
::IPCC
::ipcc_send_rec
(CFS_IPC_GET_CONFIG
, $bindata);
413 if (!defined($res)) {
415 return undef if $! == ENOENT
;
424 my $ipcc_get_status = sub {
425 my ($name, $nodename) = @_;
427 my $bindata = pack "Z[256]Z[256]", $name, ($nodename || "");
428 return PVE
::IPCC
::ipcc_send_rec
(CFS_IPC_GET_STATUS
, $bindata);
431 my $ipcc_update_status = sub {
432 my ($name, $data) = @_;
434 my $raw = ref($data) ? encode_json
($data) : $data;
436 my $bindata = pack "Z[256]Z*", $name, $raw;
438 return &$ipcc_send_rec(CFS_IPC_SET_STATUS
, $bindata);
442 my ($priority, $ident, $tag, $msg) = @_;
444 my $bindata = pack "CCCZ*Z*Z*", $priority, bytes
::length($ident) + 1,
445 bytes
::length($tag) + 1, $ident, $tag, $msg;
447 return &$ipcc_send_rec(CFS_IPC_LOG_CLUSTER_MSG
, $bindata);
450 my $ipcc_get_cluster_log = sub {
451 my ($user, $max) = @_;
453 $max = 0 if !defined($max);
455 my $bindata = pack "VVVVZ*", $max, 0, 0, 0, ($user || "");
456 return &$ipcc_send_rec(CFS_IPC_GET_CLUSTER_LOG
, $bindata);
464 my $res = &$ipcc_send_rec_json(CFS_IPC_GET_FS_VERSION
);
465 #warn "GOT1: " . Dumper($res);
466 die "no starttime\n" if !$res->{starttime
};
468 if (!$res->{starttime
} || !$versions->{starttime
} ||
469 $res->{starttime
} != $versions->{starttime
}) {
470 #print "detected changed starttime\n";
489 if (!$clinfo->{version
} || $clinfo->{version
} != $versions->{clinfo
}) {
490 #warn "detected new clinfo\n";
491 $clinfo = &$ipcc_send_rec_json(CFS_IPC_GET_CLUSTER_INFO
);
502 if (!$vmlist->{version
} || $vmlist->{version
} != $versions->{vmlist
}) {
503 #warn "detected new vmlist1\n";
504 $vmlist = &$ipcc_send_rec_json(CFS_IPC_GET_GUEST_LIST
);
524 return $clinfo->{nodelist
};
528 my $nodelist = $clinfo->{nodelist
};
530 my $nodename = PVE
::INotify
::nodename
();
532 if (!$nodelist || !$nodelist->{$nodename}) {
533 return [ $nodename ];
536 return [ keys %$nodelist ];
539 # $data must be a chronological descending ordered array of tasks
540 sub broadcast_tasklist
{
543 # the serialized list may not get bigger than 32kb (CFS_MAX_STATUS_SIZE
544 # from pmxcfs) - drop older items until we satisfy this constraint
545 my $size = length(encode_json
($data));
546 while ($size >= (32 * 1024)) {
548 $size = length(encode_json
($data));
552 &$ipcc_update_status("tasklist", $data);
558 my $tasklistcache = {};
563 my $kvstore = $versions->{kvstore
} || {};
565 my $nodelist = get_nodelist
();
568 foreach my $node (@$nodelist) {
569 next if $nodename && ($nodename ne $node);
571 my $ver = $kvstore->{$node}->{tasklist
} if $kvstore->{$node};
572 my $cd = $tasklistcache->{$node};
573 if (!$cd || !$ver || !$cd->{version
} ||
574 ($cd->{version
} != $ver)) {
575 my $raw = &$ipcc_get_status("tasklist", $node) || '[]';
576 my $data = decode_json
($raw);
578 $cd = $tasklistcache->{$node} = {
582 } elsif ($cd && $cd->{data
}) {
583 push @$res, @{$cd->{data
}};
587 syslog
('err', $err) if $err;
594 my ($rrdid, $data) = @_;
597 &$ipcc_update_status("rrd/$rrdid", $data);
604 my $last_rrd_dump = 0;
605 my $last_rrd_data = "";
611 my $diff = $ctime - $last_rrd_dump;
613 return $last_rrd_data;
618 $raw = &$ipcc_send_rec(CFS_IPC_GET_RRD_DUMP
);
630 while ($raw =~ s/^(.*)\n//) {
631 my ($key, @ela) = split(/:/, $1);
633 next if !(scalar(@ela) > 1);
634 $res->{$key} = [ map { $_ eq 'U' ?
undef : $_ } @ela ];
638 $last_rrd_dump = $ctime;
639 $last_rrd_data = $res;
644 sub create_rrd_data
{
645 my ($rrdname, $timeframe, $cf) = @_;
647 my $rrddir = "/var/lib/rrdcached/db";
649 my $rrd = "$rrddir/$rrdname";
653 day
=> [ 60*30, 70 ],
654 week
=> [ 60*180, 70 ],
655 month
=> [ 60*720, 70 ],
656 year
=> [ 60*10080, 70 ],
659 my ($reso, $count) = @{$setup->{$timeframe}};
660 my $ctime = $reso*int(time()/$reso);
661 my $req_start = $ctime - $reso*$count;
663 $cf = "AVERAGE" if !$cf;
671 my $socket = "/var/run/rrdcached.sock";
672 push @args, "--daemon" => "unix:$socket" if -S
$socket;
674 my ($start, $step, $names, $data) = RRDs
::fetch
($rrd, $cf, @args);
676 my $err = RRDs
::error
;
677 die "RRD error: $err\n" if $err;
679 die "got wrong time resolution ($step != $reso)\n"
683 my $fields = scalar(@$names);
684 for my $line (@$data) {
685 my $entry = { 'time' => $start };
687 for (my $i = 0; $i < $fields; $i++) {
688 my $name = $names->[$i];
689 if (defined(my $val = $line->[$i])) {
690 $entry->{$name} = $val;
692 # leave empty fields undefined
693 # maybe make this configurable?
702 sub create_rrd_graph
{
703 my ($rrdname, $timeframe, $ds, $cf) = @_;
705 # Using RRD graph is clumsy - maybe it
706 # is better to simply fetch the data, and do all display
707 # related things with javascript (new extjs html5 graph library).
709 my $rrddir = "/var/lib/rrdcached/db";
711 my $rrd = "$rrddir/$rrdname";
713 my @ids = PVE
::Tools
::split_list
($ds);
715 my $ds_txt = join('_', @ids);
717 my $filename = "${rrd}_${ds_txt}.png";
721 day
=> [ 60*30, 70 ],
722 week
=> [ 60*180, 70 ],
723 month
=> [ 60*720, 70 ],
724 year
=> [ 60*10080, 70 ],
727 my ($reso, $count) = @{$setup->{$timeframe}};
730 "--imgformat" => "PNG",
734 "--start" => - $reso*$count,
736 "--lower-limit" => 0,
739 my $socket = "/var/run/rrdcached.sock";
740 push @args, "--daemon" => "unix:$socket" if -S
$socket;
742 my @coldef = ('#00ddff', '#ff0000');
744 $cf = "AVERAGE" if !$cf;
747 foreach my $id (@ids) {
748 my $col = $coldef[$i++] || die "fixme: no color definition";
749 push @args, "DEF:${id}=$rrd:${id}:$cf";
751 if ($id eq 'cpu' || $id eq 'iowait') {
752 push @args, "CDEF:${id}_per=${id},100,*";
753 $dataid = "${id}_per";
755 push @args, "LINE2:${dataid}${col}:${id}";
758 push @args, '--full-size-mode';
760 # we do not really store data into the file
761 my $res = RRDs
::graphv
('-', @args);
763 my $err = RRDs
::error
;
764 die "RRD error: $err\n" if $err;
766 return { filename
=> $filename, image
=> $res->{image
} };
769 # a fast way to read files (avoid fuse overhead)
773 return &$ipcc_get_config($path);
776 sub get_cluster_log
{
777 my ($user, $max) = @_;
779 return &$ipcc_get_cluster_log($user, $max);
784 sub cfs_register_file
{
785 my ($filename, $parser, $writer) = @_;
787 $observed->{$filename} || die "unknown file '$filename'";
789 die "file '$filename' already registered" if $file_info->{$filename};
791 $file_info->{$filename} = {
797 my $ccache_read = sub {
798 my ($filename, $parser, $version) = @_;
800 $ccache->{$filename} = {} if !$ccache->{$filename};
802 my $ci = $ccache->{$filename};
804 if (!$ci->{version
} || !$version || $ci->{version
} != $version) {
805 # we always call the parser, even when the file does not exists
806 # (in that case $data is undef)
807 my $data = get_config
($filename);
808 $ci->{data
} = &$parser("/etc/pve/$filename", $data);
809 $ci->{version
} = $version;
812 my $res = ref($ci->{data
}) ? dclone
($ci->{data
}) : $ci->{data
};
817 sub cfs_file_version
{
822 if ($filename =~ m!^nodes/[^/]+/(openvz|lxc|qemu-server)/(\d+)\.conf$!) {
823 my ($type, $vmid) = ($1, $2);
824 if ($vmlist && $vmlist->{ids
} && $vmlist->{ids
}->{$vmid}) {
825 $version = $vmlist->{ids
}->{$vmid}->{version
};
827 $infotag = "/$type/";
829 $infotag = $filename;
830 $version = $versions->{$filename};
833 my $info = $file_info->{$infotag} ||
834 die "unknown file type '$filename'\n";
836 return wantarray ?
($version, $info) : $version;
842 my ($version, $info) = cfs_file_version
($filename);
843 my $parser = $info->{parser
};
845 return &$ccache_read($filename, $parser, $version);
849 my ($filename, $data) = @_;
851 my ($version, $info) = cfs_file_version
($filename);
853 my $writer = $info->{writer
} || die "no writer defined";
855 my $fsname = "/etc/pve/$filename";
857 my $raw = &$writer($fsname, $data);
859 if (my $ci = $ccache->{$filename}) {
860 $ci->{version
} = undef;
863 PVE
::Tools
::file_set_contents
($fsname, $raw);
867 my ($lockid, $timeout, $code, @param) = @_;
869 my $prev_alarm = alarm(0); # suspend outer alarm early
874 # this timeout is for acquire the lock
875 $timeout = 10 if !$timeout;
877 my $filename = "$lockdir/$lockid";
884 die "pve cluster filesystem not online.\n";
887 my $timeout_err = sub { die "got lock request timeout\n"; };
888 local $SIG{ALRM
} = $timeout_err;
892 $got_lock = mkdir($filename);
893 $timeout = alarm(0) - 1; # we'll sleep for 1s, see down below
897 $timeout_err->() if $timeout <= 0;
899 print STDERR
"trying to acquire cfs lock '$lockid' ...\n";
900 utime (0, 0, $filename); # cfs unlock request
904 # fixed command timeout: cfs locks have a timeout of 120
905 # using 60 gives us another 60 seconds to abort the task
906 local $SIG{ALRM
} = sub { die "got lock timeout - aborting command\n"; };
909 cfs_update
(); # make sure we read latest versions inside code()
911 $res = &$code(@param);
918 $err = "no quorum!\n" if !$got_lock && !check_cfs_quorum
(1);
920 rmdir $filename if $got_lock; # if we held the lock always unlock again
925 $@ = "error with cfs lock '$lockid': $err";
935 my ($filename, $timeout, $code, @param) = @_;
937 my $info = $observed->{$filename} || die "unknown file '$filename'";
939 my $lockid = "file-$filename";
940 $lockid =~ s/[.\/]/_
/g
;
942 &$cfs_lock($lockid, $timeout, $code, @param);
945 sub cfs_lock_storage
{
946 my ($storeid, $timeout, $code, @param) = @_;
948 my $lockid = "storage-$storeid";
950 &$cfs_lock($lockid, $timeout, $code, @param);
953 sub cfs_lock_domain
{
954 my ($domainname, $timeout, $code, @param) = @_;
956 my $lockid = "domain-$domainname";
958 &$cfs_lock($lockid, $timeout, $code, @param);
962 my ($account, $timeout, $code, @param) = @_;
964 my $lockid = "acme-$account";
966 &$cfs_lock($lockid, $timeout, $code, @param);
969 sub cfs_lock_authkey
{
970 my ($timeout, $code, @param) = @_;
972 $cfs_lock->('authkey', $timeout, $code, @param);
990 my ($priority, $ident, $msg) = @_;
992 if (my $tmp = $log_levels->{$priority}) {
996 die "need numeric log priority" if $priority !~ /^\d+$/;
998 my $tag = PVE
::SafeSyslog
::tag
();
1000 $msg = "empty message" if !$msg;
1002 $ident = "" if !$ident;
1003 $ident = encode
("ascii", $ident,
1004 sub { sprintf "\\u%04x", shift });
1006 my $ascii = encode
("ascii", $msg, sub { sprintf "\\u%04x", shift });
1009 syslog
($priority, "<%s> %s", $ident, $ascii);
1011 syslog
($priority, "%s", $ascii);
1014 eval { &$ipcc_log($priority, $ident, $tag, $ascii); };
1016 syslog
("err", "writing cluster log failed: $@") if $@;
1019 sub check_vmid_unused
{
1020 my ($vmid, $noerr) = @_;
1022 my $vmlist = get_vmlist
();
1024 my $d = $vmlist->{ids
}->{$vmid};
1025 return 1 if !defined($d);
1027 return undef if $noerr;
1029 my $vmtypestr = $d->{type
} eq 'qemu' ?
'VM' : 'CT';
1030 die "$vmtypestr $vmid already exists on node '$d->{node}'\n";
1033 sub check_node_exists
{
1034 my ($nodename, $noerr) = @_;
1036 my $nodelist = $clinfo->{nodelist
};
1037 return 1 if $nodelist && $nodelist->{$nodename};
1039 return undef if $noerr;
1041 die "no such cluster node '$nodename'\n";
1044 # this is also used to get the IP of the local node
1045 sub remote_node_ip
{
1046 my ($nodename, $noerr) = @_;
1048 my $nodelist = $clinfo->{nodelist
};
1049 if ($nodelist && $nodelist->{$nodename}) {
1050 if (my $ip = $nodelist->{$nodename}->{ip
}) {
1051 return $ip if !wantarray;
1052 my $family = $nodelist->{$nodename}->{address_family
};
1054 $nodelist->{$nodename}->{address_family
} =
1056 PVE
::Tools
::get_host_address_family
($ip);
1058 return wantarray ?
($ip, $family) : $ip;
1062 # fallback: try to get IP by other means
1063 return PVE
::Network
::get_ip_from_hostname
($nodename, $noerr);
1066 sub get_local_migration_ip
{
1067 my ($migration_network, $noerr) = @_;
1069 my $cidr = $migration_network;
1071 if (!defined($cidr)) {
1072 my $dc_conf = cfs_read_file
('datacenter.cfg');
1073 $cidr = $dc_conf->{migration
}->{network
}
1074 if defined($dc_conf->{migration
}->{network
});
1077 if (defined($cidr)) {
1078 my $ips = PVE
::Network
::get_local_ip_from_cidr
($cidr);
1080 die "could not get migration ip: no IP address configured on local " .
1081 "node for network '$cidr'\n" if !$noerr && (scalar(@$ips) == 0);
1083 die "could not get migration ip: multiple IP address configured for " .
1084 "network '$cidr'\n" if !$noerr && (scalar(@$ips) > 1);
1092 # ssh related utility functions
1094 sub ssh_merge_keys
{
1095 # remove duplicate keys in $sshauthkeys
1096 # ssh-copy-id simply add keys, so the file can grow to large
1099 if (-f
$sshauthkeys) {
1100 $data = PVE
::Tools
::file_get_contents
($sshauthkeys, 128*1024);
1105 if (-f
$rootsshauthkeysbackup) {
1107 $data .= PVE
::Tools
::file_get_contents
($rootsshauthkeysbackup, 128*1024);
1112 # always add ourself
1113 if (-f
$ssh_rsa_id) {
1114 my $pub = PVE
::Tools
::file_get_contents
($ssh_rsa_id);
1116 $data .= "\n$pub\n";
1121 my @lines = split(/\n/, $data);
1122 foreach my $line (@lines) {
1123 if ($line !~ /^#/ && $line =~ m/(^|\s)ssh-(rsa|dsa)\s+(\S+)\s+\S+$/) {
1124 next if $vhash->{$3}++;
1126 $newdata .= "$line\n";
1129 PVE
::Tools
::file_set_contents
($sshauthkeys, $newdata, 0600);
1131 if ($found_backup && -l
$rootsshauthkeys) {
1132 # everything went well, so we can remove the backup
1133 unlink $rootsshauthkeysbackup;
1137 sub setup_sshd_config
{
1140 my $conf = PVE
::Tools
::file_get_contents
($sshd_config_fn);
1142 return if $conf =~ m/^PermitRootLogin\s+yes\s*$/m;
1144 if ($conf !~ s/^#?PermitRootLogin.*$/PermitRootLogin yes/m) {
1146 $conf .= "\nPermitRootLogin yes\n";
1149 PVE
::Tools
::file_set_contents
($sshd_config_fn, $conf);
1151 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'sshd']);
1154 sub setup_rootsshconfig
{
1156 # create ssh key if it does not exist
1157 if (! -f
$ssh_rsa_id) {
1158 mkdir '/root/.ssh/';
1159 system ("echo|ssh-keygen -t rsa -N '' -b 2048 -f ${ssh_rsa_id_priv}");
1162 # create ssh config if it does not exist
1163 if (! -f
$rootsshconfig) {
1165 if (my $fh = IO
::File-
>new($rootsshconfig, O_CREAT
|O_WRONLY
|O_EXCL
, 0640)) {
1166 # this is the default ciphers list from Debian's OpenSSH package (OpenSSH_7.4p1 Debian-10, OpenSSL 1.0.2k 26 Jan 2017)
1167 # changed order to put AES before Chacha20 (most hardware has AESNI)
1168 print $fh "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm\@openssh.com,aes256-gcm\@openssh.com,chacha20-poly1305\@openssh.com\n";
1174 sub setup_ssh_keys
{
1180 if (! -f
$sshauthkeys) {
1182 if (-f
$rootsshauthkeys) {
1183 $old = PVE
::Tools
::file_get_contents
($rootsshauthkeys, 128*1024);
1185 if (my $fh = IO
::File-
>new ($sshauthkeys, O_CREAT
|O_WRONLY
|O_EXCL
, 0400)) {
1186 PVE
::Tools
::safe_print
($sshauthkeys, $fh, $old) if $old;
1192 warn "can't create shared ssh key database '$sshauthkeys'\n"
1193 if ! -f
$sshauthkeys;
1195 if (-f
$rootsshauthkeys && ! -l
$rootsshauthkeys) {
1196 if (!rename($rootsshauthkeys , $rootsshauthkeysbackup)) {
1197 warn "rename $rootsshauthkeys failed - $!\n";
1201 if (! -l
$rootsshauthkeys) {
1202 symlink $sshauthkeys, $rootsshauthkeys;
1205 if (! -l
$rootsshauthkeys) {
1206 warn "can't create symlink for ssh keys '$rootsshauthkeys' -> '$sshauthkeys'\n";
1208 unlink $rootsshauthkeysbackup if $import_ok;
1212 sub ssh_unmerge_known_hosts
{
1213 return if ! -l
$sshglobalknownhosts;
1216 $old = PVE
::Tools
::file_get_contents
($sshknownhosts, 128*1024)
1217 if -f
$sshknownhosts;
1219 PVE
::Tools
::file_set_contents
($sshglobalknownhosts, $old);
1222 sub ssh_merge_known_hosts
{
1223 my ($nodename, $ip_address, $createLink) = @_;
1225 die "no node name specified" if !$nodename;
1226 die "no ip address specified" if !$ip_address;
1228 # ssh lowercases hostnames (aliases) before comparision, so we need too
1229 $nodename = lc($nodename);
1230 $ip_address = lc($ip_address);
1234 if (! -f
$sshknownhosts) {
1235 if (my $fh = IO
::File-
>new($sshknownhosts, O_CREAT
|O_WRONLY
|O_EXCL
, 0600)) {
1240 my $old = PVE
::Tools
::file_get_contents
($sshknownhosts, 128*1024);
1244 if ((! -l
$sshglobalknownhosts) && (-f
$sshglobalknownhosts)) {
1245 $new = PVE
::Tools
::file_get_contents
($sshglobalknownhosts, 128*1024);
1248 my $hostkey = PVE
::Tools
::file_get_contents
($ssh_host_rsa_id);
1249 # Note: file sometimes containe emty lines at start, so we use multiline match
1250 die "can't parse $ssh_host_rsa_id" if $hostkey !~ m/^(ssh-rsa\s\S+)(\s.*)?$/m;
1259 my $merge_line = sub {
1260 my ($line, $all) = @_;
1262 return if $line =~ m/^\s*$/; # skip empty lines
1263 return if $line =~ m/^#/; # skip comments
1265 if ($line =~ m/^(\S+)\s(ssh-rsa\s\S+)(\s.*)?$/) {
1268 if (!$vhash->{$key}) {
1270 if ($key =~ m/\|1\|([^\|\s]+)\|([^\|\s]+)$/) {
1271 my $salt = decode_base64
($1);
1273 my $hmac = Digest
::HMAC_SHA1-
>new($salt);
1274 $hmac->add($nodename);
1275 my $hd = $hmac->b64digest . '=';
1276 if ($digest eq $hd) {
1277 if ($rsakey eq $hostkey) {
1278 $found_nodename = 1;
1283 $hmac = Digest
::HMAC_SHA1-
>new($salt);
1284 $hmac->add($ip_address);
1285 $hd = $hmac->b64digest . '=';
1286 if ($digest eq $hd) {
1287 if ($rsakey eq $hostkey) {
1288 $found_local_ip = 1;
1294 $key = lc($key); # avoid duplicate entries, ssh compares lowercased
1295 if ($key eq $ip_address) {
1296 $found_local_ip = 1 if $rsakey eq $hostkey;
1297 } elsif ($key eq $nodename) {
1298 $found_nodename = 1 if $rsakey eq $hostkey;
1308 while ($old && $old =~ s/^((.*?)(\n|$))//) {
1310 &$merge_line($line, 1);
1313 while ($new && $new =~ s/^((.*?)(\n|$))//) {
1315 &$merge_line($line);
1318 # add our own key if not already there
1319 $data .= "$nodename $hostkey\n" if !$found_nodename;
1320 $data .= "$ip_address $hostkey\n" if !$found_local_ip;
1322 PVE
::Tools
::file_set_contents
($sshknownhosts, $data);
1324 return if !$createLink;
1326 unlink $sshglobalknownhosts;
1327 symlink $sshknownhosts, $sshglobalknownhosts;
1329 warn "can't create symlink for ssh known hosts '$sshglobalknownhosts' -> '$sshknownhosts'\n"
1330 if ! -l
$sshglobalknownhosts;
1334 my $migration_format = {
1338 enum
=> ['secure', 'insecure'],
1339 description
=> "Migration traffic is encrypted using an SSH tunnel by " .
1340 "default. On secure, completely private networks this can be " .
1341 "disabled to increase performance.",
1342 default => 'secure',
1346 type
=> 'string', format
=> 'CIDR',
1347 format_description
=> 'CIDR',
1348 description
=> "CIDR of the (sub) network that is used for migration."
1353 shutdown_policy
=> {
1355 enum
=> ['freeze', 'failover', 'conditional'],
1356 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.",
1357 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.",
1358 default => 'conditional',
1362 PVE
::JSONSchema
::register_format
('mac-prefix', \
&pve_verify_mac_prefix
);
1363 sub pve_verify_mac_prefix
{
1364 my ($mac_prefix, $noerr) = @_;
1366 if ($mac_prefix !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){0,2}:?$/i) {
1367 return undef if $noerr;
1368 die "value is not a valid unicast MAC address prefix\n";
1376 description
=> "U2F AppId URL override. Defaults to the origin.",
1377 format_description
=> 'APPID',
1382 description
=> "U2F Origin override. Mostly useful for single nodes with a single URL.",
1383 format_description
=> 'URL',
1388 my $datacenter_schema = {
1390 additionalProperties
=> 0,
1395 description
=> "Default keybord layout for vnc server.",
1396 enum
=> PVE
::Tools
::kvmkeymaplist
(),
1401 description
=> "Default GUI language.",
1427 description
=> "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
1428 pattern
=> "http://.*",
1430 migration_unsecure
=> {
1433 description
=> "Migration is secure using SSH tunnel by default. " .
1434 "For secure private networks you can disable it to speed up " .
1435 "migration. Deprecated, use the 'migration' property instead!",
1439 type
=> 'string', format
=> $migration_format,
1440 description
=> "For cluster wide migration settings.",
1445 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.",
1446 enum
=> ['applet', 'vv', 'html5', 'xtermjs'],
1451 format
=> 'email-opt',
1452 description
=> "Specify email address to send notification from (default is root@\$hostname)",
1458 description
=> "Defines how many workers (per node) are maximal started ".
1459 " on actions like 'stopall VMs' or task from the ha-manager.",
1464 default => 'watchdog',
1465 enum
=> [ 'watchdog', 'hardware', 'both' ],
1466 description
=> "Set the fencing mode of the HA cluster. Hardware mode " .
1467 "needs a valid configuration of fence devices in /etc/pve/ha/fence.cfg." .
1468 " With both all two modes are used." .
1469 "\n\nWARNING: 'hardware' and 'both' are EXPERIMENTAL & WIP",
1473 type
=> 'string', format
=> $ha_format,
1474 description
=> "Cluster wide HA settings.",
1479 format
=> 'mac-prefix',
1480 description
=> 'Prefix for autogenerated MAC addresses.',
1482 bwlimit
=> PVE
::JSONSchema
::get_standard_option
('bwlimit'),
1486 format
=> $u2f_format,
1487 description
=> 'u2f',
1492 # make schema accessible from outside (for documentation)
1493 sub get_datacenter_schema
{ return $datacenter_schema };
1495 sub parse_datacenter_config
{
1496 my ($filename, $raw) = @_;
1498 my $res = PVE
::JSONSchema
::parse_config
($datacenter_schema, $filename, $raw // '');
1500 if (my $migration = $res->{migration
}) {
1501 $res->{migration
} = PVE
::JSONSchema
::parse_property_string
($migration_format, $migration);
1504 if (my $ha = $res->{ha
}) {
1505 $res->{ha
} = PVE
::JSONSchema
::parse_property_string
($ha_format, $ha);
1508 # for backwards compatibility only, new migration property has precedence
1509 if (defined($res->{migration_unsecure
})) {
1510 if (defined($res->{migration
}->{type
})) {
1511 warn "deprecated setting 'migration_unsecure' and new 'migration: type' " .
1512 "set at same time! Ignore 'migration_unsecure'\n";
1514 $res->{migration
}->{type
} = ($res->{migration_unsecure
}) ?
'insecure' : 'secure';
1518 # for backwards compatibility only, applet maps to html5
1519 if (defined($res->{console
}) && $res->{console
} eq 'applet') {
1520 $res->{console
} = 'html5';
1526 sub write_datacenter_config
{
1527 my ($filename, $cfg) = @_;
1529 # map deprecated setting to new one
1530 if (defined($cfg->{migration_unsecure
}) && !defined($cfg->{migration
})) {
1531 my $migration_unsecure = delete $cfg->{migration_unsecure
};
1532 $cfg->{migration
}->{type
} = ($migration_unsecure) ?
'insecure' : 'secure';
1535 # map deprecated applet setting to html5
1536 if (defined($cfg->{console
}) && $cfg->{console
} eq 'applet') {
1537 $cfg->{console
} = 'html5';
1540 if (ref($cfg->{migration
})) {
1541 my $migration = $cfg->{migration
};
1542 $cfg->{migration
} = PVE
::JSONSchema
::print_property_string
($migration, $migration_format);
1545 if (ref($cfg->{ha
})) {
1546 my $ha = $cfg->{ha
};
1547 $cfg->{ha
} = PVE
::JSONSchema
::print_property_string
($ha, $ha_format);
1550 return PVE
::JSONSchema
::dump_config
($datacenter_schema, $filename, $cfg);
1553 cfs_register_file
('datacenter.cfg',
1554 \
&parse_datacenter_config
,
1555 \
&write_datacenter_config
);
1557 # X509 Certificate cache helper
1559 my $cert_cache_nodes = {};
1560 my $cert_cache_timestamp = time();
1561 my $cert_cache_fingerprints = {};
1563 sub update_cert_cache
{
1564 my ($update_node, $clear) = @_;
1566 syslog
('info', "Clearing outdated entries from certificate cache")
1569 $cert_cache_timestamp = time() if !defined($update_node);
1571 my $node_list = defined($update_node) ?
1572 [ $update_node ] : [ keys %$cert_cache_nodes ];
1574 foreach my $node (@$node_list) {
1575 my $clear_old = sub {
1576 if (my $old_fp = $cert_cache_nodes->{$node}) {
1577 # distrust old fingerprint
1578 delete $cert_cache_fingerprints->{$old_fp};
1579 # ensure reload on next proxied request
1580 delete $cert_cache_nodes->{$node};
1584 my $fp = eval { get_node_fingerprint
($node) };
1587 &$clear_old() if $clear;
1591 my $old_fp = $cert_cache_nodes->{$node};
1592 $cert_cache_fingerprints->{$fp} = 1;
1593 $cert_cache_nodes->{$node} = $fp;
1595 if (defined($old_fp) && $fp ne $old_fp) {
1596 delete $cert_cache_fingerprints->{$old_fp};
1601 # load and cache cert fingerprint once
1602 sub initialize_cert_cache
{
1605 update_cert_cache
($node)
1606 if defined($node) && !defined($cert_cache_nodes->{$node});
1609 sub read_ssl_cert_fingerprint
{
1610 my ($cert_path) = @_;
1612 my $bio = Net
::SSLeay
::BIO_new_file
($cert_path, 'r')
1613 or die "unable to read '$cert_path' - $!\n";
1615 my $cert = Net
::SSLeay
::PEM_read_bio_X509
($bio);
1616 Net
::SSLeay
::BIO_free
($bio);
1618 die "unable to read certificate from '$cert_path'\n" if !$cert;
1620 my $fp = Net
::SSLeay
::X509_get_fingerprint
($cert, 'sha256');
1621 Net
::SSLeay
::X509_free
($cert);
1623 die "unable to get fingerprint for '$cert_path' - got empty value\n"
1624 if !defined($fp) || $fp eq '';
1629 sub get_node_fingerprint
{
1632 my $cert_path = "/etc/pve/nodes/$node/pve-ssl.pem";
1633 my $custom_cert_path = "/etc/pve/nodes/$node/pveproxy-ssl.pem";
1635 $cert_path = $custom_cert_path if -f
$custom_cert_path;
1637 return read_ssl_cert_fingerprint
($cert_path);
1641 sub check_cert_fingerprint
{
1644 # clear cache every 30 minutes at least
1645 update_cert_cache
(undef, 1) if time() - $cert_cache_timestamp >= 60*30;
1647 # get fingerprint of server certificate
1648 my $fp = Net
::SSLeay
::X509_get_fingerprint
($cert, 'sha256');
1649 return 0 if !defined($fp) || $fp eq ''; # error
1652 for my $expected (keys %$cert_cache_fingerprints) {
1653 return 1 if $fp eq $expected;
1658 return 1 if &$check();
1660 # clear cache and retry at most once every minute
1661 if (time() - $cert_cache_timestamp >= 60) {
1662 syslog
('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache");
1663 update_cert_cache
();
1670 # bash completion helpers
1672 sub complete_next_vmid
{
1674 my $vmlist = get_vmlist
() || {};
1675 my $idlist = $vmlist->{ids
} || {};
1677 for (my $i = 100; $i < 10000; $i++) {
1678 return [$i] if !defined($idlist->{$i});
1686 my $vmlist = get_vmlist
();
1687 my $ids = $vmlist->{ids
} || {};
1689 return [ keys %$ids ];
1692 sub complete_local_vmid
{
1694 my $vmlist = get_vmlist
();
1695 my $ids = $vmlist->{ids
} || {};
1697 my $nodename = PVE
::INotify
::nodename
();
1700 foreach my $vmid (keys %$ids) {
1701 my $d = $ids->{$vmid};
1702 next if !$d->{node
} || $d->{node
} ne $nodename;
1709 sub complete_migration_target
{
1713 my $nodename = PVE
::INotify
::nodename
();
1715 my $nodelist = get_nodelist
();
1716 foreach my $node (@$nodelist) {
1717 next if $node eq $nodename;
1725 my ($node, $network_cidr) = @_;
1728 if (defined($network_cidr)) {
1729 # Use mtunnel via to get the remote node's ip inside $network_cidr.
1730 # This goes over the regular network (iow. uses get_ssh_info() with
1731 # $network_cidr undefined.
1732 # FIXME: Use the REST API client for this after creating an API entry
1733 # for get_migration_ip.
1734 my $default_remote = get_ssh_info
($node, undef);
1735 my $default_ssh = ssh_info_to_command
($default_remote);
1736 my $cmd =[@$default_ssh, 'pvecm', 'mtunnel',
1737 '-migration_network', $network_cidr,
1740 PVE
::Tools
::run_command
($cmd, outfunc
=> sub {
1743 die "internal error: unexpected output from mtunnel\n"
1745 if ($line =~ /^ip: '(.*)'$/) {
1748 die "internal error: bad output from mtunnel\n"
1752 die "failed to get ip for node '$node' in network '$network_cidr'\n"
1755 $ip = remote_node_ip
($node);
1761 network
=> $network_cidr,
1765 sub ssh_info_to_command_base
{
1766 my ($info, @extra_options) = @_;
1770 '-o', 'BatchMode=yes',
1771 '-o', 'HostKeyAlias='.$info->{name
},
1776 sub ssh_info_to_command
{
1777 my ($info, @extra_options) = @_;
1778 my $cmd = ssh_info_to_command_base
($info, @extra_options);
1779 push @$cmd, "root\@$info->{ip}";
1783 sub assert_joinable
{
1784 my ($ring0_addr, $ring1_addr, $force) = @_;
1787 my $error = sub { $errors .= "* $_[0]\n"; };
1790 $error->("authentication key '$authfile' already exists");
1793 if (-f
$clusterconf) {
1794 $error->("cluster config '$clusterconf' already exists");
1797 my $vmlist = get_vmlist
();
1798 if ($vmlist && $vmlist->{ids
} && scalar(keys %{$vmlist->{ids
}})) {
1799 $error->("this host already contains virtual guests");
1802 if (run_command
(['corosync-quorumtool', '-l'], noerr
=> 1, quiet
=> 1) == 0) {
1803 $error->("corosync is already running, is this node already in a cluster?!");
1806 # check if corosync ring IPs are configured on the current nodes interfaces
1807 my $check_ip = sub {
1808 my $ip = shift // return;
1809 if (!PVE
::JSONSchema
::pve_verify_ip
($ip, 1)) {
1811 eval { $ip = PVE
::Network
::get_ip_from_hostname
($host); };
1813 $error->("cannot use '$host': $@\n") ;
1818 my $cidr = (Net
::IP
::ip_is_ipv6
($ip)) ?
"$ip/128" : "$ip/32";
1819 my $configured_ips = PVE
::Network
::get_local_ip_from_cidr
($cidr);
1821 $error->("cannot use IP '$ip', it must be configured exactly once on local node!\n")
1822 if (scalar(@$configured_ips) != 1);
1825 $check_ip->($ring0_addr);
1826 $check_ip->($ring1_addr);
1829 warn "detected the following error(s):\n$errors";
1830 die "Check if node may join a cluster failed!\n" if !$force;
1834 # NOTE: filesystem must be offline here, no DB changes allowed
1835 my $backup_cfs_database = sub {
1841 my $backup_fn = "$dbbackupdir/config-$ctime.sql.gz";
1843 print "backup old database to '$backup_fn'\n";
1845 my $cmd = [ ['sqlite3', $dbfile, '.dump'], ['gzip', '-', \
">${backup_fn}"] ];
1846 run_command
($cmd, 'errmsg' => "cannot backup old database\n");
1848 my $maxfiles = 10; # purge older backup
1849 my $backups = [ sort { $b cmp $a } <$dbbackupdir/config-*.sql
.gz
> ];
1851 if ((my $count = scalar(@$backups)) > $maxfiles) {
1852 foreach my $f (@$backups[$maxfiles..$count-1]) {
1853 next if $f !~ m/^(\S+)$/; # untaint
1854 print "delete old backup '$1'\n";
1863 my $nodename = PVE
::INotify
::nodename
();
1865 setup_sshd_config
();
1866 setup_rootsshconfig
();
1869 # check if we can join with the given parameters and current node state
1870 my ($ring0_addr, $ring1_addr) = $param->@{'ring0_addr', 'ring1_addr'};
1871 assert_joinable
($ring0_addr, $ring1_addr, $param->{force
});
1873 # make sure known_hosts is on local filesystem
1874 ssh_unmerge_known_hosts
();
1876 my $host = $param->{hostname
};
1877 my $local_ip_address = remote_node_ip
($nodename);
1880 username
=> 'root@pam',
1881 password
=> $param->{password
},
1882 cookie_name
=> 'PVEAuthCookie',
1883 protocol
=> 'https',
1888 if (my $fp = $param->{fingerprint
}) {
1889 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
1891 # API schema ensures that we can only get here from CLI handler
1892 $conn_args->{manual_verification
} = 1;
1895 print "Establishing API connection with host '$host'\n";
1897 my $conn = PVE
::APIClient
::LWP-
>new(%$conn_args);
1900 # login raises an exception on failure, so if we get here we're good
1901 print "Login succeeded.\n";
1904 $args->{force
} = $param->{force
} if defined($param->{force
});
1905 $args->{nodeid
} = $param->{nodeid
} if $param->{nodeid
};
1906 $args->{votes
} = $param->{votes
} if defined($param->{votes
});
1907 $args->{ring0_addr
} = $ring0_addr // $local_ip_address;
1908 $args->{ring1_addr
} = $ring1_addr if defined($ring1_addr);
1910 print "Request addition of this node\n";
1911 my $res = $conn->post("/cluster/config/nodes/$nodename", $args);
1913 print "Join request OK, finishing setup locally\n";
1915 # added successfuly - now prepare local node
1916 finish_join
($nodename, $res->{corosync_conf
}, $res->{corosync_authkey
});
1920 my ($nodename, $corosync_conf, $corosync_authkey) = @_;
1922 mkdir "$localclusterdir";
1923 PVE
::Tools
::file_set_contents
($authfile, $corosync_authkey);
1924 PVE
::Tools
::file_set_contents
($localclusterconf, $corosync_conf);
1926 print "stopping pve-cluster service\n";
1927 my $cmd = ['systemctl', 'stop', 'pve-cluster'];
1928 run_command
($cmd, errmsg
=> "can't stop pve-cluster service");
1930 $backup_cfs_database->($dbfile);
1933 $cmd = ['systemctl', 'start', 'corosync', 'pve-cluster'];
1934 run_command
($cmd, errmsg
=> "starting pve-cluster failed");
1938 while (!check_cfs_quorum
(1)) {
1940 print "waiting for quorum...";
1946 print "OK\n" if !$printqmsg;
1948 updatecerts_and_ssh
(1);
1950 print "generated new node certificate, restart pveproxy and pvedaemon services\n";
1951 run_command
(['systemctl', 'reload-or-restart', 'pvedaemon', 'pveproxy']);
1953 print "successfully added node '$nodename' to cluster.\n";
1956 sub updatecerts_and_ssh
{
1957 my ($force_new_cert, $silent) = @_;
1959 my $p = sub { print "$_[0]\n" if !$silent };
1961 setup_rootsshconfig
();
1963 gen_pve_vzdump_symlink
();
1965 if (!check_cfs_quorum
(1)) {
1966 return undef if $silent;
1967 die "no quorum - unable to update files\n";
1972 my $nodename = PVE
::INotify
::nodename
();
1973 my $local_ip_address = remote_node_ip
($nodename);
1975 $p->("(re)generate node files");
1976 $p->("generate new node certificate") if $force_new_cert;
1977 gen_pve_node_files
($nodename, $local_ip_address, $force_new_cert);
1979 $p->("merge authorized SSH keys and known hosts");
1981 ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
1982 gen_pve_vzdump_files
();