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
97 my $record_output = sub {
103 PVE
::Tools
::run_command
($cmd, outfunc
=> $record_output,
104 errfunc
=> $record_output);
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 mkdir $authdir || $! == EEXIST
|| die "unable to create dir '$authdir' - $!\n";
168 run_silent_cmd
(['openssl', 'genrsa', '-out', $authprivkeyfn, '2048']);
170 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
};
529 my $nodelist = $clinfo->{nodelist
};
533 my $nodename = PVE
::INotify
::nodename
();
535 if (!$nodelist || !$nodelist->{$nodename}) {
536 return [ $nodename ];
539 return [ keys %$nodelist ];
542 # $data must be a chronological descending ordered array of tasks
543 sub broadcast_tasklist
{
546 # the serialized list may not get bigger than 32kb (CFS_MAX_STATUS_SIZE
547 # from pmxcfs) - drop older items until we satisfy this constraint
548 my $size = length(encode_json
($data));
549 while ($size >= (32 * 1024)) {
551 $size = length(encode_json
($data));
555 &$ipcc_update_status("tasklist", $data);
561 my $tasklistcache = {};
566 my $kvstore = $versions->{kvstore
} || {};
568 my $nodelist = get_nodelist
();
571 foreach my $node (@$nodelist) {
572 next if $nodename && ($nodename ne $node);
574 my $ver = $kvstore->{$node}->{tasklist
} if $kvstore->{$node};
575 my $cd = $tasklistcache->{$node};
576 if (!$cd || !$ver || !$cd->{version
} ||
577 ($cd->{version
} != $ver)) {
578 my $raw = &$ipcc_get_status("tasklist", $node) || '[]';
579 my $data = decode_json
($raw);
581 $cd = $tasklistcache->{$node} = {
585 } elsif ($cd && $cd->{data
}) {
586 push @$res, @{$cd->{data
}};
590 syslog
('err', $err) if $err;
597 my ($rrdid, $data) = @_;
600 &$ipcc_update_status("rrd/$rrdid", $data);
607 my $last_rrd_dump = 0;
608 my $last_rrd_data = "";
614 my $diff = $ctime - $last_rrd_dump;
616 return $last_rrd_data;
621 $raw = &$ipcc_send_rec(CFS_IPC_GET_RRD_DUMP
);
633 while ($raw =~ s/^(.*)\n//) {
634 my ($key, @ela) = split(/:/, $1);
636 next if !(scalar(@ela) > 1);
637 $res->{$key} = [ map { $_ eq 'U' ?
undef : $_ } @ela ];
641 $last_rrd_dump = $ctime;
642 $last_rrd_data = $res;
647 sub create_rrd_data
{
648 my ($rrdname, $timeframe, $cf) = @_;
650 my $rrddir = "/var/lib/rrdcached/db";
652 my $rrd = "$rrddir/$rrdname";
656 day
=> [ 60*30, 70 ],
657 week
=> [ 60*180, 70 ],
658 month
=> [ 60*720, 70 ],
659 year
=> [ 60*10080, 70 ],
662 my ($reso, $count) = @{$setup->{$timeframe}};
663 my $ctime = $reso*int(time()/$reso);
664 my $req_start = $ctime - $reso*$count;
666 $cf = "AVERAGE" if !$cf;
674 my $socket = "/var/run/rrdcached.sock";
675 push @args, "--daemon" => "unix:$socket" if -S
$socket;
677 my ($start, $step, $names, $data) = RRDs
::fetch
($rrd, $cf, @args);
679 my $err = RRDs
::error
;
680 die "RRD error: $err\n" if $err;
682 die "got wrong time resolution ($step != $reso)\n"
686 my $fields = scalar(@$names);
687 for my $line (@$data) {
688 my $entry = { 'time' => $start };
690 for (my $i = 0; $i < $fields; $i++) {
691 my $name = $names->[$i];
692 if (defined(my $val = $line->[$i])) {
693 $entry->{$name} = $val;
695 # leave empty fields undefined
696 # maybe make this configurable?
705 sub create_rrd_graph
{
706 my ($rrdname, $timeframe, $ds, $cf) = @_;
708 # Using RRD graph is clumsy - maybe it
709 # is better to simply fetch the data, and do all display
710 # related things with javascript (new extjs html5 graph library).
712 my $rrddir = "/var/lib/rrdcached/db";
714 my $rrd = "$rrddir/$rrdname";
716 my @ids = PVE
::Tools
::split_list
($ds);
718 my $ds_txt = join('_', @ids);
720 my $filename = "${rrd}_${ds_txt}.png";
724 day
=> [ 60*30, 70 ],
725 week
=> [ 60*180, 70 ],
726 month
=> [ 60*720, 70 ],
727 year
=> [ 60*10080, 70 ],
730 my ($reso, $count) = @{$setup->{$timeframe}};
733 "--imgformat" => "PNG",
737 "--start" => - $reso*$count,
739 "--lower-limit" => 0,
742 my $socket = "/var/run/rrdcached.sock";
743 push @args, "--daemon" => "unix:$socket" if -S
$socket;
745 my @coldef = ('#00ddff', '#ff0000');
747 $cf = "AVERAGE" if !$cf;
750 foreach my $id (@ids) {
751 my $col = $coldef[$i++] || die "fixme: no color definition";
752 push @args, "DEF:${id}=$rrd:${id}:$cf";
754 if ($id eq 'cpu' || $id eq 'iowait') {
755 push @args, "CDEF:${id}_per=${id},100,*";
756 $dataid = "${id}_per";
758 push @args, "LINE2:${dataid}${col}:${id}";
761 push @args, '--full-size-mode';
763 # we do not really store data into the file
764 my $res = RRDs
::graphv
('-', @args);
766 my $err = RRDs
::error
;
767 die "RRD error: $err\n" if $err;
769 return { filename
=> $filename, image
=> $res->{image
} };
772 # a fast way to read files (avoid fuse overhead)
776 return &$ipcc_get_config($path);
779 sub get_cluster_log
{
780 my ($user, $max) = @_;
782 return &$ipcc_get_cluster_log($user, $max);
787 sub cfs_register_file
{
788 my ($filename, $parser, $writer) = @_;
790 $observed->{$filename} || die "unknown file '$filename'";
792 die "file '$filename' already registered" if $file_info->{$filename};
794 $file_info->{$filename} = {
800 my $ccache_read = sub {
801 my ($filename, $parser, $version) = @_;
803 $ccache->{$filename} = {} if !$ccache->{$filename};
805 my $ci = $ccache->{$filename};
807 if (!$ci->{version
} || !$version || $ci->{version
} != $version) {
808 # we always call the parser, even when the file does not exists
809 # (in that case $data is undef)
810 my $data = get_config
($filename);
811 $ci->{data
} = &$parser("/etc/pve/$filename", $data);
812 $ci->{version
} = $version;
815 my $res = ref($ci->{data
}) ? dclone
($ci->{data
}) : $ci->{data
};
820 sub cfs_file_version
{
825 if ($filename =~ m!^nodes/[^/]+/(openvz|lxc|qemu-server)/(\d+)\.conf$!) {
826 my ($type, $vmid) = ($1, $2);
827 if ($vmlist && $vmlist->{ids
} && $vmlist->{ids
}->{$vmid}) {
828 $version = $vmlist->{ids
}->{$vmid}->{version
};
830 $infotag = "/$type/";
832 $infotag = $filename;
833 $version = $versions->{$filename};
836 my $info = $file_info->{$infotag} ||
837 die "unknown file type '$filename'\n";
839 return wantarray ?
($version, $info) : $version;
845 my ($version, $info) = cfs_file_version
($filename);
846 my $parser = $info->{parser
};
848 return &$ccache_read($filename, $parser, $version);
852 my ($filename, $data) = @_;
854 my ($version, $info) = cfs_file_version
($filename);
856 my $writer = $info->{writer
} || die "no writer defined";
858 my $fsname = "/etc/pve/$filename";
860 my $raw = &$writer($fsname, $data);
862 if (my $ci = $ccache->{$filename}) {
863 $ci->{version
} = undef;
866 PVE
::Tools
::file_set_contents
($fsname, $raw);
870 my ($lockid, $timeout, $code, @param) = @_;
872 my $prev_alarm = alarm(0); # suspend outer alarm early
877 # this timeout is for aquire the lock
878 $timeout = 10 if !$timeout;
880 my $filename = "$lockdir/$lockid";
887 die "pve cluster filesystem not online.\n";
890 my $timeout_err = sub { die "got lock request timeout\n"; };
891 local $SIG{ALRM
} = $timeout_err;
895 $got_lock = mkdir($filename);
896 $timeout = alarm(0) - 1; # we'll sleep for 1s, see down below
900 $timeout_err->() if $timeout <= 0;
902 print STDERR
"trying to aquire cfs lock '$lockid' ...\n";
903 utime (0, 0, $filename); # cfs unlock request
907 # fixed command timeout: cfs locks have a timeout of 120
908 # using 60 gives us another 60 seconds to abort the task
909 local $SIG{ALRM
} = sub { die "got lock timeout - aborting command\n"; };
912 cfs_update
(); # make sure we read latest versions inside code()
914 $res = &$code(@param);
921 $err = "no quorum!\n" if !$got_lock && !check_cfs_quorum
(1);
923 rmdir $filename if $got_lock; # if we held the lock always unlock again
928 $@ = "error with cfs lock '$lockid': $err";
938 my ($filename, $timeout, $code, @param) = @_;
940 my $info = $observed->{$filename} || die "unknown file '$filename'";
942 my $lockid = "file-$filename";
943 $lockid =~ s/[.\/]/_
/g
;
945 &$cfs_lock($lockid, $timeout, $code, @param);
948 sub cfs_lock_storage
{
949 my ($storeid, $timeout, $code, @param) = @_;
951 my $lockid = "storage-$storeid";
953 &$cfs_lock($lockid, $timeout, $code, @param);
956 sub cfs_lock_domain
{
957 my ($domainname, $timeout, $code, @param) = @_;
959 my $lockid = "domain-$domainname";
961 &$cfs_lock($lockid, $timeout, $code, @param);
965 my ($account, $timeout, $code, @param) = @_;
967 my $lockid = "acme-$account";
969 &$cfs_lock($lockid, $timeout, $code, @param);
987 my ($priority, $ident, $msg) = @_;
989 if (my $tmp = $log_levels->{$priority}) {
993 die "need numeric log priority" if $priority !~ /^\d+$/;
995 my $tag = PVE
::SafeSyslog
::tag
();
997 $msg = "empty message" if !$msg;
999 $ident = "" if !$ident;
1000 $ident = encode
("ascii", $ident,
1001 sub { sprintf "\\u%04x", shift });
1003 my $ascii = encode
("ascii", $msg, sub { sprintf "\\u%04x", shift });
1006 syslog
($priority, "<%s> %s", $ident, $ascii);
1008 syslog
($priority, "%s", $ascii);
1011 eval { &$ipcc_log($priority, $ident, $tag, $ascii); };
1013 syslog
("err", "writing cluster log failed: $@") if $@;
1016 sub check_vmid_unused
{
1017 my ($vmid, $noerr) = @_;
1019 my $vmlist = get_vmlist
();
1021 my $d = $vmlist->{ids
}->{$vmid};
1022 return 1 if !defined($d);
1024 return undef if $noerr;
1026 my $vmtypestr = $d->{type
} eq 'qemu' ?
'VM' : 'CT';
1027 die "$vmtypestr $vmid already exists on node '$d->{node}'\n";
1030 sub check_node_exists
{
1031 my ($nodename, $noerr) = @_;
1033 my $nodelist = $clinfo->{nodelist
};
1034 return 1 if $nodelist && $nodelist->{$nodename};
1036 return undef if $noerr;
1038 die "no such cluster node '$nodename'\n";
1041 # this is also used to get the IP of the local node
1042 sub remote_node_ip
{
1043 my ($nodename, $noerr) = @_;
1045 my $nodelist = $clinfo->{nodelist
};
1046 if ($nodelist && $nodelist->{$nodename}) {
1047 if (my $ip = $nodelist->{$nodename}->{ip
}) {
1048 return $ip if !wantarray;
1049 my $family = $nodelist->{$nodename}->{address_family
};
1051 $nodelist->{$nodename}->{address_family
} =
1053 PVE
::Tools
::get_host_address_family
($ip);
1055 return wantarray ?
($ip, $family) : $ip;
1059 # fallback: try to get IP by other means
1060 return PVE
::Network
::get_ip_from_hostname
($nodename, $noerr);
1063 sub get_local_migration_ip
{
1064 my ($migration_network, $noerr) = @_;
1066 my $cidr = $migration_network;
1068 if (!defined($cidr)) {
1069 my $dc_conf = cfs_read_file
('datacenter.cfg');
1070 $cidr = $dc_conf->{migration
}->{network
}
1071 if defined($dc_conf->{migration
}->{network
});
1074 if (defined($cidr)) {
1075 my $ips = PVE
::Network
::get_local_ip_from_cidr
($cidr);
1077 die "could not get migration ip: no IP address configured on local " .
1078 "node for network '$cidr'\n" if !$noerr && (scalar(@$ips) == 0);
1080 die "could not get migration ip: multiple IP address configured for " .
1081 "network '$cidr'\n" if !$noerr && (scalar(@$ips) > 1);
1089 # ssh related utility functions
1091 sub ssh_merge_keys
{
1092 # remove duplicate keys in $sshauthkeys
1093 # ssh-copy-id simply add keys, so the file can grow to large
1096 if (-f
$sshauthkeys) {
1097 $data = PVE
::Tools
::file_get_contents
($sshauthkeys, 128*1024);
1102 if (-f
$rootsshauthkeysbackup) {
1104 $data .= PVE
::Tools
::file_get_contents
($rootsshauthkeysbackup, 128*1024);
1109 # always add ourself
1110 if (-f
$ssh_rsa_id) {
1111 my $pub = PVE
::Tools
::file_get_contents
($ssh_rsa_id);
1113 $data .= "\n$pub\n";
1118 my @lines = split(/\n/, $data);
1119 foreach my $line (@lines) {
1120 if ($line !~ /^#/ && $line =~ m/(^|\s)ssh-(rsa|dsa)\s+(\S+)\s+\S+$/) {
1121 next if $vhash->{$3}++;
1123 $newdata .= "$line\n";
1126 PVE
::Tools
::file_set_contents
($sshauthkeys, $newdata, 0600);
1128 if ($found_backup && -l
$rootsshauthkeys) {
1129 # everything went well, so we can remove the backup
1130 unlink $rootsshauthkeysbackup;
1134 sub setup_sshd_config
{
1137 my $conf = PVE
::Tools
::file_get_contents
($sshd_config_fn);
1139 return if $conf =~ m/^PermitRootLogin\s+yes\s*$/m;
1141 if ($conf !~ s/^#?PermitRootLogin.*$/PermitRootLogin yes/m) {
1143 $conf .= "\nPermitRootLogin yes\n";
1146 PVE
::Tools
::file_set_contents
($sshd_config_fn, $conf);
1148 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'sshd']);
1151 sub setup_rootsshconfig
{
1153 # create ssh key if it does not exist
1154 if (! -f
$ssh_rsa_id) {
1155 mkdir '/root/.ssh/';
1156 system ("echo|ssh-keygen -t rsa -N '' -b 2048 -f ${ssh_rsa_id_priv}");
1159 # create ssh config if it does not exist
1160 if (! -f
$rootsshconfig) {
1162 if (my $fh = IO
::File-
>new($rootsshconfig, O_CREAT
|O_WRONLY
|O_EXCL
, 0640)) {
1163 # this is the default ciphers list from Debian's OpenSSH package (OpenSSH_7.4p1 Debian-10, OpenSSL 1.0.2k 26 Jan 2017)
1164 # changed order to put AES before Chacha20 (most hardware has AESNI)
1165 print $fh "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm\@openssh.com,aes256-gcm\@openssh.com,chacha20-poly1305\@openssh.com\n";
1171 sub setup_ssh_keys
{
1177 if (! -f
$sshauthkeys) {
1179 if (-f
$rootsshauthkeys) {
1180 $old = PVE
::Tools
::file_get_contents
($rootsshauthkeys, 128*1024);
1182 if (my $fh = IO
::File-
>new ($sshauthkeys, O_CREAT
|O_WRONLY
|O_EXCL
, 0400)) {
1183 PVE
::Tools
::safe_print
($sshauthkeys, $fh, $old) if $old;
1189 warn "can't create shared ssh key database '$sshauthkeys'\n"
1190 if ! -f
$sshauthkeys;
1192 if (-f
$rootsshauthkeys && ! -l
$rootsshauthkeys) {
1193 if (!rename($rootsshauthkeys , $rootsshauthkeysbackup)) {
1194 warn "rename $rootsshauthkeys failed - $!\n";
1198 if (! -l
$rootsshauthkeys) {
1199 symlink $sshauthkeys, $rootsshauthkeys;
1202 if (! -l
$rootsshauthkeys) {
1203 warn "can't create symlink for ssh keys '$rootsshauthkeys' -> '$sshauthkeys'\n";
1205 unlink $rootsshauthkeysbackup if $import_ok;
1209 sub ssh_unmerge_known_hosts
{
1210 return if ! -l
$sshglobalknownhosts;
1213 $old = PVE
::Tools
::file_get_contents
($sshknownhosts, 128*1024)
1214 if -f
$sshknownhosts;
1216 PVE
::Tools
::file_set_contents
($sshglobalknownhosts, $old);
1219 sub ssh_merge_known_hosts
{
1220 my ($nodename, $ip_address, $createLink) = @_;
1222 die "no node name specified" if !$nodename;
1223 die "no ip address specified" if !$ip_address;
1225 # ssh lowercases hostnames (aliases) before comparision, so we need too
1226 $nodename = lc($nodename);
1227 $ip_address = lc($ip_address);
1231 if (! -f
$sshknownhosts) {
1232 if (my $fh = IO
::File-
>new($sshknownhosts, O_CREAT
|O_WRONLY
|O_EXCL
, 0600)) {
1237 my $old = PVE
::Tools
::file_get_contents
($sshknownhosts, 128*1024);
1241 if ((! -l
$sshglobalknownhosts) && (-f
$sshglobalknownhosts)) {
1242 $new = PVE
::Tools
::file_get_contents
($sshglobalknownhosts, 128*1024);
1245 my $hostkey = PVE
::Tools
::file_get_contents
($ssh_host_rsa_id);
1246 # Note: file sometimes containe emty lines at start, so we use multiline match
1247 die "can't parse $ssh_host_rsa_id" if $hostkey !~ m/^(ssh-rsa\s\S+)(\s.*)?$/m;
1256 my $merge_line = sub {
1257 my ($line, $all) = @_;
1259 return if $line =~ m/^\s*$/; # skip empty lines
1260 return if $line =~ m/^#/; # skip comments
1262 if ($line =~ m/^(\S+)\s(ssh-rsa\s\S+)(\s.*)?$/) {
1265 if (!$vhash->{$key}) {
1267 if ($key =~ m/\|1\|([^\|\s]+)\|([^\|\s]+)$/) {
1268 my $salt = decode_base64
($1);
1270 my $hmac = Digest
::HMAC_SHA1-
>new($salt);
1271 $hmac->add($nodename);
1272 my $hd = $hmac->b64digest . '=';
1273 if ($digest eq $hd) {
1274 if ($rsakey eq $hostkey) {
1275 $found_nodename = 1;
1280 $hmac = Digest
::HMAC_SHA1-
>new($salt);
1281 $hmac->add($ip_address);
1282 $hd = $hmac->b64digest . '=';
1283 if ($digest eq $hd) {
1284 if ($rsakey eq $hostkey) {
1285 $found_local_ip = 1;
1291 $key = lc($key); # avoid duplicate entries, ssh compares lowercased
1292 if ($key eq $ip_address) {
1293 $found_local_ip = 1 if $rsakey eq $hostkey;
1294 } elsif ($key eq $nodename) {
1295 $found_nodename = 1 if $rsakey eq $hostkey;
1305 while ($old && $old =~ s/^((.*?)(\n|$))//) {
1307 &$merge_line($line, 1);
1310 while ($new && $new =~ s/^((.*?)(\n|$))//) {
1312 &$merge_line($line);
1315 # add our own key if not already there
1316 $data .= "$nodename $hostkey\n" if !$found_nodename;
1317 $data .= "$ip_address $hostkey\n" if !$found_local_ip;
1319 PVE
::Tools
::file_set_contents
($sshknownhosts, $data);
1321 return if !$createLink;
1323 unlink $sshglobalknownhosts;
1324 symlink $sshknownhosts, $sshglobalknownhosts;
1326 warn "can't create symlink for ssh known hosts '$sshglobalknownhosts' -> '$sshknownhosts'\n"
1327 if ! -l
$sshglobalknownhosts;
1331 my $migration_format = {
1335 enum
=> ['secure', 'insecure'],
1336 description
=> "Migration traffic is encrypted using an SSH tunnel by " .
1337 "default. On secure, completely private networks this can be " .
1338 "disabled to increase performance.",
1339 default => 'secure',
1343 type
=> 'string', format
=> 'CIDR',
1344 format_description
=> 'CIDR',
1345 description
=> "CIDR of the (sub) network that is used for migration."
1349 my $datacenter_schema = {
1351 additionalProperties
=> 0,
1356 description
=> "Default keybord layout for vnc server.",
1357 enum
=> PVE
::Tools
::kvmkeymaplist
(),
1362 description
=> "Default GUI language.",
1363 enum
=> [ 'en', 'de' ],
1368 description
=> "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
1369 pattern
=> "http://.*",
1371 migration_unsecure
=> {
1374 description
=> "Migration is secure using SSH tunnel by default. " .
1375 "For secure private networks you can disable it to speed up " .
1376 "migration. Deprecated, use the 'migration' property instead!",
1380 type
=> 'string', format
=> $migration_format,
1381 description
=> "For cluster wide migration settings.",
1386 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.",
1387 enum
=> ['applet', 'vv', 'html5', 'xtermjs'],
1392 format
=> 'email-opt',
1393 description
=> "Specify email address to send notification from (default is root@\$hostname)",
1399 description
=> "Defines how many workers (per node) are maximal started ".
1400 " on actions like 'stopall VMs' or task from the ha-manager.",
1405 default => 'watchdog',
1406 enum
=> [ 'watchdog', 'hardware', 'both' ],
1407 description
=> "Set the fencing mode of the HA cluster. Hardware mode " .
1408 "needs a valid configuration of fence devices in /etc/pve/ha/fence.cfg." .
1409 " With both all two modes are used." .
1410 "\n\nWARNING: 'hardware' and 'both' are EXPERIMENTAL & WIP",
1415 pattern
=> qr/[a-f0-9]{2}(?::[a-f0-9]{2}){0,2}:?/i,
1416 description
=> 'Prefix for autogenerated MAC addresses.',
1418 bwlimit
=> PVE
::JSONSchema
::get_standard_option
('bwlimit'),
1422 # make schema accessible from outside (for documentation)
1423 sub get_datacenter_schema
{ return $datacenter_schema };
1425 sub parse_datacenter_config
{
1426 my ($filename, $raw) = @_;
1428 my $res = PVE
::JSONSchema
::parse_config
($datacenter_schema, $filename, $raw // '');
1430 if (my $migration = $res->{migration
}) {
1431 $res->{migration
} = PVE
::JSONSchema
::parse_property_string
($migration_format, $migration);
1434 # for backwards compatibility only, new migration property has precedence
1435 if (defined($res->{migration_unsecure
})) {
1436 if (defined($res->{migration
}->{type
})) {
1437 warn "deprecated setting 'migration_unsecure' and new 'migration: type' " .
1438 "set at same time! Ignore 'migration_unsecure'\n";
1440 $res->{migration
}->{type
} = ($res->{migration_unsecure
}) ?
'insecure' : 'secure';
1444 # for backwards compatibility only, applet maps to html5
1445 if (defined($res->{console
}) && $res->{console
} eq 'applet') {
1446 $res->{console
} = 'html5';
1452 sub write_datacenter_config
{
1453 my ($filename, $cfg) = @_;
1455 # map deprecated setting to new one
1456 if (defined($cfg->{migration_unsecure
}) && !defined($cfg->{migration
})) {
1457 my $migration_unsecure = delete $cfg->{migration_unsecure
};
1458 $cfg->{migration
}->{type
} = ($migration_unsecure) ?
'insecure' : 'secure';
1461 # map deprecated applet setting to html5
1462 if (defined($cfg->{console
}) && $cfg->{console
} eq 'applet') {
1463 $cfg->{console
} = 'html5';
1466 if (my $migration = $cfg->{migration
}) {
1467 $cfg->{migration
} = PVE
::JSONSchema
::print_property_string
($migration, $migration_format);
1470 return PVE
::JSONSchema
::dump_config
($datacenter_schema, $filename, $cfg);
1473 cfs_register_file
('datacenter.cfg',
1474 \
&parse_datacenter_config
,
1475 \
&write_datacenter_config
);
1477 # X509 Certificate cache helper
1479 my $cert_cache_nodes = {};
1480 my $cert_cache_timestamp = time();
1481 my $cert_cache_fingerprints = {};
1483 sub update_cert_cache
{
1484 my ($update_node, $clear) = @_;
1486 syslog
('info', "Clearing outdated entries from certificate cache")
1489 $cert_cache_timestamp = time() if !defined($update_node);
1491 my $node_list = defined($update_node) ?
1492 [ $update_node ] : [ keys %$cert_cache_nodes ];
1494 foreach my $node (@$node_list) {
1495 my $clear_old = sub {
1496 if (my $old_fp = $cert_cache_nodes->{$node}) {
1497 # distrust old fingerprint
1498 delete $cert_cache_fingerprints->{$old_fp};
1499 # ensure reload on next proxied request
1500 delete $cert_cache_nodes->{$node};
1504 my $fp = eval { get_node_fingerprint
($node) };
1507 &$clear_old() if $clear;
1511 my $old_fp = $cert_cache_nodes->{$node};
1512 $cert_cache_fingerprints->{$fp} = 1;
1513 $cert_cache_nodes->{$node} = $fp;
1515 if (defined($old_fp) && $fp ne $old_fp) {
1516 delete $cert_cache_fingerprints->{$old_fp};
1521 # load and cache cert fingerprint once
1522 sub initialize_cert_cache
{
1525 update_cert_cache
($node)
1526 if defined($node) && !defined($cert_cache_nodes->{$node});
1529 sub read_ssl_cert_fingerprint
{
1530 my ($cert_path) = @_;
1532 my $bio = Net
::SSLeay
::BIO_new_file
($cert_path, 'r')
1533 or die "unable to read '$cert_path' - $!\n";
1535 my $cert = Net
::SSLeay
::PEM_read_bio_X509
($bio);
1536 Net
::SSLeay
::BIO_free
($bio);
1538 die "unable to read certificate from '$cert_path'\n" if !$cert;
1540 my $fp = Net
::SSLeay
::X509_get_fingerprint
($cert, 'sha256');
1541 Net
::SSLeay
::X509_free
($cert);
1543 die "unable to get fingerprint for '$cert_path' - got empty value\n"
1544 if !defined($fp) || $fp eq '';
1549 sub get_node_fingerprint
{
1552 my $cert_path = "/etc/pve/nodes/$node/pve-ssl.pem";
1553 my $custom_cert_path = "/etc/pve/nodes/$node/pveproxy-ssl.pem";
1555 $cert_path = $custom_cert_path if -f
$custom_cert_path;
1557 return read_ssl_cert_fingerprint
($cert_path);
1561 sub check_cert_fingerprint
{
1564 # clear cache every 30 minutes at least
1565 update_cert_cache
(undef, 1) if time() - $cert_cache_timestamp >= 60*30;
1567 # get fingerprint of server certificate
1568 my $fp = Net
::SSLeay
::X509_get_fingerprint
($cert, 'sha256');
1569 return 0 if !defined($fp) || $fp eq ''; # error
1572 for my $expected (keys %$cert_cache_fingerprints) {
1573 return 1 if $fp eq $expected;
1578 return 1 if &$check();
1580 # clear cache and retry at most once every minute
1581 if (time() - $cert_cache_timestamp >= 60) {
1582 syslog
('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache");
1583 update_cert_cache
();
1590 # bash completion helpers
1592 sub complete_next_vmid
{
1594 my $vmlist = get_vmlist
() || {};
1595 my $idlist = $vmlist->{ids
} || {};
1597 for (my $i = 100; $i < 10000; $i++) {
1598 return [$i] if !defined($idlist->{$i});
1606 my $vmlist = get_vmlist
();
1607 my $ids = $vmlist->{ids
} || {};
1609 return [ keys %$ids ];
1612 sub complete_local_vmid
{
1614 my $vmlist = get_vmlist
();
1615 my $ids = $vmlist->{ids
} || {};
1617 my $nodename = PVE
::INotify
::nodename
();
1620 foreach my $vmid (keys %$ids) {
1621 my $d = $ids->{$vmid};
1622 next if !$d->{node
} || $d->{node
} ne $nodename;
1629 sub complete_migration_target
{
1633 my $nodename = PVE
::INotify
::nodename
();
1635 my $nodelist = get_nodelist
();
1636 foreach my $node (@$nodelist) {
1637 next if $node eq $nodename;
1645 my ($node, $network_cidr) = @_;
1648 if (defined($network_cidr)) {
1649 # Use mtunnel via to get the remote node's ip inside $network_cidr.
1650 # This goes over the regular network (iow. uses get_ssh_info() with
1651 # $network_cidr undefined.
1652 # FIXME: Use the REST API client for this after creating an API entry
1653 # for get_migration_ip.
1654 my $default_remote = get_ssh_info
($node, undef);
1655 my $default_ssh = ssh_info_to_command
($default_remote);
1656 my $cmd =[@$default_ssh, 'pvecm', 'mtunnel',
1657 '-migration_network', $network_cidr,
1660 PVE
::Tools
::run_command
($cmd, outfunc
=> sub {
1663 die "internal error: unexpected output from mtunnel\n"
1665 if ($line =~ /^ip: '(.*)'$/) {
1668 die "internal error: bad output from mtunnel\n"
1672 die "failed to get ip for node '$node' in network '$network_cidr'\n"
1675 $ip = remote_node_ip
($node);
1681 network
=> $network_cidr,
1685 sub ssh_info_to_command_base
{
1686 my ($info, @extra_options) = @_;
1690 '-o', 'BatchMode=yes',
1691 '-o', 'HostKeyAlias='.$info->{name
},
1696 sub ssh_info_to_command
{
1697 my ($info, @extra_options) = @_;
1698 my $cmd = ssh_info_to_command_base
($info, @extra_options);
1699 push @$cmd, "root\@$info->{ip}";
1703 sub assert_joinable
{
1704 my ($ring0_addr, $ring1_addr, $force) = @_;
1707 my $error = sub { $errors .= "* $_[0]\n"; };
1710 $error->("authentication key '$authfile' already exists");
1713 if (-f
$clusterconf) {
1714 $error->("cluster config '$clusterconf' already exists");
1717 my $vmlist = get_vmlist
();
1718 if ($vmlist && $vmlist->{ids
} && scalar(keys %{$vmlist->{ids
}})) {
1719 $error->("this host already contains virtual guests");
1722 if (run_command
(['corosync-quorumtool', '-l'], noerr
=> 1, quiet
=> 1) == 0) {
1723 $error->("corosync is already running, is this node already in a cluster?!");
1726 # check if corosync ring IPs are configured on the current nodes interfaces
1727 my $check_ip = sub {
1728 my $ip = shift // return;
1729 if (!PVE
::JSONSchema
::pve_verify_ip
($ip, 1)) {
1731 eval { $ip = PVE
::Network
::get_ip_from_hostname
($host); };
1733 $error->("cannot use '$host': $@\n") ;
1738 my $cidr = (Net
::IP
::ip_is_ipv6
($ip)) ?
"$ip/128" : "$ip/32";
1739 my $configured_ips = PVE
::Network
::get_local_ip_from_cidr
($cidr);
1741 $error->("cannot use IP '$ip', it must be configured exactly once on local node!\n")
1742 if (scalar(@$configured_ips) != 1);
1745 $check_ip->($ring0_addr);
1746 $check_ip->($ring1_addr);
1749 warn "detected the following error(s):\n$errors";
1750 die "Check if node may join a cluster failed!\n" if !$force;
1754 # NOTE: filesystem must be offline here, no DB changes allowed
1755 my $backup_cfs_database = sub {
1761 my $backup_fn = "$dbbackupdir/config-$ctime.sql.gz";
1763 print "backup old database to '$backup_fn'\n";
1765 my $cmd = [ ['sqlite3', $dbfile, '.dump'], ['gzip', '-', \
">${backup_fn}"] ];
1766 run_command
($cmd, 'errmsg' => "cannot backup old database\n");
1768 my $maxfiles = 10; # purge older backup
1769 my $backups = [ sort { $b cmp $a } <$dbbackupdir/config-*.sql
.gz
> ];
1771 if ((my $count = scalar(@$backups)) > $maxfiles) {
1772 foreach my $f (@$backups[$maxfiles..$count-1]) {
1773 next if $f !~ m/^(\S+)$/; # untaint
1774 print "delete old backup '$1'\n";
1783 my $nodename = PVE
::INotify
::nodename
();
1785 setup_sshd_config
();
1786 setup_rootsshconfig
();
1789 # check if we can join with the given parameters and current node state
1790 my ($ring0_addr, $ring1_addr) = $param->@{'ring0_addr', 'ring1_addr'};
1791 assert_joinable
($ring0_addr, $ring1_addr, $param->{force
});
1793 # make sure known_hosts is on local filesystem
1794 ssh_unmerge_known_hosts
();
1796 my $host = $param->{hostname
};
1797 my $local_ip_address = remote_node_ip
($nodename);
1800 username
=> 'root@pam',
1801 password
=> $param->{password
},
1802 cookie_name
=> 'PVEAuthCookie',
1803 protocol
=> 'https',
1808 if (my $fp = $param->{fingerprint
}) {
1809 $conn_args->{cached_fingerprints
} = { uc($fp) => 1 };
1811 # API schema ensures that we can only get here from CLI handler
1812 $conn_args->{manual_verification
} = 1;
1815 print "Etablishing API connection with host '$host'\n";
1817 my $conn = PVE
::APIClient
::LWP-
>new(%$conn_args);
1820 # login raises an exception on failure, so if we get here we're good
1821 print "Login succeeded.\n";
1824 $args->{force
} = $param->{force
} if defined($param->{force
});
1825 $args->{nodeid
} = $param->{nodeid
} if $param->{nodeid
};
1826 $args->{votes
} = $param->{votes
} if defined($param->{votes
});
1827 $args->{ring0_addr
} = $ring0_addr // $local_ip_address;
1828 $args->{ring1_addr
} = $ring1_addr if defined($ring1_addr);
1830 print "Request addition of this node\n";
1831 my $res = $conn->post("/cluster/config/nodes/$nodename", $args);
1833 print "Join request OK, finishing setup locally\n";
1835 # added successfuly - now prepare local node
1836 finish_join
($nodename, $res->{corosync_conf
}, $res->{corosync_authkey
});
1840 my ($nodename, $corosync_conf, $corosync_authkey) = @_;
1842 mkdir "$localclusterdir";
1843 PVE
::Tools
::file_set_contents
($authfile, $corosync_authkey);
1844 PVE
::Tools
::file_set_contents
($localclusterconf, $corosync_conf);
1846 print "stopping pve-cluster service\n";
1847 my $cmd = ['systemctl', 'stop', 'pve-cluster'];
1848 run_command
($cmd, errmsg
=> "can't stop pve-cluster service");
1850 $backup_cfs_database->($dbfile);
1853 $cmd = ['systemctl', 'start', 'corosync', 'pve-cluster'];
1854 run_command
($cmd, errmsg
=> "starting pve-cluster failed");
1858 while (!check_cfs_quorum
(1)) {
1860 print "waiting for quorum...";
1866 print "OK\n" if !$printqmsg;
1868 updatecerts_and_ssh
(1);
1870 print "generated new node certificate, restart pveproxy and pvedaemon services\n";
1871 run_command
(['systemctl', 'reload-or-restart', 'pvedaemon', 'pveproxy']);
1873 print "successfully added node '$nodename' to cluster.\n";
1876 sub updatecerts_and_ssh
{
1877 my ($force_new_cert, $silent) = @_;
1879 my $p = sub { print "$_[0]\n" if !$silent };
1881 setup_rootsshconfig
();
1883 gen_pve_vzdump_symlink
();
1885 if (!check_cfs_quorum
(1)) {
1886 return undef if $silent;
1887 die "no quorum - unable to update files\n";
1892 my $nodename = PVE
::INotify
::nodename
();
1893 my $local_ip_address = remote_node_ip
($nodename);
1895 $p->("(re)generate node files");
1896 $p->("generate new node certificate") if $force_new_cert;
1897 gen_pve_node_files
($nodename, $local_ip_address, $force_new_cert);
1899 $p->("merge authorized SSH keys and known hosts");
1901 ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
1902 gen_pve_vzdump_files
();