From: John Kacur <jkacur@redhat.com>
To: Tomas Glozar <tglozar@redhat.com>
Cc: linux-rt-users@vger.kernel.org
Subject: Re: [PATCH] rteval: Implement initial dmidecode support
Date: Wed, 20 Mar 2024 11:58:41 -0400 (EDT) [thread overview]
Message-ID: <fd5b0cd6-ab4c-88d7-ac7d-4450d06b0e44@redhat.com> (raw)
In-Reply-To: <20240304102603.89558-1-tglozar@redhat.com>
On Mon, 4 Mar 2024, tglozar@redhat.com wrote:
> From: Tomas Glozar <tglozar@redhat.com>
>
> 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 <tglozar@redhat.com>
> ---
> 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 <williams@redhat.com>
> # Copyright 2009 - 2013 David Sommerseth <davids@redhat.com>
> # Copyright 2022 John Kacur <jkacur@redhat.com>
> +# Copyright 2024 Tomas Glozar <tglozar@redhat.com>
> #
> """ 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)
> --
Signed-off-by: John Kacur <jkacur@redhat.com>
prev parent reply other threads:[~2024-03-20 15:58 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-03-04 10:26 [PATCH] rteval: Implement initial dmidecode support tglozar
2024-03-20 15:58 ` John Kacur [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=fd5b0cd6-ab4c-88d7-ac7d-4450d06b0e44@redhat.com \
--to=jkacur@redhat.com \
--cc=linux-rt-users@vger.kernel.org \
--cc=tglozar@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).