From b3dc98ba8c975696f61546fc3a62c9e9f0437e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20=C5=A0karvada?= Date: Thu, 19 Aug 2021 00:45:15 +0200 Subject: [PATCH] scheduler: allow exclude of processes from the specific cgroup(s) Resolves: rhbz#1980715 Switched to the configparser from the configobj Resolves: rhbz#1936386 --- tuned-2.16.0-configobj-drop.patch | 829 +++++++++++++++++++ tuned-2.16.0-scheduler-cgroups-exclude.patch | 121 +++ tuned.spec | 15 +- 3 files changed, 962 insertions(+), 3 deletions(-) create mode 100644 tuned-2.16.0-configobj-drop.patch create mode 100644 tuned-2.16.0-scheduler-cgroups-exclude.patch diff --git a/tuned-2.16.0-configobj-drop.patch b/tuned-2.16.0-configobj-drop.patch new file mode 100644 index 0000000..b4bccfb --- /dev/null +++ b/tuned-2.16.0-configobj-drop.patch @@ -0,0 +1,829 @@ +From 063277a05b3a174f9265d36032ca097ee5b7cc9c Mon Sep 17 00:00:00 2001 +From: Jan Zerdik +Date: Fri, 30 Jul 2021 11:48:59 +0200 +Subject: [PATCH] Removing dependency on python-configobj. + +Resolves: rhbz#1936386 + +Signed-off-by: Jan Zerdik +--- + recommend.conf | 2 +- + tests/unit/profiles/test_loader.py | 7 +++ + tests/unit/profiles/test_locator.py | 18 ++++++- + tests/unit/profiles/test_variables.py | 32 ++++++++++++ + tests/unit/utils/test_global_config.py | 13 ++++- + tuned-gui.py | 5 +- + tuned.spec | 3 +- + tuned/consts.py | 21 ++++++++ + tuned/gtk/gui_plugin_loader.py | 43 +++++++++------- + tuned/gtk/gui_profile_loader.py | 71 ++++++++++++++++++-------- + tuned/gtk/gui_profile_saver.py | 28 ++++++---- + tuned/profiles/loader.py | 38 ++++++-------- + tuned/profiles/locator.py | 25 ++++++--- + tuned/profiles/variables.py | 27 +++++----- + tuned/utils/global_config.py | 55 +++++++++++++++----- + tuned/utils/profile_recommender.py | 19 ++++--- + 16 files changed, 288 insertions(+), 119 deletions(-) + create mode 100644 tests/unit/profiles/test_variables.py + +diff --git a/recommend.conf b/recommend.conf +index f3442ca8..7561696c 100644 +--- a/recommend.conf ++++ b/recommend.conf +@@ -29,7 +29,7 @@ + # Limitation: + # Each profile can be specified only once, because there cannot be + # multiple sections in the configuration file with the same name +-# (ConfigObj limitation). ++# (ConfigParser limitation). + # If there is a need to specify the profile multiple times, unique + # suffix like ',ANYSTRING' can be used. Everything after the last ',' + # is stripped by the parser, e.g.: +diff --git a/tests/unit/profiles/test_loader.py b/tests/unit/profiles/test_loader.py +index b6ea76e9..149353d8 100644 +--- a/tests/unit/profiles/test_loader.py ++++ b/tests/unit/profiles/test_loader.py +@@ -46,6 +46,8 @@ def setUpClass(cls): + f.write('file_path=${i:PROFILE_DIR}/whatever\n') + f.write('script=random_name.sh\n') + f.write('[test_unit]\ntest_option=hello world\n') ++ f.write('devices=/dev/${variable1},/dev/${variable2}\n') ++ f.write('[variables]\nvariable1=net\nvariable2=cpu') + + def setUp(self): + locator = profiles.Locator([self._profiles_dir]) +@@ -105,6 +107,11 @@ def test_load_config_data(self): + self.assertEqual(config['test_unit']['test_option'],\ + 'hello world') + ++ def test_variables(self): ++ config = self._loader.load(['dummy4']) ++ self.assertEqual(config.units['test_unit'].devices,\ ++ '/dev/net,/dev/cpu') ++ + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls._test_dir) +diff --git a/tests/unit/profiles/test_locator.py b/tests/unit/profiles/test_locator.py +index cce88daa..bf2906d7 100644 +--- a/tests/unit/profiles/test_locator.py ++++ b/tests/unit/profiles/test_locator.py +@@ -30,7 +30,10 @@ def _create_profile(cls, load_dir, profile_name): + conf_name = os.path.join(profile_dir, "tuned.conf") + os.mkdir(profile_dir) + with open(conf_name, "w") as conf_file: +- pass ++ if profile_name != "custom": ++ conf_file.write("[main]\nsummary=this is " + profile_name + "\n") ++ else: ++ conf_file.write("summary=this is " + profile_name + "\n") + + def test_init(self): + Locator([]) +@@ -65,3 +68,16 @@ def test_ignore_nonexistent_dirs(self): + self.assertEqual(balanced, os.path.join(self._tmp_load_dirs[0], "balanced", "tuned.conf")) + known = locator.get_known_names() + self.assertListEqual(known, ["balanced", "powersafe"]) ++ ++ def test_get_known_names_summary(self): ++ self.assertEqual(("balanced", "this is balanced"), sorted(self.locator.get_known_names_summary())[0]) ++ ++ def test_get_profile_attrs(self): ++ attrs = self.locator.get_profile_attrs("balanced", ["summary", "wrong_attr"], ["this is default", "this is wrong attr"]) ++ self.assertEqual([True, "balanced", "this is balanced", "this is wrong attr"], attrs) ++ ++ attrs = self.locator.get_profile_attrs("custom", ["summary"], ["wrongly writen profile"]) ++ self.assertEqual([True, "custom", "wrongly writen profile"], attrs) ++ ++ attrs = self.locator.get_profile_attrs("different", ["summary"], ["non existing profile"]) ++ self.assertEqual([False, "", "", ""], attrs) +diff --git a/tests/unit/profiles/test_variables.py b/tests/unit/profiles/test_variables.py +new file mode 100644 +index 00000000..47fff2c1 +--- /dev/null ++++ b/tests/unit/profiles/test_variables.py +@@ -0,0 +1,32 @@ ++import unittest ++import tempfile ++import shutil ++from tuned.profiles import variables, profile ++ ++class VariablesTestCase(unittest.TestCase): ++ ++ @classmethod ++ def setUpClass(cls): ++ cls.test_dir = tempfile.mkdtemp() ++ ++ with open(cls.test_dir + "/variables", 'w') as f: ++ f.write("variable1=var1\n") ++ ++ def test_from_file(self): ++ v = variables.Variables() ++ v.add_from_file(self.test_dir + "/variables") ++ self.assertEqual("This is var1", v.expand("This is ${variable1}")) ++ ++ def test_from_unit(self): ++ mock_unit = { ++ "include": self.test_dir + "/variables", ++ "variable2": "var2" ++ } ++ v = variables.Variables() ++ v.add_from_cfg(mock_unit) ++ ++ self.assertEqual("This is var1 and this is var2", v.expand("This is ${variable1} and this is ${variable2}")) ++ ++ @classmethod ++ def tearDownClass(cls): ++ shutil.rmtree(cls.test_dir) +diff --git a/tests/unit/utils/test_global_config.py b/tests/unit/utils/test_global_config.py +index 5b93888c..8981d544 100644 +--- a/tests/unit/utils/test_global_config.py ++++ b/tests/unit/utils/test_global_config.py +@@ -12,7 +12,8 @@ def setUpClass(cls): + cls.test_dir = tempfile.mkdtemp() + with open(cls.test_dir + '/test_config','w') as f: + f.write('test_option = hello\ntest_bool = 1\ntest_size = 12MB\n'\ +- + 'false_bool=0\n') ++ + 'false_bool=0\n'\ ++ + consts.CFG_LOG_FILE_COUNT + " = " + str(consts.CFG_DEF_LOG_FILE_COUNT) + "1\n") + + cls._global_config = global_config.GlobalConfig(\ + cls.test_dir + '/test_config') +@@ -28,10 +29,18 @@ def test_get_size(self): + self.assertEqual(self._global_config.get_size('test_size'),\ + 12*1024*1024) + +- self._global_config.set('test_size','bad_value') ++ self._global_config.set('test_size', 'bad_value') + + self.assertIsNone(self._global_config.get_size('test_size')) + ++ def test_default(self): ++ daemon = self._global_config.get(consts.CFG_DAEMON) ++ self.assertEqual(daemon, consts.CFG_DEF_DAEMON) ++ ++ log_file_count = self._global_config.get(consts.CFG_LOG_FILE_COUNT) ++ self.assertIsNotNone(log_file_count) ++ self.assertNotEqual(log_file_count, consts.CFG_DEF_LOG_FILE_COUNT) ++ + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.test_dir) +diff --git a/tuned-gui.py b/tuned-gui.py +index a2792792..3953f82f 100755 +--- a/tuned-gui.py ++++ b/tuned-gui.py +@@ -48,7 +48,7 @@ + import sys + import os + import time +-import configobj ++import collections + import subprocess + + import tuned.logs +@@ -508,8 +508,7 @@ def on_click_button_confirm_profile_update(self, data): + + def data_to_profile_config(self): + name = self._gobj('entryProfileName').get_text() +- config = configobj.ConfigObj(list_values = False, +- interpolation = False) ++ config = collections.OrderedDict() + + activated = self._gobj('comboboxIncludeProfile').get_active() + model = self._gobj('comboboxIncludeProfile').get_model() +diff --git a/tuned.spec b/tuned.spec +index e3a494fd..7afe1935 100644 +--- a/tuned.spec ++++ b/tuned.spec +@@ -66,9 +66,8 @@ BuildRequires: %{_py}, %{_py}-devel + %if %{without python3} && ( ! 0%{?rhel} || 0%{?rhel} >= 8 ) + BuildRequires: %{_py}-mock + %endif +-BuildRequires: %{_py}-configobj + BuildRequires: %{_py}-pyudev +-Requires: %{_py}-pyudev, %{_py}-configobj ++Requires: %{_py}-pyudev + Requires: %{_py}-linux-procfs, %{_py}-perf + %if %{without python3} + Requires: %{_py}-schedutils +diff --git a/tuned/consts.py b/tuned/consts.py +index 58cbf4a3..8eb075ba 100644 +--- a/tuned/consts.py ++++ b/tuned/consts.py +@@ -16,6 +16,8 @@ + LOAD_DIRECTORIES = ["/usr/lib/tuned", "/etc/tuned"] + PERSISTENT_STORAGE_DIR = "/var/lib/tuned" + PLUGIN_MAIN_UNIT_NAME = "main" ++# Magic section header because ConfigParser does not support "headerless" config ++MAGIC_HEADER_NAME = "this_is_some_magic_section_header_because_of_compatibility" + RECOMMEND_DIRECTORIES = ["/usr/lib/tuned/recommend.d", "/etc/tuned/recommend.d"] + + TMP_FILE_SUFFIX = ".tmp" +@@ -79,6 +81,10 @@ + PREFIX_PROFILE_FACTORY = "System" + PREFIX_PROFILE_USER = "User" + ++# After adding new option to tuned-main.conf add here its name with CFG_ prefix ++# and eventually default value with CFG_DEF_ prefix (default is None) ++# and function for check with CFG_FUNC_ prefix ++# (see configobj for methods, default is get for string) + CFG_DAEMON = "daemon" + CFG_DYNAMIC_TUNING = "dynamic_tuning" + CFG_SLEEP_INTERVAL = "sleep_interval" +@@ -87,25 +93,40 @@ + CFG_REAPPLY_SYSCTL = "reapply_sysctl" + CFG_DEFAULT_INSTANCE_PRIORITY = "default_instance_priority" + CFG_UDEV_BUFFER_SIZE = "udev_buffer_size" ++CFG_LOG_FILE_COUNT = "log_file_count" ++CFG_LOG_FILE_MAX_SIZE = "log_file_max_size" + CFG_UNAME_STRING = "uname_string" + CFG_CPUINFO_STRING = "cpuinfo_string" + + # no_daemon mode + CFG_DEF_DAEMON = True ++CFG_FUNC_DAEMON = "getboolean" + # default configuration + CFG_DEF_DYNAMIC_TUNING = True ++CFG_FUNC_DYNAMIC_TUNING = "getboolean" + # how long to sleep before checking for events (in seconds) + CFG_DEF_SLEEP_INTERVAL = 1 ++CFG_FUNC_SLEEP_INTERVAL = "getint" + # update interval for dynamic tuning (in seconds) + CFG_DEF_UPDATE_INTERVAL = 10 ++CFG_FUNC_UPDATE_INTERVAL = "getint" + # recommend command availability + CFG_DEF_RECOMMEND_COMMAND = True ++CFG_FUNC_RECOMMEND_COMMAND = "getboolean" + # reapply system sysctl + CFG_DEF_REAPPLY_SYSCTL = True ++CFG_FUNC_REAPPLY_SYSCTL = "getboolean" + # default instance priority + CFG_DEF_DEFAULT_INSTANCE_PRIORITY = 0 ++CFG_FUNC_DEFAULT_INSTANCE_PRIORITY = "getint" + # default pyudev.Monitor buffer size + CFG_DEF_UDEV_BUFFER_SIZE = 1024 * 1024 ++# default log file count ++CFG_DEF_LOG_FILE_COUNT = 2 ++CFG_FUNC_LOG_FILE_COUNT = "getint" ++# default log file max size ++CFG_DEF_LOG_FILE_MAX_SIZE = 1024 * 1024 ++ + + PATH_CPU_DMA_LATENCY = "/dev/cpu_dma_latency" + +diff --git a/tuned/gtk/gui_plugin_loader.py b/tuned/gtk/gui_plugin_loader.py +index d364602d..f943a220 100644 +--- a/tuned/gtk/gui_plugin_loader.py ++++ b/tuned/gtk/gui_plugin_loader.py +@@ -25,25 +25,23 @@ + ''' + + import importlib +-from validate import Validator + + import tuned.consts as consts + import tuned.logs +- +-import configobj as ConfigObj ++try: ++ from configparser import ConfigParser, Error ++ from io import StringIO ++except ImportError: ++ # python2.7 support, remove RHEL-7 support end ++ from ConfigParser import ConfigParser, Error ++ from StringIO import StringIO + from tuned.exceptions import TunedException ++from tuned.utils.global_config import GlobalConfig + + from tuned.admin.dbus_controller import DBusController + + __all__ = ['GuiPluginLoader'] + +-global_config_spec = ['dynamic_tuning = boolean(default=%s)' +- % consts.CFG_DEF_DYNAMIC_TUNING, +- 'sleep_interval = integer(default=%s)' +- % consts.CFG_DEF_SLEEP_INTERVAL, +- 'update_interval = integer(default=%s)' +- % consts.CFG_DEF_UPDATE_INTERVAL] +- + + class GuiPluginLoader(): + +@@ -84,19 +82,26 @@ def _load_global_config(self, file_name=consts.GLOBAL_CONFIG_FILE): + """ + + try: +- config = ConfigObj.ConfigObj(file_name, +- configspec=global_config_spec, +- raise_errors = True, file_error = True, list_values = False, interpolation = False) ++ config_parser = ConfigParser() ++ config_parser.optionxform = str ++ with open(file_name) as f: ++ config_parser.readfp(StringIO("[" + consts.MAGIC_HEADER_NAME + "]\n" + f.read())) ++ config, functions = GlobalConfig.get_global_config_spec() ++ for option in config_parser.options(consts.MAGIC_HEADER_NAME): ++ if option in config: ++ try: ++ func = getattr(config_parser, functions[option]) ++ config[option] = func(consts.MAGIC_HEADER_NAME, option) ++ except Error: ++ raise TunedException("Global TuneD configuration file '%s' is not valid." ++ % file_name) ++ else: ++ config[option] = config_parser.get(consts.MAGIC_HEADER_NAME, option, raw=True) + except IOError as e: + raise TunedException("Global TuneD configuration file '%s' not found." + % file_name) +- except ConfigObj.ConfigObjError as e: ++ except Error as e: + raise TunedException("Error parsing global TuneD configuration file '%s'." + % file_name) +- vdt = Validator() +- if not config.validate(vdt, copy=True): +- raise TunedException("Global TuneD configuration file '%s' is not valid." +- % file_name) + return config + +- +diff --git a/tuned/gtk/gui_profile_loader.py b/tuned/gtk/gui_profile_loader.py +index c50dd9ff..dcd16b72 100644 +--- a/tuned/gtk/gui_profile_loader.py ++++ b/tuned/gtk/gui_profile_loader.py +@@ -25,10 +25,17 @@ + ''' + + import os +-import configobj ++try: ++ from configparser import ConfigParser, Error ++ from io import StringIO ++except ImportError: ++ # python2.7 support, remove RHEL-7 support end ++ from ConfigParser import ConfigParser, Error ++ from StringIO import StringIO + import subprocess + import json + import sys ++import collections + + import tuned.profiles.profile as p + import tuned.consts +@@ -59,14 +66,21 @@ def set_raw_profile(self, profile_name, config): + + profilePath = self._locate_profile_path(profile_name) + +- config_lines = config.split('\n') +- + if profilePath == tuned.consts.LOAD_DIRECTORIES[1]: + file_path = profilePath + '/' + profile_name + '/' + tuned.consts.PROFILE_FILE +- +- config_obj = configobj.ConfigObj(infile=config_lines,list_values = False, interpolation = False) +- config_obj.filename = file_path +- config_obj.initial_comment = ('#', 'tuned configuration', '#') ++ config_parser = ConfigParser() ++ config_parser.optionxform = str ++ config_parser.readfp(StringIO(config)) ++ ++ config_obj = { ++ 'main': collections.OrderedDict(), ++ 'filename': file_path, ++ 'initial_comment': ('#', 'tuned configuration', '#') ++ } ++ for s in config_parser.sections(): ++ config_obj['main'][s] = collections.OrderedDict() ++ for o in config_parser.options(s): ++ config_obj['main'][s][o] = config_parser.get(s, o, raw=True) + self._save_profile(config_obj) + self._refresh_profiles() + else: +@@ -76,8 +90,15 @@ def set_raw_profile(self, profile_name, config): + + def load_profile_config(self, profile_name, path): + conf_path = path + '/' + profile_name + '/' + tuned.consts.PROFILE_FILE +- profile_config = configobj.ConfigObj(conf_path, list_values = False, +- interpolation = False) ++ config = ConfigParser() ++ config.optionxform = str ++ profile_config = collections.OrderedDict() ++ with open(conf_path) as f: ++ config.readfp(f) ++ for s in config.sections(): ++ profile_config[s] = collections.OrderedDict() ++ for o in config.options(s): ++ profile_config[s][o] = config.get(s, o, raw=True) + return profile_config + + def _locate_profile_path(self, profile_name): +@@ -95,11 +116,11 @@ def _load_all_profiles(self): + try: + self.profiles[profile] = p.Profile(profile, + self.load_profile_config(profile, d)) +- except configobj.ParseError: ++ except Error: + pass + + # print "can not make \""+ profile +"\" profile without correct config on path: " + d +-# except: ++# except:StringIO + # raise managerException.ManagerException("Can not make profile") + # print "can not make \""+ profile +"\" profile without correct config with path: " + d + +@@ -113,20 +134,24 @@ def _refresh_profiles(self): + + def save_profile(self, profile): + path = tuned.consts.LOAD_DIRECTORIES[1] + '/' + profile.name +- config = configobj.ConfigObj(list_values = False, interpolation = False) +- config.filename = path + '/' + tuned.consts.PROFILE_FILE +- config.initial_comment = ('#', 'tuned configuration', '#') ++ config = { ++ 'main': collections.OrderedDict(), ++ 'filename': path + '/' + tuned.consts.PROFILE_FILE, ++ 'initial_comment': ('#', 'tuned configuration', '#') ++ } ++ config['filename'] = path + '/' + tuned.consts.PROFILE_FILE ++ config['initial_comment'] = ('#', 'tuned configuration', '#') + + try: +- config['main'] = profile.options ++ config['main']['main'] = profile.options + except KeyError: +- config['main'] = '' ++ config['main']['main'] = {} + + # profile dont have main section + + pass + for (name, unit) in list(profile.units.items()): +- config[name] = unit.options ++ config['main'][name] = unit.options + + self._save_profile(config) + +@@ -148,18 +173,20 @@ def update_profile( + if old_profile_name != profile.name: + self.remove_profile(old_profile_name, is_admin=is_admin) + +- config = configobj.ConfigObj(list_values = False, interpolation = False) +- config.filename = path + '/' + tuned.consts.PROFILE_FILE +- config.initial_comment = ('#', 'tuned configuration', '#') ++ config = { ++ 'main': collections.OrderedDict(), ++ 'filename': path + '/' + tuned.consts.PROFILE_FILE, ++ 'initial_comment': ('#', 'tuned configuration', '#') ++ } + try: +- config['main'] = profile.options ++ config['main']['main'] = profile.options + except KeyError: + + # profile dont have main section + + pass + for (name, unit) in list(profile.units.items()): +- config[name] = unit.options ++ config['main'][name] = unit.options + + self._save_profile(config) + +diff --git a/tuned/gtk/gui_profile_saver.py b/tuned/gtk/gui_profile_saver.py +index b339cba1..24b0fe3a 100644 +--- a/tuned/gtk/gui_profile_saver.py ++++ b/tuned/gtk/gui_profile_saver.py +@@ -1,7 +1,11 @@ + import os + import sys + import json +-from configobj import ConfigObj ++try: ++ from configparser import ConfigParser ++except ImportError: ++ # python2.7 support, remove RHEL-7 support end ++ from ConfigParser import ConfigParser + + + if __name__ == "__main__": +@@ -11,13 +15,19 @@ + if not os.path.exists(profile_dict['filename']): + os.makedirs(os.path.dirname(profile_dict['filename'])) + +- profile_configobj = ConfigObj() +- for section in profile_dict['sections']: +- profile_configobj[section] = profile_dict['main'][section] +- +- profile_configobj.filename = os.path.join('/etc','tuned',os.path.dirname(os.path.abspath(profile_dict['filename'])),'tuned.conf') +- profile_configobj.initial_comment = profile_dict['initial_comment'] +- +- profile_configobj.write() ++ profile_configobj = ConfigParser() ++ profile_configobj.optionxform = str ++ for section, options in profile_dict['main'].items(): ++ profile_configobj.add_section(section) ++ for option, value in options.items(): ++ profile_configobj.set(section, option, value) ++ ++ path = os.path.join('/etc','tuned',os.path.dirname(os.path.abspath(profile_dict['filename'])),'tuned.conf') ++ with open(path, 'w') as f: ++ profile_configobj.write(f) ++ with open(path, 'r+') as f: ++ content = f.read() ++ f.seek(0, 0) ++ f.write("\n".join(profile_dict['initial_comment']) + "\n" + content) + + sys.exit(0) +diff --git a/tuned/profiles/loader.py b/tuned/profiles/loader.py +index 7f132b4f..31037182 100644 +--- a/tuned/profiles/loader.py ++++ b/tuned/profiles/loader.py +@@ -1,6 +1,10 @@ + import tuned.profiles.profile + import tuned.profiles.variables +-from configobj import ConfigObj, ConfigObjError ++try: ++ from configparser import ConfigParser, Error ++except ImportError: ++ # python2.7 support, remove RHEL-7 support end ++ from ConfigParser import ConfigParser, Error + import tuned.consts as consts + import os.path + import collections +@@ -96,30 +100,22 @@ def _expand_profile_dir(self, profile_dir, string): + + def _load_config_data(self, file_name): + try: +- config_obj = ConfigObj(file_name, raise_errors = True, list_values = False, interpolation = False) +- except ConfigObjError as e: ++ config_obj = ConfigParser() ++ config_obj.optionxform=str ++ with open(file_name) as f: ++ config_obj.readfp(f) ++ except Error as e: + raise InvalidProfileException("Cannot parse '%s'." % file_name, e) + + config = collections.OrderedDict() +- for section in list(config_obj.keys()): +- config[section] = collections.OrderedDict() +- try: +- keys = list(config_obj[section].keys()) +- except AttributeError: +- raise InvalidProfileException("Error parsing section '%s' in file '%s'." % (section, file_name)) +- for option in keys: +- config[section][option] = config_obj[section][option] +- + dir_name = os.path.dirname(file_name) +- # TODO: Could we do this in the same place as the expansion of other functions? +- for section in config: +- for option in config[section]: ++ for section in list(config_obj.sections()): ++ config[section] = collections.OrderedDict() ++ for option in config_obj.options(section): ++ config[section][option] = config_obj.get(section, option, raw=True) + config[section][option] = self._expand_profile_dir(dir_name, config[section][option]) +- +- # TODO: HACK, this needs to be solved in a better way (better config parser) +- for unit_name in config: +- if "script" in config[unit_name] and config[unit_name].get("script", None) is not None: +- script_path = os.path.join(dir_name, config[unit_name]["script"]) +- config[unit_name]["script"] = [os.path.normpath(script_path)] ++ if config[section].get("script") is not None: ++ script_path = os.path.join(dir_name, config[section]["script"]) ++ config[section]["script"] = [os.path.normpath(script_path)] + + return config +diff --git a/tuned/profiles/locator.py b/tuned/profiles/locator.py +index 3fd46916..994bdfb5 100644 +--- a/tuned/profiles/locator.py ++++ b/tuned/profiles/locator.py +@@ -1,6 +1,12 @@ + import os + import tuned.consts as consts +-from configobj import ConfigObj, ConfigObjError ++try: ++ from configparser import ConfigParser, Error ++ from io import StringIO ++except ImportError: ++ # python2.7 support, remove RHEL-7 support end ++ from ConfigParser import ConfigParser, Error ++ from StringIO import StringIO + + class Locator(object): + """ +@@ -48,8 +54,12 @@ def parse_config(self, profile_name): + if config_file is None: + return None + try: +- return ConfigObj(config_file, list_values = False, interpolation = False) +- except (IOError, OSError, ConfigObjError) as e: ++ config = ConfigParser() ++ config.optionxform = str ++ with open(config_file) as f: ++ config.readfp(StringIO("[" + consts.MAGIC_HEADER_NAME + "]\n" + f.read())) ++ return config ++ except (IOError, OSError, Error) as e: + return None + + # Get profile attributes (e.g. summary, description), attrs is list of requested attributes, +@@ -75,17 +85,16 @@ def get_profile_attrs(self, profile_name, attrs, defvals = None): + config = self.parse_config(profile_name) + if config is None: + return [False, "", "", ""] +- if consts.PLUGIN_MAIN_UNIT_NAME in config: +- d = config[consts.PLUGIN_MAIN_UNIT_NAME] +- else: +- d = dict() ++ main_unit_in_config = consts.PLUGIN_MAIN_UNIT_NAME in config.sections() + vals = [True, profile_name] + for (attr, defval) in zip(attrs, defvals): + if attr == "" or attr is None: + vals[0] = False + vals = vals + [""] ++ elif main_unit_in_config and attr in config.options(consts.PLUGIN_MAIN_UNIT_NAME): ++ vals = vals + [config.get(consts.PLUGIN_MAIN_UNIT_NAME, attr, raw=True)] + else: +- vals = vals + [d.get(attr, defval)] ++ vals = vals + [defval] + return vals + + def list_profiles(self): +diff --git a/tuned/profiles/variables.py b/tuned/profiles/variables.py +index 2e101661..a9e27aea 100644 +--- a/tuned/profiles/variables.py ++++ b/tuned/profiles/variables.py +@@ -4,7 +4,13 @@ + from .functions import functions as functions + import tuned.consts as consts + from tuned.utils.commands import commands +-from configobj import ConfigObj, ConfigObjError ++try: ++ from configparser import ConfigParser, Error ++ from io import StringIO ++except ImportError: ++ # python2.7 support, remove RHEL-7 support end ++ from ConfigParser import ConfigParser, Error ++ from StringIO import StringIO + + log = tuned.logs.get() + +@@ -40,24 +46,21 @@ def add_variable(self, variable, value): + self._lookup_re[r'(? +Date: Thu, 15 Jul 2021 20:48:54 +0200 +Subject: [PATCH] scheduler: new option cgroup_ps_blacklist +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This option allows skipping processes belonging to the blacklisted +cgroups. It matches the regular expression against items from the +/proc/PID/cgroups. Items/lines from the /proc/PID/cgroups are separated +by commas ','. Each item consists of the: +hierarchy-ID:controller-list:cgroup-path + +Example of the content on which the regular expression is run: +10:hugetlb:/,9:perf_event:/,8:blkio:/ + +For cgroups v2 the hierarchy-ID is 0 and the controller-list is ''. +For details see man cgroups.7. The only difference from the man +cgroups.7 is that it uses commas for separation of the items instead +of the new lines. The commas are added by the python-linux-procfs +(it's the behavior of the python-linux-procfs-0.6.3). + +Multiple regular expressions can be separated by the semicolon ';'. + +Examples: +[scheduler] +isolated_cores=1 +cgroup_ps_blacklist=:/daemons\b + +It will move all processes away from the core 1 except processes which +belongs to the cgroup '/daemons'. The '\b' is regular expression +metacharacter that matches word boundary (i.e. it matches only +'/daemons', not e.g. '/daemonset' or '/group/daemons'). In this example +we do not care about the hierarchy-ID and the controller-list. + +[scheduler] +isolated_cores=1 +cgroup_ps_blacklist=\b8:blkio:/,|$ + +In this example it skips processes belonging to the cgroup '/', +with hierarchy-ID 8 and controller-list blkio. The ',|$' is needed +because the '\b' matches word boundary and the non-alphanumeric +character '/' is not taken as a word, thus the '\b' will not match there. + +[scheduler] +isolated_cores=1 +cgroup_ps_blacklist=:/daemons\b;:/test\b + +In this example two regular expressions are used which tries to match +'/daemons' and '/test' cgroup-path. If either matches (i.e. the OR operator), +the process is skipped (i.e. not moved away from the core 1). + +Resolves: rhbz#1980715 + +Signed-off-by: Jaroslav Škarvada +--- + tuned/plugins/plugin_scheduler.py | 19 +++++++++++++++++++ + 1 file changed, 19 insertions(+) + +diff --git a/tuned/plugins/plugin_scheduler.py b/tuned/plugins/plugin_scheduler.py +index e2f7ca2..8e77417 100644 +--- a/tuned/plugins/plugin_scheduler.py ++++ b/tuned/plugins/plugin_scheduler.py +@@ -156,6 +156,7 @@ class SchedulerPlugin(base.Plugin): + # default is to whitelist all and blacklist none + self._ps_whitelist = ".*" + self._ps_blacklist = "" ++ self._cgroup_ps_blacklist_re = "" + self._cpus = perf.cpu_map() + self._scheduler_storage_key = self._storage_key( + command_name = "scheduler") +@@ -251,6 +252,7 @@ class SchedulerPlugin(base.Plugin): + "cgroup_mount_point_init": False, + "cgroup_groups_init": True, + "cgroup_for_isolated_cores": None, ++ "cgroup_ps_blacklist": None, + "ps_whitelist": None, + "ps_blacklist": None, + "default_irq_smp_affinity": "calc", +@@ -811,6 +813,14 @@ class SchedulerPlugin(base.Plugin): + elif event.type == perf.RECORD_EXIT: + self._remove_pid(instance, int(event.tid)) + ++ @command_custom("cgroup_ps_blacklist", per_device = False) ++ def _cgroup_ps_blacklist(self, enabling, value, verify, ignore_missing): ++ # currently unsupported ++ if verify: ++ return None ++ if enabling and value is not None: ++ self._cgroup_ps_blacklist_re = "|".join(["(%s)" % v for v in re.split(r"(?= 8 ) BuildRequires: %{_py}-mock %endif -BuildRequires: %{_py}-configobj BuildRequires: %{_py}-pyudev -Requires: %{_py}-pyudev, %{_py}-configobj +Requires: %{_py}-pyudev Requires: %{_py}-linux-procfs, %{_py}-perf %if %{without python3} Requires: %{_py}-schedutils @@ -94,6 +93,10 @@ Requires: subscription-manager Requires: python3-syspurpose %endif %endif +# rhbz#1980715 +Patch0: tuned-2.16.0-scheduler-cgroups-exclude.patch +# rhbz#1936386 +Patch1: tuned-2.16.0-configobj-drop.patch %description The tuned package contains a daemon that tunes system settings dynamically. @@ -529,6 +532,12 @@ fi %{_mandir}/man7/tuned-profiles-postgresql.7* %changelog +* Wed Aug 18 2021 Jaroslav Škarvada - 2.16.0-3 +- scheduler: allow exclude of processes from the specific cgroup(s) + Resolves: rhbz#1980715 +- Switched to the configparser from the configobj + Resolves: rhbz#1936386 + * Tue Aug 10 2021 Mohan Boddu - 2.16.0-2 - Rebuilt for IMA sigs, glibc 2.34, aarch64 flags Related: rhbz#1991688