scheduler: allow exclude of processes from the specific cgroup(s)

Resolves: rhbz#1980715
Switched to the configparser from the configobj
  Resolves: rhbz#1936386
This commit is contained in:
Jaroslav Škarvada 2021-08-19 00:45:15 +02:00
parent 3d2247cb33
commit b3dc98ba8c
3 changed files with 962 additions and 3 deletions

View File

@ -0,0 +1,829 @@
From 063277a05b3a174f9265d36032ca097ee5b7cc9c Mon Sep 17 00:00:00 2001
From: Jan Zerdik <jzerdik@redhat.com>
Date: Fri, 30 Jul 2021 11:48:59 +0200
Subject: [PATCH] Removing dependency on python-configobj.
Resolves: rhbz#1936386
Signed-off-by: Jan Zerdik <jzerdik@redhat.com>
---
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'(?<!\\)\${' + re.escape(s) + r'}'] = v
self._lookup_env[self._add_env_prefix(s, consts.ENV_PREFIX)] = v
- def add_dict(self, d):
- for item in d:
- self.add_variable(item, d[item])
-
def add_from_file(self, filename):
if not os.path.exists(filename):
log.error("unable to find variables_file: '%s'" % filename)
return
try:
- config = ConfigObj(filename, raise_errors = True, file_error = True, list_values = False, interpolation = False)
- except ConfigObjError:
+ config = ConfigParser()
+ config.optionxform = str
+ with open(filename) as f:
+ config.readfp(StringIO("[" + consts.MAGIC_HEADER_NAME + "]\n" + f.read()))
+ except Error:
log.error("error parsing variables_file: '%s'" % filename)
return
- for item in config:
- if isinstance(config[item], dict):
- self.add_dict(config[item])
- else:
- self.add_variable(item, config[item])
+ for s in config.sections():
+ for o in config.options(s):
+ self.add_variable(o, config.get(s, o, raw=True))
def add_from_cfg(self, cfg):
for item in cfg:
diff --git a/tuned/utils/global_config.py b/tuned/utils/global_config.py
index 039dc9a4..f342700f 100644
--- a/tuned/utils/global_config.py
+++ b/tuned/utils/global_config.py
@@ -1,6 +1,11 @@
import tuned.logs
-from configobj import ConfigObj, ConfigObjError
-from validate import Validator
+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
import tuned.consts as consts
from tuned.utils.commands import commands
@@ -11,31 +16,55 @@
class GlobalConfig():
- 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,
- "recommend_command = boolean(default=%s)" % consts.CFG_DEF_RECOMMEND_COMMAND]
-
def __init__(self,config_file = consts.GLOBAL_CONFIG_FILE):
self._cfg = {}
self.load_config(file_name=config_file)
self._cmd = commands()
+ @staticmethod
+ def get_global_config_spec():
+ """
+ Easy validation mimicking configobj
+ Returns two dicts, firts with default values (default None)
+ global_default[consts.CFG_SOMETHING] = consts.CFG_DEF_SOMETHING or None
+ second with configobj function for value type (default "get" for string, others eg getboolean, getint)
+ global_function[consts.CFG_SOMETHING] = consts.CFG_FUNC_SOMETHING or get
+ }
+ """
+ options = [opt for opt in dir(consts)
+ if opt.startswith("CFG_") and
+ not opt.startswith("CFG_FUNC_") and
+ not opt.startswith("CFG_DEF_")]
+ global_default = dict((getattr(consts, opt), getattr(consts, "CFG_DEF_" + opt[4:], None)) for opt in options)
+ global_function = dict((getattr(consts, opt), getattr(consts, "CFG_FUNC_" + opt[4:], "get")) for opt in options)
+ return global_default, global_function
+
def load_config(self, file_name = consts.GLOBAL_CONFIG_FILE):
"""
Loads global configuration file.
"""
log.debug("reading and parsing global configuration file '%s'" % file_name)
try:
- self._cfg = ConfigObj(file_name, configspec = self.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()))
+ self._cfg, _global_config_func = self.get_global_config_spec()
+ for option in config_parser.options(consts.MAGIC_HEADER_NAME):
+ if option in self._cfg:
+ try:
+ func = getattr(config_parser, _global_config_func[option])
+ self._cfg[option] = func(consts.MAGIC_HEADER_NAME, option)
+ except Error:
+ raise TunedException("Global TuneD configuration file '%s' is not valid."
+ % file_name)
+ else:
+ log.info("Unknown option '%s' in global config file '%s'." % (option, file_name))
+ self._cfg[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 ConfigObjError as e:
+ except Error as e:
raise TunedException("Error parsing global TuneD configuration file '%s'." % file_name)
- vdt = Validator()
- if (not self._cfg.validate(vdt, copy=True)):
- raise TunedException("Global TuneD configuration file '%s' is not valid." % file_name)
def get(self, key, default = None):
return self._cfg.get(key, default)
diff --git a/tuned/utils/profile_recommender.py b/tuned/utils/profile_recommender.py
index 580465bb..7300277b 100644
--- a/tuned/utils/profile_recommender.py
+++ b/tuned/utils/profile_recommender.py
@@ -3,7 +3,11 @@
import errno
import procfs
import subprocess
-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
try:
import syspurpose.files
@@ -59,11 +63,14 @@ def process_config(self, fname, has_root=True):
try:
if not os.path.isfile(fname):
return None
- config = ConfigObj(fname, list_values = False, interpolation = False)
- for section in list(config.keys()):
+ config = ConfigParser()
+ config.optionxform = str
+ with open(fname) as f:
+ config.readfp(f)
+ for section in config.sections():
match = True
- for option in list(config[section].keys()):
- value = config[section][option]
+ for option in config.options(section):
+ value = config.get(section, option, raw=True)
if value == "":
value = r"^$"
if option == "virt":
@@ -117,7 +124,7 @@ def process_config(self, fname, has_root=True):
r = re.compile(r",[^,]*$")
matching_profile = r.sub("", section)
break
- except (IOError, OSError, ConfigObjError) as e:
+ except (IOError, OSError, Error) as e:
log.error("error processing '%s', %s" % (fname, e))
return matching_profile

View File

@ -0,0 +1,121 @@
From 438ff4f899f5eb4bc2ea679fdd2d3611f8e0d8ea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaroslav=20=C5=A0karvada?= <jskarvad@redhat.com>
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 <jskarvad@redhat.com>
---
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"(?<!\\);", str(value))])
+
@command_custom("ps_whitelist", per_device = False)
def _ps_whitelist(self, enabling, value, verify, ignore_missing):
# currently unsupported
@@ -886,6 +896,9 @@ class SchedulerPlugin(base.Plugin):
if self._ps_blacklist != "":
psl = [v for v in psl if re.search(self._ps_blacklist,
self._get_stat_comm(v)) is None]
+ if self._cgroup_ps_blacklist_re != "":
+ psl = [v for v in psl if re.search(self._cgroup_ps_blacklist_re,
+ self._get_stat_cgroup(v)) is None]
psd = dict([(v.pid, v) for v in psl])
for pid in psd:
try:
@@ -911,6 +924,12 @@ class SchedulerPlugin(base.Plugin):
psd[pid]["threads"].values(),
affinity, True)
+ def _get_stat_cgroup(self, o):
+ try:
+ return o["cgroups"]
+ except (OSError, IOError, KeyError):
+ return ""
+
def _get_stat_comm(self, o):
try:
return o["stat"]["comm"]
--
2.31.1

View File

@ -35,7 +35,7 @@
Summary: A dynamic adaptive system tuning daemon
Name: tuned
Version: 2.16.0
Release: 2%{?prerel1}%{?dist}
Release: 3%{?prerel1}%{?dist}
License: GPLv2+
Source0: https://github.com/redhat-performance/%{name}/archive/v%{version}%{?prerel2}/%{name}-%{version}%{?prerel2}.tar.gz
# RHEL-9 specific recommend.conf:
@ -58,9 +58,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
@ -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 <jskarvad@redhat.com> - 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 <mboddu@redhat.com> - 2.16.0-2
- Rebuilt for IMA sigs, glibc 2.34, aarch64 flags
Related: rhbz#1991688