Blob Blame History Raw
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