diff options
-rw-r--r-- | ChangeLog | 1 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceAltCloud.py | 27 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceCloudSigma.py | 22 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceSmartOS.py | 25 | ||||
-rw-r--r-- | cloudinit/util.py | 61 | ||||
-rw-r--r-- | tests/unittests/helpers.py | 5 | ||||
-rw-r--r-- | tests/unittests/test_datasource/test_altcloud.py | 66 | ||||
-rw-r--r-- | tests/unittests/test_util.py | 59 |
8 files changed, 175 insertions, 91 deletions
@@ -20,6 +20,7 @@ - systemd: make init stage run before login prompts shown [Steve Langasek] - hostname: on first boot apply hostname to be same as is written for persistent hostname. (LP: #1246485) + - remove usage of dmidecode on linux in favor of /sys interface [Ben Howard] 0.7.6: - open 0.7.6 - Enable vendordata on CloudSigma datasource (LP: #1303986) diff --git a/cloudinit/sources/DataSourceAltCloud.py b/cloudinit/sources/DataSourceAltCloud.py index 69053d0b..fb528ae5 100644 --- a/cloudinit/sources/DataSourceAltCloud.py +++ b/cloudinit/sources/DataSourceAltCloud.py @@ -40,7 +40,6 @@ LOG = logging.getLogger(__name__) CLOUD_INFO_FILE = '/etc/sysconfig/cloud-info' # Shell command lists -CMD_DMI_SYSTEM = ['/usr/sbin/dmidecode', '--string', 'system-product-name'] CMD_PROBE_FLOPPY = ['/sbin/modprobe', 'floppy'] CMD_UDEVADM_SETTLE = ['/sbin/udevadm', 'settle', '--quiet', '--timeout=5'] @@ -100,11 +99,7 @@ class DataSourceAltCloud(sources.DataSource): ''' Description: Get the type for the cloud back end this instance is running on - by examining the string returned by: - dmidecode --string system-product-name - - On VMWare/vSphere dmidecode returns: RHEV Hypervisor - On VMWare/vSphere dmidecode returns: VMware Virtual Platform + by examining the string returned by reading the dmi data. Input: None @@ -117,26 +112,20 @@ class DataSourceAltCloud(sources.DataSource): uname_arch = os.uname()[4] if uname_arch.startswith("arm") or uname_arch == "aarch64": - # Disabling because dmidecode in CMD_DMI_SYSTEM crashes kvm process + # Disabling because dmi data is not available on ARM processors LOG.debug("Disabling AltCloud datasource on arm (LP: #1243287)") return 'UNKNOWN' - cmd = CMD_DMI_SYSTEM - try: - (cmd_out, _err) = util.subp(cmd) - except ProcessExecutionError as _err: - LOG.debug(('Failed command: %s\n%s') % \ - (' '.join(cmd), _err.message)) - return 'UNKNOWN' - except OSError as _err: - LOG.debug(('Failed command: %s\n%s') % \ - (' '.join(cmd), _err.message)) + system_name = util.read_dmi_data("system-product-name") + if not system_name: return 'UNKNOWN' - if cmd_out.upper().startswith('RHEV'): + sys_name = system_name.upper() + + if sys_name.startswith('RHEV'): return 'RHEV' - if cmd_out.upper().startswith('VMWARE'): + if sys_name.startswith('VMWARE'): return 'VSPHERE' return 'UNKNOWN' diff --git a/cloudinit/sources/DataSourceCloudSigma.py b/cloudinit/sources/DataSourceCloudSigma.py index 707cd0ce..76597116 100644 --- a/cloudinit/sources/DataSourceCloudSigma.py +++ b/cloudinit/sources/DataSourceCloudSigma.py @@ -44,27 +44,25 @@ class DataSourceCloudSigma(sources.DataSource): def is_running_in_cloudsigma(self): """ - Uses dmidecode to detect if this instance of cloud-init is running + Uses dmi data to detect if this instance of cloud-init is running in the CloudSigma's infrastructure. """ uname_arch = os.uname()[4] if uname_arch.startswith("arm") or uname_arch == "aarch64": - # Disabling because dmidecode in CMD_DMI_SYSTEM crashes kvm process + # Disabling because dmi data on ARM processors LOG.debug("Disabling CloudSigma datasource on arm (LP: #1243287)") return False - dmidecode_path = util.which('dmidecode') - if not dmidecode_path: + LOG.debug("determining hypervisor product name via dmi data") + sys_product_name = util.read_dmi_data("system-product-name") + if not sys_product_name: + LOG.warn("failed to get hypervisor product name via dmi data") return False + else: + LOG.debug("detected hypervisor as {}".format(sys_product_name)) + return 'cloudsigma' in sys_product_name.lower() - LOG.debug("Determining hypervisor product name via dmidecode") - try: - cmd = [dmidecode_path, "--string", "system-product-name"] - system_product_name, _ = util.subp(cmd) - return 'cloudsigma' in system_product_name.lower() - except: - LOG.warn("Failed to get hypervisor product name via dmidecode") - + LOG.warn("failed to query dmi data for system product name") return False def get_data(self): diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py index d3ed40c5..f59ad3d6 100644 --- a/cloudinit/sources/DataSourceSmartOS.py +++ b/cloudinit/sources/DataSourceSmartOS.py @@ -370,26 +370,13 @@ def query_data(noun, seed_device, seed_timeout, strip=False, default=None, def dmi_data(): - sys_uuid, sys_type = None, None - dmidecode_path = util.which('dmidecode') - if not dmidecode_path: - return False + sys_uuid = util.read_dmi_data("system-uuid") + sys_type = util.read_dmi_data("system-product-name") + + if not sys_uuid or not sys_type: + return None - sys_uuid_cmd = [dmidecode_path, "-s", "system-uuid"] - try: - LOG.debug("Getting hostname from dmidecode") - (sys_uuid, _err) = util.subp(sys_uuid_cmd) - except Exception as e: - util.logexc(LOG, "Failed to get system UUID", e) - - sys_type_cmd = [dmidecode_path, "-s", "system-product-name"] - try: - LOG.debug("Determining hypervisor product name via dmidecode") - (sys_type, _err) = util.subp(sys_type_cmd) - except Exception as e: - util.logexc(LOG, "Failed to get system UUID", e) - - return (sys_uuid.lower().strip(), sys_type.strip()) + return (sys_uuid.lower(), sys_type) def write_boot_content(content, content_f, link=None, shebang=False, diff --git a/cloudinit/util.py b/cloudinit/util.py index d594b611..32c19ba2 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -90,6 +90,9 @@ def encode_text(text, encoding='utf-8'): return text return text.encode(encoding) +# Path for DMI Data +DMI_SYS_PATH = "/sys/class/dmi/id" + class ProcessExecutionError(IOError): @@ -2049,3 +2052,61 @@ def human2bytes(size): raise ValueError("'%s': cannot be negative" % size_in) return int(num * mpliers[mplier]) + + +def _read_dmi_syspath(key): + """ + Reads dmi data with from /sys/class/dmi/id + """ + + dmi_key = "{}/{}".format(DMI_SYS_PATH, key) + LOG.debug("querying dmi data {}".format(dmi_key)) + try: + if not os.path.exists(dmi_key): + LOG.debug("did not find {}".format(dmi_key)) + return None + + key_data = load_file(dmi_key) + if not key_data: + LOG.debug("{} did not return any data".format(key)) + return None + + LOG.debug("dmi data {} returned {}".format(dmi_key, key_data)) + return key_data.strip() + + except Exception as e: + logexc(LOG, "failed read of {}".format(dmi_key), e) + return None + + +def _call_dmidecode(key, dmidecode_path): + """ + Calls out to dmidecode to get the data out. This is mostly for supporting + OS's without /sys/class/dmi/id support. + """ + try: + cmd = [dmidecode_path, "--string", key] + (result, _err) = subp(cmd) + LOG.debug("dmidecode returned '{}' for '{}'".format(result, key)) + return result + except OSError as _err: + LOG.debug('failed dmidecode cmd: {}\n{}'.format(cmd, _err.message)) + return None + + +def read_dmi_data(key): + """ + Wrapper for reading DMI data. This tries to determine whether the DMI + Data can be read directly, otherwise it will fallback to using dmidecode. + """ + if os.path.exists(DMI_SYS_PATH): + return _read_dmi_syspath(key) + + dmidecode_path = which('dmidecode') + if dmidecode_path: + return _call_dmidecode(key, dmidecode_path) + + LOG.warn("did not find either path {} or dmidecode command".format( + DMI_SYS_PATH)) + + return None diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index 4b8dcc5c..828579e8 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -169,7 +169,10 @@ class FilesystemMockingTestCase(ResourceUsingTestCase): def setUp(self): ResourceUsingTestCase.setUp(self) self.patched_funcs = ExitStack() - self.addCleanup(self.patched_funcs.close) + + def tearDown(self): + self.patched_funcs.close() + ResourceUsingTestCase.tearDown(self) def replicateTestRoot(self, example_root, target_root): real_root = self.resourceLocation() diff --git a/tests/unittests/test_datasource/test_altcloud.py b/tests/unittests/test_datasource/test_altcloud.py index c74562d7..e9cd2fa5 100644 --- a/tests/unittests/test_datasource/test_altcloud.py +++ b/tests/unittests/test_datasource/test_altcloud.py @@ -26,6 +26,7 @@ import shutil import tempfile from cloudinit import helpers +from cloudinit import util from unittest import TestCase # Get the cloudinit.sources.DataSourceAltCloud import items needed. @@ -98,6 +99,16 @@ def _remove_user_data_files(mount_dir, pass +def _dmi_data(expected): + ''' + Spoof the data received over DMI + ''' + def _data(key): + return expected + + return _data + + class TestGetCloudType(TestCase): ''' Test to exercise method: DataSourceAltCloud.get_cloud_type() @@ -106,24 +117,22 @@ class TestGetCloudType(TestCase): def setUp(self): '''Set up.''' self.paths = helpers.Paths({'cloud_dir': '/tmp'}) + self.dmi_data = util.read_dmi_data # We have a different code path for arm to deal with LP1243287 # We have to switch arch to x86_64 to avoid test failure force_arch('x86_64') def tearDown(self): # Reset - cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \ - ['dmidecode', '--string', 'system-product-name'] - # Return back to original arch + util.read_dmi_data = self.dmi_data force_arch() def test_rhev(self): ''' Test method get_cloud_type() for RHEVm systems. - Forcing dmidecode return to match a RHEVm system: RHEV Hypervisor + Forcing read_dmi_data return to match a RHEVm system: RHEV Hypervisor ''' - cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \ - ['echo', 'RHEV Hypervisor'] + util.read_dmi_data = _dmi_data('RHEV') dsrc = DataSourceAltCloud({}, None, self.paths) self.assertEquals('RHEV', \ dsrc.get_cloud_type()) @@ -131,10 +140,9 @@ class TestGetCloudType(TestCase): def test_vsphere(self): ''' Test method get_cloud_type() for vSphere systems. - Forcing dmidecode return to match a vSphere system: RHEV Hypervisor + Forcing read_dmi_data return to match a vSphere system: RHEV Hypervisor ''' - cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \ - ['echo', 'VMware Virtual Platform'] + util.read_dmi_data = _dmi_data('VMware Virtual Platform') dsrc = DataSourceAltCloud({}, None, self.paths) self.assertEquals('VSPHERE', \ dsrc.get_cloud_type()) @@ -142,30 +150,9 @@ class TestGetCloudType(TestCase): def test_unknown(self): ''' Test method get_cloud_type() for unknown systems. - Forcing dmidecode return to match an unrecognized return. - ''' - cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \ - ['echo', 'Unrecognized Platform'] - dsrc = DataSourceAltCloud({}, None, self.paths) - self.assertEquals('UNKNOWN', \ - dsrc.get_cloud_type()) - - def test_exception1(self): - ''' - Test method get_cloud_type() where command dmidecode fails. - ''' - cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \ - ['ls', 'bad command'] - dsrc = DataSourceAltCloud({}, None, self.paths) - self.assertEquals('UNKNOWN', \ - dsrc.get_cloud_type()) - - def test_exception2(self): - ''' - Test method get_cloud_type() where command dmidecode is not available. + Forcing read_dmi_data return to match an unrecognized return. ''' - cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \ - ['bad command'] + util.read_dmi_data = _dmi_data('Unrecognized Platform') dsrc = DataSourceAltCloud({}, None, self.paths) self.assertEquals('UNKNOWN', \ dsrc.get_cloud_type()) @@ -180,6 +167,7 @@ class TestGetDataCloudInfoFile(TestCase): '''Set up.''' self.paths = helpers.Paths({'cloud_dir': '/tmp'}) self.cloud_info_file = tempfile.mkstemp()[1] + self.dmi_data = util.read_dmi_data cloudinit.sources.DataSourceAltCloud.CLOUD_INFO_FILE = \ self.cloud_info_file @@ -192,6 +180,7 @@ class TestGetDataCloudInfoFile(TestCase): except OSError: pass + util.read_dmi_data = self.dmi_data cloudinit.sources.DataSourceAltCloud.CLOUD_INFO_FILE = \ '/etc/sysconfig/cloud-info' @@ -243,6 +232,7 @@ class TestGetDataNoCloudInfoFile(TestCase): def setUp(self): '''Set up.''' self.paths = helpers.Paths({'cloud_dir': '/tmp'}) + self.dmi_data = util.read_dmi_data cloudinit.sources.DataSourceAltCloud.CLOUD_INFO_FILE = \ 'no such file' # We have a different code path for arm to deal with LP1243287 @@ -253,16 +243,14 @@ class TestGetDataNoCloudInfoFile(TestCase): # Reset cloudinit.sources.DataSourceAltCloud.CLOUD_INFO_FILE = \ '/etc/sysconfig/cloud-info' - cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \ - ['dmidecode', '--string', 'system-product-name'] + util.read_dmi_data = self.dmi_data # Return back to original arch force_arch() def test_rhev_no_cloud_file(self): '''Test No cloud info file module get_data() forcing RHEV.''' - cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \ - ['echo', 'RHEV Hypervisor'] + util.read_dmi_data = _dmi_data('RHEV Hypervisor') dsrc = DataSourceAltCloud({}, None, self.paths) dsrc.user_data_rhevm = lambda: True self.assertEquals(True, dsrc.get_data()) @@ -270,8 +258,7 @@ class TestGetDataNoCloudInfoFile(TestCase): def test_vsphere_no_cloud_file(self): '''Test No cloud info file module get_data() forcing VSPHERE.''' - cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \ - ['echo', 'VMware Virtual Platform'] + util.read_dmi_data = _dmi_data('VMware Virtual Platform') dsrc = DataSourceAltCloud({}, None, self.paths) dsrc.user_data_vsphere = lambda: True self.assertEquals(True, dsrc.get_data()) @@ -279,8 +266,7 @@ class TestGetDataNoCloudInfoFile(TestCase): def test_failure_no_cloud_file(self): '''Test No cloud info file module get_data() forcing unrecognized.''' - cloudinit.sources.DataSourceAltCloud.CMD_DMI_SYSTEM = \ - ['echo', 'Unrecognized Platform'] + util.read_dmi_data = _dmi_data('Unrecognized Platform') dsrc = DataSourceAltCloud({}, None, self.paths) self.assertEquals(False, dsrc.get_data()) diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py index f537d332..7a224230 100644 --- a/tests/unittests/test_util.py +++ b/tests/unittests/test_util.py @@ -1,3 +1,5 @@ +from __future__ import print_function + import os import stat import yaml @@ -319,4 +321,61 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase): expected = ('none', 'tmpfs', '/run/lock') self.assertEqual(expected, util.parse_mount_info('/run/lock', lines)) + +class TestReadDMIData(helpers.FilesystemMockingTestCase): + + def _patchIn(self, root): + self.patchOS(root) + self.patchUtils(root) + + def _write_key(self, key, content): + """Mocks the sys path found on Linux systems.""" + new_root = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, new_root) + self._patchIn(new_root) + util.ensure_dir(os.path.join('sys', 'class', 'dmi', 'id')) + + dmi_key = "/sys/class/dmi/id/{}".format(key) + util.write_file(dmi_key, content) + + def _no_syspath(self, key, content): + """ + In order to test a missing sys path and call outs to dmidecode, this + function fakes the results of dmidecode to test the results. + """ + new_root = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, new_root) + self._patchIn(new_root) + self.real_which = util.which + self.real_subp = util.subp + + def _which(key): + return True + util.which = _which + + def _cdd(_key, error=None): + return (content, error) + util.subp = _cdd + + def test_key(self): + key_content = "TEST-KEY-DATA" + self._write_key("key", key_content) + self.assertEquals(key_content, util.read_dmi_data("key")) + + def test_key_mismatch(self): + self._write_key("test", "ABC") + self.assertNotEqual("123", util.read_dmi_data("test")) + + def test_no_key(self): + self._no_syspath(None, None) + self.assertFalse(util.read_dmi_data("key")) + + def test_callout_dmidecode(self): + """test to make sure that dmidecode is used when no syspath""" + self._no_syspath("key", "stuff") + self.assertEquals("stuff", util.read_dmi_data("key")) + self._no_syspath("key", None) + self.assertFalse(None, util.read_dmi_data("key")) + + # vi: ts=4 expandtab |