summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudinit/dmi.py86
-rw-r--r--cloudinit/tests/test_dmi.py27
-rw-r--r--cloudinit/util.py55
-rw-r--r--tests/unittests/test_ds_identify.py25
-rwxr-xr-xtools/build-on-freebsd1
-rwxr-xr-xtools/ds-identify42
6 files changed, 185 insertions, 51 deletions
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