[explorer/machine_type] Reimplement

This commit is contained in:
Dennis Camera 2021-07-21 13:22:34 +02:00
parent fbc9594729
commit edcac70b2a

View file

@ -1,8 +1,6 @@
#!/bin/sh #!/bin/sh -e
# #
# 2014 Daniel Heule (hda at sfs.biz) # 2021 Dennis Camera (cdist at dtnr.ch)
# 2014 Thomas Oettli (otho at sfs.biz)
# 2020 Evilham (contact at evilham.com)
# #
# This file is part of cdist. # This file is part of cdist.
# #
@ -19,91 +17,915 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>. # along with cdist. If not, see <http://www.gnu.org/licenses/>.
# #
# This explorer tries to determine what type of machine the target to be
# configured is (container, virtual machine, bare-metal).
#
# It will print one line for each layer it can detect.
# The format of all lines is: TYPE[ VERB VENDOR]
#
# VERB does not have a special meaning, it is just for better readability.
#
# e.g.
# container on lxc
# virtual by kvm-spapr
#
# The third word of each line can be composed of different parts concatenated with a `-'
# (minus) character, with each component being a specification of the previous,
# e.g.:
# - lxc-libvirt (LXC container, managed by libvirt)
# - lpar-s390 / lpar-power (LPAR running on IBM S/390 or POWER, respectively)
# - xen-hvm / xen-pv (Xen HVM vs para-virtualization)
#
# If this explorer cannot determine any information it will print nothing.
#
os=$("$__explorer/os") # Add /sbin and /usr/sbin to the path so we can find system
# binaries like dmidecode.
PATH=$(getconf PATH 2>/dev/null) || PATH='/usr/bin:/bin'
PATH="/sbin:/usr/sbin:${PATH}"
export PATH
vendor_string_to_machine_type() { arch=$(uname -m | sed -e 's/i.86/i386/' -e 's/arm.*/arm/')
for vendor in vmware bochs kvm qemu virtualbox bhyve; do uname_s=$(uname -s)
if echo "${1}" | grep -q -i "${vendor}"; then
if [ "${vendor}" = "bochs" ] || [ "${vendor}" = "qemu" ]; then
vendor="kvm" is_command() { command -v "$1" >/dev/null 2>&1; }
fi
echo "virtual_by_${vendor}" is_oneof() (
exit x=$1; shift
fi for y
do
test "${x}" = "${y}" || continue
return 0
done done
return 1
)
tolower() { LC_ALL=C tr '[:upper:]' '[:lower:]'; }
# shellcheck disable=SC2086
glob_exists() { set -- $1; test -e "$1"; }
get_dmi_field() {
if is_oneof "${uname_s}" NetBSD
then
case $1
in
(system-manufacturer) _mib=machdep.dmi.system-vendor ;;
(system-product-name) _mib=machdep.dmi.system-product ;;
(system-version|system-uuid) _mib=machdep.dmi.$1 ;;
(bios-vendor|bios-version) _mib=machdep.dmi.$1 ;;
(biod-release-date) _mib=machdep.dmi.bios-date ;;
(*) _mib= ;;
esac
test -n "${_mib}" && get_sysctl "${_mib}" | grep -e . && return
fi
if is_command dmidecode
then
dmidecode -s "$1"
elif test -d "${dmi_sysfs-}"
then
case $1
in
(system-manufacturer) _filename=sys_vendor ;;
(system-product-name) _filename=product_name ;;
(*) _filename=$(echo "$1" | tr - _) ;;
esac
if test -r "${dmi_sysfs-}/${_filename}"
then
cat "${dmi_sysfs}/${_filename}"
fi
unset _filename
elif test "${uname_s}" = OpenBSD
then
# NOTE: something similar to system-manufacutrer and system-product-name
# is available on OpenBSD in sysctl
case $1
in
(system-manufacturer) _mib=hw.vendor ;;
(system-product-name) _mib=hw.product ;;
(*) _mib= ;;
esac
test -n "${_mib}" && get_sysctl "${_mib}" | grep -e . && return
fi
return 1
} }
case "$os" in has_cpuinfo() { test -e /proc/cpuinfo; }
"freebsd")
# FreeBSD does not have /proc/cpuinfo even when procfs is used. get_sysctl() {
# Instead there is a sysctl kern.vm_guest. is_command sysctl && sysctl -n "$1" 2>/dev/null
# Which is 'none' if physical, else the virtualisation. }
vm_guest="$(sysctl -n kern.vm_guest 2>/dev/null || true)"
if [ -n "${vm_guest}" ]; then
if [ "${vm_guest}" = "none" ]; then # Check for container
echo "physical"
exit has_ct_pid_1() {
test -r /run/systemd/container -o -r /proc/1/environ
}
translate_container_name() {
case $1
in
('lxc')
echo lxc ;;
('lxc-libvirt')
echo lxc-libvirt ;;
('podman')
echo podman ;;
('systemd-nspawn')
echo systemd_nspawn ;;
(*)
return 1 ;;
esac
return 0
}
check_ct_pid_1() {
if test -r /run/systemd/container
then
translate_container_name "$(head -n1 /run/systemd/container)" \
&& return 0
fi fi
echo "virtual_by_${vm_guest}"
exit if test -r /proc/1/environ
then
translate_container_name "$(
LC_ALL=C tr '\000' '\n' </proc/1/environ \
| sed -n -e 's/^container=//p')" \
&& return 0
fi fi
;;
"openbsd") return 1
# OpenBSD can also use the sysctl's: hw.vendor or hw.product. }
# Note we can be reasonably sure about a machine being virtualised
# as long as we can identify the virtualisation technology.
# But not so much about it being physical...
# Patches are welcome / reach out if you have better ideas.
for sysctl in hw.vendor hw.product; do
# This exits if we can make a reasonable judgement
vendor_string_to_machine_type "$(sysctl -n "${sysctl}")"
done
;;
*) has_ct_cgroup() {
# Defaulting to linux for compatibility with previous cdist behaviour test -r /proc/self/cgroup
}
check_ct_cgroup() {
if grep -q -e ':/docker/' /proc/self/cgroup
then
echo docker
elif grep -q -e ':/lxc/.*\.libvirt-lxc$' /proc/self/cgroup
then
echo libvirt-lxc
elif grep -q -F '/libpod-' /proc/self/cgroup
then
echo podman
else
return 1
fi
}
check_ct_files() {
if test -e /.dockerenv -o -e /run/.dockerenv -o -e /.dockerinit
then
echo docker
elif test -f /run/.containerenv
then
# https://github.com/containers/podman/issues/3586#issuecomment-661918679
echo podman
elif test -d /proc/vz -a ! -d /proc/bc -a -f /proc/user_beancounters \
|| test -d /.cpt_hardlink_dir_*/
then
# Virtuozzo / OpenVZ
#
# /proc/vz: always exists if OpenVZ kernel is running
# /proc/bc: exists only on node (not inside container)
# /proc/user_beancounters - exists inside container
#
# /.cpt_hardlink_dir_*/ may be (?) created by vzctl:
# https://github.com/kolyshkin/vzctl/commit/09e974f
if [ -d "/proc/vz" ] && [ ! -d "/proc/bc" ]; then
echo openvz echo openvz
exit elif grep -qie 'Microsoft\|WSL' /proc/sys/kernel/osrelease /proc/version 2>/dev/null
fi then
# https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364
if [ -e "/proc/1/environ" ] && echo wsl
tr '\000' '\n' < "/proc/1/environ" | grep -Eiq '^container='; then elif test -d /var/.cagefs
echo lxc then
exit # https://docs.cloudlinux.com/cloudlinux_os_components/#cagefs
fi # CageFS is not "really" a container, but it isn't a chroot either.
echo cagefs
if [ -r /proc/cpuinfo ]; then elif test -e /proc/self/status && grep -q -e '^VxID: [0-9]\{1,\}' /proc/self/status
# this should only exist on virtual guest machines, then
# tested on vmware, xen, kvm, bhyve # Linux-VServer
if grep -q "hypervisor" /proc/cpuinfo; then if grep -q -x -F 'VxID: 0' /proc/self/status
# this file is aviable in xen guest systems then
if [ -r /sys/hypervisor/type ]; then # host
if grep -q -i "xen" /sys/hypervisor/type; then return 1
echo virtual_by_xen else
exit # guest
echo linux_vserver
fi fi
else else
for vendor_file in /sys/class/dmi/id/product_name \ return 1
/sys/class/dmi/id/sys_vendor \
/sys/class/dmi/id/chasis_vendor; do
if [ -r ${vendor_file} ]; then
# This exits if we can make a reasonable judgement
vendor_string_to_machine_type "$(cat "${vendor_file}")"
fi fi
}
check_ct_os_specific() (
if jailed=$(get_sysctl security.jail.jailed) && test "${jailed}" = 1
then
# FreeBSD jail
echo jail
return 0
fi
if is_command zonename && test "$(zonename)" != global
then
# Solaris zone
echo zone
return 0
fi
return 1
)
# Check for hypervisor
guess_hypervisor_from_cpu_model() {
case $1
in
(*\ KVM\ *)
echo kvm ;;
(*\ QEMU\ *|QEMU\ *)
echo qemu ;;
(*)
return 1 ;;
esac
}
has_vm_cpuinfo() { has_cpuinfo; }
check_vm_cpuinfo() {
if grep -q -F 'User Mode Linux' /proc/cpuinfo \
|| grep -q -F 'UML' /proc/cpuinfo
then
# User Mode Linux
echo uml
elif grep -q -e '^vendor_id.*: PowerVM Lx86' /proc/cpuinfo
then
# IBM PowerVM Lx86 (Linux/x86 emulator)
echo powervm_lx86
elif grep -q -e '^vendor_id.*: IBM/S390' /proc/cpuinfo
then
# IBM SystemZ (S/390)
if test -f /proc/sysinfo
then
if grep -q -e '^VM[0-9]* Control Program: KVM/Linux' /proc/sysinfo
then
echo kvm-s390
return 0
elif grep -q -e '^VM[0-9]* Control Program: z/VM' /proc/sysinfo
then
echo zvm
return 0
elif grep -q -e '^LPAR ' /proc/sysinfo
then
echo zvm-lpar
return 0
fi
fi
return 1
else
if grep -q -e '^model name.*:' /proc/cpuinfo
then
sed -n -e 's/^model name[^:]*: *//p' /proc/cpuinfo \
| while read -r _cpu_model
do
guess_hypervisor_from_cpu_model "${_cpu_model}"
done \
| sort \
| uniq -c \
| awk '
{ if ($1 > most_c) { most_c = $1; most_s = $2 } }
END {
if (most_s) print most_s
exit !most_s
}' \
&& return 0
fi
return 1
fi
}
check_vm_arch_specific() {
case ${arch}
in
(ppc64|ppc64le)
# Check PPC64 LPAR, KVM
# example /proc/cpuinfo line indicating 'not baremetal'
# platform : pSeries
#
# example /proc/ppc64/lparcfg systemtype line
# system_type=IBM pSeries (emulated by qemu)
if has_cpuinfo && grep -q -e 'platform.**pSeries' /proc/cpuinfo
then
if test -e /proc/ppc64/lparcfg
then
# Assume LPAR, now detect shared or dedicated
if grep -q -x -F 'shared_processor_mode=1' /proc/ppc64/lparcfg
then
echo powervm-shared
return 0
else
echo powervm-dedicated
return 0
fi
fi
fi
;;
(sparc*)
# Check for SPARC LDoms
if test -e /dev/mdesc
then
if test -d /sys/class/vlds/ctrl -a -d /sys/class/vlds/sp
then
# control LDom
return 1
else
# guest LDom
echo ldom-sparc
fi
# MDPROP=/usr/lib/ldoms/mdprop.py
# if test -x "${MDPROP}"
# then
# if test -n "$("${MDPROP}" -v iodevice device-type=pciex)"
# then
# echo ldoms-root
# echo ldoms-io
# elif test -n "$("${MDPROP}" -v iov-device vf-id=0)"
# then
# echo ldoms-io
# fi
# fi
return 0
fi
;;
(i?86|x86*|amd64|i86pc)
# Check CPUID
#
# Many fullvirt hypervisors give an indication through CPUID. Use
# the virt-what helper program to get this information if available.
for CPUID_HELPER in \
$(command -v virt-what-cpuid-helper 2>/dev/null) \
/usr/lib/x86_64-*/virt-what-cpuid-helper \
/usr/lib/i?86-*/virt-what-cpuid-helper \
/usr/lib/virt-what/virt-what-cpuid-helper
do
if test -x "${CPUID_HELPER:?}"; then break; fi
done done
fi
echo "virtual_by_unknown" if test -x "${CPUID_HELPER-}"
exit then
case $(command "${CPUID_HELPER}")
in
('bhyve bhyve ')
echo bhyve
;;
('LKVMLKVMLKVM')
echo lkvm
;;
('KVMKVMKVM')
echo kvm
;;
('TCGTCGTCGTCG')
echo qemu-tcg
;;
('Microsoft Hv')
# http://blogs.msdn.com/b/sqlosteam/archive/2010/10/30/is-this-real-the-metaphysics-of-hardware-virtualization.aspx
echo hyperv
;;
('OpenBSDVMM58')
# OpenBSD/VMM
echo openbsd_vmm
;;
('VMwareVMware')
# check added by Chetan Loke.
echo vmware
;;
('XenVMMXenVMM')
if has dmi
then
# https://access.redhat.com/solutions/222903
echo xen-hvm
else else
echo "physical" echo xen-paravirt
exit
fi fi
;;
(*)
return 1 ;;
esac
return 0
fi
unset CPUID_HELPER
# VMM CPUID flag denotes that this system is running under a VMM
if is_oneof "${uname_s}" Darwin
then
get_sysctl machdep.cpu.features | tr ' ' '\n' | grep -qixF VMM \
&& return 0
fi
if has_cpuinfo \
&& grep -q -i -e '^flags.*:.*\(hypervisor\|vmm\)' /proc/cpuinfo
then
return 0
fi
;;
(ia64)
if test -d /sys/bus/xen -a ! -d /sys/bus/xen-backend
then
# PV-on-HVM drivers installed in a Xen guest
echo xen-hvm
return 0
fi
;;
esac
return 1
}
has_vm_dmi() {
# Check for various products in SMBIOS/DMI.
# Note that DMI doesn't exist on all architectures (only x86 and some ARM).
# On other architectures the $dmi variable will be empty.
if test -d /sys/class/dmi/id/
then
dmi_sysfs=/sys/class/dmi/id
elif test -d /sys/devices/virtual/dmi/id/
then
dmi_sysfs=/sys/devices/virtual/dmi/id
fi
# shellcheck disable=SC2015
{
is_command dmidecode \
&& (
# dmidecode needs to exit 0 and not print the No SMBIOS/DMI line
dmi_out=$(dmidecode 2>&1) \
&& ! printf '%s\n' "${dmi_out}" \
| grep -qF 'No SMBIOS nor DMI entry point found, sorry.'
) \
|| test -d "${dmi_sysfs}"
}
}
check_vm_dmi() {
case $(get_dmi_field system-product-name)
in
(*.metal)
if test "$(get_dmi_field system-manufacturer)" = 'Amazon EC2'
then
# AWS EC2 bare metal -> no virtualisation
return 1
fi
;;
('BHYVE')
echo bhyve
return 0
;;
('Google Compute Engine')
echo gce
return 0
;;
('RHEV Hypervisor')
# Red Hat Enterprise Virtualization
echo rhev
return 0
;;
('KVM'|'Bochs'|'KVM Virtual Machine')
echo kvm
return 0
;;
('Parallels Virtual Platform')
echo parallels
return 0
;;
('VirtualBox')
echo virtualbox
return 0
;;
('VMware Virtual Platform')
echo vmware
return 0
;;
esac
case $(get_dmi_field system-manufacturer)
in
('Alibaba'*)
case $(get_dmi_field system-product-name)
in
('Alibaba Cloud ECS')
echo alibaba-ecs
;;
(*)
echo alibaba
;;
esac
return 0
;;
('Amazon EC2')
# AWS on bare-metal or KVM
echo aws-ec2
return 0
;;
('innotek GmbH'|'Oracle Corporation')
echo virtualbox
return 0
;;
('Joyent')
if test "$(get_dmi_field system-product-name)" = 'SmartDC HVM'
then
# SmartOS KVM
echo kvm-smartdc_hvm
return 0
fi
;;
('Microsoft Corporation'*)
if test "$(get_dmi_field system-product-name)" = 'Virtual Machine'
then
if test -e /proc/irq/7/hyperv \
|| expr "$(get_dmi_field bios-version)" : 'VRTUAL.*' >/dev/null
then
echo hyperv
return 0
fi
case $(get_dmi_field system-version)
in
(VPC[0-9]*|VS2005*|*[Vv]irtual*[Pp][Cc]*)
echo virtualpc
return 0
;;
(*)
echo hyperv
return 0
;;
esac
fi
;;
('Nutanix')
# Nutanix AHV. Similar to KVM.
if test "$(get_dmi_field system-product-name)" = 'AHV'
then
echo nutanix_ahv
return 0
fi
;;
('oVirt')
echo ovirt
return 0
;;
('Parallels Software International Inc.')
echo parallels
return 0
;;
('QEMU')
echo qemu
return 0
;;
('VMware, Inc.')
echo vmware
return 0
;;
esac
case $(get_dmi_field bios-vendor)
in
('Amazon EC2')
# AWS on bare-metal or KVM
echo aws-ec2
return 0
;;
('BHYVE')
echo bhyve
return 0
;;
('innotek GmbH')
echo virtualbox
return 0
;;
('Parallels Software International Inc.')
echo parallels
return 0
;;
('Xen')
if get_dmi_field bios-version | grep -q -e '\([0-9]\{1,\}\.\)\{2\}amazon'
then
# AWS on Xen
echo aws-xen
return 0
fi fi
;; ;;
esac esac
echo "unknown" return 1
}
check_vm_hyp_specific() {
if is_command vmware-checkvm && vmware-checkvm >/dev/null
then
# vmware-checkvm is provided by VMware's open-vm-tools
echo vmware
return 0
elif test -d /proc/xen
then
test -r /proc/xen/capabilities &&
if grep -q -F 'control_d' /proc/xen/capabilities 2>/dev/null
then
# Xen dom0
return 1
else
# Xen domU
echo xen
return 0
fi
fi
return 1
}
has_vm_dt() {
# OpenFirmware/Das U-Boot device-tree
test -d /proc/device-tree
}
check_vm_dt() {
case ${arch}
in
(arm|aarch64)
if test -r /proc/device-tree/hypervisor/compatible
then
if grep -q -F 'xen' /proc/device-tree/hypervisor/compatible
then
echo xen
return 0
elif grep -q -F 'vmware' /proc/device-tree/hypervisor/compatible
then
# e.g. VMware ESXi on ARM
echo vmware
return 0
fi
fi
if glob_exists /proc/device-tree/fw-cfg@*/compatible
then
# qemu,fw-cfg-mmio
sed -e 's/,.*$//' /proc/device-tree/fw-cfg@*/compatible | head -n1
return 0
fi
if grep -q -F 'dummy-virt' /proc/device-tree/compatible
then
echo lkvm
return 0
fi
;;
(ppc64*)
if test -d /proc/device-tree/hypervisor \
&& grep -qF 'linux,kvm' /proc/device-tree/hypervisor/compatible
then
# We are running as a spapr KVM guest on ppc64
echo kvm-spapr
return 0
fi
if test -r /proc/device-tree/ibm,partition-name \
&& test -r /proc/device-tree/hmc-managed\? \
&& test -r /proc/device-tree/chosen/qemu,graphic-width
then
echo powervm
fi
;;
esac
return 1
}
has_vm_sys_hypervisor() {
test -d /sys/hypervisor/
}
check_vm_sys_hypervisor() {
test -r /sys/hypervisor/type &&
case $(head -n1 /sys/hypervisor/type)
in
(xen)
# Ordinary kernel with pv_ops. There does not seem to be
# enough information at present to tell whether this is dom0
# or domU.
echo xen
return 0
;;
esac
return 1
}
check_vm_os_specific() {
_hyp_generic=false
case ${uname_s}
in
(Darwin)
if hv_vmm_present=$(get_sysctl kern.hv_vmm_present) \
&& test "${hv_vmm_present}" -ne 0
then
_hyp_generic=true
fi
;;
(FreeBSD)
# FreeBSD does not have /proc/cpuinfo even when procfs is used.
# Instead there is a sysctl kern.vm_guest.
# Which is 'none' if physical, else the virtualisation.
vm_guest=$(get_sysctl kern.vm_guest | tolower) &&
case ${vm_guest}
in
(none) ;;
(generic) _hyp_generic=true ;;
(*)
# kernel could detect hypervisor
case ${vm_guest}
in
(hv) echo hyperv ;;
(vbox) echo virtualbox ;;
(*) echo "${vm_guest}" ;;
esac
return 0
;;
esac
;;
(NetBSD)
machdep_hv=$(get_sysctl machdep.hypervisor | tolower) &&
case ${machdep_hv}
in
(none) ;;
(generic) _hyp_generic=true ;;
(*)
# kernel could detect hypervisor
case ${machdep_hv}
in
(hyper-v) echo hyperv ;;
(xenhvm*) echo xen-hvm ;;
(xenpv*) echo xen-pv ;;
(xen*) echo xen ;;
(*) echo "${machdep_hv}" ;;
esac
return 0
;;
esac
;;
(OpenBSD)
if is_command hostctl && glob_exists /dev/pvbus[0-9]*
then
for _pvbus in /dev/pvbus[0-9]*
do
_h_out=$(hostctl -f "${_pvbus}" -t 2>/dev/null) || continue
case $(expr "${_h_out}" : '[^:]*: *\(.*\)$')
in
(KVM) echo kvm ;;
(Hyper-V) echo hyperv ;;
(VMware) echo vmware ;;
(Xen) echo xen ;;
(bhyve) echo bhyve ;;
(OpenBSD) echo openbsd_vmm ;;
esac
return 0
done
fi
;;
(SunOS)
diag_conf=$(prtdiag | sed -n -e 's/.*Configuration: *//p' -e '/^$/q')
# NOTE: Don't use -e or -F in Solaris grep
if printf '%s\n' "${diag_conf}" | grep -q -i QEMU
then
echo qemu
return 0
elif printf '%s\n' "${diag_conf}" | grep -q -i VMware
then
echo vmware
return 0
fi
;;
(Linux)
if is_command dmesg
then
while read -r line
do
case ${line}
in
('Booting paravirtualized kernel on ')
case $(expr "${line}" : '.* kernel on \(.*\)')
in
('Xen')
echo xen-pv; return 0 ;;
('bare hardware')
return 1 ;;
esac
;;
('Hypervisor detected')
case $(expr "${line}" : '.*: *\(.*\)')
in
('ACRN')
echo acrn ;;
('Jailhouse')
echo jailhouse ;;
('KVM')
echo kvm ;;
('Microsoft Hyper-V')
echo hyperv ;;
('VMware')
echo vmware ;;
('Xen HVM')
echo xen-hvm ;;
('Xen PV')
echo xen-pv ;;
esac
return 0
;;
(lpar:*' under hypervisor')
return 0 ;;
esac
done <<-EOF
$(dmesg 2>/dev/null | awk '
/Booting paravirtualized kernel on /
/Hypervisor detected: /
/lpar: .* under hypervisor/
')
EOF
fi
esac
# Try to guess hypervisor based on CPU model (sysctl hw.model if available)
if cpu_model=$(get_sysctl hw.model)
then
guess_hypervisor_from_cpu_model "${cpu_model}" && return 0
fi
if ${_hyp_generic}
then
# cannot say which hypervisor, but one was detected
return 0
else
return 1
fi
}
run_stage() {
if type "has_$1_$2" >/dev/null 2>&1
then
"has_$1_$2"
else
true
fi \
&& "check_$1_$2"
}
# Execute container stages
for stage in \
pid_1 cgroup files os_specific
do
ctengine=$(run_stage ct ${stage}) || continue
is_contained=true
if test -n "${ctengine}"
then
echo container on "${ctengine}"
break
fi
done
if ${is_contained:-false} && test -z "${ctengine}"
then
# none of the stages could determine the specific container engine, but
# we are running in some container.
echo container
fi
# Execute virtual machine / hypervisor stages
for stage in \
os_specific hyp_specific sys_hypervisor dt dmi cpuinfo arch_specific
do
hypervisor=$(run_stage vm ${stage}) || continue
is_virtual=true
if test -n "${hypervisor}"
then
echo virtual by "${hypervisor}"
break
fi
done
if ${is_virtual:-false} && test -z "${hypervisor}"
then
# none of the stages could determine the specific hypervisor, but
# we are virtual.
echo virtual
fi