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";
73 'datacenter.cfg' => 1,
74 'replication.cfg' => 1,
76 'corosync.conf.new' => 1,
79 'priv/shadow.cfg' => 1,
83 'ha/crm_commands' => 1,
84 'ha/manager_status' => 1,
85 'ha/resources.cfg' => 1,
91 # only write output if something fails
96 my $record = sub { $outbuf .= shift . "\n"; };
98 eval { run_command
($cmd, outfunc
=> $record, errfunc
=> $record) };
101 print STDERR
$outbuf;
106 sub check_cfs_quorum
{
109 # note: -w filename always return 1 for root, so wee need
110 # to use File::lstat here
111 my $st = File
::stat::lstat("$basedir/local");
112 my $quorate = ($st && (($st->mode & 0200) != 0));
114 die "cluster not ready - no quorum?\n" if !$quorate && !$noerr;
119 sub check_cfs_is_mounted
{
122 my $res = -l
"$basedir/local";
124 die "pve configuration filesystem not mounted\n"
133 check_cfs_is_mounted
();
135 my @required_dirs = (
138 "$basedir/nodes/$nodename",
139 "$basedir/nodes/$nodename/lxc",
140 "$basedir/nodes/$nodename/qemu-server",
141 "$basedir/nodes/$nodename/openvz",
142 "$basedir/nodes/$nodename/priv");
144 foreach my $dir (@required_dirs) {
146 mkdir($dir) || $! == EEXIST
|| die "unable to create directory '$dir' - $!\n";
153 return if -f
"$authprivkeyfn";
155 check_cfs_is_mounted
();
157 mkdir $authdir || $! == EEXIST
|| die "unable to create dir '$authdir' - $!\n";
159 run_silent_cmd
(['openssl', 'genrsa', '-out', $authprivkeyfn, '2048']);
161 run_silent_cmd
(['openssl', 'rsa', '-in', $authprivkeyfn, '-pubout', '-out', $authpubkeyfn]);
166 return if -f
$pveca_key_fn;
169 run_silent_cmd
(['openssl', 'genrsa', '-out', $pveca_key_fn, '4096']);
172 die "unable to generate pve ca key:\n$@" if $@;
177 if (-f
$pveca_key_fn && -f
$pveca_cert_fn) {
183 # we try to generate an unique 'subject' to avoid browser problems
184 # (reused serial numbers, ..)
186 UUID
::generate
($uuid);
188 UUID
::unparse
($uuid, $uuid_str);
191 # wrap openssl with faketime to prevent bug #904
192 run_silent_cmd
(['faketime', 'yesterday', 'openssl', 'req', '-batch',
193 '-days', '3650', '-new', '-x509', '-nodes', '-key',
194 $pveca_key_fn, '-out', $pveca_cert_fn, '-subj',
195 "/CN=Proxmox Virtual Environment/OU=$uuid_str/O=PVE Cluster Manager CA/"]);
198 die "generating pve root certificate failed:\n$@" if $@;
203 sub gen_pve_ssl_key
{
206 die "no node name specified" if !$nodename;
208 my $pvessl_key_fn = "$basedir/nodes/$nodename/pve-ssl.key";
210 return if -f
$pvessl_key_fn;
213 run_silent_cmd
(['openssl', 'genrsa', '-out', $pvessl_key_fn, '2048']);
216 die "unable to generate pve ssl key for node '$nodename':\n$@" if $@;
219 sub gen_pve_www_key
{
221 return if -f
$pvewww_key_fn;
224 run_silent_cmd
(['openssl', 'genrsa', '-out', $pvewww_key_fn, '2048']);
227 die "unable to generate pve www key:\n$@" if $@;
233 PVE
::Tools
::file_set_contents
($pveca_srl_fn, $serial);
236 sub gen_pve_ssl_cert
{
237 my ($force, $nodename, $ip) = @_;
239 die "no node name specified" if !$nodename;
240 die "no IP specified" if !$ip;
242 my $pvessl_cert_fn = "$basedir/nodes/$nodename/pve-ssl.pem";
244 return if !$force && -f
$pvessl_cert_fn;
246 my $names = "IP:127.0.0.1,IP:::1,DNS:localhost";
248 my $rc = PVE
::INotify
::read_file
('resolvconf');
252 my $fqdn = $nodename;
254 $names .= ",DNS:$nodename";
256 if ($rc && $rc->{search
}) {
257 $fqdn = $nodename . "." . $rc->{search
};
258 $names .= ",DNS:$fqdn";
261 my $sslconf = <<__EOD;
262 RANDFILE = /root/.rnd
267 distinguished_name = req_distinguished_name
268 req_extensions = v3_req
270 string_mask = nombstr
272 [ req_distinguished_name ]
273 organizationalUnitName = PVE Cluster Node
274 organizationName = Proxmox Virtual Environment
278 basicConstraints = CA:FALSE
279 extendedKeyUsage = serverAuth
280 subjectAltName = $names
283 my $cfgfn = "/tmp/pvesslconf-$$.tmp";
284 my $fh = IO
::File-
>new ($cfgfn, "w");
288 my $reqfn = "/tmp/pvecertreq-$$.tmp";
291 my $pvessl_key_fn = "$basedir/nodes/$nodename/pve-ssl.key";
293 run_silent_cmd
(['openssl', 'req', '-batch', '-new', '-config', $cfgfn,
294 '-key', $pvessl_key_fn, '-out', $reqfn]);
300 die "unable to generate pve certificate request:\n$err";
303 update_serial
("0000000000000000") if ! -f
$pveca_srl_fn;
306 # wrap openssl with faketime to prevent bug #904
307 run_silent_cmd
(['faketime', 'yesterday', 'openssl', 'x509', '-req',
308 '-in', $reqfn, '-days', '3650', '-out', $pvessl_cert_fn,
309 '-CAkey', $pveca_key_fn, '-CA', $pveca_cert_fn,
310 '-CAserial', $pveca_srl_fn, '-extfile', $cfgfn]);
316 die "unable to generate pve ssl certificate:\n$err";
323 sub gen_pve_node_files
{
324 my ($nodename, $ip, $opt_force) = @_;
326 gen_local_dirs
($nodename);
330 # make sure we have a (cluster wide) secret
331 # for CSRFR prevention
334 # make sure we have a (per node) private key
335 gen_pve_ssl_key
($nodename);
337 # make sure we have a CA
338 my $force = gen_pveca_cert
();
340 $force = 1 if $opt_force;
342 gen_pve_ssl_cert
($force, $nodename, $ip);
345 my $vzdump_cron_dummy = <<__EOD;
346 # cluster wide vzdump cron schedule
347 # Atomatically generated file - do not edit
349 PATH="/usr/sbin:/usr/bin:/sbin:/bin"
353 sub gen_pve_vzdump_symlink
{
355 my $filename = "/etc/pve/vzdump.cron";
357 my $link_fn = "/etc/cron.d/vzdump";
359 if ((-f
$filename) && (! -l
$link_fn)) {
360 rename($link_fn, "/root/etc_cron_vzdump.org"); # make backup if file exists
361 symlink($filename, $link_fn);
365 sub gen_pve_vzdump_files
{
367 my $filename = "/etc/pve/vzdump.cron";
369 PVE
::Tools
::file_set_contents
($filename, $vzdump_cron_dummy)
372 gen_pve_vzdump_symlink
();
379 my $ipcc_send_rec = sub {
380 my ($msgid, $data) = @_;
382 my $res = PVE
::IPCC
::ipcc_send_rec
($msgid, $data);
384 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
389 my $ipcc_send_rec_json = sub {
390 my ($msgid, $data) = @_;
392 my $res = PVE
::IPCC
::ipcc_send_rec
($msgid, $data);
394 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
396 return decode_json
($res);
399 my $ipcc_get_config = sub {
402 my $bindata = pack "Z*", $path;
403 my $res = PVE
::IPCC
::ipcc_send_rec
(CFS_IPC_GET_CONFIG
, $bindata);
404 if (!defined($res)) {
406 return undef if $! == ENOENT
;
415 my $ipcc_get_status = sub {
416 my ($name, $nodename) = @_;
418 my $bindata = pack "Z[256]Z[256]", $name, ($nodename || "");
419 return PVE
::IPCC
::ipcc_send_rec
(CFS_IPC_GET_STATUS
, $bindata);
422 my $ipcc_update_status = sub {
423 my ($name, $data) = @_;
425 my $raw = ref($data) ? encode_json
($data) : $data;
427 my $bindata = pack "Z[256]Z*", $name, $raw;
429 return &$ipcc_send_rec(CFS_IPC_SET_STATUS
, $bindata);
433 my ($priority, $ident, $tag, $msg) = @_;
435 my $bindata = pack "CCCZ*Z*Z*", $priority, bytes
::length($ident) + 1,
436 bytes
::length($tag) + 1, $ident, $tag, $msg;
438 return &$ipcc_send_rec(CFS_IPC_LOG_CLUSTER_MSG
, $bindata);
441 my $ipcc_get_cluster_log = sub {
442 my ($user, $max) = @_;
444 $max = 0 if !defined($max);
446 my $bindata = pack "VVVVZ*", $max, 0, 0, 0, ($user || "");
447 return &$ipcc_send_rec(CFS_IPC_GET_CLUSTER_LOG
, $bindata);
455 my $res = &$ipcc_send_rec_json(CFS_IPC_GET_FS_VERSION
);
456 #warn "GOT1: " . Dumper($res);
457 die "no starttime\n" if !$res->{starttime
};
459 if (!$res->{starttime
} || !$versions->{starttime
} ||
460 $res->{starttime
} != $versions->{starttime
}) {
461 #print "detected changed starttime\n";
480 if (!$clinfo->{version
} || $clinfo->{version
} != $versions->{clinfo
}) {
481 #warn "detected new clinfo\n";
482 $clinfo = &$ipcc_send_rec_json(CFS_IPC_GET_CLUSTER_INFO
);
493 if (!$vmlist->{version
} || $vmlist->{version
} != $versions->{vmlist
}) {
494 #warn "detected new vmlist1\n";
495 $vmlist = &$ipcc_send_rec_json(CFS_IPC_GET_GUEST_LIST
);
515 return $clinfo->{nodelist
};
520 my $nodelist = $clinfo->{nodelist
};
524 my $nodename = PVE
::INotify
::nodename
();
526 if (!$nodelist || !$nodelist->{$nodename}) {
527 return [ $nodename ];
530 return [ keys %$nodelist ];
533 # $data must be a chronological descending ordered array of tasks
534 sub broadcast_tasklist
{
537 # the serialized list may not get bigger than 32kb (CFS_MAX_STATUS_SIZE
538 # from pmxcfs) - drop older items until we satisfy this constraint
539 my $size = length(encode_json
($data));
540 while ($size >= (32 * 1024)) {
542 $size = length(encode_json
($data));
546 &$ipcc_update_status("tasklist", $data);
552 my $tasklistcache = {};
557 my $kvstore = $versions->{kvstore
} || {};
559 my $nodelist = get_nodelist
();
562 foreach my $node (@$nodelist) {
563 next if $nodename && ($nodename ne $node);
565 my $ver = $kvstore->{$node}->{tasklist
} if $kvstore->{$node};
566 my $cd = $tasklistcache->{$node};
567 if (!$cd || !$ver || !$cd->{version
} ||
568 ($cd->{version
} != $ver)) {
569 my $raw = &$ipcc_get_status("tasklist", $node) || '[]';
570 my $data = decode_json
($raw);
572 $cd = $tasklistcache->{$node} = {
576 } elsif ($cd && $cd->{data
}) {
577 push @$res, @{$cd->{data
}};
581 syslog
('err', $err) if $err;
588 my ($rrdid, $data) = @_;
591 &$ipcc_update_status("rrd/$rrdid", $data);
598 my $last_rrd_dump = 0;
599 my $last_rrd_data = "";
605 my $diff = $ctime - $last_rrd_dump;
607 return $last_rrd_data;
612 $raw = &$ipcc_send_rec(CFS_IPC_GET_RRD_DUMP
);
624 while ($raw =~ s/^(.*)\n//) {
625 my ($key, @ela) = split(/:/, $1);
627 next if !(scalar(@ela) > 1);
628 $res->{$key} = [ map { $_ eq 'U' ?
undef : $_ } @ela ];
632 $last_rrd_dump = $ctime;
633 $last_rrd_data = $res;
638 sub create_rrd_data
{
639 my ($rrdname, $timeframe, $cf) = @_;
641 my $rrddir = "/var/lib/rrdcached/db";
643 my $rrd = "$rrddir/$rrdname";
647 day
=> [ 60*30, 70 ],
648 week
=> [ 60*180, 70 ],
649 month
=> [ 60*720, 70 ],
650 year
=> [ 60*10080, 70 ],
653 my ($reso, $count) = @{$setup->{$timeframe}};
654 my $ctime = $reso*int(time()/$reso);
655 my $req_start = $ctime - $reso*$count;
657 $cf = "AVERAGE" if !$cf;
665 my $socket = "/var/run/rrdcached.sock";
666 push @args, "--daemon" => "unix:$socket" if -S
$socket;
668 my ($start, $step, $names, $data) = RRDs
::fetch
($rrd, $cf, @args);
670 my $err = RRDs
::error
;
671 die "RRD error: $err\n" if $err;
673 die "got wrong time resolution ($step != $reso)\n"
677 my $fields = scalar(@$names);
678 for my $line (@$data) {
679 my $entry = { 'time' => $start };
681 for (my $i = 0; $i < $fields; $i++) {
682 my $name = $names->[$i];
683 if (defined(my $val = $line->[$i])) {
684 $entry->{$name} = $val;
686 # leave empty fields undefined
687 # maybe make this configurable?
696 sub create_rrd_graph
{
697 my ($rrdname, $timeframe, $ds, $cf) = @_;
699 # Using RRD graph is clumsy - maybe it
700 # is better to simply fetch the data, and do all display
701 # related things with javascript (new extjs html5 graph library).
703 my $rrddir = "/var/lib/rrdcached/db";
705 my $rrd = "$rrddir/$rrdname";
707 my @ids = PVE
::Tools
::split_list
($ds);
709 my $ds_txt = join('_', @ids);
711 my $filename = "${rrd}_${ds_txt}.png";
715 day
=> [ 60*30, 70 ],
716 week
=> [ 60*180, 70 ],
717 month
=> [ 60*720, 70 ],
718 year
=> [ 60*10080, 70 ],
721 my ($reso, $count) = @{$setup->{$timeframe}};
724 "--imgformat" => "PNG",
728 "--start" => - $reso*$count,
730 "--lower-limit" => 0,
733 my $socket = "/var/run/rrdcached.sock";
734 push @args, "--daemon" => "unix:$socket" if -S
$socket;
736 my @coldef = ('#00ddff', '#ff0000');
738 $cf = "AVERAGE" if !$cf;
741 foreach my $id (@ids) {
742 my $col = $coldef[$i++] || die "fixme: no color definition";
743 push @args, "DEF:${id}=$rrd:${id}:$cf";
745 if ($id eq 'cpu' || $id eq 'iowait') {
746 push @args, "CDEF:${id}_per=${id},100,*";
747 $dataid = "${id}_per";
749 push @args, "LINE2:${dataid}${col}:${id}";
752 push @args, '--full-size-mode';
754 # we do not really store data into the file
755 my $res = RRDs
::graphv
('-', @args);
757 my $err = RRDs
::error
;
758 die "RRD error: $err\n" if $err;
760 return { filename
=> $filename, image
=> $res->{image
} };
763 # a fast way to read files (avoid fuse overhead)
767 return &$ipcc_get_config($path);
770 sub get_cluster_log
{
771 my ($user, $max) = @_;
773 return &$ipcc_get_cluster_log($user, $max);
778 sub cfs_register_file
{
779 my ($filename, $parser, $writer) = @_;
781 $observed->{$filename} || die "unknown file '$filename'";
783 die "file '$filename' already registered" if $file_info->{$filename};
785 $file_info->{$filename} = {
791 my $ccache_read = sub {
792 my ($filename, $parser, $version) = @_;
794 $ccache->{$filename} = {} if !$ccache->{$filename};
796 my $ci = $ccache->{$filename};
798 if (!$ci->{version
} || !$version || $ci->{version
} != $version) {
799 # we always call the parser, even when the file does not exists
800 # (in that case $data is undef)
801 my $data = get_config
($filename);
802 $ci->{data
} = &$parser("/etc/pve/$filename", $data);
803 $ci->{version
} = $version;
806 my $res = ref($ci->{data
}) ? dclone
($ci->{data
}) : $ci->{data
};
811 sub cfs_file_version
{
816 if ($filename =~ m!^nodes/[^/]+/(openvz|lxc|qemu-server)/(\d+)\.conf$!) {
817 my ($type, $vmid) = ($1, $2);
818 if ($vmlist && $vmlist->{ids
} && $vmlist->{ids
}->{$vmid}) {
819 $version = $vmlist->{ids
}->{$vmid}->{version
};
821 $infotag = "/$type/";
823 $infotag = $filename;
824 $version = $versions->{$filename};
827 my $info = $file_info->{$infotag} ||
828 die "unknown file type '$filename'\n";
830 return wantarray ?
($version, $info) : $version;
836 my ($version, $info) = cfs_file_version
($filename);
837 my $parser = $info->{parser
};
839 return &$ccache_read($filename, $parser, $version);
843 my ($filename, $data) = @_;
845 my ($version, $info) = cfs_file_version
($filename);
847 my $writer = $info->{writer
} || die "no writer defined";
849 my $fsname = "/etc/pve/$filename";
851 my $raw = &$writer($fsname, $data);
853 if (my $ci = $ccache->{$filename}) {
854 $ci->{version
} = undef;
857 PVE
::Tools
::file_set_contents
($fsname, $raw);
861 my ($lockid, $timeout, $code, @param) = @_;
863 my $prev_alarm = alarm(0); # suspend outer alarm early
868 # this timeout is for acquire the lock
869 $timeout = 10 if !$timeout;
871 my $filename = "$lockdir/$lockid";
878 die "pve cluster filesystem not online.\n";
881 my $timeout_err = sub { die "got lock request timeout\n"; };
882 local $SIG{ALRM
} = $timeout_err;
886 $got_lock = mkdir($filename);
887 $timeout = alarm(0) - 1; # we'll sleep for 1s, see down below
891 $timeout_err->() if $timeout <= 0;
893 print STDERR
"trying to acquire cfs lock '$lockid' ...\n";
894 utime (0, 0, $filename); # cfs unlock request
898 # fixed command timeout: cfs locks have a timeout of 120
899 # using 60 gives us another 60 seconds to abort the task
900 local $SIG{ALRM
} = sub { die "got lock timeout - aborting command\n"; };
903 cfs_update
(); # make sure we read latest versions inside code()
905 $res = &$code(@param);
912 $err = "no quorum!\n" if !$got_lock && !check_cfs_quorum
(1);
914 rmdir $filename if $got_lock; # if we held the lock always unlock again
919 $@ = "error with cfs lock '$lockid': $err";
929 my ($filename, $timeout, $code, @param) = @_;
931 my $info = $observed->{$filename} || die "unknown file '$filename'";
933 my $lockid = "file-$filename";
934 $lockid =~ s/[.\/]/_
/g
;
936 &$cfs_lock($lockid, $timeout, $code, @param);
939 sub cfs_lock_storage
{
940 my ($storeid, $timeout, $code, @param) = @_;
942 my $lockid = "storage-$storeid";
944 &$cfs_lock($lockid, $timeout, $code, @param);
947 sub cfs_lock_domain
{
948 my ($domainname, $timeout, $code, @param) = @_;
950 my $lockid = "domain-$domainname";
952 &$cfs_lock($lockid, $timeout, $code, @param);
956 my ($account, $timeout, $code, @param) = @_;
958 my $lockid = "acme-$account";
960 &$cfs_lock($lockid, $timeout, $code, @param);
978 my ($priority, $ident, $msg) = @_;
980 if (my $tmp = $log_levels->{$priority}) {
984 die "need numeric log priority" if $priority !~ /^\d+$/;
986 my $tag = PVE
::SafeSyslog
::tag
();
988 $msg = "empty message" if !$msg;
990 $ident = "" if !$ident;
991 $ident = encode
("ascii", $ident,
992 sub { sprintf "\\u%04x", shift });
994 my $ascii = encode
("ascii", $msg, sub { sprintf "\\u%04x", shift });
997 syslog
($priority, "<%s> %s", $ident, $ascii);
999 syslog
($priority, "%s", $ascii);
1002 eval { &$ipcc_log($priority, $ident, $tag, $ascii); };
1004 syslog
("err", "writing cluster log failed: $@") if $@;
1007 sub check_vmid_unused
{
1008 my ($vmid, $noerr) = @_;
1010 my $vmlist = get_vmlist
();
1012 my $d = $vmlist->{ids
}->{$vmid};
1013 return 1 if !defined($d);
1015 return undef if $noerr;
1017 my $vmtypestr = $d->{type
} eq 'qemu' ?
'VM' : 'CT';
1018 die "$vmtypestr $vmid already exists on node '$d->{node}'\n";
1021 sub check_node_exists
{
1022 my ($nodename, $noerr) = @_;
1024 my $nodelist = $clinfo->{nodelist
};
1025 return 1 if $nodelist && $nodelist->{$nodename};
1027 return undef if $noerr;
1029 die "no such cluster node '$nodename'\n";
1032 # this is also used to get the IP of the local node
1033 sub remote_node_ip
{
1034 my ($nodename, $noerr) = @_;
1036 my $nodelist = $clinfo->{nodelist
};
1037 if ($nodelist && $nodelist->{$nodename}) {
1038 if (my $ip = $nodelist->{$nodename}->{ip
}) {
1039 return $ip if !wantarray;
1040 my $family = $nodelist->{$nodename}->{address_family
};
1042 $nodelist->{$nodename}->{address_family
} =
1044 PVE
::Tools
::get_host_address_family
($ip);
1046 return wantarray ?
($ip, $family) : $ip;
1050 # fallback: try to get IP by other means
1051 return PVE
::Network
::get_ip_from_hostname
($nodename, $noerr);
1054 sub get_local_migration_ip
{
1055 my ($migration_network, $noerr) = @_;
1057 my $cidr = $migration_network;
1059 if (!defined($cidr)) {
1060 my $dc_conf = cfs_read_file
('datacenter.cfg');
1061 $cidr = $dc_conf->{migration
}->{network
}
1062 if defined($dc_conf->{migration
}->{network
});
1065 if (defined($cidr)) {
1066 my $ips = PVE
::Network
::get_local_ip_from_cidr
($cidr);
1068 die "could not get migration ip: no IP address configured on local " .
1069 "node for network '$cidr'\n" if !$noerr && (scalar(@$ips) == 0);
1071 die "could not get migration ip: multiple IP address configured for " .
1072 "network '$cidr'\n" if !$noerr && (scalar(@$ips) > 1);
1080 # ssh related utility functions
1082 sub ssh_merge_keys
{
1083 # remove duplicate keys in $sshauthkeys
1084 # ssh-copy-id simply add keys, so the file can grow to large
1087 if (-f
$sshauthkeys) {
1088 $data = PVE
::Tools
::file_get_contents
($sshauthkeys, 128*1024);
1093 if (-f
$rootsshauthkeysbackup) {
1095 $data .= PVE
::Tools
::file_get_contents
($rootsshauthkeysbackup, 128*1024);
1100 # always add ourself
1101 if (-f
$ssh_rsa_id) {
1102 my $pub = PVE
::Tools
::file_get_contents
($ssh_rsa_id);
1104 $data .= "\n$pub\n";
1109 my @lines = split(/\n/, $data);
1110 foreach my $line (@lines) {
1111 if ($line !~ /^#/ && $line =~ m/(^|\s)ssh-(rsa|dsa)\s+(\S+)\s+\S+$/) {
1112 next if $vhash->{$3}++;
1114 $newdata .= "$line\n";
1117 PVE
::Tools
::file_set_contents
($sshauthkeys, $newdata, 0600);
1119 if ($found_backup && -l
$rootsshauthkeys) {
1120 # everything went well, so we can remove the backup
1121 unlink $rootsshauthkeysbackup;
1125 sub setup_sshd_config
{
1128 my $conf = PVE
::Tools
::file_get_contents
($sshd_config_fn);
1130 return if $conf =~ m/^PermitRootLogin\s+yes\s*$/m;
1132 if ($conf !~ s/^#?PermitRootLogin.*$/PermitRootLogin yes/m) {
1134 $conf .= "\nPermitRootLogin yes\n";
1137 PVE
::Tools
::file_set_contents
($sshd_config_fn, $conf);
1139 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'sshd']);
1142 sub setup_rootsshconfig
{
1144 # create ssh key if it does not exist
1145 if (! -f
$ssh_rsa_id) {
1146 mkdir '/root/.ssh/';
1147 system ("echo|ssh-keygen -t rsa -N '' -b 2048 -f ${ssh_rsa_id_priv}");
1150 # create ssh config if it does not exist
1151 if (! -f
$rootsshconfig) {
1153 if (my $fh = IO
::File-
>new($rootsshconfig, O_CREAT
|O_WRONLY
|O_EXCL
, 0640)) {
1154 # this is the default ciphers list from Debian's OpenSSH package (OpenSSH_7.4p1 Debian-10, OpenSSL 1.0.2k 26 Jan 2017)
1155 # changed order to put AES before Chacha20 (most hardware has AESNI)
1156 print $fh "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm\@openssh.com,aes256-gcm\@openssh.com,chacha20-poly1305\@openssh.com\n";
1162 sub setup_ssh_keys
{
1168 if (! -f
$sshauthkeys) {
1170 if (-f
$rootsshauthkeys) {
1171 $old = PVE
::Tools
::file_get_contents
($rootsshauthkeys, 128*1024);
1173 if (my $fh = IO
::File-
>new ($sshauthkeys, O_CREAT
|O_WRONLY
|O_EXCL
, 0400)) {
1174 PVE
::Tools
::safe_print
($sshauthkeys, $fh, $old) if $old;
1180 warn "can't create shared ssh key database '$sshauthkeys'\n"
1181 if ! -f
$sshauthkeys;
1183 if (-f
$rootsshauthkeys && ! -l
$rootsshauthkeys) {
1184 if (!rename($rootsshauthkeys , $rootsshauthkeysbackup)) {
1185 warn "rename $rootsshauthkeys failed - $!\n";
1189 if (! -l
$rootsshauthkeys) {
1190 symlink $sshauthkeys, $rootsshauthkeys;
1193 if (! -l
$rootsshauthkeys) {
1194 warn "can't create symlink for ssh keys '$rootsshauthkeys' -> '$sshauthkeys'\n";
1196 unlink $rootsshauthkeysbackup if $import_ok;
1200 sub ssh_unmerge_known_hosts
{
1201 return if ! -l
$sshglobalknownhosts;
1204 $old = PVE
::Tools
::file_get_contents
($sshknownhosts, 128*1024)
1205 if -f
$sshknownhosts;
1207 PVE
::Tools
::file_set_contents
($sshglobalknownhosts, $old);
1210 sub ssh_merge_known_hosts
{
1211 my ($nodename, $ip_address, $createLink) = @_;
1213 die "no node name specified" if !$nodename;
1214 die "no ip address specified" if !$ip_address;
1216 # ssh lowercases hostnames (aliases) before comparision, so we need too
1217 $nodename = lc($nodename);
1218 $ip_address = lc($ip_address);
1222 if (! -f
$sshknownhosts) {
1223 if (my $fh = IO
::File-
>new($sshknownhosts, O_CREAT
|O_WRONLY
|O_EXCL
, 0600)) {
1228 my $old = PVE
::Tools
::file_get_contents
($sshknownhosts, 128*1024);
1232 if ((! -l
$sshglobalknownhosts) && (-f
$sshglobalknownhosts)) {
1233 $new = PVE
::Tools
::file_get_contents
($sshglobalknownhosts, 128*1024);
1236 my $hostkey = PVE
::Tools
::file_get_contents
($ssh_host_rsa_id);
1237 # Note: file sometimes containe emty lines at start, so we use multiline match
1238 die "can't parse $ssh_host_rsa_id" if $hostkey !~ m/^(ssh-rsa\s\S+)(\s.*)?$/m;
1247 my $merge_line = sub {
1248 my ($line, $all) = @_;
1250 return if $line =~ m/^\s*$/; # skip empty lines
1251 return if $line =~ m/^#/; # skip comments
1253 if ($line =~ m/^(\S+)\s(ssh-rsa\s\S+)(\s.*)?$/) {
1256 if (!$vhash->{$key}) {
1258 if ($key =~ m/\|1\|([^\|\s]+)\|([^\|\s]+)$/) {
1259 my $salt = decode_base64
($1);
1261 my $hmac = Digest
::HMAC_SHA1-
>new($salt);
1262 $hmac->add($nodename);
1263 my $hd = $hmac->b64digest . '=';
1264 if ($digest eq $hd) {
1265 if ($rsakey eq $hostkey) {
1266 $found_nodename = 1;
1271 $hmac = Digest
::HMAC_SHA1-
>new($salt);
1272 $hmac->add($ip_address);
1273 $hd = $hmac->b64digest . '=';
1274 if ($digest eq $hd) {
1275 if ($rsakey eq $hostkey) {
1276 $found_local_ip = 1;
1282 $key = lc($key); # avoid duplicate entries, ssh compares lowercased
1283 if ($key eq $ip_address) {
1284 $found_local_ip = 1 if $rsakey eq $hostkey;
1285 } elsif ($key eq $nodename) {
1286 $found_nodename = 1 if $rsakey eq $hostkey;
1296 while ($old && $old =~ s/^((.*?)(\n|$))//) {
1298 &$merge_line($line, 1);
1301 while ($new && $new =~ s/^((.*?)(\n|$))//) {
1303 &$merge_line($line);
1306 # add our own key if not already there
1307 $data .= "$nodename $hostkey\n" if !$found_nodename;
1308 $data .= "$ip_address $hostkey\n" if !$found_local_ip;
1310 PVE
::Tools
::file_set_contents
($sshknownhosts, $data);
1312 return if !$createLink;
1314 unlink $sshglobalknownhosts;
1315 symlink $sshknownhosts, $sshglobalknownhosts;
1317 warn "can't create symlink for ssh known hosts '$sshglobalknownhosts' -> '$sshknownhosts'\n"
1318 if ! -l
$sshglobalknownhosts;
1322 my $migration_format = {
1326 enum
=> ['secure', 'insecure'],
1327 description
=> "Migration traffic is encrypted using an SSH tunnel by " .
1328 "default. On secure, completely private networks this can be " .
1329 "disabled to increase performance.",
1330 default => 'secure',
1334 type
=> 'string', format
=> 'CIDR',
1335 format_description
=> 'CIDR',
1336 description
=> "CIDR of the (sub) network that is used for migration."
1340 my $datacenter_schema = {
1342 additionalProperties
=> 0,
1347 description
=> "Default keybord layout for vnc server.",
1348 enum
=> PVE
::Tools
::kvmkeymaplist
(),
1353 description
=> "Default GUI language.",
1379 description
=> "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
1380 pattern
=> "http://.*",
1382 migration_unsecure
=> {
1385 description
=> "Migration is secure using SSH tunnel by default. " .
1386 "For secure private networks you can disable it to speed up " .
1387 "migration. Deprecated, use the 'migration' property instead!",
1391 type
=> 'string', format
=> $migration_format,
1392 description
=> "For cluster wide migration settings.",
1397 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.",
1398 enum
=> ['applet', 'vv', 'html5', 'xtermjs'],
1403 format
=> 'email-opt',
1404 description
=> "Specify email address to send notification from (default is root@\$hostname)",
1410 description
=> "Defines how many workers (per node) are maximal started ".
1411 " on actions like 'stopall VMs' or task from the ha-manager.",
1416 default => 'watchdog',
1417 enum
=> [ 'watchdog', 'hardware', 'both' ],
1418 description
=> "Set the fencing mode of the HA cluster. Hardware mode " .
1419 "needs a valid configuration of fence devices in /etc/pve/ha/fence.cfg." .
1420 " With both all two modes are used." .
1421 "\n\nWARNING: 'hardware' and 'both' are EXPERIMENTAL & WIP",
1426 pattern
=> qr/[a-f0-9]{2}(?::[a-f0-9]{2}){0,2}:?/i,
1427 description
=> 'Prefix for autogenerated MAC addresses.',
1429 bwlimit
=> PVE
::JSONSchema
::get_standard_option
('bwlimit'),
1433 # make schema accessible from outside (for documentation)
1434 sub get_datacenter_schema
{ return $datacenter_schema };
1436 sub parse_datacenter_config
{
1437 my ($filename, $raw) = @_;
1439 my $res = PVE
::JSONSchema
::parse_config
($datacenter_schema, $filename, $raw // '');
1441 if (my $migration = $res->{migration
}) {
1442 $res->{migration
} = PVE
::JSONSchema
::parse_property_string
($migration_format, $migration);
1445 # for backwards compatibility only, new migration property has precedence
1446 if (defined($res->{migration_unsecure
})) {
1447 if (defined($res->{migration
}->{type
})) {
1448 warn "deprecated setting 'migration_unsecure' and new 'migration: type' " .
1449 "set at same time! Ignore 'migration_unsecure'\n";
1451 $res->{migration
}->{type
} = ($res->{migration_unsecure
}) ?
'insecure' : 'secure';
1455 # for backwards compatibility only, applet maps to html5
1456 if (defined($res->{console
}) && $res->{console
} eq 'applet') {
1457 $res->{console
} = 'html5';
1463 sub write_datacenter_config
{
1464 my ($filename, $cfg) = @_;
1466 # map deprecated setting to new one
1467 if (defined($cfg->{migration_unsecure
}) && !defined($cfg->{migration
})) {
1468 my $migration_unsecure = delete $cfg->{migration_unsecure
};
1469 $cfg->{migration
}->{type
} = ($migration_unsecure) ?
'insecure' : 'secure';
1472 # map deprecated applet setting to html5
1473 if (defined($cfg->{console
}) && $cfg->{console
} eq 'applet') {
1474 $cfg->{console
} = 'html5';
1477 if (my $migration = $cfg->{migration
}) {
1478 $cfg->{migration
} = PVE
::JSONSchema
::print_property_string
($migration, $migration_format);
1481 return PVE
::JSONSchema
::dump_config
($datacenter_schema, $filename, $cfg);
1484 cfs_register_file
('datacenter.cfg',
1485 \
&parse_datacenter_config
,
1486 \
&write_datacenter_config
);
1488 # X509 Certificate cache helper
1490 my $cert_cache_nodes = {};
1491 my $cert_cache_timestamp = time();
1492 my $cert_cache_fingerprints = {};
1494 sub update_cert_cache
{
1495 my ($update_node, $clear) = @_;
1497 syslog
('info', "Clearing outdated entries from certificate cache")
1500 $cert_cache_timestamp = time() if !defined($update_node);
1502 my $node_list = defined($update_node) ?
1503 [ $update_node ] : [ keys %$cert_cache_nodes ];
1505 foreach my $node (@$node_list) {
1506 my $clear_old = sub {
1507 if (my $old_fp = $cert_cache_nodes->{$node}) {
1508 # distrust old fingerprint
1509 delete $cert_cache_fingerprints->{$old_fp};
1510 # ensure reload on next proxied request
1511 delete $cert_cache_nodes->{$node};
1515 my $fp = eval { get_node_fingerprint
($node) };
1518 &$clear_old() if $clear;
1522 my $old_fp = $cert_cache_nodes->{$node};
1523 $cert_cache_fingerprints->{$fp} = 1;
1524 $cert_cache_nodes->{$node} = $fp;
1526 if (defined($old_fp) && $fp ne $old_fp) {
1527 delete $cert_cache_fingerprints->{$old_fp};
1532 # load and cache cert fingerprint once
1533 sub initialize_cert_cache
{
1536 update_cert_cache
($node)
1537 if defined($node) && !defined($cert_cache_nodes->{$node});
1540 sub read_ssl_cert_fingerprint
{
1541 my ($cert_path) = @_;
1543 my $bio = Net
::SSLeay
::BIO_new_file
($cert_path, 'r')
1544 or die "unable to read '$cert_path' - $!\n";
1546 my $cert = Net
::SSLeay
::PEM_read_bio_X509
($bio);
1547 Net
::SSLeay
::BIO_free
($bio);
1549 die "unable to read certificate from '$cert_path'\n" if !$cert;
1551 my $fp = Net
::SSLeay
::X509_get_fingerprint
($cert, 'sha256');
1552 Net
::SSLeay
::X509_free
($cert);
1554 die "unable to get fingerprint for '$cert_path' - got empty value\n"
1555 if !defined($fp) || $fp eq '';
1560 sub get_node_fingerprint
{
1563 my $cert_path = "/etc/pve/nodes/$node/pve-ssl.pem";
1564 my $custom_cert_path = "/etc/pve/nodes/$node/pveproxy-ssl.pem";
1566 $cert_path = $custom_cert_path if -f
$custom_cert_path;
1568 return read_ssl_cert_fingerprint
($cert_path);
1572 sub check_cert_fingerprint
{
1575 # clear cache every 30 minutes at least
1576 update_cert_cache
(undef, 1) if time() - $cert_cache_timestamp >= 60*30;
1578 # get fingerprint of server certificate
1579 my $fp = Net
::SSLeay
::X509_get_fingerprint
($cert, 'sha256');
1580 return 0 if !defined($fp) || $fp eq ''; # error
1583 for my $expected (keys %$cert_cache_fingerprints) {
1584 return 1 if $fp eq $expected;
1589 return 1 if &$check();
1591 # clear cache and retry at most once every minute
1592 if (time() - $cert_cache_timestamp >= 60) {
1593 syslog
('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache");
1594 update_cert_cache
();
1601 # bash completion helpers
1603 sub complete_next_vmid
{
1605 my $vmlist = get_vmlist
() || {};
1606 my $idlist = $vmlist->{ids
} || {};
1608 for (my $i = 100; $i < 10000; $i++) {
1609 return [$i] if !defined($idlist->{$i});
1617 my $vmlist = get_vmlist
();
1618 my $ids = $vmlist->{ids
} || {};
1620 return [ keys %$ids ];
1623 sub complete_local_vmid
{
1625 my $vmlist = get_vmlist
();
1626 my $ids = $vmlist->{ids
} || {};
1628 my $nodename = PVE
::INotify
::nodename
();
1631 foreach my $vmid (keys %$ids) {
1632 my $d = $ids->{$vmid};
1633 next if !$d->{node
} || $d->{node
} ne $nodename;
1640 sub complete_migration_target
{
1644 my $nodename = PVE
::INotify
::nodename
();
1646 my $nodelist = get_nodelist
();
1647 foreach my $node (@$nodelist) {
1648 next if $node eq $nodename;
1656 my ($node, $network_cidr) = @_;
1659 if (defined($network_cidr)) {
1660 # Use mtunnel via to get the remote node's ip inside $network_cidr.
1661 # This goes over the regular network (iow. uses get_ssh_info() with
1662 # $network_cidr undefined.
1663 # FIXME: Use the REST API client for this after creating an API entry
1664 # for get_migration_ip.
1665 my $default_remote = get_ssh_info
($node, undef);
1666 my $default_ssh = ssh_info_to_command
($default_remote);
1667 my $cmd =[@$default_ssh, 'pvecm', 'mtunnel',
1668 '-migration_network', $network_cidr,
1671 PVE
::Tools
::run_command
($cmd, outfunc
=> sub {
1674 die "internal error: unexpected output from mtunnel\n"
1676 if ($line =~ /^ip: '(.*)'$/) {
1679 die "internal error: bad output from mtunnel\n"
1683 die "failed to get ip for node '$node' in network '$network_cidr'\n"
1686 $ip = remote_node_ip
($node);
1692 network
=> $network_cidr,
1696 sub ssh_info_to_command_base
{
1697 my ($info, @extra_options) = @_;
1701 '-o', 'BatchMode=yes',
1702 '-o', 'HostKeyAlias='.$info->{name
},
1707 sub ssh_info_to_command
{
1708 my ($info, @extra_options) = @_;
1709 my $cmd = ssh_info_to_command_base
($info, @extra_options);
1710 push @$cmd, "root\@$info->{ip}";
1714 sub assert_joinable
{
1715 my ($ring0_addr, $ring1_addr, $force) = @_;
1718 my $error = sub { $errors .= "* $_[0]\n"; };
1721 $error->("authentication key '$authfile' already exists");
1724 if (-f
$clusterconf) {
1725 $error->("cluster config '$clusterconf' already exists");
1728 my $vmlist = get_vmlist
();
1729 if ($vmlist && $vmlist->{ids
} && scalar(keys %{$vmlist->{ids
}})) {
1730 $error->("this host already contains virtual guests");
1733 if (run_command
(['corosync-quorumtool', '-l'], noerr
=> 1, quiet
=> 1) == 0) {
1734 $error->("corosync is already running, is this node already in a cluster?!");
1737 # check if corosync ring IPs are configured on the current nodes interfaces
1738 my $check_ip = sub {
1739 my $ip = shift // return;
1740 if (!PVE
::JSONSchema
::pve_verify_ip
($ip, 1)) {
1742 eval { $ip = PVE
::Network
::get_ip_from_hostname
($host); };
1744 $error->("cannot use '$host': $@\n") ;
1749 my $cidr = (Net
::IP
::ip_is_ipv6
($ip)) ?
"$ip/128" : "$ip/32";
1750 my $configured_ips = PVE
::Network
::get_local_ip_from_cidr
($cidr);
1752 $error->("cannot use IP '$ip', it must be configured exactly once on local node!\n")
1753 if (scalar(@$configured_ips) != 1);
1756 $check_ip->($ring0_addr);
1757 $check_ip->($ring1_addr);
1760 warn "detected the following error(s):\n$errors";
1761 die "Check if node may join a cluster failed!\n" if !$force;
1765 # NOTE: filesystem must be offline here, no DB changes allowed
1766 my $backup_cfs_database = sub {
1772 my $backup_fn = "$dbbackupdir/config-$ctime.sql.gz";
1774 print "backup old database to '$backup_fn'\n";
1776 my $cmd = [ ['sqlite3', $dbfile, '.dump'], ['gzip', '-', \
">${backup_fn}"] ];
1777 run_command
($cmd, 'errmsg' => "cannot backup old database\n");
1779 my $maxfiles = 10; # purge older backup
1780 my $backups = [ sort { $b cmp $a } <$dbbackupdir/config-*.sql
.gz
> ];
1782 if ((my $count = scalar(@$backups)) > $maxfiles) {
1783 foreach my $f (@$backups[$maxfiles..$count-1]) {
1784 next if $f !~ m/^(\S+)$/; # untaint
1785 print "delete old backup '$1'\n";
1794 my $nodename = PVE
::INotify
::nodename
();
1796 setup_sshd_config
();
1797 setup_rootsshconfig
();
1800 # check if we can join with the given parameters and current node state
1801 my ($ring0_addr, $ring1_addr) = $param->@{'ring0_addr', 'ring1_addr'};
1802 assert_joinable
($ring0_addr, $ring1_addr, $param->{force
});
1804 # make sure known_hosts is on local filesystem
1805 ssh_unmerge_known_hosts
();
1807 my $host = $param->{hostname
};
1808 my $local_ip_address = remote_node_ip
($nodename);
1811 username
=> 'root@pam',
1812 password
=> $param->{password
},
1813 cookie_name
=> 'PVEAuthCookie',
1814 protocol
=> 'https',
1819 if (my $fp = $param->{fingerprint
}) {
1820 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
1822 # API schema ensures that we can only get here from CLI handler
1823 $conn_args->{manual_verification
} = 1;
1826 print "Establishing API connection with host '$host'\n";
1828 my $conn = PVE
::APIClient
::LWP-
>new(%$conn_args);
1831 # login raises an exception on failure, so if we get here we're good
1832 print "Login succeeded.\n";
1835 $args->{force
} = $param->{force
} if defined($param->{force
});
1836 $args->{nodeid
} = $param->{nodeid
} if $param->{nodeid
};
1837 $args->{votes
} = $param->{votes
} if defined($param->{votes
});
1838 $args->{ring0_addr
} = $ring0_addr // $local_ip_address;
1839 $args->{ring1_addr
} = $ring1_addr if defined($ring1_addr);
1841 print "Request addition of this node\n";
1842 my $res = $conn->post("/cluster/config/nodes/$nodename", $args);
1844 print "Join request OK, finishing setup locally\n";
1846 # added successfuly - now prepare local node
1847 finish_join
($nodename, $res->{corosync_conf
}, $res->{corosync_authkey
});
1851 my ($nodename, $corosync_conf, $corosync_authkey) = @_;
1853 mkdir "$localclusterdir";
1854 PVE
::Tools
::file_set_contents
($authfile, $corosync_authkey);
1855 PVE
::Tools
::file_set_contents
($localclusterconf, $corosync_conf);
1857 print "stopping pve-cluster service\n";
1858 my $cmd = ['systemctl', 'stop', 'pve-cluster'];
1859 run_command
($cmd, errmsg
=> "can't stop pve-cluster service");
1861 $backup_cfs_database->($dbfile);
1864 $cmd = ['systemctl', 'start', 'corosync', 'pve-cluster'];
1865 run_command
($cmd, errmsg
=> "starting pve-cluster failed");
1869 while (!check_cfs_quorum
(1)) {
1871 print "waiting for quorum...";
1877 print "OK\n" if !$printqmsg;
1879 updatecerts_and_ssh
(1);
1881 print "generated new node certificate, restart pveproxy and pvedaemon services\n";
1882 run_command
(['systemctl', 'reload-or-restart', 'pvedaemon', 'pveproxy']);
1884 print "successfully added node '$nodename' to cluster.\n";
1887 sub updatecerts_and_ssh
{
1888 my ($force_new_cert, $silent) = @_;
1890 my $p = sub { print "$_[0]\n" if !$silent };
1892 setup_rootsshconfig
();
1894 gen_pve_vzdump_symlink
();
1896 if (!check_cfs_quorum
(1)) {
1897 return undef if $silent;
1898 die "no quorum - unable to update files\n";
1903 my $nodename = PVE
::INotify
::nodename
();
1904 my $local_ip_address = remote_node_ip
($nodename);
1906 $p->("(re)generate node files");
1907 $p->("generate new node certificate") if $force_new_cert;
1908 gen_pve_node_files
($nodename, $local_ip_address, $force_new_cert);
1910 $p->("merge authorized SSH keys and known hosts");
1912 ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
1913 gen_pve_vzdump_files
();