From 43c45ba7dae488c16beb359834b3a71ebf6068d6 Mon Sep 17 00:00:00 2001 From: Tomas Glozar Date: Mon, 4 Mar 2024 11:26:03 +0100 Subject: [PATCH] rteval: Implement initial dmidecode support Previously rteval used python-dmidecode to gather DMI data from a system. Since python-dmidecode is without a maintainer, its support was removed in d142f0d2 ("rteval: Disable use of python-dmidecode"). Add get_dmidecode_xml() function into rteval/sysinfo/dmi.py that does simple parsing of dmidecode command-line tool output without any structure changes and include it into the rteval report. Notes: - ProcessWarnings() in rteval.sysinfo.dmi was reworked into a class method of DMIinfo and to use the class's __log field as logger. It now also does not ignore warnings that appear when running rteval as non-root, since that is no longer supported. Additionally, a duplicate call in rteval-cmd was removed. - rteval/rteval_dmi.xsl XSLT template was left untouched and is currectly not used. In a future commit, it is expected to be rewritten to transform the XML format outputted by get_dmidecode_xml() into the same format that was used with python-dmidecode. Signed-off-by: Tomas Glozar Signed-off-by: John Kacur --- rteval-cmd | 2 - rteval/sysinfo/__init__.py | 2 +- rteval/sysinfo/dmi.py | 178 ++++++++++++++++++++++++------------- 3 files changed, 118 insertions(+), 64 deletions(-) diff --git a/rteval-cmd b/rteval-cmd index a5e8746..018a414 100755 --- a/rteval-cmd +++ b/rteval-cmd @@ -268,8 +268,6 @@ if __name__ == '__main__': | (rtevcfg.debugging and Log.DEBUG) logger.SetLogVerbosity(loglev) - dmi.ProcessWarnings(logger=logger) - # Load modules loadmods = LoadModules(config, logger=logger) measuremods = MeasurementModules(config, logger=logger) diff --git a/rteval/sysinfo/__init__.py b/rteval/sysinfo/__init__.py index d3f9efb..09af52e 100644 --- a/rteval/sysinfo/__init__.py +++ b/rteval/sysinfo/__init__.py @@ -30,7 +30,7 @@ class SystemInfo(KernelInfo, SystemServices, dmi.DMIinfo, CPUtopology, NetworkInfo.__init__(self, logger=logger) # Parse initial DMI decoding errors - dmi.ProcessWarnings(logger=logger) + self.ProcessWarnings() # Parse CPU info CPUtopology._parse(self) diff --git a/rteval/sysinfo/dmi.py b/rteval/sysinfo/dmi.py index c01a0ee..f1aab9f 100644 --- a/rteval/sysinfo/dmi.py +++ b/rteval/sysinfo/dmi.py @@ -3,6 +3,7 @@ # Copyright 2009 - 2013 Clark Williams # Copyright 2009 - 2013 David Sommerseth # Copyright 2022 John Kacur +# Copyright 2024 Tomas Glozar # """ dmi.py class to wrap DMI Table Information """ @@ -10,65 +11,125 @@ import sys import os import libxml2 import lxml.etree +import shutil +import re +from subprocess import Popen, PIPE, SubprocessError from rteval.Log import Log from rteval import xmlout from rteval import rtevalConfig -try: - # import dmidecode - dmidecode_avail = False -except ModuleNotFoundError: - dmidecode_avail = False - -def set_dmidecode_avail(val): - """ Used to set global variable dmidecode_avail from a function """ - global dmidecode_avail - dmidecode_avail = val - -def ProcessWarnings(logger=None): - """ Process Warnings from dmidecode """ - - if not dmidecode_avail: - return - - if not hasattr(dmidecode, 'get_warnings'): - return - - warnings = dmidecode.get_warnings() - if warnings is None: - return - - ignore1 = '/dev/mem: Permission denied' - ignore2 = 'No SMBIOS nor DMI entry point found, sorry.' - ignore3 = 'Failed to open memory buffer (/dev/mem): Permission denied' - ignore = (ignore1, ignore2, ignore3) - for warnline in warnings.split('\n'): - # Ignore these warnings, as they are "valid" if not running as root - if warnline in ignore: - continue - # All other warnings will be printed - if len(warnline) > 0: - logger.log(Log.DEBUG, f"** DMI WARNING ** {warnline}") - set_dmidecode_avail(False) +def get_dmidecode_xml(dmidecode_executable): + """ + Transform human-readable dmidecode output into machine-processable XML format + :param dmidecode_executable: Path to dmidecode tool executable + :return: Tuple of values with resulting XML and dmidecode warnings + """ + proc = Popen(dmidecode_executable, text=True, stdout=PIPE, stderr=PIPE) + outs, errs = proc.communicate() + parts = outs.split("\n\n") + if len(parts) < 2: + raise RuntimeError("Parsing dmidecode output failed") + header = parts[0] + handles = parts[1:] + root = lxml.etree.Element("dmidecode") + # Parse dmidecode output header + # Note: Only supports SMBIOS data currently + regex = re.compile(r"# dmidecode (\d+\.\d+)\n" + r"Getting SMBIOS data from sysfs\.\n" + r"SMBIOS ((?:\d+\.)+\d+) present\.\n" + r"(?:(\d+) structures occupying (\d+) bytes\.\n)?" + r"Table at (0x[0-9A-Fa-f]+)\.", re.MULTILINE) + match = re.match(regex, header) + if match is None: + raise RuntimeError("Parsing dmidecode output failed") + root.attrib["dmidecodeversion"] = match.group(1) + root.attrib["smbiosversion"] = match.group(2) + if match.group(3) is not None: + root.attrib["structures"] = match.group(3) + if match.group(4) is not None: + root.attrib["size"] = match.group(4) + root.attrib["address"] = match.group(5) + + # Generate element per handle in dmidecode output + for handle_text in handles: + if not handle_text: + # Empty line + continue - dmidecode.clear_warnings() + handle = lxml.etree.Element("Handle") + lines = handle_text.splitlines() + # Parse handle header + if len(lines) < 2: + raise RuntimeError("Parsing dmidecode handle failed") + header, name, content = lines[0], lines[1], lines[2:] + match = re.match(r"Handle (0x[0-9A-Fa-f]{4}), " + r"DMI type (\d+), (\d+) bytes", header) + if match is None: + raise RuntimeError("Parsing dmidecode handle failed") + handle.attrib["address"] = match.group(1) + handle.attrib["type"] = match.group(2) + handle.attrib["bytes"] = match.group(3) + handle.attrib["name"] = name + + # Parse all fields in handle and create an element for each + list_field = None + for index, line in enumerate(content): + line = content[index] + if line.rfind("\t") > 0: + # We are inside a list field, add value to it + value = lxml.etree.Element("Value") + value.text = line.strip() + list_field.append(value) + continue + line = line.lstrip().split(":", 1) + if len(line) != 2: + raise RuntimeError("Parsing dmidecode field failed") + if not line[1] or (index + 1 < len(content) and + content[index + 1].rfind("\t") > 0): + # No characters after : or next line is inside list field + # means a list field + # Note: there are list fields which specify a number of + # items, for example "Installable Languages", so merely + # checking for no characters after : is not enough + list_field = lxml.etree.Element("List") + list_field.attrib["Name"] = line[0].strip() + handle.append(list_field) + else: + # Regular field + field = lxml.etree.Element("Field") + field.attrib["Name"] = line[0].strip() + field.text = line[1].strip() + handle.append(field) + + root.append(handle) + + return root, errs class DMIinfo: - '''class used to obtain DMI info via python-dmidecode''' + '''class used to obtain DMI info via dmidecode''' def __init__(self, logger=None): self.__version = '0.6' self._log = logger - if not dmidecode_avail: - logger.log(Log.DEBUG, "DMI info unavailable, ignoring DMI tables") + dmidecode_executable = shutil.which("dmidecode") + if dmidecode_executable is None: + logger.log(Log.DEBUG, "DMI info unavailable," + " ignoring DMI tables") self.__fake = True return self.__fake = False - self.__dmixml = dmidecode.dmidecodeXML() + try: + self.__dmixml, self.__warnings = get_dmidecode_xml( + dmidecode_executable) + except (RuntimeError, OSError, SubprocessError) as error: + logger.log(Log.DEBUG, "DMI info unavailable: {};" + " ignoring DMI tables".format(str(error))) + self.__fake = True + return self.__xsltparser = self.__load_xslt('rteval_dmi.xsl') @@ -88,30 +149,25 @@ class DMIinfo: raise RuntimeError(f'Could not locate XSLT template for DMI data ({fname})') + def ProcessWarnings(self): + """Prints out warnings from dmidecode into log if there were any""" + if self.__fake or self._log is None: + return + for warnline in self.__warnings.split('\n'): + if len(warnline) > 0: + self._log.log(Log.DEBUG, f"** DMI WARNING ** {warnline}") + def MakeReport(self): """ Add DMI information to final report """ - rep_n = libxml2.newNode("DMIinfo") - rep_n.newProp("version", self.__version) if self.__fake: + rep_n = libxml2.newNode("DMIinfo") + rep_n.newProp("version", self.__version) rep_n.addContent("No DMI tables available") rep_n.newProp("not_available", "1") - else: - self.__dmixml.SetResultType(dmidecode.DMIXML_DOC) - try: - dmiqry = xmlout.convert_libxml2_to_lxml_doc(self.__dmixml.QuerySection('all')) - except Exception as ex1: - self._log.log(Log.DEBUG, f'** EXCEPTION {str(ex1)}, will query BIOS only') - try: - # If we can't query 'all', at least query 'bios' - dmiqry = xmlout.convert_libxml2_to_lxml_doc(self.__dmixml.QuerySection('bios')) - except Exception as ex2: - rep_n.addContent("No DMI tables available") - rep_n.newProp("not_available", "1") - self._log.log(Log.DEBUG, f'** EXCEPTION {str(ex2)}, dmi info will not be reported') - return rep_n - resdoc = self.__xsltparser(dmiqry) - dmi_n = xmlout.convert_lxml_to_libxml2_nodes(resdoc.getroot()) - rep_n.addChild(dmi_n) + return rep_n + rep_n = xmlout.convert_lxml_to_libxml2_nodes(self.__dmixml) + rep_n.setName("DMIinfo") + rep_n.newProp("version", self.__version) return rep_n def unit_test(rootdir): @@ -130,12 +186,12 @@ def unit_test(rootdir): log = Log() log.SetLogVerbosity(Log.DEBUG|Log.INFO) - ProcessWarnings(logger=log) if os.getuid() != 0: print("** ERROR ** Must be root to run this unit_test()") return 1 d = DMIinfo(logger=log) + d.ProcessWarnings() dx = d.MakeReport() x = libxml2.newDoc("1.0") x.setRootElement(dx) -- 2.44.0