Merge lp:~daniel-thewatkins/cloud-init/fix-dmi into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Dan Watkins on 2015-03-04
Status: Merged
Merged at revision: 1073
Proposed branch: lp:~daniel-thewatkins/cloud-init/fix-dmi
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 228 lines (+103/-62)
2 files modified
cloudinit/util.py (+51/-19)
tests/unittests/test_util.py (+52/-43)
To merge this branch: bzr merge lp:~daniel-thewatkins/cloud-init/fix-dmi
Reviewer Review Type Date Requested Status
cloud-init commiters 2015-03-04 Pending
Review via email: mp+251715@code.launchpad.net

Description of the Change

Convert dmidecode values to sysfs names before looking for them.

dmidecode and /sys/class/dmi/id/* use different names for the same
information. This modified the logic in util.read_dmi_data to map from
dmidecode names to sysfs names before looking in sysfs.

To post a comment you must log in.
Dan Watkins (daniel-thewatkins) wrote :

I've tested the code in this branch on Joyent, and it fixes the problem there.

1073. By Dan Watkins on 2015-03-04

Use more consistent logging invocation.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/util.py'
2--- cloudinit/util.py 2015-03-02 20:56:15 +0000
3+++ cloudinit/util.py 2015-03-04 15:57:55 +0000
4@@ -128,6 +128,28 @@
5 # Path for DMI Data
6 DMI_SYS_PATH = "/sys/class/dmi/id"
7
8+# dmidecode and /sys/class/dmi/id/* use different names for the same value,
9+# this allows us to refer to them by one canonical name
10+DMIDECODE_TO_DMI_SYS_MAPPING = {
11+ 'baseboard-asset-tag': 'board_asset_tag',
12+ 'baseboard-manufacturer': 'board_vendor',
13+ 'baseboard-product-name': 'board_name',
14+ 'baseboard-serial-number': 'board_serial',
15+ 'baseboard-version': 'board_version',
16+ 'bios-release-date': 'bios_date',
17+ 'bios-vendor': 'bios_vendor',
18+ 'bios-version': 'bios_version',
19+ 'chassis-asset-tag': 'chassis_asset_tag',
20+ 'chassis-manufacturer': 'chassis_vendor',
21+ 'chassis-serial-number': 'chassis_serial',
22+ 'chassis-version': 'chassis_version',
23+ 'system-manufacturer': 'sys_vendor',
24+ 'system-product-name': 'product_name',
25+ 'system-serial-number': 'product_serial',
26+ 'system-uuid': 'product_uuid',
27+ 'system-version': 'product_version',
28+}
29+
30
31 class ProcessExecutionError(IOError):
32
33@@ -2103,24 +2125,26 @@
34 """
35 Reads dmi data with from /sys/class/dmi/id
36 """
37-
38- dmi_key = "{0}/{1}".format(DMI_SYS_PATH, key)
39- LOG.debug("querying dmi data {0}".format(dmi_key))
40+ if key not in DMIDECODE_TO_DMI_SYS_MAPPING:
41+ return None
42+ mapped_key = DMIDECODE_TO_DMI_SYS_MAPPING[key]
43+ dmi_key_path = "{0}/{1}".format(DMI_SYS_PATH, mapped_key)
44+ LOG.debug("querying dmi data %s", dmi_key_path)
45 try:
46- if not os.path.exists(dmi_key):
47- LOG.debug("did not find {0}".format(dmi_key))
48+ if not os.path.exists(dmi_key_path):
49+ LOG.debug("did not find %s", dmi_key_path)
50 return None
51
52- key_data = load_file(dmi_key)
53+ key_data = load_file(dmi_key_path)
54 if not key_data:
55- LOG.debug("{0} did not return any data".format(key))
56+ LOG.debug("%s did not return any data", dmi_key_path)
57 return None
58
59- LOG.debug("dmi data {0} returned {0}".format(dmi_key, key_data))
60+ LOG.debug("dmi data %s returned %s", dmi_key_path, key_data)
61 return key_data.strip()
62
63 except Exception as e:
64- logexc(LOG, "failed read of {0}".format(dmi_key), e)
65+ logexc(LOG, "failed read of %s", dmi_key_path, e)
66 return None
67
68
69@@ -2132,26 +2156,34 @@
70 try:
71 cmd = [dmidecode_path, "--string", key]
72 (result, _err) = subp(cmd)
73- LOG.debug("dmidecode returned '{0}' for '{0}'".format(result, key))
74+ LOG.debug("dmidecode returned '%s' for '%s'", result, key)
75 return result
76- except OSError as _err:
77- LOG.debug('failed dmidecode cmd: {0}\n{0}'.format(cmd, _err.message))
78+ except (IOError, OSError) as _err:
79+ LOG.debug('failed dmidecode cmd: %s\n%s', cmd, _err.message)
80 return None
81
82
83 def read_dmi_data(key):
84 """
85- Wrapper for reading DMI data. This tries to determine whether the DMI
86- Data can be read directly, otherwise it will fallback to using dmidecode.
87+ Wrapper for reading DMI data.
88+
89+ This will do the following (returning the first that produces a
90+ result):
91+ 1) Use a mapping to translate `key` from dmidecode naming to
92+ sysfs naming and look in /sys/class/dmi/... for a value.
93+ 2) Use `key` as a sysfs key directly and look in /sys/class/dmi/...
94+ 3) Fall-back to passing `key` to `dmidecode --string`.
95+
96+ If all of the above fail to find a value, None will be returned.
97 """
98- if os.path.exists(DMI_SYS_PATH):
99- return _read_dmi_syspath(key)
100+ syspath_value = _read_dmi_syspath(key)
101+ if syspath_value is not None:
102+ return syspath_value
103
104 dmidecode_path = which('dmidecode')
105 if dmidecode_path:
106 return _call_dmidecode(key, dmidecode_path)
107
108- LOG.warn("did not find either path {0} or dmidecode command".format(
109- DMI_SYS_PATH))
110-
111+ LOG.warn("did not find either path %s or dmidecode command",
112+ DMI_SYS_PATH)
113 return None
114
115=== modified file 'tests/unittests/test_util.py'
116--- tests/unittests/test_util.py 2015-02-13 20:49:40 +0000
117+++ tests/unittests/test_util.py 2015-03-04 15:57:55 +0000
118@@ -323,58 +323,67 @@
119
120 class TestReadDMIData(helpers.FilesystemMockingTestCase):
121
122- def _patchIn(self, root):
123- self.patchOS(root)
124- self.patchUtils(root)
125-
126- def _write_key(self, key, content):
127+ def setUp(self):
128+ super(TestReadDMIData, self).setUp()
129+ self.new_root = tempfile.mkdtemp()
130+ self.addCleanup(shutil.rmtree, self.new_root)
131+ self.patchOS(self.new_root)
132+ self.patchUtils(self.new_root)
133+
134+ def _create_sysfs_parent_directory(self):
135+ util.ensure_dir(os.path.join('sys', 'class', 'dmi', 'id'))
136+
137+ def _create_sysfs_file(self, key, content):
138 """Mocks the sys path found on Linux systems."""
139- new_root = tempfile.mkdtemp()
140- self.addCleanup(shutil.rmtree, new_root)
141- self._patchIn(new_root)
142- util.ensure_dir(os.path.join('sys', 'class', 'dmi', 'id'))
143-
144+ self._create_sysfs_parent_directory()
145 dmi_key = "/sys/class/dmi/id/{0}".format(key)
146 util.write_file(dmi_key, content)
147
148- def _no_syspath(self, key, content):
149+ def _configure_dmidecode_return(self, key, content, error=None):
150 """
151 In order to test a missing sys path and call outs to dmidecode, this
152 function fakes the results of dmidecode to test the results.
153 """
154- new_root = tempfile.mkdtemp()
155- self.addCleanup(shutil.rmtree, new_root)
156- self._patchIn(new_root)
157- self.real_which = util.which
158- self.real_subp = util.subp
159-
160- def _which(key):
161- return True
162- util.which = _which
163-
164- def _cdd(_key, error=None):
165+ def _dmidecode_subp(cmd):
166+ if cmd[-1] != key:
167+ raise util.ProcessExecutionError()
168 return (content, error)
169- util.subp = _cdd
170-
171- def test_key(self):
172- key_content = "TEST-KEY-DATA"
173- self._write_key("key", key_content)
174- self.assertEquals(key_content, util.read_dmi_data("key"))
175-
176- def test_key_mismatch(self):
177- self._write_key("test", "ABC")
178- self.assertNotEqual("123", util.read_dmi_data("test"))
179-
180- def test_no_key(self):
181- self._no_syspath(None, None)
182- self.assertFalse(util.read_dmi_data("key"))
183-
184- def test_callout_dmidecode(self):
185- """test to make sure that dmidecode is used when no syspath"""
186- self._no_syspath("key", "stuff")
187- self.assertEquals("stuff", util.read_dmi_data("key"))
188- self._no_syspath("key", None)
189- self.assertFalse(None, util.read_dmi_data("key"))
190+
191+ self.patched_funcs.enter_context(
192+ mock.patch.object(util, 'which', lambda _: True))
193+ self.patched_funcs.enter_context(
194+ mock.patch.object(util, 'subp', _dmidecode_subp))
195+
196+ def patch_mapping(self, new_mapping):
197+ self.patched_funcs.enter_context(
198+ mock.patch('cloudinit.util.DMIDECODE_TO_DMI_SYS_MAPPING',
199+ new_mapping))
200+
201+ def test_sysfs_used_with_key_in_mapping_and_file_on_disk(self):
202+ self.patch_mapping({'mapped-key': 'mapped-value'})
203+ expected_dmi_value = 'sys-used-correctly'
204+ self._create_sysfs_file('mapped-value', expected_dmi_value)
205+ self._configure_dmidecode_return('mapped-key', 'wrong-wrong-wrong')
206+ self.assertEqual(expected_dmi_value, util.read_dmi_data('mapped-key'))
207+
208+ def test_dmidecode_used_if_no_sysfs_file_on_disk(self):
209+ self.patch_mapping({})
210+ self._create_sysfs_parent_directory()
211+ expected_dmi_value = 'dmidecode-used'
212+ self._configure_dmidecode_return('use-dmidecode', expected_dmi_value)
213+ self.assertEqual(expected_dmi_value,
214+ util.read_dmi_data('use-dmidecode'))
215+
216+ def test_none_returned_if_neither_source_has_data(self):
217+ self.patch_mapping({})
218+ self._configure_dmidecode_return('key', 'value')
219+ self.assertEqual(None, util.read_dmi_data('expect-fail'))
220+
221+ def test_none_returned_if_dmidecode_not_in_path(self):
222+ self.patched_funcs.enter_context(
223+ mock.patch.object(util, 'which', lambda _: False))
224+ self.patch_mapping({})
225+ self.assertEqual(None, util.read_dmi_data('expect-fail'))
226
227
228 class TestMultiLog(helpers.FilesystemMockingTestCase):