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'(?