From d5825a51f44a73fa34a5018e8518e0918d96c1ed Mon Sep 17 00:00:00 2001 From: Michal Bajer Date: Sun, 8 Mar 2020 12:55:00 +0100 Subject: [PATCH] Replaced python-dmidecode with reading sysfs dmi and dmidecode tool. Fixes #232 sysfs exports virtual dmi device structs which provides chassis type in numeric form, it should be available in all RHEL6+, so I used it as primary source (not much overhead). If for some reasons it couldn't be read, dmidecode output will be parsed instead. Removed any references to python-dmidecode module. chassis type is gathered only once, and only one intance of ProfileRecommender will be created (in daemon). Signed-off-by: Michal Bajer Fix whitespace and other minor issues in solution for #232 Signed-off-by: Michal Bajer --- INSTALL | 5 -- requirements.txt | 1 - tuned.spec | 4 +- tuned/daemon/controller.py | 3 +- tuned/daemon/daemon.py | 7 ++- tuned/utils/profile_recommender.py | 82 ++++++++++++++++++++++-------- 6 files changed, 71 insertions(+), 31 deletions(-) delete mode 100644 requirements.txt diff --git a/INSTALL b/INSTALL index 9dff6b9..bb5e34a 100644 --- a/INSTALL +++ b/INSTALL @@ -10,9 +10,4 @@ Python files are modified to use Python3. If you want tuned to use Python2 instead, set PYTHON to the full path of Python2. Example: make PYTHON=/usr/bin/python2 install -tuned requires some Python packages in order to run. -Those packages are listed in 'requirements.txt'. - -Use: 'pip3 install -r requirements.txt' (or 'pip install -r requirements.txt' for Python2) to install them. - diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 4a331a1..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -dmidecode \ No newline at end of file diff --git a/tuned.spec b/tuned.spec index 60ed55c..d70ced2 100644 --- a/tuned.spec +++ b/tuned.spec @@ -70,14 +70,14 @@ Requires: %{_py}-schedutils, %{_py}-linux-procfs, %{_py}-perf BuildRequires: python3-dbus, python3-gobject-base Requires: python3-dbus, python3-gobject-base %if 0%{?fedora} > 22 || 0%{?rhel} > 7 -Recommends: python3-dmidecode +Recommends: dmidecode %endif %else # BuildRequires for 'make test' BuildRequires: dbus-python, pygobject3-base Requires: dbus-python, pygobject3-base %if 0%{?fedora} > 22 || 0%{?rhel} > 7 -Recommends: python-dmidecode +Recommends: dmidecode %endif %endif Requires: virt-what, ethtool, gawk, hdparm diff --git a/tuned/daemon/controller.py b/tuned/daemon/controller.py index 60cba34..40db825 100644 --- a/tuned/daemon/controller.py +++ b/tuned/daemon/controller.py @@ -5,7 +5,6 @@ from tuned.exceptions import TunedException import threading import tuned.consts as consts from tuned.utils.commands import commands -from tuned.utils.profile_recommender import ProfileRecommender __all__ = ["Controller"] @@ -257,7 +256,7 @@ class Controller(tuned.exports.interfaces.ExportableInterface): def recommend_profile(self, caller = None): if caller == "": return "" - return ProfileRecommender().recommend(hardcoded = not self._global_config.get_bool(consts.CFG_RECOMMEND_COMMAND, consts.CFG_DEF_RECOMMEND_COMMAND)) + return self._daemon.profile_recommender.recommend() @exports.export("", "b") def verify_profile(self, caller = None): diff --git a/tuned/daemon/daemon.py b/tuned/daemon/daemon.py index e2c5c32..15642df 100644 --- a/tuned/daemon/daemon.py +++ b/tuned/daemon/daemon.py @@ -40,6 +40,7 @@ class Daemon(object): log.info("dynamic tuning is enabled (can be overridden by plugins)") log.info("using update interval of %d second(s) (%d times of the sleep interval)" % (self._sleep_cycles * self._sleep_interval, self._sleep_cycles)) + self._profile_recommender = ProfileRecommender(is_hardcoded = not self._recommend_command) self._unit_manager = unit_manager self._profile_loader = profile_loader self._init_threads() @@ -167,6 +168,10 @@ class Daemon(object): # the profile is not active, then the value is meaningless. return self._post_loaded_profile if self._profile else None + @property + def profile_recommender(self): + return self._profile_recommender + @property def profile_loader(self): return self._profile_loader @@ -263,7 +268,7 @@ class Daemon(object): def _get_recommended_profile(self): log.info("Running in automatic mode, checking what profile is recommended for your configuration.") - profile = ProfileRecommender().recommend(hardcoded = not self._recommend_command) + profile = self._profile_recommender.recommend() log.info("Using '%s' profile" % profile) return profile diff --git a/tuned/utils/profile_recommender.py b/tuned/utils/profile_recommender.py index 8649d93..b5ff7a0 100644 --- a/tuned/utils/profile_recommender.py +++ b/tuned/utils/profile_recommender.py @@ -2,16 +2,9 @@ import os import re import errno import procfs -import platform +import subprocess from configobj import ConfigObj, ConfigObjError -have_dmidecode = False -try: - if (os.geteuid() == 0 and platform.machine() in ["i386", "i486", "i586", "i686", "x86_64"]): - import dmidecode - have_dmidecode = True -except: - pass try: import syspurpose.files have_syspurpose = True @@ -26,13 +19,16 @@ log = tuned.logs.get() class ProfileRecommender: - def __init__(self): + def __init__(self, is_hardcoded = False): + self._is_hardcoded = is_hardcoded self._commands = commands() + self._chassis_type = None - def recommend(self, hardcoded = False): + def recommend(self): profile = consts.DEFAULT_PROFILE - if hardcoded: + if self._is_hardcoded: return profile + has_root = os.geteuid() == 0 if not has_root: log.warning("Profile recommender is running without root privileges. Profiles with virt recommendation condition will be omitted.") @@ -93,17 +89,13 @@ class ProfileRecommender: if len(ps.find_by_regex(re.compile(value))) == 0: match = False elif option == "chassis_type": - if have_dmidecode: - for chassis in dmidecode.chassis().values(): - chassis_type = chassis["data"]["Type"].decode( - "ascii") - if re.match(value, chassis_type, re.IGNORECASE): - break - else: + chassis_type = self._get_chassis_type() + + if chassis_type: + if not re.match(value, chassis_type, re.IGNORECASE): match = False else: - log.debug("Ignoring 'chassis_type' in '%s',\ - dmidecode is not available." % fname) + log.debug("Ignoring 'chassis_type' in '%s', could not read DMI value." % fname) elif option == "syspurpose_role": if have_syspurpose: s = syspurpose.files.SyspurposeStore( @@ -131,3 +123,53 @@ class ProfileRecommender: except (IOError, OSError, ConfigObjError) as e: log.error("error processing '%s', %s" % (fname, e)) return matching_profile + + def _get_chassis_type(self): + if self._chassis_type is not None: + log.debug("returning cached chassis type '%s'" % self._chassis_type) + return self._chassis_type + + # Check DMI sysfs first + # Based on SMBios 3.3.0 specs (https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.3.0.pdf) + DMI_CHASSIS_TYPES = ["", "Other", "Unknown", "Desktop", "Low Profile Desktop", "Pizza Box", "Mini Tower", "Tower", + "Portable", "Laptop", "Notebook", "Hand Held", "Docking Station", "All In One", "Sub Notebook", + "Space-saving", "Lunch Box", "Main Server Chassis", "Expansion Chassis", "Sub Chassis", + "Bus Expansion Chassis", "Peripheral Chassis", "RAID Chassis", "Rack Mount Chassis", "Sealed-case PC", + "Multi-system", "CompactPCI", "AdvancedTCA", "Blade", "Blade Enclosing", "Tablet", + "Convertible", "Detachable", "IoT Gateway", "Embedded PC", "Mini PC", "Stick PC"] + try: + with open('/sys/devices/virtual/dmi/id/chassis_type', 'r') as sysfs_chassis_type: + chassis_type_id = int(sysfs_chassis_type.read()) + + self._chassis_type = DMI_CHASSIS_TYPES[chassis_type_id] + except IndexError: + log.error("Unknown chassis type id read from dmi sysfs: %d" % chassis_type_id) + except (OSError, IOError) as e: + log.warn("error accessing dmi sysfs file: %s" % e) + + if self._chassis_type: + log.debug("chassis type - %s" % self._chassis_type) + return self._chassis_type + + # Fallback - try parsing dmidecode output + try: + p_dmi = subprocess.Popen(['dmidecode', '-s', 'chassis-type'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + close_fds=True) + + (dmi_output, dmi_error) = p_dmi.communicate() + + if p_dmi.returncode: + log.error("dmidecode finished with error (ret %d): '%s'" % (p_dmi.returncode, dmi_error)) + else: + self._chassis_type = dmi_output.strip().decode() + except (OSError, IOError) as e: + log.warn("error executing dmidecode tool : %s" % e) + + if not self._chassis_type: + log.debug("could not determine chassis type.") + self._chassis_type = "" + else: + log.debug("chassis type - %s" % self._chassis_type) + + return self._chassis_type