diff --git a/SOURCES/rteval-Fix-aNone-being-passed-to-cyclictest.patch b/SOURCES/rteval-Fix-aNone-being-passed-to-cyclictest.patch new file mode 100644 index 0000000..263145f --- /dev/null +++ b/SOURCES/rteval-Fix-aNone-being-passed-to-cyclictest.patch @@ -0,0 +1,45 @@ +From 17a39863e5f89fbaf51dc158bc7e27f46676d46d Mon Sep 17 00:00:00 2001 +From: Tomas Glozar +Date: Tue, 11 Jun 2024 16:45:53 +0200 +Subject: [PATCH] rteval: Fix -aNone being passed to cyclictest + +When rteval is called via the command line, cpulists for both +measurements and loads default to an empty string and are further +processed by parse_cpulist_from_config. However, this is not true when +rteval is used as a module: in that case, neither the default +command-line value is used nor parse_cpulist_from_config is run, leading +to None being set to the config property of measurements which is +explicitely passed down to the corresponding cyclictest config property. + +After 64ce7848dfab ("rteval: Add relative cpulists for measurements"), +where the check for None was removed from the cyclictest module, rteval +passes "-aNone" to cyclictest when being used as a module. + +Call parse_cpulist_from_config with an empty string to get the default +cpulist to pass to cyclictest module if cpulist is empty in +the measurement config. + +Fixes: 64ce7848dfab ("rteval: Add relative cpulists for measurements") +Signed-off-by: Tomas Glozar +Signed-off-by: John Kacur +--- + rteval/modules/measurement/__init__.py | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/rteval/modules/measurement/__init__.py b/rteval/modules/measurement/__init__.py +index 11bd7b0fce69..43c0fda30ce1 100644 +--- a/rteval/modules/measurement/__init__.py ++++ b/rteval/modules/measurement/__init__.py +@@ -148,6 +148,9 @@ measurement profiles, based on their characteristics""" + modcfg = self.__cfg.GetSection("measurement") + cpulist = modcfg.cpulist + run_on_isolcpus = modcfg.run_on_isolcpus ++ if cpulist is None: ++ # Get default cpulist value ++ cpulist = cpulist_utils.collapse_cpulist(parse_cpulist_from_config("", run_on_isolcpus)) + + for (modname, modtype) in modcfg: + if isinstance(modtype, str) and modtype.lower() == 'module': # Only 'module' will be supported (ds) +-- +2.45.2 + diff --git a/SOURCES/rteval-Implement-initial-dmidecode-support.patch b/SOURCES/rteval-Implement-initial-dmidecode-support.patch new file mode 100644 index 0000000..1320d37 --- /dev/null +++ b/SOURCES/rteval-Implement-initial-dmidecode-support.patch @@ -0,0 +1,298 @@ +From 43c45ba7dae488c16beb359834b3a71ebf6068d6 Mon Sep 17 00:00:00 2001 +From: Tomas Glozar +Date: Mon, 4 Mar 2024 11:26:03 +0100 +Subject: 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) +-- +cgit 1.2.3-korg diff --git a/SPECS/rteval.spec b/SPECS/rteval.spec index a8bcfbd..5c46938 100644 --- a/SPECS/rteval.spec +++ b/SPECS/rteval.spec @@ -1,6 +1,6 @@ Name: rteval Version: 3.7 -Release: 7%{?dist} +Release: 10%{?dist}.alma.1 Summary: Utility to evaluate system suitability for RT Linux Group: Development/Tools @@ -27,6 +27,8 @@ Requires: stress-ng Requires: perl-interpreter, perl-devel, perl-generators Requires: libmpc, libmpc-devel Requires: dwarves +Requires: dmidecode +Requires: procps-ng BuildArch: noarch #Patches @@ -38,6 +40,11 @@ Patch5: rteval-Refactor-collapse_cpulist-in-systopology.patch Patch6: rteval-Minor-improvements-to-CpuList-class.patch Patch7: rteval-Convert-CpuList-class-to-a-module.patch Patch8: rteval-Add-relative-cpulists-for-measurements.patch +# Patches were taken from: +# https://gitlab.com/redhat/centos-stream/rpms/rteval/-/commit/f0f764c0aecf1770679636294a3232159c5f825e +Patch9: rteval-Fix-aNone-being-passed-to-cyclictest.patch +# https://git.kernel.org/pub/scm/utils/rteval/rteval.git/patch/?id=43c45ba7dae488c16beb359834b3a71ebf6068d6 +Patch10: rteval-Implement-initial-dmidecode-support.patch %description The rteval script is a utility for measuring various aspects of @@ -58,6 +65,8 @@ to the screen. %patch6 -p1 %patch7 -p1 %patch8 -p1 +%patch9 -p1 +%patch10 -p1 %build %{__python3} setup.py build @@ -79,6 +88,10 @@ to the screen. %{_bindir}/rteval %changelog +* Tue Sep 03 2024 Eduard Abdullin - 3.7-10.alma.1 +- rteval: Implement initial dmidecode support +- rteval: Fix -aNone being passed to cyclictest + * Wed Jan 31 2024 Tomas Glozar - 3.7-7 - Added patchset for relative cpuset functionality from upstream Resolves: RHEL-9912