Merge lp:~utlemming/cloud-init/no_demidecode into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Ben Howard on 2015-01-14
Status: Merged
Merged at revision: 1053
Proposed branch: lp:~utlemming/cloud-init/no_demidecode
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 450 lines (+172/-95)
6 files modified
cloudinit/sources/DataSourceAltCloud.py (+10/-21)
cloudinit/sources/DataSourceCloudSigma.py (+11/-13)
cloudinit/sources/DataSourceSmartOS.py (+7/-20)
cloudinit/util.py (+61/-0)
tests/unittests/test_datasource/test_altcloud.py (+27/-41)
tests/unittests/test_util.py (+56/-0)
To merge this branch: bzr merge lp:~utlemming/cloud-init/no_demidecode
Reviewer Review Type Date Requested Status
cloud-init commiters 2015-01-14 Pending
Review via email: mp+246486@code.launchpad.net

Description of the Change

Drop the reliance on /sbin/dmidecode.

include test cases.

To post a comment you must log in.
Scott Moser (smoser) wrote :

sadly, i dont think this is as easy as we'd like.
we were using dmidecode on freebsd also.

so taking this would almost certainly break freebsd.

Harm Weites (harmw) wrote :

I'll happily take a look into it's purpose on fbsd, but I do would like to know why you'd want to drop it in the first place... Nonetheless, I'm all for dropping useless weight :)

I do like the idea of moving the whole dmi reading stuff to a separate function() and returning some abstract thing, so even if fbsd still depends on a dmidecode binary, I can simply add some specifics to just that function.

1050. By Ben Howard on 2015-01-21

Use either syspath or dmidecode based on the availability.

Ben Howard (utlemming) wrote :

To address the concerns of FreeBSD/other distros, I've updated the code to select either the /sys path or dmidecode and added a test case for the code detection.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/sources/DataSourceAltCloud.py'
2--- cloudinit/sources/DataSourceAltCloud.py 2014-02-27 14:04:17 +0000
3+++ cloudinit/sources/DataSourceAltCloud.py 2015-01-21 22:43:46 +0000
4@@ -40,7 +40,6 @@
5 CLOUD_INFO_FILE = '/etc/sysconfig/cloud-info'
6
7 # Shell command lists
8-CMD_DMI_SYSTEM = ['/usr/sbin/dmidecode', '--string', 'system-product-name']
9 CMD_PROBE_FLOPPY = ['/sbin/modprobe', 'floppy']
10 CMD_UDEVADM_SETTLE = ['/sbin/udevadm', 'settle', '--quiet', '--timeout=5']
11
12@@ -100,11 +99,7 @@
13 '''
14 Description:
15 Get the type for the cloud back end this instance is running on
16- by examining the string returned by:
17- dmidecode --string system-product-name
18-
19- On VMWare/vSphere dmidecode returns: RHEV Hypervisor
20- On VMWare/vSphere dmidecode returns: VMware Virtual Platform
21+ by examining the string returned by reading the dmi data.
22
23 Input:
24 None
25@@ -117,26 +112,20 @@
26
27 uname_arch = os.uname()[4]
28 if uname_arch.startswith("arm") or uname_arch == "aarch64":
29- # Disabling because dmidecode in CMD_DMI_SYSTEM crashes kvm process
30+ # Disabling because dmi data is not available on ARM processors
31 LOG.debug("Disabling AltCloud datasource on arm (LP: #1243287)")
32 return 'UNKNOWN'
33
34- cmd = CMD_DMI_SYSTEM
35- try:
36- (cmd_out, _err) = util.subp(cmd)
37- except ProcessExecutionError, _err:
38- LOG.debug(('Failed command: %s\n%s') % \
39- (' '.join(cmd), _err.message))
40- return 'UNKNOWN'
41- except OSError, _err:
42- LOG.debug(('Failed command: %s\n%s') % \
43- (' '.join(cmd), _err.message))
44- return 'UNKNOWN'
45-
46- if cmd_out.upper().startswith('RHEV'):
47+ system_name = util.read_dmi_data("system-product-name")
48+ if not system_name:
49+ return 'UNKNOWN'
50+
51+ sys_name = system_name.upper()
52+
53+ if sys_name.startswith('RHEV'):
54 return 'RHEV'
55
56- if cmd_out.upper().startswith('VMWARE'):
57+ if sys_name.startswith('VMWARE'):
58 return 'VSPHERE'
59
60 return 'UNKNOWN'
61
62=== modified file 'cloudinit/sources/DataSourceCloudSigma.py'
63--- cloudinit/sources/DataSourceCloudSigma.py 2014-05-30 18:50:57 +0000
64+++ cloudinit/sources/DataSourceCloudSigma.py 2015-01-21 22:43:46 +0000
65@@ -44,27 +44,25 @@
66
67 def is_running_in_cloudsigma(self):
68 """
69- Uses dmidecode to detect if this instance of cloud-init is running
70+ Uses dmi data to detect if this instance of cloud-init is running
71 in the CloudSigma's infrastructure.
72 """
73 uname_arch = os.uname()[4]
74 if uname_arch.startswith("arm") or uname_arch == "aarch64":
75- # Disabling because dmidecode in CMD_DMI_SYSTEM crashes kvm process
76+ # Disabling because dmi data on ARM processors
77 LOG.debug("Disabling CloudSigma datasource on arm (LP: #1243287)")
78 return False
79
80- dmidecode_path = util.which('dmidecode')
81- if not dmidecode_path:
82+ LOG.debug("determining hypervisor product name via dmi data")
83+ sys_product_name = util.read_dmi_data("system-product-name")
84+ if not sys_product_name:
85+ LOG.warn("failed to get hypervisor product name via dmi data")
86 return False
87-
88- LOG.debug("Determining hypervisor product name via dmidecode")
89- try:
90- cmd = [dmidecode_path, "--string", "system-product-name"]
91- system_product_name, _ = util.subp(cmd)
92- return 'cloudsigma' in system_product_name.lower()
93- except:
94- LOG.warn("Failed to get hypervisor product name via dmidecode")
95-
96+ else:
97+ LOG.debug("detected hypervisor as {}".format(sys_product_name))
98+ return 'cloudsigma' in sys_product_name.lower()
99+
100+ LOG.warn("failed to query dmi data for system product name")
101 return False
102
103 def get_data(self):
104
105=== modified file 'cloudinit/sources/DataSourceSmartOS.py'
106--- cloudinit/sources/DataSourceSmartOS.py 2014-08-26 18:50:11 +0000
107+++ cloudinit/sources/DataSourceSmartOS.py 2015-01-21 22:43:46 +0000
108@@ -358,26 +358,13 @@
109
110
111 def dmi_data():
112- sys_uuid, sys_type = None, None
113- dmidecode_path = util.which('dmidecode')
114- if not dmidecode_path:
115- return False
116-
117- sys_uuid_cmd = [dmidecode_path, "-s", "system-uuid"]
118- try:
119- LOG.debug("Getting hostname from dmidecode")
120- (sys_uuid, _err) = util.subp(sys_uuid_cmd)
121- except Exception as e:
122- util.logexc(LOG, "Failed to get system UUID", e)
123-
124- sys_type_cmd = [dmidecode_path, "-s", "system-product-name"]
125- try:
126- LOG.debug("Determining hypervisor product name via dmidecode")
127- (sys_type, _err) = util.subp(sys_type_cmd)
128- except Exception as e:
129- util.logexc(LOG, "Failed to get system UUID", e)
130-
131- return (sys_uuid.lower().strip(), sys_type.strip())
132+ sys_uuid = util.read_dmi_data("system-uuid")
133+ sys_type = util.read_dmi_data("system-product-name")
134+
135+ if not sys_uuid or not sys_type:
136+ return None
137+
138+ return (sys_uuid.lower(), sys_type)
139
140
141 def write_boot_content(content, content_f, link=None, shebang=False,
142
143=== modified file 'cloudinit/util.py'
144--- cloudinit/util.py 2015-01-06 17:02:38 +0000
145+++ cloudinit/util.py 2015-01-21 22:43:46 +0000
146@@ -72,6 +72,9 @@
147 # Helper utils to see if running in a container
148 CONTAINER_TESTS = ['running-in-container', 'lxc-is-container']
149
150+# Path for DMI Data
151+DMI_SYS_PATH = "/sys/class/dmi/id"
152+
153
154 class ProcessExecutionError(IOError):
155
156@@ -2011,3 +2014,61 @@
157 raise ValueError("'%s': cannot be negative" % size_in)
158
159 return int(num * mpliers[mplier])
160+
161+
162+def _read_dmi_syspath(key):
163+ """
164+ Reads dmi data with from /sys/class/dmi/id
165+ """
166+
167+ dmi_key = "{}/{}".format(DMI_SYS_PATH, key)
168+ LOG.debug("querying dmi data {}".format(dmi_key))
169+ try:
170+ if not os.path.exists(dmi_key):
171+ LOG.debug("did not find {}".format(dmi_key))
172+ return None
173+
174+ key_data = load_file(dmi_key)
175+ if not key_data:
176+ LOG.debug("{} did not return any data".format(key))
177+ return None
178+
179+ LOG.debug("dmi data {} returned {}".format(dmi_key, key_data))
180+ return key_data.strip()
181+
182+ except Exception as e:
183+ logexc(LOG, "failed read of {}".format(dmi_key), e)
184+ return None
185+
186+
187+def _call_dmidecode(key, dmidecode_path):
188+ """
189+ Calls out to dmidecode to get the data out. This is mostly for supporting
190+ OS's without /sys/class/dmi/id support.
191+ """
192+ try:
193+ cmd = [dmidecode_path, "--string", key]
194+ (result, _err) = subp(cmd)
195+ LOG.debug("dmidecode returned '{}' for '{}'".format(result, key))
196+ return result
197+ except OSError, _err:
198+ LOG.debug('failed dmidecode cmd: {}\n{}'.format(cmd, _err.message))
199+ return None
200+
201+
202+def read_dmi_data(key):
203+ """
204+ Wrapper for reading DMI data. This tries to determine whether the DMI
205+ Data can be read directly, otherwise it will fallback to using dmidecode.
206+ """
207+ if os.path.exists(DMI_SYS_PATH):
208+ return _read_dmi_syspath(key)
209+
210+ dmidecode_path = which('dmidecode')
211+ if dmidecode_path:
212+ return _call_dmidecode(key, dmidecode_path)
213+
214+ LOG.warn("did not find either path {} or dmidecode command".format(
215+ DMI_SYS_PATH))
216+
217+ return None
218
219=== modified file 'tests/unittests/test_datasource/test_altcloud.py'
220--- tests/unittests/test_datasource/test_altcloud.py 2014-02-27 19:12:01 +0000
221+++ tests/unittests/test_datasource/test_altcloud.py 2015-01-21 22:43:46 +0000
222@@ -26,6 +26,7 @@
223 import tempfile
224
225 from cloudinit import helpers
226+from cloudinit import util
227 from unittest import TestCase
228
229 # Get the cloudinit.sources.DataSourceAltCloud import items needed.
230@@ -98,6 +99,16 @@
231 pass
232
233
234+def _dmi_data(expected):
235+ '''
236+ Spoof the data received over DMI
237+ '''
238+ def _data(key):
239+ return expected
240+
241+ return _data
242+
243+
244 class TestGetCloudType(TestCase):
245 '''
246 Test to exercise method: DataSourceAltCloud.get_cloud_type()
247@@ -106,24 +117,22 @@
248 def setUp(self):
249 '''Set up.'''
250 self.paths = helpers.Paths({'cloud_dir': '/tmp'})
251+ self.dmi_data = util.read_dmi_data
252 # We have a different code path for arm to deal with LP1243287
253 # We have to switch arch to x86_64 to avoid test failure
254 force_arch('x86_64')
255
256 def tearDown(self):
257 # Reset
258- cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \
259- ['dmidecode', '--string', 'system-product-name']
260- # Return back to original arch
261+ util.read_dmi_data = self.dmi_data
262 force_arch()
263
264 def test_rhev(self):
265 '''
266 Test method get_cloud_type() for RHEVm systems.
267- Forcing dmidecode return to match a RHEVm system: RHEV Hypervisor
268+ Forcing read_dmi_data return to match a RHEVm system: RHEV Hypervisor
269 '''
270- cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \
271- ['echo', 'RHEV Hypervisor']
272+ util.read_dmi_data = _dmi_data('RHEV')
273 dsrc = DataSourceAltCloud({}, None, self.paths)
274 self.assertEquals('RHEV', \
275 dsrc.get_cloud_type())
276@@ -131,10 +140,9 @@
277 def test_vsphere(self):
278 '''
279 Test method get_cloud_type() for vSphere systems.
280- Forcing dmidecode return to match a vSphere system: RHEV Hypervisor
281+ Forcing read_dmi_data return to match a vSphere system: RHEV Hypervisor
282 '''
283- cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \
284- ['echo', 'VMware Virtual Platform']
285+ util.read_dmi_data = _dmi_data('VMware Virtual Platform')
286 dsrc = DataSourceAltCloud({}, None, self.paths)
287 self.assertEquals('VSPHERE', \
288 dsrc.get_cloud_type())
289@@ -142,30 +150,9 @@
290 def test_unknown(self):
291 '''
292 Test method get_cloud_type() for unknown systems.
293- Forcing dmidecode return to match an unrecognized return.
294- '''
295- cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \
296- ['echo', 'Unrecognized Platform']
297- dsrc = DataSourceAltCloud({}, None, self.paths)
298- self.assertEquals('UNKNOWN', \
299- dsrc.get_cloud_type())
300-
301- def test_exception1(self):
302- '''
303- Test method get_cloud_type() where command dmidecode fails.
304- '''
305- cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \
306- ['ls', 'bad command']
307- dsrc = DataSourceAltCloud({}, None, self.paths)
308- self.assertEquals('UNKNOWN', \
309- dsrc.get_cloud_type())
310-
311- def test_exception2(self):
312- '''
313- Test method get_cloud_type() where command dmidecode is not available.
314- '''
315- cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \
316- ['bad command']
317+ Forcing read_dmi_data return to match an unrecognized return.
318+ '''
319+ util.read_dmi_data = _dmi_data('Unrecognized Platform')
320 dsrc = DataSourceAltCloud({}, None, self.paths)
321 self.assertEquals('UNKNOWN', \
322 dsrc.get_cloud_type())
323@@ -180,6 +167,7 @@
324 '''Set up.'''
325 self.paths = helpers.Paths({'cloud_dir': '/tmp'})
326 self.cloud_info_file = tempfile.mkstemp()[1]
327+ self.dmi_data = util.read_dmi_data
328 cloudinit.sources.DataSourceAltCloud.CLOUD_INFO_FILE = \
329 self.cloud_info_file
330
331@@ -192,6 +180,7 @@
332 except OSError:
333 pass
334
335+ util.read_dmi_data = self.dmi_data
336 cloudinit.sources.DataSourceAltCloud.CLOUD_INFO_FILE = \
337 '/etc/sysconfig/cloud-info'
338
339@@ -243,6 +232,7 @@
340 def setUp(self):
341 '''Set up.'''
342 self.paths = helpers.Paths({'cloud_dir': '/tmp'})
343+ self.dmi_data = util.read_dmi_data
344 cloudinit.sources.DataSourceAltCloud.CLOUD_INFO_FILE = \
345 'no such file'
346 # We have a different code path for arm to deal with LP1243287
347@@ -253,16 +243,14 @@
348 # Reset
349 cloudinit.sources.DataSourceAltCloud.CLOUD_INFO_FILE = \
350 '/etc/sysconfig/cloud-info'
351- cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \
352- ['dmidecode', '--string', 'system-product-name']
353+ util.read_dmi_data = self.dmi_data
354 # Return back to original arch
355 force_arch()
356
357 def test_rhev_no_cloud_file(self):
358 '''Test No cloud info file module get_data() forcing RHEV.'''
359
360- cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \
361- ['echo', 'RHEV Hypervisor']
362+ util.read_dmi_data = _dmi_data('RHEV Hypervisor')
363 dsrc = DataSourceAltCloud({}, None, self.paths)
364 dsrc.user_data_rhevm = lambda: True
365 self.assertEquals(True, dsrc.get_data())
366@@ -270,8 +258,7 @@
367 def test_vsphere_no_cloud_file(self):
368 '''Test No cloud info file module get_data() forcing VSPHERE.'''
369
370- cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \
371- ['echo', 'VMware Virtual Platform']
372+ util.read_dmi_data = _dmi_data('VMware Virtual Platform')
373 dsrc = DataSourceAltCloud({}, None, self.paths)
374 dsrc.user_data_vsphere = lambda: True
375 self.assertEquals(True, dsrc.get_data())
376@@ -279,8 +266,7 @@
377 def test_failure_no_cloud_file(self):
378 '''Test No cloud info file module get_data() forcing unrecognized.'''
379
380- cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \
381- ['echo', 'Unrecognized Platform']
382+ util.read_dmi_data = _dmi_data('Unrecognized Platform')
383 dsrc = DataSourceAltCloud({}, None, self.paths)
384 self.assertEquals(False, dsrc.get_data())
385
386
387=== modified file 'tests/unittests/test_util.py'
388--- tests/unittests/test_util.py 2014-08-26 19:53:41 +0000
389+++ tests/unittests/test_util.py 2015-01-21 22:43:46 +0000
390@@ -310,4 +310,60 @@
391 expected = ('none', 'tmpfs', '/run/lock')
392 self.assertEqual(expected, util.parse_mount_info('/run/lock', lines))
393
394+
395+class TestReadDMIData(helpers.FilesystemMockingTestCase):
396+
397+ def _patchIn(self, root):
398+ self.restore()
399+ self.patchOS(root)
400+ self.patchUtils(root)
401+
402+ def _write_key(self, key, content):
403+ """Mocks the sys path found on Linux systems."""
404+ new_root = self.makeDir()
405+ self._patchIn(new_root)
406+ util.ensure_dir(os.path.join('sys', 'class', 'dmi', 'id'))
407+
408+ dmi_key = "/sys/class/dmi/id/{}".format(key)
409+ util.write_file(dmi_key, content)
410+
411+ def _no_syspath(self, key, content):
412+ """
413+ In order to test a missing sys path and call outs to dmidecode, this
414+ function fakes the results of dmidecode to test the results.
415+ """
416+ new_root = self.makeDir()
417+ self._patchIn(new_root)
418+ self.real_which = util.which
419+ self.real_subp = util.subp
420+
421+ def _which(key):
422+ return True
423+ util.which = _which
424+
425+ def _cdd(_key, error=None):
426+ return (content, error)
427+ util.subp = _cdd
428+
429+ def test_key(self):
430+ key_content = "TEST-KEY-DATA"
431+ self._write_key("key", key_content)
432+ self.assertEquals(key_content, util.read_dmi_data("key"))
433+
434+ def test_key_mismatch(self):
435+ self._write_key("test", "ABC")
436+ self.assertNotEqual("123", util.read_dmi_data("test"))
437+
438+ def test_no_key(self):
439+ self._no_syspath(None, None)
440+ self.assertFalse(util.read_dmi_data("key"))
441+
442+ def test_callout_dmidecode(self):
443+ """test to make sure that dmidecode is used when no syspath"""
444+ self._no_syspath("key", "stuff")
445+ self.assertEquals("stuff", util.read_dmi_data("key"))
446+ self._no_syspath("key", None)
447+ self.assertFalse(None, util.read_dmi_data("key"))
448+
449+
450 # vi: ts=4 expandtab