Skip to content

Commit d83c0bb

Browse files
igalicsmoser
andauthored
replace usage of dmidecode with kenv on FreeBSD (canonical#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 <smoser@brickies.net>
1 parent b542ce7 commit d83c0bb

6 files changed

Lines changed: 185 additions & 51 deletions

File tree

cloudinit/dmi.py

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,53 @@
11
# This file is part of cloud-init. See LICENSE file for license information.
22
from cloudinit import log as logging
33
from cloudinit import subp
4-
from cloudinit.util import is_container
4+
from cloudinit.util import is_container, is_FreeBSD
55

6+
from collections import namedtuple
67
import os
78

89
LOG = logging.getLogger(__name__)
910

1011
# Path for DMI Data
1112
DMI_SYS_PATH = "/sys/class/dmi/id"
1213

13-
# dmidecode and /sys/class/dmi/id/* use different names for the same value,
14-
# this allows us to refer to them by one canonical name
15-
DMIDECODE_TO_DMI_SYS_MAPPING = {
16-
'baseboard-asset-tag': 'board_asset_tag',
17-
'baseboard-manufacturer': 'board_vendor',
18-
'baseboard-product-name': 'board_name',
19-
'baseboard-serial-number': 'board_serial',
20-
'baseboard-version': 'board_version',
21-
'bios-release-date': 'bios_date',
22-
'bios-vendor': 'bios_vendor',
23-
'bios-version': 'bios_version',
24-
'chassis-asset-tag': 'chassis_asset_tag',
25-
'chassis-manufacturer': 'chassis_vendor',
26-
'chassis-serial-number': 'chassis_serial',
27-
'chassis-version': 'chassis_version',
28-
'system-manufacturer': 'sys_vendor',
29-
'system-product-name': 'product_name',
30-
'system-serial-number': 'product_serial',
31-
'system-uuid': 'product_uuid',
32-
'system-version': 'product_version',
14+
kdmi = namedtuple('KernelNames', ['linux', 'freebsd'])
15+
kdmi.__new__.defaults__ = (None, None)
16+
17+
# FreeBSD's kenv(1) and Linux /sys/class/dmi/id/* both use different names from
18+
# dmidecode. The values are the same, and ultimately what we're interested in.
19+
# These tools offer a "cheaper" way to access those values over dmidecode.
20+
# This is our canonical translation table. If we add more tools on other
21+
# platforms to find dmidecode's values, their keys need to be put in here.
22+
DMIDECODE_TO_KERNEL = {
23+
'baseboard-asset-tag': kdmi('board_asset_tag', 'smbios.planar.tag'),
24+
'baseboard-manufacturer': kdmi('board_vendor', 'smbios.planar.maker'),
25+
'baseboard-product-name': kdmi('board_name', 'smbios.planar.product'),
26+
'baseboard-serial-number': kdmi('board_serial', 'smbios.planar.serial'),
27+
'baseboard-version': kdmi('board_version', 'smbios.planar.version'),
28+
'bios-release-date': kdmi('bios_date', 'smbios.bios.reldate'),
29+
'bios-vendor': kdmi('bios_vendor', 'smbios.bios.vendor'),
30+
'bios-version': kdmi('bios_version', 'smbios.bios.version'),
31+
'chassis-asset-tag': kdmi('chassis_asset_tag', 'smbios.chassis.tag'),
32+
'chassis-manufacturer': kdmi('chassis_vendor', 'smbios.chassis.maker'),
33+
'chassis-serial-number': kdmi('chassis_serial', 'smbios.chassis.serial'),
34+
'chassis-version': kdmi('chassis_version', 'smbios.chassis.version'),
35+
'system-manufacturer': kdmi('sys_vendor', 'smbios.system.maker'),
36+
'system-product-name': kdmi('product_name', 'smbios.system.product'),
37+
'system-serial-number': kdmi('product_serial', 'smbios.system.serial'),
38+
'system-uuid': kdmi('product_uuid', 'smbios.system.uuid'),
39+
'system-version': kdmi('product_version', 'smbios.system.version'),
3340
}
3441

3542

3643
def _read_dmi_syspath(key):
3744
"""
38-
Reads dmi data with from /sys/class/dmi/id
45+
Reads dmi data from /sys/class/dmi/id
3946
"""
40-
if key not in DMIDECODE_TO_DMI_SYS_MAPPING:
47+
kmap = DMIDECODE_TO_KERNEL.get(key)
48+
if kmap is None or kmap.linux is None:
4149
return None
42-
mapped_key = DMIDECODE_TO_DMI_SYS_MAPPING[key]
43-
dmi_key_path = "{0}/{1}".format(DMI_SYS_PATH, mapped_key)
44-
50+
dmi_key_path = "{0}/{1}".format(DMI_SYS_PATH, kmap.linux)
4551
LOG.debug("querying dmi data %s", dmi_key_path)
4652
if not os.path.exists(dmi_key_path):
4753
LOG.debug("did not find %s", dmi_key_path)
@@ -68,6 +74,29 @@ def _read_dmi_syspath(key):
6874
return None
6975

7076

77+
def _read_kenv(key):
78+
"""
79+
Reads dmi data from FreeBSD's kenv(1)
80+
"""
81+
kmap = DMIDECODE_TO_KERNEL.get(key)
82+
if kmap is None or kmap.freebsd is None:
83+
return None
84+
85+
LOG.debug("querying dmi data %s", kmap.freebsd)
86+
87+
try:
88+
cmd = ["kenv", "-q", kmap.freebsd]
89+
(result, _err) = subp.subp(cmd)
90+
result = result.strip()
91+
LOG.debug("kenv returned '%s' for '%s'", result, kmap.freebsd)
92+
return result
93+
except subp.ProcessExecutionError as e:
94+
LOG.debug('failed kenv cmd: %s\n%s', cmd, e)
95+
return None
96+
97+
return None
98+
99+
71100
def _call_dmidecode(key, dmidecode_path):
72101
"""
73102
Calls out to dmidecode to get the data out. This is mostly for supporting
@@ -81,7 +110,7 @@ def _call_dmidecode(key, dmidecode_path):
81110
if result.replace(".", "") == "":
82111
return ""
83112
return result
84-
except (IOError, OSError) as e:
113+
except subp.ProcessExecutionError as e:
85114
LOG.debug('failed dmidecode cmd: %s\n%s', cmd, e)
86115
return None
87116

@@ -107,6 +136,9 @@ def read_dmi_data(key):
107136
if is_container():
108137
return None
109138

139+
if is_FreeBSD():
140+
return _read_kenv(key)
141+
110142
syspath_value = _read_dmi_syspath(key)
111143
if syspath_value is not None:
112144
return syspath_value

cloudinit/tests/test_dmi.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ def setUp(self):
1919
p = mock.patch("cloudinit.dmi.is_container", return_value=False)
2020
self.addCleanup(p.stop)
2121
self._m_is_container = p.start()
22+
p = mock.patch("cloudinit.dmi.is_FreeBSD", return_value=False)
23+
self.addCleanup(p.stop)
24+
self._m_is_FreeBSD = p.start()
2225

2326
def _create_sysfs_parent_directory(self):
2427
util.ensure_dir(os.path.join('sys', 'class', 'dmi', 'id'))
@@ -44,13 +47,26 @@ def _dmidecode_subp(cmd):
4447
self.patched_funcs.enter_context(
4548
mock.patch("cloudinit.dmi.subp.subp", side_effect=_dmidecode_subp))
4649

50+
def _configure_kenv_return(self, key, content, error=None):
51+
"""
52+
In order to test a FreeBSD system call outs to kenv, this
53+
function fakes the results of kenv to test the results.
54+
"""
55+
def _kenv_subp(cmd):
56+
if cmd[-1] != dmi.DMIDECODE_TO_KERNEL[key].freebsd:
57+
raise subp.ProcessExecutionError()
58+
return (content, error)
59+
60+
self.patched_funcs.enter_context(
61+
mock.patch("cloudinit.dmi.subp.subp", side_effect=_kenv_subp))
62+
4763
def patch_mapping(self, new_mapping):
4864
self.patched_funcs.enter_context(
49-
mock.patch('cloudinit.dmi.DMIDECODE_TO_DMI_SYS_MAPPING',
65+
mock.patch('cloudinit.dmi.DMIDECODE_TO_KERNEL',
5066
new_mapping))
5167

5268
def test_sysfs_used_with_key_in_mapping_and_file_on_disk(self):
53-
self.patch_mapping({'mapped-key': 'mapped-value'})
69+
self.patch_mapping({'mapped-key': dmi.kdmi('mapped-value', None)})
5470
expected_dmi_value = 'sys-used-correctly'
5571
self._create_sysfs_file('mapped-value', expected_dmi_value)
5672
self._configure_dmidecode_return('mapped-key', 'wrong-wrong-wrong')
@@ -129,3 +145,10 @@ def test_container_returns_none_on_unknown(self):
129145
self._create_sysfs_file('product_name', "should-be-ignored")
130146
self.assertIsNone(dmi.read_dmi_data("bogus"))
131147
self.assertIsNone(dmi.read_dmi_data("system-product-name"))
148+
149+
def test_freebsd_uses_kenv(self):
150+
"""On a FreeBSD system, kenv is called."""
151+
self._m_is_FreeBSD.return_value = True
152+
key, val = ("system-product-name", "my_product")
153+
self._configure_kenv_return(key, val)
154+
self.assertEqual(dmi.read_dmi_data(key), val)

cloudinit/util.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,6 @@
6262
FALSE_STRINGS = ('off', '0', 'no', 'false')
6363

6464

65-
# Helper utils to see if running in a container
66-
CONTAINER_TESTS = (['systemd-detect-virt', '--quiet', '--container'],
67-
['running-in-container'],
68-
['lxc-is-container'])
69-
70-
7165
def kernel_version():
7266
return tuple(map(int, os.uname().release.split('.')[:2]))
7367

@@ -1928,19 +1922,52 @@ def strip_prefix_suffix(line, prefix=None, suffix=None):
19281922
return line
19291923

19301924

1925+
def _cmd_exits_zero(cmd):
1926+
if subp.which(cmd[0]) is None:
1927+
return False
1928+
try:
1929+
subp.subp(cmd)
1930+
except subp.ProcessExecutionError:
1931+
return False
1932+
return True
1933+
1934+
1935+
def _is_container_systemd():
1936+
return _cmd_exits_zero(["systemd-detect-virt", "--quiet", "--container"])
1937+
1938+
1939+
def _is_container_upstart():
1940+
return _cmd_exits_zero(["running-in-container"])
1941+
1942+
1943+
def _is_container_old_lxc():
1944+
return _cmd_exits_zero(["lxc-is-container"])
1945+
1946+
1947+
def _is_container_freebsd():
1948+
if not is_FreeBSD():
1949+
return False
1950+
cmd = ["sysctl", "-qn", "security.jail.jailed"]
1951+
if subp.which(cmd[0]) is None:
1952+
return False
1953+
out, _ = subp.subp(cmd)
1954+
return out.strip() == "1"
1955+
1956+
1957+
@lru_cache()
19311958
def is_container():
19321959
"""
19331960
Checks to see if this code running in a container of some sort
19341961
"""
1935-
1936-
for helper in CONTAINER_TESTS:
1937-
try:
1938-
# try to run a helper program. if it returns true/zero
1939-
# then we're inside a container. otherwise, no
1940-
subp.subp(helper)
1962+
checks = (
1963+
_is_container_systemd,
1964+
_is_container_freebsd,
1965+
_is_container_upstart,
1966+
_is_container_old_lxc)
1967+
1968+
for helper in checks:
1969+
if helper():
19411970
return True
1942-
except (IOError, OSError):
1943-
pass
19441971

19451972
# this code is largely from the logic in
19461973
# ubuntu's /etc/init/container-detect.conf

tests/unittests/test_ds_identify.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ def write_mock(data):
146146
'out': 'No value found', 'ret': 1},
147147
{'name': 'dmi_decode', 'ret': 1,
148148
'err': 'No dmidecode program. ERROR.'},
149+
{'name': 'get_kenv_field', 'ret': 1,
150+
'err': 'No kenv program. ERROR.'},
149151
]
150152

151153
written = [d['name'] for d in mocks]
@@ -651,14 +653,22 @@ def test_e24cloud_not_active(self):
651653
class TestBSDNoSys(DsIdentifyBase):
652654
"""Test *BSD code paths
653655
654-
FreeBSD doesn't have /sys so we use dmidecode(8) here
655-
It also doesn't have systemd-detect-virt(8), so we use sysctl(8) to query
656+
FreeBSD doesn't have /sys so we use kenv(1) here.
657+
Other BSD systems fallback to dmidecode(8).
658+
BSDs also doesn't have systemd-detect-virt(8), so we use sysctl(8) to query
656659
kern.vm_guest, and optionally map it"""
657660

658-
def test_dmi_decode(self):
661+
def test_dmi_kenv(self):
662+
"""Test that kenv(1) works on systems which don't have /sys
663+
664+
This will be used on FreeBSD systems.
665+
"""
666+
self._test_ds_found('Hetzner-kenv')
667+
668+
def test_dmi_dmidecode(self):
659669
"""Test that dmidecode(8) works on systems which don't have /sys
660670
661-
This will be used on *BSD systems.
671+
This will be used on all other BSD systems.
662672
"""
663673
self._test_ds_found('Hetzner-dmidecode')
664674

@@ -1026,6 +1036,13 @@ def _print_run_output(rc, out, err, cfg, files):
10261036
'ds': 'Hetzner',
10271037
'files': {P_SYS_VENDOR: 'Hetzner\n'},
10281038
},
1039+
'Hetzner-kenv': {
1040+
'ds': 'Hetzner',
1041+
'mocks': [
1042+
MOCK_UNAME_IS_FREEBSD,
1043+
{'name': 'get_kenv_field', 'ret': 0, 'RET': 'Hetzner'}
1044+
],
1045+
},
10291046
'Hetzner-dmidecode': {
10301047
'ds': 'Hetzner',
10311048
'mocks': [

tools/build-on-freebsd

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ py_prefix=$(${PYTHON} -c 'import sys; print("py%d%d" % (sys.version_info.major,
2121
depschecked=/tmp/c-i.dependencieschecked
2222
pkgs="
2323
bash
24-
dmidecode
2524
e2fsprogs
2625
$py_prefix-Jinja2
2726
$py_prefix-boto

tools/ds-identify

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,43 @@ debug() {
180180
echo "$@" 1>&3
181181
}
182182

183+
get_kenv_field() {
184+
local sys_field="$1" kenv_field="" val=""
185+
command -v kenv >/dev/null 2>&1 || {
186+
warn "No kenv program. Cannot read $sys_field."
187+
return 1
188+
}
189+
case "$sys_field" in
190+
board_asset_tag) kenv_field="smbios.planar.tag";;
191+
board_vendor) kenv_field='smbios.planar.maker';;
192+
board_name) kenv_field='smbios.planar.product';;
193+
board_serial) kenv_field='smbios.planar.serial';;
194+
board_version) kenv_field='smbios.planar.version';;
195+
bios_date) kenv_field='smbios.bios.reldate';;
196+
bios_vendor) kenv_field='smbios.bios.vendor';;
197+
bios_version) kenv_field='smbios.bios.version';;
198+
chassis_asset_tag) kenv_field='smbios.chassis.tag';;
199+
chassis_vendor) kenv_field='smbios.chassis.maker';;
200+
chassis_serial) kenv_field='smbios.chassis.serial';;
201+
chassis_version) kenv_field='smbios.chassis.version';;
202+
sys_vendor) kenv_field='smbios.system.maker';;
203+
product_name) kenv_field='smbios.system.product';;
204+
product_serial) kenv_field='smbios.system.serial';;
205+
product_uuid) kenv_field='smbios.system.uuid';;
206+
*) error "Unknown field $sys_field. Cannot call kenv."
207+
return 1;;
208+
esac
209+
val=$(kenv -q "$kenv_field" 2>/dev/null) || return 1
210+
_RET="$val"
211+
}
212+
183213
dmi_decode() {
184214
local sys_field="$1" dmi_field="" val=""
185215
command -v dmidecode >/dev/null 2>&1 || {
186216
warn "No dmidecode program. Cannot read $sys_field."
187217
return 1
188218
}
189-
case "$1" in
219+
case "$sys_field" in
190220
sys_vendor) dmi_field="system-manufacturer";;
191221
product_name) dmi_field="system-product-name";;
192222
product_uuid) dmi_field="system-uuid";;
@@ -200,8 +230,14 @@ dmi_decode() {
200230
}
201231

202232
get_dmi_field() {
203-
local path="${PATH_SYS_CLASS_DMI_ID}/$1"
204233
_RET="$UNAVAILABLE"
234+
235+
if [ "$DI_UNAME_KERNEL_NAME" = "FreeBSD" ]; then
236+
get_kenv_field "$1" || _RET="$ERROR"
237+
return $?
238+
fi
239+
240+
local path="${PATH_SYS_CLASS_DMI_ID}/$1"
205241
if [ -d "${PATH_SYS_CLASS_DMI_ID}" ]; then
206242
if [ -f "$path" ] && [ -r "$path" ]; then
207243
read _RET < "${path}" || _RET="$ERROR"
@@ -1310,10 +1346,10 @@ dscheck_IBMCloud() {
13101346
}
13111347

13121348
collect_info() {
1349+
read_uname_info
13131350
read_virt
13141351
read_pid1_product_name
13151352
read_kernel_cmdline
1316-
read_uname_info
13171353
read_config
13181354
read_datasource_list
13191355
read_dmi_sys_vendor

0 commit comments

Comments
 (0)