From cf6c36a18de233190996f7c2dde1e04908bad50a Mon Sep 17 00:00:00 2001 From: Mina Galić Date: Wed, 4 Nov 2020 15:49:34 +0100 Subject: split read_fs_info into linux & freebsd parts (#625) FreeBSD doesn't have blkid, so we want to use geom to list devices and their fstypes and labels. This PR also adds `jail` to the list of is_container() And we now also properly cache geom and blkid output! A test is added to verify the new behaviour by correctly identifying NoCloud on FreeBSD. Co-authored-by: Scott Moser --- tools/ds-identify | 106 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 9 deletions(-) (limited to 'tools/ds-identify') diff --git a/tools/ds-identify b/tools/ds-identify index 4e5700fc..ec9e775e 100755 --- a/tools/ds-identify +++ b/tools/ds-identify @@ -92,7 +92,8 @@ _DI_LOGGED="" # set DI_MAIN='noop' in environment to source this file with no main called. DI_MAIN=${DI_MAIN:-main} -DI_BLKID_OUTPUT="" +DI_BLKID_EXPORT_OUT="" +DI_GEOM_LABEL_STATUS_OUT="" DI_DEFAULT_POLICY="search,found=all,maybe=all,notfound=${DI_DISABLED}" DI_DEFAULT_POLICY_NO_DMI="search,found=all,maybe=all,notfound=${DI_ENABLED}" DI_DMI_CHASSIS_ASSET_TAG="" @@ -231,8 +232,19 @@ ensure_sane_path() { done } -read_fs_info() { - cached "${DI_BLKID_OUTPUT}" && return 0 +blkid_export() { + # call 'blkid -c /dev/null export', set DI_BLKID_EXPORT_OUT + cached "$DI_BLKID_EXPORT_OUT" && return 0 + local out="" ret=0 + out=$(blkid -c /dev/null -o export) && DI_BLKID_EXPORT_OUT="$out" || { + ret=$? + error "failed running [$ret]: blkid -c /dev/null -o export" + DI_BLKID_EXPORT_OUT="$UNAVAILABLE" + } + return $ret +} + +read_fs_info_linux() { # do not rely on links in /dev/disk which might not be present yet. # Note that blkid < 2.22 (centos6, trusty) do not output DEVNAME. # that means that DI_ISO9660_DEVS will not be set. @@ -244,20 +256,23 @@ read_fs_info() { return fi local oifs="$IFS" line="" delim="," - local ret=0 out="" labels="" dev="" label="" ftype="" isodevs="" uuids="" - out=$(blkid -c /dev/null -o export) || { - ret=$? - error "failed running [$ret]: blkid -c /dev/null -o export" + local ret=0 labels="" dev="" label="" ftype="" isodevs="" uuids="" + + blkid_export + ret=$? + [ "$DI_BLKID_EXPORT_OUT" = "$UNAVAILABLE" ] && { DI_FS_LABELS="$UNAVAILABLE:error" DI_ISO9660_DEVS="$UNAVAILABLE:error" + DI_FS_UUIDS="$UNAVAILABLE:error" return $ret } + # 'set --' will collapse multiple consecutive entries in IFS for # whitespace characters (\n, tab, " ") so we cannot rely on getting # empty lines in "$@" below. # shellcheck disable=2086 - { IFS="$CR"; set -- $out; IFS="$oifs"; } + { IFS="$CR"; set -- $DI_BLKID_EXPORT_OUT; IFS="$oifs"; } for line in "$@"; do case "${line}" in @@ -281,6 +296,74 @@ read_fs_info() { DI_ISO9660_DEVS="${isodevs#,}" } +geom_label_status_as() { + # call 'geom label status -as', set DI_GEOM_LABEL_STATUS_OUT + cached "$DI_GEOM_LABEL_STATUS_OUT" && return 0 + local out="" ret=0 + out=$(geom label status -as) && DI_GEOM_LABEL_STATUS_OUT="$out" || { + ret=$? + error "failed running [$ret]: geom label status -as" + DI_GEOM_LABEL_STATUS_OUT="$UNAVAILABLE" + } + return $ret +} + + +read_fs_info_freebsd() { + local oifs="$IFS" line="" delim="," + local ret=0 labels="" dev="" label="" ftype="" isodevs="" + + geom_label_status_as + ret=$? + [ "$DI_GEOM_LABEL_STATUS_OUT" = "$UNAVAILABLE" ] && { + DI_FS_LABELS="$UNAVAILABLE:error" + DI_ISO9660_DEVS="$UNAVAILABLE:error" + return $ret + } + + # The expected output looks like this: + # gpt/gptboot0 N/A vtbd1p1 + # gpt/swap0 N/A vtbd1p2 + # iso9660/cidata N/A vtbd2 + + # shellcheck disable=2086 + { IFS="$CR"; set -- $DI_GEOM_LABEL_STATUS_OUT; IFS="$oifs"; } + + for line in "$@"; do + # shellcheck disable=2086 + set -- $line + provider=$1 + ftype="${provider%/*}" + label="${provider#*/}" + dev=$3 + + [ -n "$dev" -a "$ftype" = "iso9660" ] && + isodevs="${isodevs},${dev}=$label" + + labels="${labels}${label}${delim}" + done + + DI_FS_LABELS="${labels%${delim}}" + DI_ISO9660_DEVS="${isodevs#,}" +} + +read_fs_info() { + # After calling its subfunctions, read_fs_info() will set the following + # variables: + # + # - DI_FS_LABELS + # - DI_ISO9660_DEVS + # - DI_FS_UUIDS + + if [ "$DI_UNAME_KERNEL_NAME" = "FreeBSD" ]; then + read_fs_info_freebsd + return $? + else + read_fs_info_linux + return $? + fi +} + cached() { [ -n "$1" ] && _RET="$1" && return || return 1 } @@ -319,6 +402,11 @@ detect_virt() { *) virt="$out" esac } + out=$(sysctl -qn security.jail.jailed 2>/dev/null) && { + if [ "$out" = "1" ]; then + virt="jail" + fi + } fi _RET="$virt" } @@ -331,7 +419,7 @@ read_virt() { is_container() { case "${DI_VIRT}" in - container-other|lxc|lxc-libvirt|systemd-nspawn|docker|rkt) return 0;; + container-other|lxc|lxc-libvirt|systemd-nspawn|docker|rkt|jail) return 0;; *) return 1;; esac } -- cgit v1.2.3 From d83c0bb4baca0b57166a74055f410fa4f75a08f5 Mon Sep 17 00:00:00 2001 From: Mina Galić Date: Fri, 6 Nov 2020 19:49:05 +0100 Subject: replace usage of dmidecode with kenv on FreeBSD (#621) FreeBSD lets us read out kernel parameters with kenv(1), a user-space utility that's shipped in "base" We can use it in place of dmidecode(8), thus removing the dependency on sysutils/dmidecode, and the restrictions to i386 and x86_64 architectures that this utility imposes on FreeBSD. Co-authored-by: Scott Moser --- cloudinit/dmi.py | 86 +++++++++++++++++++++++++------------ cloudinit/tests/test_dmi.py | 27 +++++++++++- cloudinit/util.py | 55 ++++++++++++++++++------ tests/unittests/test_ds_identify.py | 25 +++++++++-- tools/build-on-freebsd | 1 - tools/ds-identify | 42 ++++++++++++++++-- 6 files changed, 185 insertions(+), 51 deletions(-) (limited to 'tools/ds-identify') diff --git a/cloudinit/dmi.py b/cloudinit/dmi.py index 96e0e423..f0e69a5a 100644 --- a/cloudinit/dmi.py +++ b/cloudinit/dmi.py @@ -1,8 +1,9 @@ # This file is part of cloud-init. See LICENSE file for license information. from cloudinit import log as logging from cloudinit import subp -from cloudinit.util import is_container +from cloudinit.util import is_container, is_FreeBSD +from collections import namedtuple import os LOG = logging.getLogger(__name__) @@ -10,38 +11,43 @@ LOG = logging.getLogger(__name__) # Path for DMI Data DMI_SYS_PATH = "/sys/class/dmi/id" -# dmidecode and /sys/class/dmi/id/* use different names for the same value, -# this allows us to refer to them by one canonical name -DMIDECODE_TO_DMI_SYS_MAPPING = { - 'baseboard-asset-tag': 'board_asset_tag', - 'baseboard-manufacturer': 'board_vendor', - 'baseboard-product-name': 'board_name', - 'baseboard-serial-number': 'board_serial', - 'baseboard-version': 'board_version', - 'bios-release-date': 'bios_date', - 'bios-vendor': 'bios_vendor', - 'bios-version': 'bios_version', - 'chassis-asset-tag': 'chassis_asset_tag', - 'chassis-manufacturer': 'chassis_vendor', - 'chassis-serial-number': 'chassis_serial', - 'chassis-version': 'chassis_version', - 'system-manufacturer': 'sys_vendor', - 'system-product-name': 'product_name', - 'system-serial-number': 'product_serial', - 'system-uuid': 'product_uuid', - 'system-version': 'product_version', +kdmi = namedtuple('KernelNames', ['linux', 'freebsd']) +kdmi.__new__.defaults__ = (None, None) + +# FreeBSD's kenv(1) and Linux /sys/class/dmi/id/* both use different names from +# dmidecode. The values are the same, and ultimately what we're interested in. +# These tools offer a "cheaper" way to access those values over dmidecode. +# This is our canonical translation table. If we add more tools on other +# platforms to find dmidecode's values, their keys need to be put in here. +DMIDECODE_TO_KERNEL = { + 'baseboard-asset-tag': kdmi('board_asset_tag', 'smbios.planar.tag'), + 'baseboard-manufacturer': kdmi('board_vendor', 'smbios.planar.maker'), + 'baseboard-product-name': kdmi('board_name', 'smbios.planar.product'), + 'baseboard-serial-number': kdmi('board_serial', 'smbios.planar.serial'), + 'baseboard-version': kdmi('board_version', 'smbios.planar.version'), + 'bios-release-date': kdmi('bios_date', 'smbios.bios.reldate'), + 'bios-vendor': kdmi('bios_vendor', 'smbios.bios.vendor'), + 'bios-version': kdmi('bios_version', 'smbios.bios.version'), + 'chassis-asset-tag': kdmi('chassis_asset_tag', 'smbios.chassis.tag'), + 'chassis-manufacturer': kdmi('chassis_vendor', 'smbios.chassis.maker'), + 'chassis-serial-number': kdmi('chassis_serial', 'smbios.chassis.serial'), + 'chassis-version': kdmi('chassis_version', 'smbios.chassis.version'), + 'system-manufacturer': kdmi('sys_vendor', 'smbios.system.maker'), + 'system-product-name': kdmi('product_name', 'smbios.system.product'), + 'system-serial-number': kdmi('product_serial', 'smbios.system.serial'), + 'system-uuid': kdmi('product_uuid', 'smbios.system.uuid'), + 'system-version': kdmi('product_version', 'smbios.system.version'), } def _read_dmi_syspath(key): """ - Reads dmi data with from /sys/class/dmi/id + Reads dmi data from /sys/class/dmi/id """ - if key not in DMIDECODE_TO_DMI_SYS_MAPPING: + kmap = DMIDECODE_TO_KERNEL.get(key) + if kmap is None or kmap.linux is None: return None - mapped_key = DMIDECODE_TO_DMI_SYS_MAPPING[key] - dmi_key_path = "{0}/{1}".format(DMI_SYS_PATH, mapped_key) - + dmi_key_path = "{0}/{1}".format(DMI_SYS_PATH, kmap.linux) LOG.debug("querying dmi data %s", dmi_key_path) if not os.path.exists(dmi_key_path): LOG.debug("did not find %s", dmi_key_path) @@ -68,6 +74,29 @@ def _read_dmi_syspath(key): return None +def _read_kenv(key): + """ + Reads dmi data from FreeBSD's kenv(1) + """ + kmap = DMIDECODE_TO_KERNEL.get(key) + if kmap is None or kmap.freebsd is None: + return None + + LOG.debug("querying dmi data %s", kmap.freebsd) + + try: + cmd = ["kenv", "-q", kmap.freebsd] + (result, _err) = subp.subp(cmd) + result = result.strip() + LOG.debug("kenv returned '%s' for '%s'", result, kmap.freebsd) + return result + except subp.ProcessExecutionError as e: + LOG.debug('failed kenv cmd: %s\n%s', cmd, e) + return None + + return None + + def _call_dmidecode(key, dmidecode_path): """ Calls out to dmidecode to get the data out. This is mostly for supporting @@ -81,7 +110,7 @@ def _call_dmidecode(key, dmidecode_path): if result.replace(".", "") == "": return "" return result - except (IOError, OSError) as e: + except subp.ProcessExecutionError as e: LOG.debug('failed dmidecode cmd: %s\n%s', cmd, e) return None @@ -107,6 +136,9 @@ def read_dmi_data(key): if is_container(): return None + if is_FreeBSD(): + return _read_kenv(key) + syspath_value = _read_dmi_syspath(key) if syspath_value is not None: return syspath_value diff --git a/cloudinit/tests/test_dmi.py b/cloudinit/tests/test_dmi.py index 4a8af257..78a72122 100644 --- a/cloudinit/tests/test_dmi.py +++ b/cloudinit/tests/test_dmi.py @@ -19,6 +19,9 @@ class TestReadDMIData(helpers.FilesystemMockingTestCase): p = mock.patch("cloudinit.dmi.is_container", return_value=False) self.addCleanup(p.stop) self._m_is_container = p.start() + p = mock.patch("cloudinit.dmi.is_FreeBSD", return_value=False) + self.addCleanup(p.stop) + self._m_is_FreeBSD = p.start() def _create_sysfs_parent_directory(self): util.ensure_dir(os.path.join('sys', 'class', 'dmi', 'id')) @@ -44,13 +47,26 @@ class TestReadDMIData(helpers.FilesystemMockingTestCase): self.patched_funcs.enter_context( mock.patch("cloudinit.dmi.subp.subp", side_effect=_dmidecode_subp)) + def _configure_kenv_return(self, key, content, error=None): + """ + In order to test a FreeBSD system call outs to kenv, this + function fakes the results of kenv to test the results. + """ + def _kenv_subp(cmd): + if cmd[-1] != dmi.DMIDECODE_TO_KERNEL[key].freebsd: + raise subp.ProcessExecutionError() + return (content, error) + + self.patched_funcs.enter_context( + mock.patch("cloudinit.dmi.subp.subp", side_effect=_kenv_subp)) + def patch_mapping(self, new_mapping): self.patched_funcs.enter_context( - mock.patch('cloudinit.dmi.DMIDECODE_TO_DMI_SYS_MAPPING', + mock.patch('cloudinit.dmi.DMIDECODE_TO_KERNEL', new_mapping)) def test_sysfs_used_with_key_in_mapping_and_file_on_disk(self): - self.patch_mapping({'mapped-key': 'mapped-value'}) + self.patch_mapping({'mapped-key': dmi.kdmi('mapped-value', None)}) expected_dmi_value = 'sys-used-correctly' self._create_sysfs_file('mapped-value', expected_dmi_value) self._configure_dmidecode_return('mapped-key', 'wrong-wrong-wrong') @@ -129,3 +145,10 @@ class TestReadDMIData(helpers.FilesystemMockingTestCase): self._create_sysfs_file('product_name', "should-be-ignored") self.assertIsNone(dmi.read_dmi_data("bogus")) self.assertIsNone(dmi.read_dmi_data("system-product-name")) + + def test_freebsd_uses_kenv(self): + """On a FreeBSD system, kenv is called.""" + self._m_is_FreeBSD.return_value = True + key, val = ("system-product-name", "my_product") + self._configure_kenv_return(key, val) + self.assertEqual(dmi.read_dmi_data(key), val) diff --git a/cloudinit/util.py b/cloudinit/util.py index bdb3694d..769f3425 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -62,12 +62,6 @@ TRUE_STRINGS = ('true', '1', 'on', 'yes') FALSE_STRINGS = ('off', '0', 'no', 'false') -# Helper utils to see if running in a container -CONTAINER_TESTS = (['systemd-detect-virt', '--quiet', '--container'], - ['running-in-container'], - ['lxc-is-container']) - - def kernel_version(): return tuple(map(int, os.uname().release.split('.')[:2])) @@ -1928,19 +1922,52 @@ def strip_prefix_suffix(line, prefix=None, suffix=None): return line +def _cmd_exits_zero(cmd): + if subp.which(cmd[0]) is None: + return False + try: + subp.subp(cmd) + except subp.ProcessExecutionError: + return False + return True + + +def _is_container_systemd(): + return _cmd_exits_zero(["systemd-detect-virt", "--quiet", "--container"]) + + +def _is_container_upstart(): + return _cmd_exits_zero(["running-in-container"]) + + +def _is_container_old_lxc(): + return _cmd_exits_zero(["lxc-is-container"]) + + +def _is_container_freebsd(): + if not is_FreeBSD(): + return False + cmd = ["sysctl", "-qn", "security.jail.jailed"] + if subp.which(cmd[0]) is None: + return False + out, _ = subp.subp(cmd) + return out.strip() == "1" + + +@lru_cache() def is_container(): """ Checks to see if this code running in a container of some sort """ - - for helper in CONTAINER_TESTS: - try: - # try to run a helper program. if it returns true/zero - # then we're inside a container. otherwise, no - subp.subp(helper) + checks = ( + _is_container_systemd, + _is_container_freebsd, + _is_container_upstart, + _is_container_old_lxc) + + for helper in checks: + if helper(): return True - except (IOError, OSError): - pass # this code is largely from the logic in # ubuntu's /etc/init/container-detect.conf diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py index 5f8a4a29..1d8aaf18 100644 --- a/tests/unittests/test_ds_identify.py +++ b/tests/unittests/test_ds_identify.py @@ -146,6 +146,8 @@ class DsIdentifyBase(CiTestCase): 'out': 'No value found', 'ret': 1}, {'name': 'dmi_decode', 'ret': 1, 'err': 'No dmidecode program. ERROR.'}, + {'name': 'get_kenv_field', 'ret': 1, + 'err': 'No kenv program. ERROR.'}, ] written = [d['name'] for d in mocks] @@ -651,14 +653,22 @@ class TestDsIdentify(DsIdentifyBase): class TestBSDNoSys(DsIdentifyBase): """Test *BSD code paths - FreeBSD doesn't have /sys so we use dmidecode(8) here - It also doesn't have systemd-detect-virt(8), so we use sysctl(8) to query + FreeBSD doesn't have /sys so we use kenv(1) here. + Other BSD systems fallback to dmidecode(8). + BSDs also doesn't have systemd-detect-virt(8), so we use sysctl(8) to query kern.vm_guest, and optionally map it""" - def test_dmi_decode(self): + def test_dmi_kenv(self): + """Test that kenv(1) works on systems which don't have /sys + + This will be used on FreeBSD systems. + """ + self._test_ds_found('Hetzner-kenv') + + def test_dmi_dmidecode(self): """Test that dmidecode(8) works on systems which don't have /sys - This will be used on *BSD systems. + This will be used on all other BSD systems. """ self._test_ds_found('Hetzner-dmidecode') @@ -1026,6 +1036,13 @@ VALID_CFG = { 'ds': 'Hetzner', 'files': {P_SYS_VENDOR: 'Hetzner\n'}, }, + 'Hetzner-kenv': { + 'ds': 'Hetzner', + 'mocks': [ + MOCK_UNAME_IS_FREEBSD, + {'name': 'get_kenv_field', 'ret': 0, 'RET': 'Hetzner'} + ], + }, 'Hetzner-dmidecode': { 'ds': 'Hetzner', 'mocks': [ diff --git a/tools/build-on-freebsd b/tools/build-on-freebsd index 94b03433..1e876905 100755 --- a/tools/build-on-freebsd +++ b/tools/build-on-freebsd @@ -21,7 +21,6 @@ py_prefix=$(${PYTHON} -c 'import sys; print("py%d%d" % (sys.version_info.major, depschecked=/tmp/c-i.dependencieschecked pkgs=" bash - dmidecode e2fsprogs $py_prefix-Jinja2 $py_prefix-boto diff --git a/tools/ds-identify b/tools/ds-identify index ec9e775e..496dbb8a 100755 --- a/tools/ds-identify +++ b/tools/ds-identify @@ -180,13 +180,43 @@ debug() { echo "$@" 1>&3 } +get_kenv_field() { + local sys_field="$1" kenv_field="" val="" + command -v kenv >/dev/null 2>&1 || { + warn "No kenv program. Cannot read $sys_field." + return 1 + } + case "$sys_field" in + board_asset_tag) kenv_field="smbios.planar.tag";; + board_vendor) kenv_field='smbios.planar.maker';; + board_name) kenv_field='smbios.planar.product';; + board_serial) kenv_field='smbios.planar.serial';; + board_version) kenv_field='smbios.planar.version';; + bios_date) kenv_field='smbios.bios.reldate';; + bios_vendor) kenv_field='smbios.bios.vendor';; + bios_version) kenv_field='smbios.bios.version';; + chassis_asset_tag) kenv_field='smbios.chassis.tag';; + chassis_vendor) kenv_field='smbios.chassis.maker';; + chassis_serial) kenv_field='smbios.chassis.serial';; + chassis_version) kenv_field='smbios.chassis.version';; + sys_vendor) kenv_field='smbios.system.maker';; + product_name) kenv_field='smbios.system.product';; + product_serial) kenv_field='smbios.system.serial';; + product_uuid) kenv_field='smbios.system.uuid';; + *) error "Unknown field $sys_field. Cannot call kenv." + return 1;; + esac + val=$(kenv -q "$kenv_field" 2>/dev/null) || return 1 + _RET="$val" +} + dmi_decode() { local sys_field="$1" dmi_field="" val="" command -v dmidecode >/dev/null 2>&1 || { warn "No dmidecode program. Cannot read $sys_field." return 1 } - case "$1" in + case "$sys_field" in sys_vendor) dmi_field="system-manufacturer";; product_name) dmi_field="system-product-name";; product_uuid) dmi_field="system-uuid";; @@ -200,8 +230,14 @@ dmi_decode() { } get_dmi_field() { - local path="${PATH_SYS_CLASS_DMI_ID}/$1" _RET="$UNAVAILABLE" + + if [ "$DI_UNAME_KERNEL_NAME" = "FreeBSD" ]; then + get_kenv_field "$1" || _RET="$ERROR" + return $? + fi + + local path="${PATH_SYS_CLASS_DMI_ID}/$1" if [ -d "${PATH_SYS_CLASS_DMI_ID}" ]; then if [ -f "$path" ] && [ -r "$path" ]; then read _RET < "${path}" || _RET="$ERROR" @@ -1310,10 +1346,10 @@ dscheck_IBMCloud() { } collect_info() { + read_uname_info read_virt read_pid1_product_name read_kernel_cmdline - read_uname_info read_config read_datasource_list read_dmi_sys_vendor -- cgit v1.2.3