diff --git a/doc/source/admin/how_it_works.rst b/doc/source/admin/how_it_works.rst index e4d63a2f2..803ac7a83 100644 --- a/doc/source/admin/how_it_works.rst +++ b/doc/source/admin/how_it_works.rst @@ -198,7 +198,9 @@ fields: ``system_vendor`` system vendor information from SMBIOS as reported by ``dmidecode``: - ``product_name``, ``serial_number`` and ``manufacturer``. + ``product_name``, ``serial_number`` and ``manufacturer``, as well as + a ``firmware`` structure with fields ``vendor``, ``version`` and + ``build_date``. ``boot`` boot information with fields: ``current_boot_mode`` (boot mode used for diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py index e3d408e04..8dd3a9612 100644 --- a/ironic_python_agent/hardware.py +++ b/ironic_python_agent/hardware.py @@ -339,22 +339,16 @@ def get_component_devices(raid_device): def _calc_memory(sys_dict): physical = 0 - for sys_child in sys_dict['children']: - if sys_child['id'] != 'core': - continue - for core_child in sys_child['children']: - if not _MEMORY_ID_RE.match(core_child['id']): - continue - if core_child.get('size'): - value = ("%(size)s %(units)s" % core_child) - physical += int(UNIT_CONVERTER(value).to - ('MB').magnitude) - else: - for bank in core_child.get('children', ()): - if bank.get('size'): - value = ("%(size)s %(units)s" % bank) - physical += int(UNIT_CONVERTER(value).to - ('MB').magnitude) + core_dict = next(utils.find_in_lshw(sys_dict, 'core'), {}) + for core_child in utils.find_in_lshw(core_dict, _MEMORY_ID_RE): + if core_child.get('size'): + value = ("%(size)s %(units)s" % core_child) + physical += int(UNIT_CONVERTER(value).to('MB').magnitude) + else: + for bank in core_child.get('children', ()): + if bank.get('size'): + value = ("%(size)s %(units)s" % bank) + physical += int(UNIT_CONVERTER(value).to('MB').magnitude) return physical @@ -835,13 +829,24 @@ class Memory(encoding.SerializableComparable): self.physical_mb = physical_mb -class SystemVendorInfo(encoding.SerializableComparable): - serializable_fields = ('product_name', 'serial_number', 'manufacturer') +class SystemFirmware(encoding.SerializableComparable): + serializable_fields = ('vendor', 'version', 'build_date') - def __init__(self, product_name, serial_number, manufacturer): + def __init__(self, vendor, version, build_date): + self.version = version + self.build_date = build_date + self.vendor = vendor + + +class SystemVendorInfo(encoding.SerializableComparable): + serializable_fields = ('product_name', 'serial_number', 'manufacturer', + 'firmware') + + def __init__(self, product_name, serial_number, manufacturer, firmware): self.product_name = product_name self.serial_number = serial_number self.manufacturer = manufacturer + self.firmware = firmware class BootInfo(encoding.SerializableComparable): @@ -1512,9 +1517,17 @@ class GenericHardwareManager(HardwareManager): except (processutils.ProcessExecutionError, OSError, ValueError) as e: LOG.warning('Could not retrieve vendor info from lshw: %s', e) sys_dict = {} + + core_dict = next(utils.find_in_lshw(sys_dict, 'core'), {}) + fw_dict = next(utils.find_in_lshw(core_dict, 'firmware'), {}) + + firmware = SystemFirmware(vendor=fw_dict.get('vendor', ''), + version=fw_dict.get('version', ''), + build_date=fw_dict.get('date', '')) return SystemVendorInfo(product_name=sys_dict.get('product', ''), serial_number=sys_dict.get('serial', ''), - manufacturer=sys_dict.get('vendor', '')) + manufacturer=sys_dict.get('vendor', ''), + firmware=firmware) 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/samples/hardware_samples.py b/ironic_python_agent/tests/unit/samples/hardware_samples.py index c00d637b0..88378a18d 100644 --- a/ironic_python_agent/tests/unit/samples/hardware_samples.py +++ b/ironic_python_agent/tests/unit/samples/hardware_samples.py @@ -490,6 +490,34 @@ LSHW_JSON_OUTPUT_V2 = (""" "serial" : "1234", "slot" : "NULL", "children" : [ + { + "id": "firmware", + "class": "memory", + "claimed": true, + "description": "BIOS", + "vendor": "BIOSVNDR", + "physid": "0", + "version": "1.2.3", + "date": "03/30/2023", + "units": "bytes", + "size": 65536, + "capacity": 16777216, + "capabilities": { + "isa": "ISA bus", + "pci": "PCI bus", + "pnp": "Plug-and-Play", + "upgrade": "BIOS EEPROM can be upgraded", + "shadowing": "BIOS shadowing", + "cdboot": "Booting from CD-ROM/DVD", + "bootselect": "Selectable boot path", + "edd": "Enhanced Disk Drive extensions", + "acpi": "ACPI", + "usb": "USB legacy emulation", + "biosbootspecification": "BIOS boot specification", + "netboot": "Function-key initiated network service boot", + "uefi": "UEFI specification is supported" + } + }, { "id" : "memory:0", "class" : "memory", diff --git a/ironic_python_agent/tests/unit/test_hardware.py b/ironic_python_agent/tests/unit/test_hardware.py index fdb5c8dfd..d0122f7aa 100644 --- a/ironic_python_agent/tests/unit/test_hardware.py +++ b/ironic_python_agent/tests/unit/test_hardware.py @@ -5247,6 +5247,10 @@ class TestGenericHardwareManager(base.IronicAgentTest): self.assertEqual('ABC123 (GENERIC_SERVER)', vendor_info.product_name) self.assertEqual('1234567', vendor_info.serial_number) self.assertEqual('GENERIC', vendor_info.manufacturer) + # This sample does not have firmware information + self.assertEqual('', vendor_info.firmware.vendor) + self.assertEqual('', vendor_info.firmware.build_date) + self.assertEqual('', vendor_info.firmware.version) @mock.patch.object(il_utils, 'execute', autospec=True) def test_get_system_vendor_info_lshw_list(self, mocked_execute): @@ -5255,6 +5259,9 @@ class TestGenericHardwareManager(base.IronicAgentTest): self.assertEqual('ABCD', vendor_info.product_name) self.assertEqual('1234', vendor_info.serial_number) self.assertEqual('ABCD', vendor_info.manufacturer) + self.assertEqual('BIOSVNDR', vendor_info.firmware.vendor) + self.assertEqual('03/30/2023', vendor_info.firmware.build_date) + self.assertEqual('1.2.3', vendor_info.firmware.version) @mock.patch.object(il_utils, 'execute', autospec=True) def test_get_system_vendor_info_failure(self, mocked_execute): @@ -5263,6 +5270,9 @@ class TestGenericHardwareManager(base.IronicAgentTest): self.assertEqual('', vendor_info.product_name) self.assertEqual('', vendor_info.serial_number) self.assertEqual('', vendor_info.manufacturer) + self.assertEqual('', vendor_info.firmware.vendor) + self.assertEqual('', vendor_info.firmware.build_date) + self.assertEqual('', vendor_info.firmware.version) @mock.patch.object(utils, 'get_agent_params', lambda: {'BOOTIF': 'boot:if'}) diff --git a/ironic_python_agent/utils.py b/ironic_python_agent/utils.py index b7b98c031..c490b1af6 100644 --- a/ironic_python_agent/utils.py +++ b/ironic_python_agent/utils.py @@ -917,3 +917,15 @@ def rescan_device(device): except processutils.ProcessExecutionError as e: LOG.warning('Something went wrong when waiting for udev ' 'to settle. Error: %s', e) + + +def find_in_lshw(lshw, by_id): + """Yield all suitable records from lshw.""" + for child in lshw.get('children', ()): + lshw_id = child.get('id', '') + if isinstance(by_id, re.Pattern): + if by_id.match(lshw_id) is not None: + yield child + else: + if by_id == lshw_id: + yield child diff --git a/releasenotes/notes/bmo-extra-147559c8d1776e8c.yaml b/releasenotes/notes/bmo-extra-147559c8d1776e8c.yaml new file mode 100644 index 000000000..009745460 --- /dev/null +++ b/releasenotes/notes/bmo-extra-147559c8d1776e8c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The hardware inventory now contains information about the system firmware: + vendor, version and the build date.