summaryrefslogtreecommitdiff
path: root/cloudinit
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit')
-rw-r--r--cloudinit/dmi.py131
-rw-r--r--cloudinit/sources/DataSourceAliYun.py4
-rw-r--r--cloudinit/sources/DataSourceAltCloud.py3
-rwxr-xr-xcloudinit/sources/DataSourceAzure.py5
-rw-r--r--cloudinit/sources/DataSourceCloudSigma.py4
-rw-r--r--cloudinit/sources/DataSourceEc2.py9
-rw-r--r--cloudinit/sources/DataSourceExoscale.py3
-rw-r--r--cloudinit/sources/DataSourceGCE.py5
-rw-r--r--cloudinit/sources/DataSourceHetzner.py5
-rw-r--r--cloudinit/sources/DataSourceNoCloud.py3
-rw-r--r--cloudinit/sources/DataSourceOVF.py5
-rw-r--r--cloudinit/sources/DataSourceOpenStack.py5
-rw-r--r--cloudinit/sources/DataSourceOracle.py5
-rw-r--r--cloudinit/sources/DataSourceScaleway.py3
-rw-r--r--cloudinit/sources/DataSourceSmartOS.py3
-rw-r--r--cloudinit/sources/__init__.py3
-rw-r--r--cloudinit/sources/helpers/digitalocean.py5
-rw-r--r--cloudinit/sources/tests/test_oracle.py6
-rw-r--r--cloudinit/tests/test_dmi.py131
-rw-r--r--cloudinit/util.py119
20 files changed, 307 insertions, 150 deletions
diff --git a/cloudinit/dmi.py b/cloudinit/dmi.py
new file mode 100644
index 00000000..96e0e423
--- /dev/null
+++ b/cloudinit/dmi.py
@@ -0,0 +1,131 @@
+# 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
+
+import os
+
+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',
+}
+
+
+def _read_dmi_syspath(key):
+ """
+ Reads dmi data with from /sys/class/dmi/id
+ """
+ if key not in DMIDECODE_TO_DMI_SYS_MAPPING:
+ return None
+ mapped_key = DMIDECODE_TO_DMI_SYS_MAPPING[key]
+ dmi_key_path = "{0}/{1}".format(DMI_SYS_PATH, mapped_key)
+
+ 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)
+ return None
+
+ try:
+ with open(dmi_key_path, "rb") as fp:
+ key_data = fp.read()
+ except PermissionError:
+ LOG.debug("Could not read %s", dmi_key_path)
+ return None
+
+ # uninitialized dmi values show as all \xff and /sys appends a '\n'.
+ # in that event, return empty string.
+ if key_data == b'\xff' * (len(key_data) - 1) + b'\n':
+ key_data = b""
+
+ try:
+ return key_data.decode('utf8').strip()
+ except UnicodeDecodeError as e:
+ LOG.error("utf-8 decode of content (%s) in %s failed: %s",
+ dmi_key_path, key_data, 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.subp(cmd)
+ result = result.strip()
+ LOG.debug("dmidecode returned '%s' for '%s'", result, key)
+ if result.replace(".", "") == "":
+ return ""
+ return result
+ except (IOError, OSError) as e:
+ LOG.debug('failed dmidecode cmd: %s\n%s', cmd, e)
+ return None
+
+
+def read_dmi_data(key):
+ """
+ Wrapper for reading DMI data.
+
+ If running in a container return None. This is because DMI data is
+ assumed to be not useful in a container as it does not represent the
+ container but rather the host.
+
+ This will do the following (returning the first that produces a
+ result):
+ 1) Use a mapping to translate `key` from dmidecode naming to
+ sysfs naming and look in /sys/class/dmi/... for a value.
+ 2) Use `key` as a sysfs key directly and look in /sys/class/dmi/...
+ 3) Fall-back to passing `key` to `dmidecode --string`.
+
+ If all of the above fail to find a value, None will be returned.
+ """
+
+ if is_container():
+ return None
+
+ syspath_value = _read_dmi_syspath(key)
+ if syspath_value is not None:
+ return syspath_value
+
+ def is_x86(arch):
+ return (arch == 'x86_64' or (arch[0] == 'i' and arch[2:] == '86'))
+
+ # running dmidecode can be problematic on some arches (LP: #1243287)
+ uname_arch = os.uname()[4]
+ if not (is_x86(uname_arch) or uname_arch in ('aarch64', 'amd64')):
+ LOG.debug("dmidata is not supported on %s", uname_arch)
+ return None
+
+ dmidecode_path = subp.which('dmidecode')
+ if dmidecode_path:
+ return _call_dmidecode(key, dmidecode_path)
+
+ LOG.warning("did not find either path %s or dmidecode command",
+ DMI_SYS_PATH)
+ return None
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/sources/DataSourceAliYun.py b/cloudinit/sources/DataSourceAliYun.py
index 45cc9f00..09052873 100644
--- a/cloudinit/sources/DataSourceAliYun.py
+++ b/cloudinit/sources/DataSourceAliYun.py
@@ -1,8 +1,8 @@
# This file is part of cloud-init. See LICENSE file for license information.
+from cloudinit import dmi
from cloudinit import sources
from cloudinit.sources import DataSourceEc2 as EC2
-from cloudinit import util
ALIYUN_PRODUCT = "Alibaba Cloud ECS"
@@ -30,7 +30,7 @@ class DataSourceAliYun(EC2.DataSourceEc2):
def _is_aliyun():
- return util.read_dmi_data('system-product-name') == ALIYUN_PRODUCT
+ return dmi.read_dmi_data('system-product-name') == ALIYUN_PRODUCT
def parse_public_keys(public_keys):
diff --git a/cloudinit/sources/DataSourceAltCloud.py b/cloudinit/sources/DataSourceAltCloud.py
index ac3ecc3d..cd93412a 100644
--- a/cloudinit/sources/DataSourceAltCloud.py
+++ b/cloudinit/sources/DataSourceAltCloud.py
@@ -16,6 +16,7 @@ import errno
import os
import os.path
+from cloudinit import dmi
from cloudinit import log as logging
from cloudinit import sources
from cloudinit import subp
@@ -109,7 +110,7 @@ class DataSourceAltCloud(sources.DataSource):
CLOUD_INFO_FILE)
return 'UNKNOWN'
return cloud_type
- system_name = util.read_dmi_data("system-product-name")
+ system_name = dmi.read_dmi_data("system-product-name")
if not system_name:
return 'UNKNOWN'
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 70e32f46..fa3e0a2b 100755
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -15,6 +15,7 @@ from time import time
from xml.dom import minidom
import xml.etree.ElementTree as ET
+from cloudinit import dmi
from cloudinit import log as logging
from cloudinit import net
from cloudinit.event import EventType
@@ -630,7 +631,7 @@ class DataSourceAzure(sources.DataSource):
def _iid(self, previous=None):
prev_iid_path = os.path.join(
self.paths.get_cpath('data'), 'instance-id')
- iid = util.read_dmi_data('system-uuid')
+ iid = dmi.read_dmi_data('system-uuid')
if os.path.exists(prev_iid_path):
previous = util.load_file(prev_iid_path).strip()
if is_byte_swapped(previous, iid):
@@ -1630,7 +1631,7 @@ def _is_platform_viable(seed_dir):
description="found azure asset tag",
parent=azure_ds_reporter
) as evt:
- asset_tag = util.read_dmi_data('chassis-asset-tag')
+ asset_tag = dmi.read_dmi_data('chassis-asset-tag')
if asset_tag == AZURE_CHASSIS_ASSET_TAG:
return True
msg = "Non-Azure DMI asset tag '%s' discovered." % asset_tag
diff --git a/cloudinit/sources/DataSourceCloudSigma.py b/cloudinit/sources/DataSourceCloudSigma.py
index df88f677..f63baf74 100644
--- a/cloudinit/sources/DataSourceCloudSigma.py
+++ b/cloudinit/sources/DataSourceCloudSigma.py
@@ -9,9 +9,9 @@ import re
from cloudinit.cs_utils import Cepko, SERIAL_PORT
+from cloudinit import dmi
from cloudinit import log as logging
from cloudinit import sources
-from cloudinit import util
LOG = logging.getLogger(__name__)
@@ -38,7 +38,7 @@ class DataSourceCloudSigma(sources.DataSource):
"""
LOG.debug("determining hypervisor product name via dmi data")
- sys_product_name = util.read_dmi_data("system-product-name")
+ sys_product_name = dmi.read_dmi_data("system-product-name")
if not sys_product_name:
LOG.debug("system-product-name not available in dmi data")
return False
diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
index 1d09c12a..1930a509 100644
--- a/cloudinit/sources/DataSourceEc2.py
+++ b/cloudinit/sources/DataSourceEc2.py
@@ -11,6 +11,7 @@
import os
import time
+from cloudinit import dmi
from cloudinit import ec2_utils as ec2
from cloudinit import log as logging
from cloudinit import net
@@ -699,26 +700,26 @@ def _collect_platform_data():
uuid = util.load_file("/sys/hypervisor/uuid").strip()
data['uuid_source'] = 'hypervisor'
except Exception:
- uuid = util.read_dmi_data('system-uuid')
+ uuid = dmi.read_dmi_data('system-uuid')
data['uuid_source'] = 'dmi'
if uuid is None:
uuid = ''
data['uuid'] = uuid.lower()
- serial = util.read_dmi_data('system-serial-number')
+ serial = dmi.read_dmi_data('system-serial-number')
if serial is None:
serial = ''
data['serial'] = serial.lower()
- asset_tag = util.read_dmi_data('chassis-asset-tag')
+ asset_tag = dmi.read_dmi_data('chassis-asset-tag')
if asset_tag is None:
asset_tag = ''
data['asset_tag'] = asset_tag.lower()
- vendor = util.read_dmi_data('system-manufacturer')
+ vendor = dmi.read_dmi_data('system-manufacturer')
data['vendor'] = (vendor if vendor else '').lower()
return data
diff --git a/cloudinit/sources/DataSourceExoscale.py b/cloudinit/sources/DataSourceExoscale.py
index d59aefd1..adee6d79 100644
--- a/cloudinit/sources/DataSourceExoscale.py
+++ b/cloudinit/sources/DataSourceExoscale.py
@@ -3,6 +3,7 @@
#
# This file is part of cloud-init. See LICENSE file for license information.
+from cloudinit import dmi
from cloudinit import ec2_utils as ec2
from cloudinit import log as logging
from cloudinit import sources
@@ -135,7 +136,7 @@ class DataSourceExoscale(sources.DataSource):
return self.extra_config
def _is_platform_viable(self):
- return util.read_dmi_data('system-product-name').startswith(
+ return dmi.read_dmi_data('system-product-name').startswith(
EXOSCALE_DMI_NAME)
diff --git a/cloudinit/sources/DataSourceGCE.py b/cloudinit/sources/DataSourceGCE.py
index 0ec5f6ec..746caddb 100644
--- a/cloudinit/sources/DataSourceGCE.py
+++ b/cloudinit/sources/DataSourceGCE.py
@@ -7,6 +7,7 @@ import json
from base64 import b64decode
+from cloudinit import dmi
from cloudinit.distros import ug_util
from cloudinit import log as logging
from cloudinit import sources
@@ -248,12 +249,12 @@ def read_md(address=None, platform_check=True):
def platform_reports_gce():
- pname = util.read_dmi_data('system-product-name') or "N/A"
+ pname = dmi.read_dmi_data('system-product-name') or "N/A"
if pname == "Google Compute Engine":
return True
# system-product-name is not always guaranteed (LP: #1674861)
- serial = util.read_dmi_data('system-serial-number') or "N/A"
+ serial = dmi.read_dmi_data('system-serial-number') or "N/A"
if serial.startswith("GoogleCloud-"):
return True
diff --git a/cloudinit/sources/DataSourceHetzner.py b/cloudinit/sources/DataSourceHetzner.py
index 8e4d4b69..c7c88dd7 100644
--- a/cloudinit/sources/DataSourceHetzner.py
+++ b/cloudinit/sources/DataSourceHetzner.py
@@ -6,6 +6,7 @@
"""Hetzner Cloud API Documentation
https://docs.hetzner.cloud/"""
+from cloudinit import dmi
from cloudinit import log as logging
from cloudinit import net as cloudnet
from cloudinit import sources
@@ -113,11 +114,11 @@ class DataSourceHetzner(sources.DataSource):
def get_hcloud_data():
- vendor_name = util.read_dmi_data('system-manufacturer')
+ vendor_name = dmi.read_dmi_data('system-manufacturer')
if vendor_name != "Hetzner":
return (False, None)
- serial = util.read_dmi_data("system-serial-number")
+ serial = dmi.read_dmi_data("system-serial-number")
if serial:
LOG.debug("Running on Hetzner Cloud: serial=%s", serial)
else:
diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py
index d4a175e8..a126aad3 100644
--- a/cloudinit/sources/DataSourceNoCloud.py
+++ b/cloudinit/sources/DataSourceNoCloud.py
@@ -11,6 +11,7 @@
import errno
import os
+from cloudinit import dmi
from cloudinit import log as logging
from cloudinit.net import eni
from cloudinit import sources
@@ -61,7 +62,7 @@ class DataSourceNoCloud(sources.DataSource):
# Parse the system serial label from dmi. If not empty, try parsing
# like the commandline
md = {}
- serial = util.read_dmi_data('system-serial-number')
+ serial = dmi.read_dmi_data('system-serial-number')
if serial and load_cmdline_data(md, serial):
found.append("dmi")
mydata = _merge_new_seed(mydata, {'meta-data': md})
diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py
index a5ccb8f6..741c140a 100644
--- a/cloudinit/sources/DataSourceOVF.py
+++ b/cloudinit/sources/DataSourceOVF.py
@@ -14,6 +14,7 @@ import re
import time
from xml.dom import minidom
+from cloudinit import dmi
from cloudinit import log as logging
from cloudinit import sources
from cloudinit import subp
@@ -83,7 +84,7 @@ class DataSourceOVF(sources.DataSource):
(seedfile, contents) = get_ovf_env(self.paths.seed_dir)
- system_type = util.read_dmi_data("system-product-name")
+ system_type = dmi.read_dmi_data("system-product-name")
if system_type is None:
LOG.debug("No system-product-name found")
@@ -322,7 +323,7 @@ class DataSourceOVF(sources.DataSource):
return True
def _get_subplatform(self):
- system_type = util.read_dmi_data("system-product-name").lower()
+ system_type = dmi.read_dmi_data("system-product-name").lower()
if system_type == 'vmware':
return 'vmware (%s)' % self.seed
return 'ovf (%s)' % self.seed
diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py
index 0ede0a0e..b3406c67 100644
--- a/cloudinit/sources/DataSourceOpenStack.py
+++ b/cloudinit/sources/DataSourceOpenStack.py
@@ -6,6 +6,7 @@
import time
+from cloudinit import dmi
from cloudinit import log as logging
from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError
from cloudinit import sources
@@ -225,10 +226,10 @@ def detect_openstack(accept_oracle=False):
"""Return True when a potential OpenStack platform is detected."""
if not util.is_x86():
return True # Non-Intel cpus don't properly report dmi product names
- product_name = util.read_dmi_data('system-product-name')
+ product_name = dmi.read_dmi_data('system-product-name')
if product_name in VALID_DMI_PRODUCT_NAMES:
return True
- elif util.read_dmi_data('chassis-asset-tag') in VALID_DMI_ASSET_TAGS:
+ elif dmi.read_dmi_data('chassis-asset-tag') in VALID_DMI_ASSET_TAGS:
return True
elif accept_oracle and oracle._is_platform_viable():
return True
diff --git a/cloudinit/sources/DataSourceOracle.py b/cloudinit/sources/DataSourceOracle.py
index 20d6487d..bf81b10b 100644
--- a/cloudinit/sources/DataSourceOracle.py
+++ b/cloudinit/sources/DataSourceOracle.py
@@ -17,6 +17,7 @@ import base64
from collections import namedtuple
from contextlib import suppress as noop
+from cloudinit import dmi
from cloudinit import log as logging
from cloudinit import net, sources, util
from cloudinit.net import (
@@ -273,12 +274,12 @@ class DataSourceOracle(sources.DataSource):
def _read_system_uuid():
- sys_uuid = util.read_dmi_data('system-uuid')
+ sys_uuid = dmi.read_dmi_data('system-uuid')
return None if sys_uuid is None else sys_uuid.lower()
def _is_platform_viable():
- asset_tag = util.read_dmi_data('chassis-asset-tag')
+ asset_tag = dmi.read_dmi_data('chassis-asset-tag')
return asset_tag == CHASSIS_ASSET_TAG
diff --git a/cloudinit/sources/DataSourceScaleway.py b/cloudinit/sources/DataSourceScaleway.py
index 83c2bf65..41be7665 100644
--- a/cloudinit/sources/DataSourceScaleway.py
+++ b/cloudinit/sources/DataSourceScaleway.py
@@ -25,6 +25,7 @@ import requests
from requests.packages.urllib3.connection import HTTPConnection
from requests.packages.urllib3.poolmanager import PoolManager
+from cloudinit import dmi
from cloudinit import log as logging
from cloudinit import sources
from cloudinit import url_helper
@@ -56,7 +57,7 @@ def on_scaleway():
* the initrd created the file /var/run/scaleway.
* "scaleway" is in the kernel cmdline.
"""
- vendor_name = util.read_dmi_data('system-manufacturer')
+ vendor_name = dmi.read_dmi_data('system-manufacturer')
if vendor_name == 'Scaleway':
return True
diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py
index f1f903bc..fd292baa 100644
--- a/cloudinit/sources/DataSourceSmartOS.py
+++ b/cloudinit/sources/DataSourceSmartOS.py
@@ -30,6 +30,7 @@ import random
import re
import socket
+from cloudinit import dmi
from cloudinit import log as logging
from cloudinit import serial
from cloudinit import sources
@@ -767,7 +768,7 @@ def get_smartos_environ(uname_version=None, product_name=None):
return SMARTOS_ENV_LX_BRAND
if product_name is None:
- system_type = util.read_dmi_data("system-product-name")
+ system_type = dmi.read_dmi_data("system-product-name")
else:
system_type = product_name
diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
index c4d60fff..9dccc687 100644
--- a/cloudinit/sources/__init__.py
+++ b/cloudinit/sources/__init__.py
@@ -14,6 +14,7 @@ import json
import os
from collections import namedtuple
+from cloudinit import dmi
from cloudinit import importer
from cloudinit import log as logging
from cloudinit import net
@@ -809,7 +810,7 @@ def instance_id_matches_system_uuid(instance_id, field='system-uuid'):
if not instance_id:
return False
- dmi_value = util.read_dmi_data(field)
+ dmi_value = dmi.read_dmi_data(field)
if not dmi_value:
return False
return instance_id.lower() == dmi_value.lower()
diff --git a/cloudinit/sources/helpers/digitalocean.py b/cloudinit/sources/helpers/digitalocean.py
index b545c4d6..f9be4ecb 100644
--- a/cloudinit/sources/helpers/digitalocean.py
+++ b/cloudinit/sources/helpers/digitalocean.py
@@ -5,6 +5,7 @@
import json
import random
+from cloudinit import dmi
from cloudinit import log as logging
from cloudinit import net as cloudnet
from cloudinit import url_helper
@@ -195,11 +196,11 @@ def read_sysinfo():
# SMBIOS information
# Detect if we are on DigitalOcean and return the Droplet's ID
- vendor_name = util.read_dmi_data("system-manufacturer")
+ vendor_name = dmi.read_dmi_data("system-manufacturer")
if vendor_name != "DigitalOcean":
return (False, None)
- droplet_id = util.read_dmi_data("system-serial-number")
+ droplet_id = dmi.read_dmi_data("system-serial-number")
if droplet_id:
LOG.debug("system identified via SMBIOS as DigitalOcean Droplet: %s",
droplet_id)
diff --git a/cloudinit/sources/tests/test_oracle.py b/cloudinit/sources/tests/test_oracle.py
index 7bd23813..a7bbdfd9 100644
--- a/cloudinit/sources/tests/test_oracle.py
+++ b/cloudinit/sources/tests/test_oracle.py
@@ -153,20 +153,20 @@ class TestDataSourceOracle:
class TestIsPlatformViable(test_helpers.CiTestCase):
- @mock.patch(DS_PATH + ".util.read_dmi_data",
+ @mock.patch(DS_PATH + ".dmi.read_dmi_data",
return_value=oracle.CHASSIS_ASSET_TAG)
def test_expected_viable(self, m_read_dmi_data):
"""System with known chassis tag is viable."""
self.assertTrue(oracle._is_platform_viable())
m_read_dmi_data.assert_has_calls([mock.call('chassis-asset-tag')])
- @mock.patch(DS_PATH + ".util.read_dmi_data", return_value=None)
+ @mock.patch(DS_PATH + ".dmi.read_dmi_data", return_value=None)
def test_expected_not_viable_dmi_data_none(self, m_read_dmi_data):
"""System without known chassis tag is not viable."""
self.assertFalse(oracle._is_platform_viable())
m_read_dmi_data.assert_has_calls([mock.call('chassis-asset-tag')])
- @mock.patch(DS_PATH + ".util.read_dmi_data", return_value="LetsGoCubs")
+ @mock.patch(DS_PATH + ".dmi.read_dmi_data", return_value="LetsGoCubs")
def test_expected_not_viable_other(self, m_read_dmi_data):
"""System with unnown chassis tag is not viable."""
self.assertFalse(oracle._is_platform_viable())
diff --git a/cloudinit/tests/test_dmi.py b/cloudinit/tests/test_dmi.py
new file mode 100644
index 00000000..4a8af257
--- /dev/null
+++ b/cloudinit/tests/test_dmi.py
@@ -0,0 +1,131 @@
+from cloudinit.tests import helpers
+from cloudinit import dmi
+from cloudinit import util
+from cloudinit import subp
+
+import os
+import tempfile
+import shutil
+from unittest import mock
+
+
+class TestReadDMIData(helpers.FilesystemMockingTestCase):
+
+ def setUp(self):
+ super(TestReadDMIData, self).setUp()
+ self.new_root = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, self.new_root)
+ self.reRoot(self.new_root)
+ p = mock.patch("cloudinit.dmi.is_container", return_value=False)
+ self.addCleanup(p.stop)
+ self._m_is_container = p.start()
+
+ def _create_sysfs_parent_directory(self):
+ util.ensure_dir(os.path.join('sys', 'class', 'dmi', 'id'))
+
+ def _create_sysfs_file(self, key, content):
+ """Mocks the sys path found on Linux systems."""
+ self._create_sysfs_parent_directory()
+ dmi_key = "/sys/class/dmi/id/{0}".format(key)
+ util.write_file(dmi_key, content)
+
+ def _configure_dmidecode_return(self, key, content, error=None):
+ """
+ In order to test a missing sys path and call outs to dmidecode, this
+ function fakes the results of dmidecode to test the results.
+ """
+ def _dmidecode_subp(cmd):
+ if cmd[-1] != key:
+ raise subp.ProcessExecutionError()
+ return (content, error)
+
+ self.patched_funcs.enter_context(
+ mock.patch("cloudinit.dmi.subp.which", side_effect=lambda _: True))
+ self.patched_funcs.enter_context(
+ mock.patch("cloudinit.dmi.subp.subp", side_effect=_dmidecode_subp))
+
+ def patch_mapping(self, new_mapping):
+ self.patched_funcs.enter_context(
+ mock.patch('cloudinit.dmi.DMIDECODE_TO_DMI_SYS_MAPPING',
+ new_mapping))
+
+ def test_sysfs_used_with_key_in_mapping_and_file_on_disk(self):
+ self.patch_mapping({'mapped-key': 'mapped-value'})
+ expected_dmi_value = 'sys-used-correctly'
+ self._create_sysfs_file('mapped-value', expected_dmi_value)
+ self._configure_dmidecode_return('mapped-key', 'wrong-wrong-wrong')
+ self.assertEqual(expected_dmi_value, dmi.read_dmi_data('mapped-key'))
+
+ def test_dmidecode_used_if_no_sysfs_file_on_disk(self):
+ self.patch_mapping({})
+ self._create_sysfs_parent_directory()
+ expected_dmi_value = 'dmidecode-used'
+ self._configure_dmidecode_return('use-dmidecode', expected_dmi_value)
+ with mock.patch("cloudinit.util.os.uname") as m_uname:
+ m_uname.return_value = ('x-sysname', 'x-nodename',
+ 'x-release', 'x-version', 'x86_64')
+ self.assertEqual(expected_dmi_value,
+ dmi.read_dmi_data('use-dmidecode'))
+
+ def test_dmidecode_not_used_on_arm(self):
+ self.patch_mapping({})
+ print("current =%s", subp)
+ self._create_sysfs_parent_directory()
+ dmi_val = 'from-dmidecode'
+ dmi_name = 'use-dmidecode'
+ self._configure_dmidecode_return(dmi_name, dmi_val)
+ print("now =%s", subp)
+
+ expected = {'armel': None, 'aarch64': dmi_val, 'x86_64': dmi_val}
+ found = {}
+ # we do not run the 'dmi-decode' binary on some arches
+ # verify that anything requested that is not in the sysfs dir
+ # will return None on those arches.
+ with mock.patch("cloudinit.util.os.uname") as m_uname:
+ for arch in expected:
+ m_uname.return_value = ('x-sysname', 'x-nodename',
+ 'x-release', 'x-version', arch)
+ print("now2 =%s", subp)
+ found[arch] = dmi.read_dmi_data(dmi_name)
+ self.assertEqual(expected, found)
+
+ def test_none_returned_if_neither_source_has_data(self):
+ self.patch_mapping({})
+ self._configure_dmidecode_return('key', 'value')
+ self.assertIsNone(dmi.read_dmi_data('expect-fail'))
+
+ def test_none_returned_if_dmidecode_not_in_path(self):
+ self.patched_funcs.enter_context(
+ mock.patch.object(subp, 'which', lambda _: False))
+ self.patch_mapping({})
+ self.assertIsNone(dmi.read_dmi_data('expect-fail'))
+
+ def test_empty_string_returned_instead_of_foxfox(self):
+ # uninitialized dmi values show as \xff, return empty string
+ my_len = 32
+ dmi_value = b'\xff' * my_len + b'\n'
+ expected = ""
+ dmi_key = 'system-product-name'
+ sysfs_key = 'product_name'
+ self._create_sysfs_file(sysfs_key, dmi_value)
+ self.assertEqual(expected, dmi.read_dmi_data(dmi_key))
+
+ def test_container_returns_none(self):
+ """In a container read_dmi_data should always return None."""
+
+ # first verify we get the value if not in container
+ self._m_is_container.return_value = False
+ key, val = ("system-product-name", "my_product")
+ self._create_sysfs_file('product_name', val)
+ self.assertEqual(val, dmi.read_dmi_data(key))
+
+ # then verify in container returns None
+ self._m_is_container.return_value = True
+ self.assertIsNone(dmi.read_dmi_data(key))
+
+ def test_container_returns_none_on_unknown(self):
+ """In a container even bogus keys return None."""
+ self._m_is_container.return_value = True
+ 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"))
diff --git a/cloudinit/util.py b/cloudinit/util.py
index b8856af1..bdb3694d 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -159,32 +159,6 @@ def fully_decoded_payload(part):
return cte_payload
-# 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',
-}
-
-
class SeLinuxGuard(object):
def __init__(self, path, recursive=False):
# Late import since it might not always
@@ -2421,57 +2395,6 @@ def human2bytes(size):
return int(num * mpliers[mplier])
-def _read_dmi_syspath(key):
- """
- Reads dmi data with from /sys/class/dmi/id
- """
- if key not in DMIDECODE_TO_DMI_SYS_MAPPING:
- return None
- mapped_key = DMIDECODE_TO_DMI_SYS_MAPPING[key]
- dmi_key_path = "{0}/{1}".format(DMI_SYS_PATH, mapped_key)
- LOG.debug("querying dmi data %s", dmi_key_path)
- try:
- if not os.path.exists(dmi_key_path):
- LOG.debug("did not find %s", dmi_key_path)
- return None
-
- key_data = load_file(dmi_key_path, decode=False)
- if not key_data:
- LOG.debug("%s did not return any data", dmi_key_path)
- return None
-
- # uninitialized dmi values show as all \xff and /sys appends a '\n'.
- # in that event, return a string of '.' in the same length.
- if key_data == b'\xff' * (len(key_data) - 1) + b'\n':
- key_data = b""
-
- str_data = key_data.decode('utf8').strip()
- LOG.debug("dmi data %s returned %s", dmi_key_path, str_data)
- return str_data
-
- except Exception:
- logexc(LOG, "failed read of %s", dmi_key_path)
- 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.subp(cmd)
- result = result.strip()
- LOG.debug("dmidecode returned '%s' for '%s'", result, key)
- if result.replace(".", "") == "":
- return ""
- return result
- except (IOError, OSError) as e:
- LOG.debug('failed dmidecode cmd: %s\n%s', cmd, e)
- return None
-
-
def is_x86(uname_arch=None):
"""Return True if platform is x86-based"""
if uname_arch is None:
@@ -2482,48 +2405,6 @@ def is_x86(uname_arch=None):
return x86_arch_match
-def read_dmi_data(key):
- """
- Wrapper for reading DMI data.
-
- If running in a container return None. This is because DMI data is
- assumed to be not useful in a container as it does not represent the
- container but rather the host.
-
- This will do the following (returning the first that produces a
- result):
- 1) Use a mapping to translate `key` from dmidecode naming to
- sysfs naming and look in /sys/class/dmi/... for a value.
- 2) Use `key` as a sysfs key directly and look in /sys/class/dmi/...
- 3) Fall-back to passing `key` to `dmidecode --string`.
-
- If all of the above fail to find a value, None will be returned.
- """
-
- if is_container():
- return None
-
- syspath_value = _read_dmi_syspath(key)
- if syspath_value is not None:
- return syspath_value
-
- # running dmidecode can be problematic on some arches (LP: #1243287)
- uname_arch = os.uname()[4]
- if not (is_x86(uname_arch) or
- uname_arch == 'aarch64' or
- uname_arch == 'amd64'):
- LOG.debug("dmidata is not supported on %s", uname_arch)
- return None
-
- dmidecode_path = subp.which('dmidecode')
- if dmidecode_path:
- return _call_dmidecode(key, dmidecode_path)
-
- LOG.warning("did not find either path %s or dmidecode command",
- DMI_SYS_PATH)
- return None
-
-
def message_from_string(string):
if sys.version_info[:2] < (2, 7):
return email.message_from_file(io.StringIO(string))