blob: f1aab9f3e64bd499acf2b1e9dace78408a07fbff [file] [log] [blame]
# SPDX-License-Identifier: GPL-2.0-or-later
#
# 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 """
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
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
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 dmidecode'''
def __init__(self, logger=None):
self.__version = '0.6'
self._log = logger
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
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')
@staticmethod
def __load_xslt(fname):
xsltf = None
if os.path.exists(fname):
xsltf = fname
else:
xsltf = rtevalConfig.default_config_search([fname], os.path.isfile)
if xsltf:
with open(xsltf, "r") as xsltfile:
xsltdoc = lxml.etree.parse(xsltfile)
ret = lxml.etree.XSLT(xsltdoc)
return ret
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 """
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")
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):
""" unit_test for dmi.py """
class UnittestConfigDummy:
def __init__(self, rootdir):
self.config = {'installdir': '/usr/share/rteval'}
self.__update_vars()
def __update_vars(self):
for k in list(self.config.keys()):
self.__dict__[k] = self.config[k]
try:
log = Log()
log.SetLogVerbosity(Log.DEBUG|Log.INFO)
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)
x.saveFormatFileEnc("-", "UTF-8", 1)
return 0
except Exception as e:
print(f"** EXCEPTION: {str(e)}")
return 1
if __name__ == '__main__':
sys.exit(unit_test('.'))