14 use POSIX
qw(EEXIST ENOENT);
17 use Storable
qw(dclone);
25 use PVE
::Tools
qw(run_command);
27 use PVE
::Cluster
::IPCConst
;
37 use Data
::Dumper
; # fixme: remove
39 # x509 certificate utils
41 my $basedir = "/etc/pve";
42 my $authdir = "$basedir/priv";
43 my $lockdir = "/etc/pve/priv/lock";
45 # cfs and corosync files
46 my $dbfile = "/var/lib/pve-cluster/config.db";
47 my $dbbackupdir = "/var/lib/pve-cluster/backup";
48 my $localclusterdir = "/etc/corosync";
49 my $localclusterconf = "$localclusterdir/corosync.conf";
50 my $authfile = "$localclusterdir/authkey";
51 my $clusterconf = "$basedir/corosync.conf";
53 my $authprivkeyfn = "$authdir/authkey.key";
54 my $authpubkeyfn = "$basedir/authkey.pub";
55 my $pveca_key_fn = "$authdir/pve-root-ca.key";
56 my $pveca_srl_fn = "$authdir/pve-root-ca.srl";
57 my $pveca_cert_fn = "$basedir/pve-root-ca.pem";
58 # this is just a secret accessable by the web browser
59 # and is used for CSRF prevention
60 my $pvewww_key_fn = "$basedir/pve-www.key";
63 my $ssh_rsa_id_priv = "/root/.ssh/id_rsa";
64 my $ssh_rsa_id = "/root/.ssh/id_rsa.pub";
65 my $ssh_host_rsa_id = "/etc/ssh/ssh_host_rsa_key.pub";
66 my $sshglobalknownhosts = "/etc/ssh/ssh_known_hosts";
67 my $sshknownhosts = "/etc/pve/priv/known_hosts";
68 my $sshauthkeys = "/etc/pve/priv/authorized_keys";
69 my $sshd_config_fn = "/etc/ssh/sshd_config";
70 my $rootsshauthkeys = "/root/.ssh/authorized_keys";
71 my $rootsshauthkeysbackup = "${rootsshauthkeys}.org";
72 my $rootsshconfig = "/root/.ssh/config";
74 # this is just a readonly copy, the relevant one is in status.c from pmxcfs
75 # observed files are the one we can get directly through IPCC, they are cached
76 # using a computed version and only those can be used by the cfs_*_file methods
80 'datacenter.cfg' => 1,
81 'replication.cfg' => 1,
83 'corosync.conf.new' => 1,
86 'priv/shadow.cfg' => 1,
91 'ha/crm_commands' => 1,
92 'ha/manager_status' => 1,
93 'ha/resources.cfg' => 1,
100 # only write output if something fails
105 my $record = sub { $outbuf .= shift . "\n"; };
107 eval { run_command
($cmd, outfunc
=> $record, errfunc
=> $record) };
110 print STDERR
$outbuf;
115 sub check_cfs_quorum
{
118 # note: -w filename always return 1 for root, so wee need
119 # to use File::lstat here
120 my $st = File
::stat::lstat("$basedir/local");
121 my $quorate = ($st && (($st->mode & 0200) != 0));
123 die "cluster not ready - no quorum?\n" if !$quorate && !$noerr;
128 sub check_cfs_is_mounted
{
131 my $res = -l
"$basedir/local";
133 die "pve configuration filesystem not mounted\n"
142 check_cfs_is_mounted
();
144 my @required_dirs = (
147 "$basedir/nodes/$nodename",
148 "$basedir/nodes/$nodename/lxc",
149 "$basedir/nodes/$nodename/qemu-server",
150 "$basedir/nodes/$nodename/openvz",
151 "$basedir/nodes/$nodename/priv");
153 foreach my $dir (@required_dirs) {
155 mkdir($dir) || $! == EEXIST
|| die "unable to create directory '$dir' - $!\n";
162 return if -f
"$authprivkeyfn";
164 check_cfs_is_mounted
();
166 cfs_lock_authkey
(undef, sub {
167 mkdir $authdir || $! == EEXIST
|| die "unable to create dir '$authdir' - $!\n";
169 run_silent_cmd
(['openssl', 'genrsa', '-out', $authprivkeyfn, '2048']);
171 run_silent_cmd
(['openssl', 'rsa', '-in', $authprivkeyfn, '-pubout', '-out', $authpubkeyfn]);
179 return if -f
$pveca_key_fn;
182 run_silent_cmd
(['openssl', 'genrsa', '-out', $pveca_key_fn, '4096']);
185 die "unable to generate pve ca key:\n$@" if $@;
190 if (-f
$pveca_key_fn && -f
$pveca_cert_fn) {
196 # we try to generate an unique 'subject' to avoid browser problems
197 # (reused serial numbers, ..)
199 UUID
::generate
($uuid);
201 UUID
::unparse
($uuid, $uuid_str);
204 # wrap openssl with faketime to prevent bug #904
205 run_silent_cmd
(['faketime', 'yesterday', 'openssl', 'req', '-batch',
206 '-days', '3650', '-new', '-x509', '-nodes', '-key',
207 $pveca_key_fn, '-out', $pveca_cert_fn, '-subj',
208 "/CN=Proxmox Virtual Environment/OU=$uuid_str/O=PVE Cluster Manager CA/"]);
211 die "generating pve root certificate failed:\n$@" if $@;
216 sub gen_pve_ssl_key
{
219 die "no node name specified" if !$nodename;
221 my $pvessl_key_fn = "$basedir/nodes/$nodename/pve-ssl.key";
223 return if -f
$pvessl_key_fn;
226 run_silent_cmd
(['openssl', 'genrsa', '-out', $pvessl_key_fn, '2048']);
229 die "unable to generate pve ssl key for node '$nodename':\n$@" if $@;
232 sub gen_pve_www_key
{
234 return if -f
$pvewww_key_fn;
237 run_silent_cmd
(['openssl', 'genrsa', '-out', $pvewww_key_fn, '2048']);
240 die "unable to generate pve www key:\n$@" if $@;
246 PVE
::Tools
::file_set_contents
($pveca_srl_fn, $serial);
249 sub gen_pve_ssl_cert
{
250 my ($force, $nodename, $ip) = @_;
252 die "no node name specified" if !$nodename;
253 die "no IP specified" if !$ip;
255 my $pvessl_cert_fn = "$basedir/nodes/$nodename/pve-ssl.pem";
257 return if !$force && -f
$pvessl_cert_fn;
259 my $names = "IP:127.0.0.1,IP:::1,DNS:localhost";
261 my $rc = PVE
::INotify
::read_file
('resolvconf');
265 my $fqdn = $nodename;
267 $names .= ",DNS:$nodename";
269 if ($rc && $rc->{search
}) {
270 $fqdn = $nodename . "." . $rc->{search
};
271 $names .= ",DNS:$fqdn";
274 my $sslconf = <<__EOD;
275 RANDFILE = /root/.rnd
280 distinguished_name = req_distinguished_name
281 req_extensions = v3_req
283 string_mask = nombstr
285 [ req_distinguished_name ]
286 organizationalUnitName = PVE Cluster Node
287 organizationName = Proxmox Virtual Environment
291 basicConstraints = CA:FALSE
292 extendedKeyUsage = serverAuth
293 subjectAltName = $names
296 my $cfgfn = "/tmp/pvesslconf-$$.tmp";
297 my $fh = IO
::File-
>new ($cfgfn, "w");
301 my $reqfn = "/tmp/pvecertreq-$$.tmp";
304 my $pvessl_key_fn = "$basedir/nodes/$nodename/pve-ssl.key";
306 run_silent_cmd
(['openssl', 'req', '-batch', '-new', '-config', $cfgfn,
307 '-key', $pvessl_key_fn, '-out', $reqfn]);
313 die "unable to generate pve certificate request:\n$err";
316 update_serial
("0000000000000000") if ! -f
$pveca_srl_fn;
319 # wrap openssl with faketime to prevent bug #904
320 run_silent_cmd
(['faketime', 'yesterday', 'openssl', 'x509', '-req',
321 '-in', $reqfn, '-days', '3650', '-out', $pvessl_cert_fn,
322 '-CAkey', $pveca_key_fn, '-CA', $pveca_cert_fn,
323 '-CAserial', $pveca_srl_fn, '-extfile', $cfgfn]);
329 die "unable to generate pve ssl certificate:\n$err";
336 sub gen_pve_node_files
{
337 my ($nodename, $ip, $opt_force) = @_;
339 gen_local_dirs
($nodename);
343 # make sure we have a (cluster wide) secret
344 # for CSRFR prevention
347 # make sure we have a (per node) private key
348 gen_pve_ssl_key
($nodename);
350 # make sure we have a CA
351 my $force = gen_pveca_cert
();
353 $force = 1 if $opt_force;
355 gen_pve_ssl_cert
($force, $nodename, $ip);
358 my $vzdump_cron_dummy = <<__EOD;
359 # cluster wide vzdump cron schedule
360 # Atomatically generated file - do not edit
362 PATH="/usr/sbin:/usr/bin:/sbin:/bin"
366 sub gen_pve_vzdump_symlink
{
368 my $filename = "/etc/pve/vzdump.cron";
370 my $link_fn = "/etc/cron.d/vzdump";
372 if ((-f
$filename) && (! -l
$link_fn)) {
373 rename($link_fn, "/root/etc_cron_vzdump.org"); # make backup if file exists
374 symlink($filename, $link_fn);
378 sub gen_pve_vzdump_files
{
380 my $filename = "/etc/pve/vzdump.cron";
382 PVE
::Tools
::file_set_contents
($filename, $vzdump_cron_dummy)
385 gen_pve_vzdump_symlink
();
392 my $ipcc_send_rec = sub {
393 my ($msgid, $data) = @_;
395 my $res = PVE
::IPCC
::ipcc_send_rec
($msgid, $data);
397 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
402 my $ipcc_send_rec_json = sub {
403 my ($msgid, $data) = @_;
405 my $res = PVE
::IPCC
::ipcc_send_rec
($msgid, $data);
407 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
409 return decode_json
($res);
412 my $ipcc_get_config = sub {
415 my $bindata = pack "Z*", $path;
416 my $res = PVE
::IPCC
::ipcc_send_rec
(CFS_IPC_GET_CONFIG
, $bindata);
417 if (!defined($res)) {
419 return undef if $! == ENOENT
;
428 my $ipcc_get_status = sub {
429 my ($name, $nodename) = @_;
431 my $bindata = pack "Z[256]Z[256]", $name, ($nodename || "");
432 return PVE
::IPCC
::ipcc_send_rec
(CFS_IPC_GET_STATUS
, $bindata);
435 my $ipcc_remove_status = sub {
437 my $bindata = pack "Z[256]", $name;
438 return &$ipcc_send_rec(CFS_IPC_SET_STATUS
, $bindata);
441 my $ipcc_update_status = sub {
442 my ($name, $data) = @_;
444 my $raw = ref($data) ? encode_json
($data) : $data;
446 my $bindata = pack "Z[256]Z*", $name, $raw;
448 return &$ipcc_send_rec(CFS_IPC_SET_STATUS
, $bindata);
452 my ($priority, $ident, $tag, $msg) = @_;
454 my $bindata = pack "CCCZ*Z*Z*", $priority, bytes
::length($ident) + 1,
455 bytes
::length($tag) + 1, $ident, $tag, $msg;
457 return &$ipcc_send_rec(CFS_IPC_LOG_CLUSTER_MSG
, $bindata);
460 my $ipcc_get_cluster_log = sub {
461 my ($user, $max) = @_;
463 $max = 0 if !defined($max);
465 my $bindata = pack "VVVVZ*", $max, 0, 0, 0, ($user || "");
466 return &$ipcc_send_rec(CFS_IPC_GET_CLUSTER_LOG
, $bindata);
474 my $res = &$ipcc_send_rec_json(CFS_IPC_GET_FS_VERSION
);
475 #warn "GOT1: " . Dumper($res);
476 die "no starttime\n" if !$res->{starttime
};
478 if (!$res->{starttime
} || !$versions->{starttime
} ||
479 $res->{starttime
} != $versions->{starttime
}) {
480 #print "detected changed starttime\n";
499 if (!$clinfo->{version
} || $clinfo->{version
} != $versions->{clinfo
}) {
500 #warn "detected new clinfo\n";
501 $clinfo = &$ipcc_send_rec_json(CFS_IPC_GET_CLUSTER_INFO
);
512 if (!$vmlist->{version
} || $vmlist->{version
} != $versions->{vmlist
}) {
513 #warn "detected new vmlist1\n";
514 $vmlist = &$ipcc_send_rec_json(CFS_IPC_GET_GUEST_LIST
);
534 return $clinfo->{nodelist
};
538 my $nodelist = $clinfo->{nodelist
};
540 my $nodename = PVE
::INotify
::nodename
();
542 if (!$nodelist || !$nodelist->{$nodename}) {
543 return [ $nodename ];
546 return [ keys %$nodelist ];
549 # best effort data store for cluster
550 # this data is gone if the pmxcfs is restarted, but only the local data,
551 # so we should not use this for very important data
552 sub broadcast_node_kv
{
553 my ($key, $data) = @_;
555 if (!defined($data)) {
557 $ipcc_remove_status->("kv/$key");
560 die "cannot send a reference\n" if ref($data);
561 my $size = length($data);
562 # pmxcfs has an upper bound of 32k for each entry
563 die "data for '$key' too big\n"
564 if $size >= (32*1024);
567 $ipcc_update_status->("kv/$key", $data);
575 my ($key, $nodename) = @_;
578 my $get_node_data = sub {
580 my $raw = $ipcc_get_status->("kv/$key", $node);
581 $res->{$node} = $raw if $raw;
585 $get_node_data->($nodename);
587 my $nodelist = get_nodelist
();
589 foreach my $node (@$nodelist) {
590 $get_node_data->($node);
597 # $data must be a chronological descending ordered array of tasks
598 sub broadcast_tasklist
{
601 # the serialized list may not get bigger than 32kb (CFS_MAX_STATUS_SIZE
602 # from pmxcfs) - drop older items until we satisfy this constraint
603 my $size = length(encode_json
($data));
604 while ($size >= (32 * 1024)) {
606 $size = length(encode_json
($data));
610 &$ipcc_update_status("tasklist", $data);
616 my $tasklistcache = {};
621 my $kvstore = $versions->{kvstore
} || {};
623 my $nodelist = get_nodelist
();
626 foreach my $node (@$nodelist) {
627 next if $nodename && ($nodename ne $node);
629 my $ver = $kvstore->{$node}->{tasklist
} if $kvstore->{$node};
630 my $cd = $tasklistcache->{$node};
631 if (!$cd || !$ver || !$cd->{version
} ||
632 ($cd->{version
} != $ver)) {
633 my $raw = &$ipcc_get_status("tasklist", $node) || '[]';
634 my $data = decode_json
($raw);
636 $cd = $tasklistcache->{$node} = {
640 } elsif ($cd && $cd->{data
}) {
641 push @$res, @{$cd->{data
}};
645 syslog
('err', $err) if $err;
652 my ($rrdid, $data) = @_;
655 &$ipcc_update_status("rrd/$rrdid", $data);
662 my $last_rrd_dump = 0;
663 my $last_rrd_data = "";
669 my $diff = $ctime - $last_rrd_dump;
671 return $last_rrd_data;
676 $raw = &$ipcc_send_rec(CFS_IPC_GET_RRD_DUMP
);
688 while ($raw =~ s/^(.*)\n//) {
689 my ($key, @ela) = split(/:/, $1);
691 next if !(scalar(@ela) > 1);
692 $res->{$key} = [ map { $_ eq 'U' ?
undef : $_ } @ela ];
696 $last_rrd_dump = $ctime;
697 $last_rrd_data = $res;
702 sub create_rrd_data
{
703 my ($rrdname, $timeframe, $cf) = @_;
705 my $rrddir = "/var/lib/rrdcached/db";
707 my $rrd = "$rrddir/$rrdname";
711 day
=> [ 60*30, 70 ],
712 week
=> [ 60*180, 70 ],
713 month
=> [ 60*720, 70 ],
714 year
=> [ 60*10080, 70 ],
717 my ($reso, $count) = @{$setup->{$timeframe}};
718 my $ctime = $reso*int(time()/$reso);
719 my $req_start = $ctime - $reso*$count;
721 $cf = "AVERAGE" if !$cf;
729 my $socket = "/var/run/rrdcached.sock";
730 push @args, "--daemon" => "unix:$socket" if -S
$socket;
732 my ($start, $step, $names, $data) = RRDs
::fetch
($rrd, $cf, @args);
734 my $err = RRDs
::error
;
735 die "RRD error: $err\n" if $err;
737 die "got wrong time resolution ($step != $reso)\n"
741 my $fields = scalar(@$names);
742 for my $line (@$data) {
743 my $entry = { 'time' => $start };
745 for (my $i = 0; $i < $fields; $i++) {
746 my $name = $names->[$i];
747 if (defined(my $val = $line->[$i])) {
748 $entry->{$name} = $val;
750 # leave empty fields undefined
751 # maybe make this configurable?
760 sub create_rrd_graph
{
761 my ($rrdname, $timeframe, $ds, $cf) = @_;
763 # Using RRD graph is clumsy - maybe it
764 # is better to simply fetch the data, and do all display
765 # related things with javascript (new extjs html5 graph library).
767 my $rrddir = "/var/lib/rrdcached/db";
769 my $rrd = "$rrddir/$rrdname";
771 my @ids = PVE
::Tools
::split_list
($ds);
773 my $ds_txt = join('_', @ids);
775 my $filename = "${rrd}_${ds_txt}.png";
779 day
=> [ 60*30, 70 ],
780 week
=> [ 60*180, 70 ],
781 month
=> [ 60*720, 70 ],
782 year
=> [ 60*10080, 70 ],
785 my ($reso, $count) = @{$setup->{$timeframe}};
788 "--imgformat" => "PNG",
792 "--start" => - $reso*$count,
794 "--lower-limit" => 0,
797 my $socket = "/var/run/rrdcached.sock";
798 push @args, "--daemon" => "unix:$socket" if -S
$socket;
800 my @coldef = ('#00ddff', '#ff0000');
802 $cf = "AVERAGE" if !$cf;
805 foreach my $id (@ids) {
806 my $col = $coldef[$i++] || die "fixme: no color definition";
807 push @args, "DEF:${id}=$rrd:${id}:$cf";
809 if ($id eq 'cpu' || $id eq 'iowait') {
810 push @args, "CDEF:${id}_per=${id},100,*";
811 $dataid = "${id}_per";
813 push @args, "LINE2:${dataid}${col}:${id}";
816 push @args, '--full-size-mode';
818 # we do not really store data into the file
819 my $res = RRDs
::graphv
('-', @args);
821 my $err = RRDs
::error
;
822 die "RRD error: $err\n" if $err;
824 return { filename
=> $filename, image
=> $res->{image
} };
827 # a fast way to read files (avoid fuse overhead)
831 return &$ipcc_get_config($path);
834 sub get_cluster_log
{
835 my ($user, $max) = @_;
837 return &$ipcc_get_cluster_log($user, $max);
842 sub cfs_register_file
{
843 my ($filename, $parser, $writer) = @_;
845 $observed->{$filename} || die "unknown file '$filename'";
847 die "file '$filename' already registered" if $file_info->{$filename};
849 $file_info->{$filename} = {
855 my $ccache_read = sub {
856 my ($filename, $parser, $version) = @_;
858 $ccache->{$filename} = {} if !$ccache->{$filename};
860 my $ci = $ccache->{$filename};
862 if (!$ci->{version
} || !$version || $ci->{version
} != $version) {
863 # we always call the parser, even when the file does not exists
864 # (in that case $data is undef)
865 my $data = get_config
($filename);
866 $ci->{data
} = &$parser("/etc/pve/$filename", $data);
867 $ci->{version
} = $version;
870 my $res = ref($ci->{data
}) ? dclone
($ci->{data
}) : $ci->{data
};
875 sub cfs_file_version
{
880 if ($filename =~ m!^nodes/[^/]+/(openvz|lxc|qemu-server)/(\d+)\.conf$!) {
881 my ($type, $vmid) = ($1, $2);
882 if ($vmlist && $vmlist->{ids
} && $vmlist->{ids
}->{$vmid}) {
883 $version = $vmlist->{ids
}->{$vmid}->{version
};
885 $infotag = "/$type/";
887 $infotag = $filename;
888 $version = $versions->{$filename};
891 my $info = $file_info->{$infotag} ||
892 die "unknown file type '$filename'\n";
894 return wantarray ?
($version, $info) : $version;
900 my ($version, $info) = cfs_file_version
($filename);
901 my $parser = $info->{parser
};
903 return &$ccache_read($filename, $parser, $version);
907 my ($filename, $data) = @_;
909 my ($version, $info) = cfs_file_version
($filename);
911 my $writer = $info->{writer
} || die "no writer defined";
913 my $fsname = "/etc/pve/$filename";
915 my $raw = &$writer($fsname, $data);
917 if (my $ci = $ccache->{$filename}) {
918 $ci->{version
} = undef;
921 PVE
::Tools
::file_set_contents
($fsname, $raw);
925 my ($lockid, $timeout, $code, @param) = @_;
927 my $prev_alarm = alarm(0); # suspend outer alarm early
932 # this timeout is for acquire the lock
933 $timeout = 10 if !$timeout;
935 my $filename = "$lockdir/$lockid";
942 die "pve cluster filesystem not online.\n";
945 my $timeout_err = sub { die "got lock request timeout\n"; };
946 local $SIG{ALRM
} = $timeout_err;
950 $got_lock = mkdir($filename);
951 $timeout = alarm(0) - 1; # we'll sleep for 1s, see down below
955 $timeout_err->() if $timeout <= 0;
957 print STDERR
"trying to acquire cfs lock '$lockid' ...\n";
958 utime (0, 0, $filename); # cfs unlock request
962 # fixed command timeout: cfs locks have a timeout of 120
963 # using 60 gives us another 60 seconds to abort the task
964 local $SIG{ALRM
} = sub { die "got lock timeout - aborting command\n"; };
967 cfs_update
(); # make sure we read latest versions inside code()
969 $res = &$code(@param);
976 $err = "no quorum!\n" if !$got_lock && !check_cfs_quorum
(1);
978 rmdir $filename if $got_lock; # if we held the lock always unlock again
983 $@ = "error with cfs lock '$lockid': $err";
993 my ($filename, $timeout, $code, @param) = @_;
995 my $info = $observed->{$filename} || die "unknown file '$filename'";
997 my $lockid = "file-$filename";
998 $lockid =~ s/[.\/]/_
/g
;
1000 &$cfs_lock($lockid, $timeout, $code, @param);
1003 sub cfs_lock_storage
{
1004 my ($storeid, $timeout, $code, @param) = @_;
1006 my $lockid = "storage-$storeid";
1008 &$cfs_lock($lockid, $timeout, $code, @param);
1011 sub cfs_lock_domain
{
1012 my ($domainname, $timeout, $code, @param) = @_;
1014 my $lockid = "domain-$domainname";
1016 &$cfs_lock($lockid, $timeout, $code, @param);
1020 my ($account, $timeout, $code, @param) = @_;
1022 my $lockid = "acme-$account";
1024 &$cfs_lock($lockid, $timeout, $code, @param);
1027 sub cfs_lock_authkey
{
1028 my ($timeout, $code, @param) = @_;
1030 $cfs_lock->('authkey', $timeout, $code, @param);
1048 my ($priority, $ident, $msg) = @_;
1050 if (my $tmp = $log_levels->{$priority}) {
1054 die "need numeric log priority" if $priority !~ /^\d+$/;
1056 my $tag = PVE
::SafeSyslog
::tag
();
1058 $msg = "empty message" if !$msg;
1060 $ident = "" if !$ident;
1061 $ident = encode
("ascii", $ident,
1062 sub { sprintf "\\u%04x", shift });
1064 my $ascii = encode
("ascii", $msg, sub { sprintf "\\u%04x", shift });
1067 syslog
($priority, "<%s> %s", $ident, $ascii);
1069 syslog
($priority, "%s", $ascii);
1072 eval { &$ipcc_log($priority, $ident, $tag, $ascii); };
1074 syslog
("err", "writing cluster log failed: $@") if $@;
1077 sub check_vmid_unused
{
1078 my ($vmid, $noerr) = @_;
1080 my $vmlist = get_vmlist
();
1082 my $d = $vmlist->{ids
}->{$vmid};
1083 return 1 if !defined($d);
1085 return undef if $noerr;
1087 my $vmtypestr = $d->{type
} eq 'qemu' ?
'VM' : 'CT';
1088 die "$vmtypestr $vmid already exists on node '$d->{node}'\n";
1091 sub check_node_exists
{
1092 my ($nodename, $noerr) = @_;
1094 my $nodelist = $clinfo->{nodelist
};
1095 return 1 if $nodelist && $nodelist->{$nodename};
1097 return undef if $noerr;
1099 die "no such cluster node '$nodename'\n";
1102 # this is also used to get the IP of the local node
1103 sub remote_node_ip
{
1104 my ($nodename, $noerr) = @_;
1106 my $nodelist = $clinfo->{nodelist
};
1107 if ($nodelist && $nodelist->{$nodename}) {
1108 if (my $ip = $nodelist->{$nodename}->{ip
}) {
1109 return $ip if !wantarray;
1110 my $family = $nodelist->{$nodename}->{address_family
};
1112 $nodelist->{$nodename}->{address_family
} =
1114 PVE
::Tools
::get_host_address_family
($ip);
1116 return wantarray ?
($ip, $family) : $ip;
1120 # fallback: try to get IP by other means
1121 return PVE
::Network
::get_ip_from_hostname
($nodename, $noerr);
1124 sub get_local_migration_ip
{
1125 my ($migration_network, $noerr) = @_;
1127 my $cidr = $migration_network;
1129 if (!defined($cidr)) {
1130 my $dc_conf = cfs_read_file
('datacenter.cfg');
1131 $cidr = $dc_conf->{migration
}->{network
}
1132 if defined($dc_conf->{migration
}->{network
});
1135 if (defined($cidr)) {
1136 my $ips = PVE
::Network
::get_local_ip_from_cidr
($cidr);
1138 die "could not get migration ip: no IP address configured on local " .
1139 "node for network '$cidr'\n" if !$noerr && (scalar(@$ips) == 0);
1141 die "could not get migration ip: multiple IP address configured for " .
1142 "network '$cidr'\n" if !$noerr && (scalar(@$ips) > 1);
1150 # ssh related utility functions
1152 sub ssh_merge_keys
{
1153 # remove duplicate keys in $sshauthkeys
1154 # ssh-copy-id simply add keys, so the file can grow to large
1157 if (-f
$sshauthkeys) {
1158 $data = PVE
::Tools
::file_get_contents
($sshauthkeys, 128*1024);
1163 if (-f
$rootsshauthkeysbackup) {
1165 $data .= PVE
::Tools
::file_get_contents
($rootsshauthkeysbackup, 128*1024);
1170 # always add ourself
1171 if (-f
$ssh_rsa_id) {
1172 my $pub = PVE
::Tools
::file_get_contents
($ssh_rsa_id);
1174 $data .= "\n$pub\n";
1179 my @lines = split(/\n/, $data);
1180 foreach my $line (@lines) {
1181 if ($line !~ /^#/ && $line =~ m/(^|\s)ssh-(rsa|dsa)\s+(\S+)\s+\S+$/) {
1182 next if $vhash->{$3}++;
1184 $newdata .= "$line\n";
1187 PVE
::Tools
::file_set_contents
($sshauthkeys, $newdata, 0600);
1189 if ($found_backup && -l
$rootsshauthkeys) {
1190 # everything went well, so we can remove the backup
1191 unlink $rootsshauthkeysbackup;
1195 sub setup_sshd_config
{
1198 my $conf = PVE
::Tools
::file_get_contents
($sshd_config_fn);
1200 return if $conf =~ m/^PermitRootLogin\s+yes\s*$/m;
1202 if ($conf !~ s/^#?PermitRootLogin.*$/PermitRootLogin yes/m) {
1204 $conf .= "\nPermitRootLogin yes\n";
1207 PVE
::Tools
::file_set_contents
($sshd_config_fn, $conf);
1209 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'sshd']);
1212 sub setup_rootsshconfig
{
1214 # create ssh key if it does not exist
1215 if (! -f
$ssh_rsa_id) {
1216 mkdir '/root/.ssh/';
1217 system ("echo|ssh-keygen -t rsa -N '' -b 2048 -f ${ssh_rsa_id_priv}");
1220 # create ssh config if it does not exist
1221 if (! -f
$rootsshconfig) {
1223 if (my $fh = IO
::File-
>new($rootsshconfig, O_CREAT
|O_WRONLY
|O_EXCL
, 0640)) {
1224 # this is the default ciphers list from Debian's OpenSSH package (OpenSSH_7.4p1 Debian-10, OpenSSL 1.0.2k 26 Jan 2017)
1225 # changed order to put AES before Chacha20 (most hardware has AESNI)
1226 print $fh "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm\@openssh.com,aes256-gcm\@openssh.com,chacha20-poly1305\@openssh.com\n";
1232 sub setup_ssh_keys
{
1238 if (! -f
$sshauthkeys) {
1240 if (-f
$rootsshauthkeys) {
1241 $old = PVE
::Tools
::file_get_contents
($rootsshauthkeys, 128*1024);
1243 if (my $fh = IO
::File-
>new ($sshauthkeys, O_CREAT
|O_WRONLY
|O_EXCL
, 0400)) {
1244 PVE
::Tools
::safe_print
($sshauthkeys, $fh, $old) if $old;
1250 warn "can't create shared ssh key database '$sshauthkeys'\n"
1251 if ! -f
$sshauthkeys;
1253 if (-f
$rootsshauthkeys && ! -l
$rootsshauthkeys) {
1254 if (!rename($rootsshauthkeys , $rootsshauthkeysbackup)) {
1255 warn "rename $rootsshauthkeys failed - $!\n";
1259 if (! -l
$rootsshauthkeys) {
1260 symlink $sshauthkeys, $rootsshauthkeys;
1263 if (! -l
$rootsshauthkeys) {
1264 warn "can't create symlink for ssh keys '$rootsshauthkeys' -> '$sshauthkeys'\n";
1266 unlink $rootsshauthkeysbackup if $import_ok;
1270 sub ssh_unmerge_known_hosts
{
1271 return if ! -l
$sshglobalknownhosts;
1274 $old = PVE
::Tools
::file_get_contents
($sshknownhosts, 128*1024)
1275 if -f
$sshknownhosts;
1277 PVE
::Tools
::file_set_contents
($sshglobalknownhosts, $old);
1280 sub ssh_merge_known_hosts
{
1281 my ($nodename, $ip_address, $createLink) = @_;
1283 die "no node name specified" if !$nodename;
1284 die "no ip address specified" if !$ip_address;
1286 # ssh lowercases hostnames (aliases) before comparision, so we need too
1287 $nodename = lc($nodename);
1288 $ip_address = lc($ip_address);
1292 if (! -f
$sshknownhosts) {
1293 if (my $fh = IO
::File-
>new($sshknownhosts, O_CREAT
|O_WRONLY
|O_EXCL
, 0600)) {
1298 my $old = PVE
::Tools
::file_get_contents
($sshknownhosts, 128*1024);
1302 if ((! -l
$sshglobalknownhosts) && (-f
$sshglobalknownhosts)) {
1303 $new = PVE
::Tools
::file_get_contents
($sshglobalknownhosts, 128*1024);
1306 my $hostkey = PVE
::Tools
::file_get_contents
($ssh_host_rsa_id);
1307 # Note: file sometimes containe emty lines at start, so we use multiline match
1308 die "can't parse $ssh_host_rsa_id" if $hostkey !~ m/^(ssh-rsa\s\S+)(\s.*)?$/m;
1317 my $merge_line = sub {
1318 my ($line, $all) = @_;
1320 return if $line =~ m/^\s*$/; # skip empty lines
1321 return if $line =~ m/^#/; # skip comments
1323 if ($line =~ m/^(\S+)\s(ssh-rsa\s\S+)(\s.*)?$/) {
1326 if (!$vhash->{$key}) {
1328 if ($key =~ m/\|1\|([^\|\s]+)\|([^\|\s]+)$/) {
1329 my $salt = decode_base64
($1);
1331 my $hmac = Digest
::HMAC_SHA1-
>new($salt);
1332 $hmac->add($nodename);
1333 my $hd = $hmac->b64digest . '=';
1334 if ($digest eq $hd) {
1335 if ($rsakey eq $hostkey) {
1336 $found_nodename = 1;
1341 $hmac = Digest
::HMAC_SHA1-
>new($salt);
1342 $hmac->add($ip_address);
1343 $hd = $hmac->b64digest . '=';
1344 if ($digest eq $hd) {
1345 if ($rsakey eq $hostkey) {
1346 $found_local_ip = 1;
1352 $key = lc($key); # avoid duplicate entries, ssh compares lowercased
1353 if ($key eq $ip_address) {
1354 $found_local_ip = 1 if $rsakey eq $hostkey;
1355 } elsif ($key eq $nodename) {
1356 $found_nodename = 1 if $rsakey eq $hostkey;
1366 while ($old && $old =~ s/^((.*?)(\n|$))//) {
1368 &$merge_line($line, 1);
1371 while ($new && $new =~ s/^((.*?)(\n|$))//) {
1373 &$merge_line($line);
1376 # add our own key if not already there
1377 $data .= "$nodename $hostkey\n" if !$found_nodename;
1378 $data .= "$ip_address $hostkey\n" if !$found_local_ip;
1380 PVE
::Tools
::file_set_contents
($sshknownhosts, $data);
1382 return if !$createLink;
1384 unlink $sshglobalknownhosts;
1385 symlink $sshknownhosts, $sshglobalknownhosts;
1387 warn "can't create symlink for ssh known hosts '$sshglobalknownhosts' -> '$sshknownhosts'\n"
1388 if ! -l
$sshglobalknownhosts;
1392 my $migration_format = {
1396 enum
=> ['secure', 'insecure'],
1397 description
=> "Migration traffic is encrypted using an SSH tunnel by " .
1398 "default. On secure, completely private networks this can be " .
1399 "disabled to increase performance.",
1400 default => 'secure',
1404 type
=> 'string', format
=> 'CIDR',
1405 format_description
=> 'CIDR',
1406 description
=> "CIDR of the (sub) network that is used for migration."
1411 shutdown_policy
=> {
1413 enum
=> ['freeze', 'failover', 'conditional'],
1414 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.",
1415 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.",
1416 default => 'conditional',
1420 PVE
::JSONSchema
::register_format
('mac-prefix', \
&pve_verify_mac_prefix
);
1421 sub pve_verify_mac_prefix
{
1422 my ($mac_prefix, $noerr) = @_;
1424 if ($mac_prefix !~ m/^[a-f0-9][02468ace](?::[a-f0-9]{2}){0,2}:?$/i) {
1425 return undef if $noerr;
1426 die "value is not a valid unicast MAC address prefix\n";
1434 description
=> "U2F AppId URL override. Defaults to the origin.",
1435 format_description
=> 'APPID',
1440 description
=> "U2F Origin override. Mostly useful for single nodes with a single URL.",
1441 format_description
=> 'URL',
1446 my $datacenter_schema = {
1448 additionalProperties
=> 0,
1453 description
=> "Default keybord layout for vnc server.",
1454 enum
=> PVE
::Tools
::kvmkeymaplist
(),
1459 description
=> "Default GUI language.",
1485 description
=> "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
1486 pattern
=> "http://.*",
1488 migration_unsecure
=> {
1491 description
=> "Migration is secure using SSH tunnel by default. " .
1492 "For secure private networks you can disable it to speed up " .
1493 "migration. Deprecated, use the 'migration' property instead!",
1497 type
=> 'string', format
=> $migration_format,
1498 description
=> "For cluster wide migration settings.",
1503 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.",
1504 enum
=> ['applet', 'vv', 'html5', 'xtermjs'],
1509 format
=> 'email-opt',
1510 description
=> "Specify email address to send notification from (default is root@\$hostname)",
1516 description
=> "Defines how many workers (per node) are maximal started ".
1517 " on actions like 'stopall VMs' or task from the ha-manager.",
1522 default => 'watchdog',
1523 enum
=> [ 'watchdog', 'hardware', 'both' ],
1524 description
=> "Set the fencing mode of the HA cluster. Hardware mode " .
1525 "needs a valid configuration of fence devices in /etc/pve/ha/fence.cfg." .
1526 " With both all two modes are used." .
1527 "\n\nWARNING: 'hardware' and 'both' are EXPERIMENTAL & WIP",
1531 type
=> 'string', format
=> $ha_format,
1532 description
=> "Cluster wide HA settings.",
1537 format
=> 'mac-prefix',
1538 description
=> 'Prefix for autogenerated MAC addresses.',
1540 bwlimit
=> PVE
::JSONSchema
::get_standard_option
('bwlimit'),
1544 format
=> $u2f_format,
1545 description
=> 'u2f',
1550 # make schema accessible from outside (for documentation)
1551 sub get_datacenter_schema
{ return $datacenter_schema };
1553 sub parse_datacenter_config
{
1554 my ($filename, $raw) = @_;
1556 my $res = PVE
::JSONSchema
::parse_config
($datacenter_schema, $filename, $raw // '');
1558 if (my $migration = $res->{migration
}) {
1559 $res->{migration
} = PVE
::JSONSchema
::parse_property_string
($migration_format, $migration);
1562 if (my $ha = $res->{ha
}) {
1563 $res->{ha
} = PVE
::JSONSchema
::parse_property_string
($ha_format, $ha);
1566 # for backwards compatibility only, new migration property has precedence
1567 if (defined($res->{migration_unsecure
})) {
1568 if (defined($res->{migration
}->{type
})) {
1569 warn "deprecated setting 'migration_unsecure' and new 'migration: type' " .
1570 "set at same time! Ignore 'migration_unsecure'\n";
1572 $res->{migration
}->{type
} = ($res->{migration_unsecure
}) ?
'insecure' : 'secure';
1576 # for backwards compatibility only, applet maps to html5
1577 if (defined($res->{console
}) && $res->{console
} eq 'applet') {
1578 $res->{console
} = 'html5';
1584 sub write_datacenter_config
{
1585 my ($filename, $cfg) = @_;
1587 # map deprecated setting to new one
1588 if (defined($cfg->{migration_unsecure
}) && !defined($cfg->{migration
})) {
1589 my $migration_unsecure = delete $cfg->{migration_unsecure
};
1590 $cfg->{migration
}->{type
} = ($migration_unsecure) ?
'insecure' : 'secure';
1593 # map deprecated applet setting to html5
1594 if (defined($cfg->{console
}) && $cfg->{console
} eq 'applet') {
1595 $cfg->{console
} = 'html5';
1598 if (ref($cfg->{migration
})) {
1599 my $migration = $cfg->{migration
};
1600 $cfg->{migration
} = PVE
::JSONSchema
::print_property_string
($migration, $migration_format);
1603 if (ref($cfg->{ha
})) {
1604 my $ha = $cfg->{ha
};
1605 $cfg->{ha
} = PVE
::JSONSchema
::print_property_string
($ha, $ha_format);
1608 return PVE
::JSONSchema
::dump_config
($datacenter_schema, $filename, $cfg);
1611 cfs_register_file
('datacenter.cfg',
1612 \
&parse_datacenter_config
,
1613 \
&write_datacenter_config
);
1615 # X509 Certificate cache helper
1617 my $cert_cache_nodes = {};
1618 my $cert_cache_timestamp = time();
1619 my $cert_cache_fingerprints = {};
1621 sub update_cert_cache
{
1622 my ($update_node, $clear) = @_;
1624 syslog
('info', "Clearing outdated entries from certificate cache")
1627 $cert_cache_timestamp = time() if !defined($update_node);
1629 my $node_list = defined($update_node) ?
1630 [ $update_node ] : [ keys %$cert_cache_nodes ];
1632 foreach my $node (@$node_list) {
1633 my $clear_old = sub {
1634 if (my $old_fp = $cert_cache_nodes->{$node}) {
1635 # distrust old fingerprint
1636 delete $cert_cache_fingerprints->{$old_fp};
1637 # ensure reload on next proxied request
1638 delete $cert_cache_nodes->{$node};
1642 my $fp = eval { get_node_fingerprint
($node) };
1645 &$clear_old() if $clear;
1649 my $old_fp = $cert_cache_nodes->{$node};
1650 $cert_cache_fingerprints->{$fp} = 1;
1651 $cert_cache_nodes->{$node} = $fp;
1653 if (defined($old_fp) && $fp ne $old_fp) {
1654 delete $cert_cache_fingerprints->{$old_fp};
1659 # load and cache cert fingerprint once
1660 sub initialize_cert_cache
{
1663 update_cert_cache
($node)
1664 if defined($node) && !defined($cert_cache_nodes->{$node});
1667 sub read_ssl_cert_fingerprint
{
1668 my ($cert_path) = @_;
1670 my $bio = Net
::SSLeay
::BIO_new_file
($cert_path, 'r')
1671 or die "unable to read '$cert_path' - $!\n";
1673 my $cert = Net
::SSLeay
::PEM_read_bio_X509
($bio);
1674 Net
::SSLeay
::BIO_free
($bio);
1676 die "unable to read certificate from '$cert_path'\n" if !$cert;
1678 my $fp = Net
::SSLeay
::X509_get_fingerprint
($cert, 'sha256');
1679 Net
::SSLeay
::X509_free
($cert);
1681 die "unable to get fingerprint for '$cert_path' - got empty value\n"
1682 if !defined($fp) || $fp eq '';
1687 sub get_node_fingerprint
{
1690 my $cert_path = "/etc/pve/nodes/$node/pve-ssl.pem";
1691 my $custom_cert_path = "/etc/pve/nodes/$node/pveproxy-ssl.pem";
1693 $cert_path = $custom_cert_path if -f
$custom_cert_path;
1695 return read_ssl_cert_fingerprint
($cert_path);
1699 sub check_cert_fingerprint
{
1702 # clear cache every 30 minutes at least
1703 update_cert_cache
(undef, 1) if time() - $cert_cache_timestamp >= 60*30;
1705 # get fingerprint of server certificate
1706 my $fp = Net
::SSLeay
::X509_get_fingerprint
($cert, 'sha256');
1707 return 0 if !defined($fp) || $fp eq ''; # error
1710 for my $expected (keys %$cert_cache_fingerprints) {
1711 return 1 if $fp eq $expected;
1716 return 1 if &$check();
1718 # clear cache and retry at most once every minute
1719 if (time() - $cert_cache_timestamp >= 60) {
1720 syslog
('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache");
1721 update_cert_cache
();
1728 # bash completion helpers
1730 sub complete_next_vmid
{
1732 my $vmlist = get_vmlist
() || {};
1733 my $idlist = $vmlist->{ids
} || {};
1735 for (my $i = 100; $i < 10000; $i++) {
1736 return [$i] if !defined($idlist->{$i});
1744 my $vmlist = get_vmlist
();
1745 my $ids = $vmlist->{ids
} || {};
1747 return [ keys %$ids ];
1750 sub complete_local_vmid
{
1752 my $vmlist = get_vmlist
();
1753 my $ids = $vmlist->{ids
} || {};
1755 my $nodename = PVE
::INotify
::nodename
();
1758 foreach my $vmid (keys %$ids) {
1759 my $d = $ids->{$vmid};
1760 next if !$d->{node
} || $d->{node
} ne $nodename;
1767 sub complete_migration_target
{
1771 my $nodename = PVE
::INotify
::nodename
();
1773 my $nodelist = get_nodelist
();
1774 foreach my $node (@$nodelist) {
1775 next if $node eq $nodename;
1783 my ($node, $network_cidr) = @_;
1786 if (defined($network_cidr)) {
1787 # Use mtunnel via to get the remote node's ip inside $network_cidr.
1788 # This goes over the regular network (iow. uses get_ssh_info() with
1789 # $network_cidr undefined.
1790 # FIXME: Use the REST API client for this after creating an API entry
1791 # for get_migration_ip.
1792 my $default_remote = get_ssh_info
($node, undef);
1793 my $default_ssh = ssh_info_to_command
($default_remote);
1794 my $cmd =[@$default_ssh, 'pvecm', 'mtunnel',
1795 '-migration_network', $network_cidr,
1798 PVE
::Tools
::run_command
($cmd, outfunc
=> sub {
1801 die "internal error: unexpected output from mtunnel\n"
1803 if ($line =~ /^ip: '(.*)'$/) {
1806 die "internal error: bad output from mtunnel\n"
1810 die "failed to get ip for node '$node' in network '$network_cidr'\n"
1813 $ip = remote_node_ip
($node);
1819 network
=> $network_cidr,
1823 sub ssh_info_to_command_base
{
1824 my ($info, @extra_options) = @_;
1828 '-o', 'BatchMode=yes',
1829 '-o', 'HostKeyAlias='.$info->{name
},
1834 sub ssh_info_to_command
{
1835 my ($info, @extra_options) = @_;
1836 my $cmd = ssh_info_to_command_base
($info, @extra_options);
1837 push @$cmd, "root\@$info->{ip}";
1841 sub assert_joinable
{
1842 my ($local_addr, $ring0_addr, $ring1_addr, $force) = @_;
1845 my $error = sub { $errors .= "* $_[0]\n"; };
1848 $error->("authentication key '$authfile' already exists");
1851 if (-f
$clusterconf) {
1852 $error->("cluster config '$clusterconf' already exists");
1855 my $vmlist = get_vmlist
();
1856 if ($vmlist && $vmlist->{ids
} && scalar(keys %{$vmlist->{ids
}})) {
1857 $error->("this host already contains virtual guests");
1860 if (run_command
(['corosync-quorumtool', '-l'], noerr
=> 1, quiet
=> 1) == 0) {
1861 $error->("corosync is already running, is this node already in a cluster?!");
1864 # check if corosync ring IPs are configured on the current nodes interfaces
1865 my $check_ip = sub {
1866 my $ip = shift // return;
1868 if (!PVE
::JSONSchema
::pve_verify_ip
($ip, 1)) {
1870 eval { $ip = PVE
::Network
::get_ip_from_hostname
($host); };
1872 $error->("$logid: cannot use '$host': $@\n") ;
1877 my $cidr = (Net
::IP
::ip_is_ipv6
($ip)) ?
"$ip/128" : "$ip/32";
1878 my $configured_ips = PVE
::Network
::get_local_ip_from_cidr
($cidr);
1880 $error->("$logid: cannot use IP '$ip', it must be configured exactly once on local node!\n")
1881 if (scalar(@$configured_ips) != 1);
1884 $check_ip->($local_addr, 'local node address');
1885 $check_ip->($ring0_addr, 'ring0');
1886 $check_ip->($ring1_addr, 'ring1');
1889 warn "detected the following error(s):\n$errors";
1890 die "Check if node may join a cluster failed!\n" if !$force;
1894 # NOTE: filesystem must be offline here, no DB changes allowed
1895 my $backup_cfs_database = sub {
1901 my $backup_fn = "$dbbackupdir/config-$ctime.sql.gz";
1903 print "backup old database to '$backup_fn'\n";
1905 my $cmd = [ ['sqlite3', $dbfile, '.dump'], ['gzip', '-', \
">${backup_fn}"] ];
1906 run_command
($cmd, 'errmsg' => "cannot backup old database\n");
1908 my $maxfiles = 10; # purge older backup
1909 my $backups = [ sort { $b cmp $a } <$dbbackupdir/config-*.sql
.gz
> ];
1911 if ((my $count = scalar(@$backups)) > $maxfiles) {
1912 foreach my $f (@$backups[$maxfiles..$count-1]) {
1913 next if $f !~ m/^(\S+)$/; # untaint
1914 print "delete old backup '$1'\n";
1923 my $nodename = PVE
::INotify
::nodename
();
1924 my $local_ip_address = remote_node_ip
($nodename);
1926 my ($ring0_addr, $ring1_addr) = $param->@{'ring0_addr', 'ring1_addr'};
1927 # check if we can join with the given parameters and current node state
1928 assert_joinable
($local_ip_address, $ring0_addr, $ring1_addr, $param->{force
});
1930 setup_sshd_config
();
1931 setup_rootsshconfig
();
1934 # make sure known_hosts is on local filesystem
1935 ssh_unmerge_known_hosts
();
1937 my $host = $param->{hostname
};
1939 username
=> 'root@pam',
1940 password
=> $param->{password
},
1941 cookie_name
=> 'PVEAuthCookie',
1942 protocol
=> 'https',
1947 if (my $fp = $param->{fingerprint
}) {
1948 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
1950 # API schema ensures that we can only get here from CLI handler
1951 $conn_args->{manual_verification
} = 1;
1954 print "Establishing API connection with host '$host'\n";
1956 my $conn = PVE
::APIClient
::LWP-
>new(%$conn_args);
1959 # login raises an exception on failure, so if we get here we're good
1960 print "Login succeeded.\n";
1963 $args->{force
} = $param->{force
} if defined($param->{force
});
1964 $args->{nodeid
} = $param->{nodeid
} if $param->{nodeid
};
1965 $args->{votes
} = $param->{votes
} if defined($param->{votes
});
1966 $args->{ring0_addr
} = $ring0_addr // $local_ip_address;
1967 $args->{ring1_addr
} = $ring1_addr if defined($ring1_addr);
1969 print "Request addition of this node\n";
1970 my $res = $conn->post("/cluster/config/nodes/$nodename", $args);
1972 print "Join request OK, finishing setup locally\n";
1974 # added successfuly - now prepare local node
1975 finish_join
($nodename, $res->{corosync_conf
}, $res->{corosync_authkey
});
1979 my ($nodename, $corosync_conf, $corosync_authkey) = @_;
1981 mkdir "$localclusterdir";
1982 PVE
::Tools
::file_set_contents
($authfile, $corosync_authkey);
1983 PVE
::Tools
::file_set_contents
($localclusterconf, $corosync_conf);
1985 print "stopping pve-cluster service\n";
1986 my $cmd = ['systemctl', 'stop', 'pve-cluster'];
1987 run_command
($cmd, errmsg
=> "can't stop pve-cluster service");
1989 $backup_cfs_database->($dbfile);
1992 $cmd = ['systemctl', 'start', 'corosync', 'pve-cluster'];
1993 run_command
($cmd, errmsg
=> "starting pve-cluster failed");
1997 while (!check_cfs_quorum
(1)) {
1999 print "waiting for quorum...";
2005 print "OK\n" if !$printqmsg;
2007 updatecerts_and_ssh
(1);
2009 print "generated new node certificate, restart pveproxy and pvedaemon services\n";
2010 run_command
(['systemctl', 'reload-or-restart', 'pvedaemon', 'pveproxy']);
2012 print "successfully added node '$nodename' to cluster.\n";
2015 sub updatecerts_and_ssh
{
2016 my ($force_new_cert, $silent) = @_;
2018 my $p = sub { print "$_[0]\n" if !$silent };
2020 setup_rootsshconfig
();
2022 gen_pve_vzdump_symlink
();
2024 if (!check_cfs_quorum
(1)) {
2025 return undef if $silent;
2026 die "no quorum - unable to update files\n";
2031 my $nodename = PVE
::INotify
::nodename
();
2032 my $local_ip_address = remote_node_ip
($nodename);
2034 $p->("(re)generate node files");
2035 $p->("generate new node certificate") if $force_new_cert;
2036 gen_pve_node_files
($nodename, $local_ip_address, $force_new_cert);
2038 $p->("merge authorized SSH keys and known hosts");
2040 ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
2041 gen_pve_vzdump_files
();