diff --git a/imagebuild/tinyipa/build-tinyipa.sh b/imagebuild/tinyipa/build-tinyipa.sh index 40e26bb7a..1112e1ede 100755 --- a/imagebuild/tinyipa/build-tinyipa.sh +++ b/imagebuild/tinyipa/build-tinyipa.sh @@ -62,6 +62,7 @@ sudo sh -c "echo $TINYCORE_MIRROR_URL > $BUILDDIR/opt/tcemirror" # Download TGT, Qemu-utils, Biosdevname and IPMItool source clone_and_checkout "https://github.com/fujita/tgt.git" "${BUILDDIR}/tmp/tgt" "v1.0.62" clone_and_checkout "https://github.com/qemu/qemu.git" "${BUILDDIR}/tmp/qemu" "v2.5.0" +clone_and_checkout "https://github.com/lyonel/lshw.git" "${BUILDDIR}/tmp/lshw" "B.02.18" if $TINYIPA_REQUIRE_BIOSDEVNAME; then wget -N -O - https://linux.dell.com/biosdevname/biosdevname-0.7.2/biosdevname-0.7.2.tar.gz | tar -xz -C "${BUILDDIR}/tmp" -f - fi @@ -137,6 +138,13 @@ cd $WORKDIR/build_files && mksquashfs $BUILDDIR/tmp/qemu-utils qemu-utils.tcz && # Create qemu-utils.tcz.dep echo "glib2.tcz" > qemu-utils.tcz.dep +# Build lshw +rm -rf $WORKDIR/build_files/lshw.tcz +# NOTE(mjturek): We touch src/lshw.1 and clear src/po/Makefile to avoid building the man pages, as they aren't used and require large dependencies to build. +$CHROOT_CMD /bin/sh -c "cd /tmp/lshw && touch src/lshw.1 && echo install: > src/po/Makefile && make && make install DESTDIR=/tmp/lshw-installed" +find $BUILDDIR/tmp/lshw-installed/ -type f -executable | xargs file | awk -F ':' '/ELF/ {print $1}' | sudo xargs strip +cd $WORKDIR/build_files && mksquashfs $BUILDDIR/tmp/lshw-installed lshw.tcz && md5sum lshw.tcz > lshw.tcz.md5.txt + # Build biosdevname if $TINYIPA_REQUIRE_BIOSDEVNAME; then rm -rf $WORKDIR/build_files/biosdevname.tcz diff --git a/imagebuild/tinyipa/finalise-tinyipa.sh b/imagebuild/tinyipa/finalise-tinyipa.sh index 3a1a115d1..ed24ba4b2 100755 --- a/imagebuild/tinyipa/finalise-tinyipa.sh +++ b/imagebuild/tinyipa/finalise-tinyipa.sh @@ -76,6 +76,8 @@ cp -Rp "$BUILDDIR/tmp/wheels" "$FINALDIR/tmp/wheelhouse" cp $WORKDIR/build_files/tgt.* $FINALDIR/tmp/builtin/optional cp $WORKDIR/build_files/qemu-utils.* $FINALDIR/tmp/builtin/optional +cp $WORKDIR/build_files/lshw.* $FINALDIR/tmp/builtin/optional + if $TINYIPA_REQUIRE_BIOSDEVNAME; then cp $WORKDIR/build_files/biosdevname.* $FINALDIR/tmp/builtin/optional fi @@ -118,6 +120,7 @@ fi $TC_CHROOT_CMD tce-load -ic /tmp/builtin/optional/tgt.tcz $TC_CHROOT_CMD tce-load -ic /tmp/builtin/optional/qemu-utils.tcz +$TC_CHROOT_CMD tce-load -ic /tmp/builtin/optional/lshw.tcz if $TINYIPA_REQUIRE_BIOSDEVNAME; then $TC_CHROOT_CMD tce-load -ic /tmp/builtin/optional/biosdevname.tcz fi diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py index 5972a21c0..bd12a209c 100644 --- a/ironic_python_agent/hardware.py +++ b/ironic_python_agent/hardware.py @@ -1,4 +1,4 @@ -# Copyright 2013 Rackspace, Inc. +# Copyright 2013 Rackspace, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ import abc import binascii import functools +import json import os import shlex import time @@ -43,8 +44,8 @@ CONF = cfg.CONF WARN_BIOSDEVNAME_NOT_FOUND = False UNIT_CONVERTER = pint.UnitRegistry(filename=None) -UNIT_CONVERTER.define('MB = []') -UNIT_CONVERTER.define('GB = 1024 MB') +UNIT_CONVERTER.define('bytes = []') +UNIT_CONVERTER.define('MB = 1048576 bytes') NODE = None @@ -62,6 +63,18 @@ def _get_device_info(dev, devclass, field): field, dev, devclass)) +def _get_system_lshw_dict(): + """Get a dict representation of the system from lshw + + Retrieves a json representation of the system from lshw and converts + it to a python dict + + :return: A python dict from the lshw json output + """ + out, _e = utils.execute('lshw', '-quiet', '-json') + return json.loads(out) + + def _udev_settle(): """Wait for the udev event queue to settle. @@ -670,38 +683,25 @@ class GenericHardwareManager(HardwareManager): total = None LOG.exception(("Cannot fetch total memory size using psutil " "version %s"), psutil.version_info[0]) - + sys_dict = None try: - out, _e = utils.execute("dmidecode --type 17 | grep Size", - shell=True) - except (processutils.ProcessExecutionError, OSError) as e: - LOG.warning("Cannot get real physical memory size: %s", e) + sys_dict = _get_system_lshw_dict() + except (processutils.ProcessExecutionError, OSError, ValueError) as e: + LOG.warning('Could not get real physical RAM from lshw: %s', e) physical = None else: physical = 0 - for line in out.strip().split('\n'): - line = line.strip() - if not line: - continue - - if 'Size:' not in line: - continue - - value = None - try: - value = line.split('Size: ', 1)[1] - physical += int(UNIT_CONVERTER(value).to_base_units()) - except Exception as exc: - if (value == "No Module Installed" or - value == "Not Installed"): - LOG.debug('One memory slot is empty') - else: - LOG.error('Cannot parse size expression %s: %s', - line, exc) - + # locate memory information in system_dict + for sys_child in sys_dict['children']: + if sys_child['id'] == 'core': + for core_child in sys_child['children']: + if core_child['id'] == 'memory': + if core_child.get('size'): + value = "%(size)s %(units)s" % core_child + physical += int(UNIT_CONVERTER(value).to( + 'MB').magnitude) if not physical: - LOG.warning('failed to get real physical RAM, dmidecode ' - 'returned %s', out) + LOG.warning('Did not find any physical RAM') return Memory(total=total, physical_mb=physical) @@ -748,28 +748,14 @@ class GenericHardwareManager(HardwareManager): return dev_name def get_system_vendor_info(self): - product_name = None - serial_number = None - manufacturer = None try: - out, _e = utils.execute("dmidecode --type system", - shell=True) - except (processutils.ProcessExecutionError, OSError) as e: - LOG.warning("Cannot get system vendor information: %s", e) - else: - for line in out.split('\n'): - line_arr = line.split(':', 1) - if len(line_arr) != 2: - continue - if line_arr[0].strip() == 'Product Name': - product_name = line_arr[1].strip() - elif line_arr[0].strip() == 'Serial Number': - serial_number = line_arr[1].strip() - elif line_arr[0].strip() == 'Manufacturer': - manufacturer = line_arr[1].strip() - return SystemVendorInfo(product_name=product_name, - serial_number=serial_number, - manufacturer=manufacturer) + sys_dict = _get_system_lshw_dict() + except (processutils.ProcessExecutionError, OSError, ValueError) as e: + LOG.warning('Could not retrieve vendor info from lshw: %e', e) + sys_dict = {} + return SystemVendorInfo(product_name=sys_dict.get('product', ''), + serial_number=sys_dict.get('serial', ''), + manufacturer=sys_dict.get('vendor', '')) def get_boot_info(self): boot_mode = 'uefi' if os.path.isdir('/sys/firmware/efi') else 'bios' diff --git a/ironic_python_agent/tests/unit/test_hardware.py b/ironic_python_agent/tests/unit/test_hardware.py index 48096e90a..da7f20268 100644 --- a/ironic_python_agent/tests/unit/test_hardware.py +++ b/ironic_python_agent/tests/unit/test_hardware.py @@ -239,13 +239,113 @@ CPUINFO_FLAGS_OUTPUT = """ flags : fpu vme de pse """ -DMIDECODE_MEMORY_OUTPUT = (""" -Foo -Size: 2048 MB -Size: 2 GB -Installed Size: Not Installed -Enabled Size: Not Installed -Size: No Module Installed +LSHW_JSON_OUTPUT = (""" +{ + "id": "fuzzypickles", + "product": "ABC123 (GENERIC_SERVER)", + "vendor": "GENERIC", + "serial": "1234567", + "width": 64, + "capabilities": { + "smbios-2.7": "SMBIOS version 2.7", + "dmi-2.7": "DMI version 2.7", + "vsyscall32": "32-bit processes" + }, + "children": [ + { + "id": "core", + "description": "Motherboard", + "product": "ABC123", + "vendor": "GENERIC", + "serial": "ABCDEFGHIJK", + "children": [ + { + "id": "memory", + "class": "memory", + "description": "System Memory", + "units": "bytes", + "size": 4294967296, + "children": [ + { + "id": "bank:0", + "class": "memory", + "physid": "0", + "units": "bytes", + "size": 2147483648, + "width": 64, + "clock": 1600000000 + }, + { + "id": "bank:1", + "class": "memory", + "physid": "1" + }, + { + "id": "bank:2", + "class": "memory", + "physid": "2", + "units": "bytes", + "size": 1073741824, + "width": 64, + "clock": 1600000000 + }, + { + "id": "bank:3", + "class": "memory", + "physid": "3", + "units": "bytes", + "size": 1073741824, + "width": 64, + "clock": 1600000000 + } + ] + }, + { + "id": "cpu:0", + "class": "processor", + "claimed": true, + "product": "Intel Xeon E312xx (Sandy Bridge)", + "vendor": "Intel Corp.", + "physid": "1", + "businfo": "cpu@0", + "width": 64, + "capabilities": { + "fpu": "mathematical co-processor", + "fpu_exception": "FPU exceptions reporting", + "wp": true, + "mmx": "multimedia extensions (MMX)" + } + } + ] + }, + { + "id": "network:0", + "class": "network", + "claimed": true, + "description": "Ethernet interface", + "physid": "1", + "logicalname": "ovs-tap", + "serial": "1c:90:c0:f9:4e:a1", + "units": "bit/s", + "size": 10000000000, + "configuration": { + "autonegotiation": "off", + "broadcast": "yes", + "driver": "veth", + "driverversion": "1.0", + "duplex": "full", + "link": "yes", + "multicast": "yes", + "port": "twisted pair", + "speed": "10Gbit/s" + }, + "capabilities": { + "ethernet": true, + "physical": "Physical interface" + } + } + ] +} """, "") @@ -861,7 +961,7 @@ class TestGenericHardwareManager(base.IronicAgentTest): @mock.patch.object(utils, 'execute', autospec=True) def test_get_memory_psutil(self, mocked_execute, mocked_psutil): mocked_psutil.return_value.total = 3952 * 1024 * 1024 - mocked_execute.return_value = DMIDECODE_MEMORY_OUTPUT + mocked_execute.return_value = LSHW_JSON_OUTPUT mem = self.hardware.get_memory() self.assertEqual(3952 * 1024 * 1024, mem.total) @@ -870,13 +970,23 @@ class TestGenericHardwareManager(base.IronicAgentTest): @mock.patch('psutil.virtual_memory', autospec=True) @mock.patch.object(utils, 'execute', autospec=True) def test_get_memory_psutil_exception(self, mocked_execute, mocked_psutil): - mocked_execute.return_value = DMIDECODE_MEMORY_OUTPUT + mocked_execute.return_value = LSHW_JSON_OUTPUT mocked_psutil.side_effect = AttributeError() mem = self.hardware.get_memory() self.assertIsNone(mem.total) self.assertEqual(4096, mem.physical_mb) + @mock.patch('psutil.virtual_memory', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_memory_lshw_exception(self, mocked_execute, mocked_psutil): + mocked_execute.side_effect = OSError() + mocked_psutil.return_value.total = 3952 * 1024 * 1024 + mem = self.hardware.get_memory() + + self.assertEqual(3952 * 1024 * 1024, mem.total) + self.assertIsNone(mem.physical_mb) + def test_list_hardware_info(self): self.hardware.list_network_interfaces = mock.Mock() self.hardware.list_network_interfaces.return_value = [ @@ -1625,38 +1735,19 @@ class TestGenericHardwareManager(base.IronicAgentTest): @mock.patch.object(utils, 'execute', autospec=True) def test_get_system_vendor_info(self, mocked_execute): - mocked_execute.return_value = ( - '# dmidecode 2.12\n' - 'SMBIOS 2.6 present.\n' - '\n' - 'Handle 0x0001, DMI type 1, 27 bytes\n' - 'System Information\n' - '\tManufacturer: NEC\n' - '\tProduct Name: Express5800/R120b-2 [N8100-1653]\n' - '\tVersion: FR1.3\n' - '\tSerial Number: 0800113\n' - '\tUUID: 00433468-26A5-DF11-8001-406186F5A681\n' - '\tWake-up Type: Power Switch\n' - '\tSKU Number: Not Specified\n' - '\tFamily: Not Specified\n' - '\n' - 'Handle 0x002E, DMI type 12, 5 bytes\n' - 'System Configuration Options\n' - '\tOption 1: CLR_CMOS: Close to clear CMOS\n' - '\tOption 2: BMC_FRB3: Close to stop FRB3 Timer\n' - '\tOption 3: BIOS_RECOVERY: Close to run BIOS Recovery\n' - '\tOption 4: PASS_DIS: Close to clear Password\n' - '\n' - 'Handle 0x0059, DMI type 32, 11 bytes\n' - 'System Boot Information\n' - '\tStatus: No errors detected\n' - ), '' - self.assertEqual('Express5800/R120b-2 [N8100-1653]', - self.hardware.get_system_vendor_info().product_name) - self.assertEqual('0800113', - self.hardware.get_system_vendor_info().serial_number) - self.assertEqual('NEC', - self.hardware.get_system_vendor_info().manufacturer) + mocked_execute.return_value = LSHW_JSON_OUTPUT + vendor_info = self.hardware.get_system_vendor_info() + self.assertEqual('ABC123 (GENERIC_SERVER)', vendor_info.product_name) + self.assertEqual('1234567', vendor_info.serial_number) + self.assertEqual('GENERIC', vendor_info.manufacturer) + + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_system_vendor_info_failure(self, mocked_execute): + mocked_execute.side_effect = processutils.ProcessExecutionError() + vendor_info = self.hardware.get_system_vendor_info() + self.assertEqual('', vendor_info.product_name) + self.assertEqual('', vendor_info.serial_number) + self.assertEqual('', vendor_info.manufacturer) @mock.patch.object(hardware.GenericHardwareManager, 'get_os_install_device', autospec=True) diff --git a/releasenotes/notes/lshw-for-memory-and-system-info-35c69da067c72b36.yaml b/releasenotes/notes/lshw-for-memory-and-system-info-35c69da067c72b36.yaml new file mode 100644 index 000000000..1649cfdc5 --- /dev/null +++ b/releasenotes/notes/lshw-for-memory-and-system-info-35c69da067c72b36.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + Switched to ``lshw`` for memory configuration and system information collection + when using the default hardware manager. This information can now be retrieved + on both DMI capable and OpenFirmware capable systems. ``dmidecode`` is no longer + used by the default hardware manager. +fixes: + - | + The default hardware manager is now capable of collecting memory configuration + and system information on OpenFirmware (PowerPC) capable systems, in addition + to the already supported DMI (x86 and ARM) capable systems. +upgrade: + - | + ``lshw`` is now a dependency of the default hardware manager.