From 55daf4aee794c88c064910597ef2bc2498a7270a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavol=20=C5=BD=C3=A1=C4=8Dik?= Date: Mon, 25 Mar 2024 16:08:00 +0100 Subject: [PATCH 1/3] Add a function to parse lists in the global configuration --- tuned/utils/global_config.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tuned/utils/global_config.py b/tuned/utils/global_config.py index 5593fff60..e0ed43652 100644 --- a/tuned/utils/global_config.py +++ b/tuned/utils/global_config.py @@ -1,3 +1,4 @@ +import re import tuned.logs from tuned.utils.config_parser import ConfigParser, Error from tuned.exceptions import TunedException @@ -77,6 +78,14 @@ def get_int(self, key, default = 0): return int(i, 0) return default + def get_list(self, key, default = []): + value = self._cfg.get(key, default) + if isinstance(value, list): + return value + if value.strip() == "": + return [] + return [x.strip() for x in re.split(r",|;", value)] + def set(self, key, value): self._cfg[key] = value From a916594a494ae17258925b90279f95e221b58aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavol=20=C5=BD=C3=A1=C4=8Dik?= Date: Mon, 25 Mar 2024 16:13:21 +0100 Subject: [PATCH 2/3] Use get_list to parse unix_socket_signal_path --- tuned/daemon/application.py | 2 +- tuned/exports/unix_socket_exporter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tuned/daemon/application.py b/tuned/daemon/application.py index d1bf72a2b..607f3498a 100644 --- a/tuned/daemon/application.py +++ b/tuned/daemon/application.py @@ -83,7 +83,7 @@ def attach_to_unix_socket(self): raise TunedException("Unix socket interface is already initialized.") self._unix_socket_exporter = exports.unix_socket.UnixSocketExporter(self.config.get(consts.CFG_UNIX_SOCKET_PATH), - self.config.get(consts.CFG_UNIX_SOCKET_SIGNAL_PATHS), + self.config.get_list(consts.CFG_UNIX_SOCKET_SIGNAL_PATHS), self.config.get(consts.CFG_UNIX_SOCKET_OWNERSHIP), self.config.get_int(consts.CFG_UNIX_SOCKET_PERMISIONS), self.config.get_int(consts.CFG_UNIX_SOCKET_CONNECTIONS_BACKLOG)) diff --git a/tuned/exports/unix_socket_exporter.py b/tuned/exports/unix_socket_exporter.py index a69b06a57..4c526a2c2 100644 --- a/tuned/exports/unix_socket_exporter.py +++ b/tuned/exports/unix_socket_exporter.py @@ -30,7 +30,7 @@ def __init__(self, socket_path=consts.CFG_DEF_UNIX_SOCKET_PATH, self._socket_path = socket_path self._socket_object = None - self._socket_signal_paths = re.split(r",;", signal_paths) if signal_paths else [] + self._socket_signal_paths = signal_paths self._socket_signal_objects = [] self._ownership = [-1, -1] if ownership: From 90ea23b8b36926253347986535c6801fb09ae47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavol=20=C5=BD=C3=A1=C4=8Dik?= Date: Mon, 25 Mar 2024 16:30:18 +0100 Subject: [PATCH 3/3] Add an option to configure profile directories Resolves: RHEL-26157 --- .../con_the-location-of-tuned-profiles.adoc | 13 +++++++-- tuned-adm.py | 3 +- tuned-gui.py | 10 +++---- tuned-main.conf | 5 ++++ tuned/admin/admin.py | 6 ++-- tuned/consts.py | 5 +++- tuned/daemon/application.py | 2 +- tuned/gtk/gui_profile_loader.py | 29 +++++++++---------- 8 files changed, 45 insertions(+), 28 deletions(-) diff --git a/doc/manual/modules/performance/con_the-location-of-tuned-profiles.adoc b/doc/manual/modules/performance/con_the-location-of-tuned-profiles.adoc index e42886178..678ff6ae5 100644 --- a/doc/manual/modules/performance/con_the-location-of-tuned-profiles.adoc +++ b/doc/manual/modules/performance/con_the-location-of-tuned-profiles.adoc @@ -6,10 +6,19 @@ *TuneD* stores profiles in the following directories: [filename]`/usr/lib/tuned/`:: -Distribution-specific profiles are stored in the directory. Each profile has its own directory. The profile consists of the main configuration file called `tuned.conf`, and optionally other files, for example helper scripts. +Distribution-specific profiles are stored in the [filename]`/usr/lib/tuned/` directory. Each profile has its own directory. The profile consists of the main configuration file called `tuned.conf`, and optionally other files, for example helper scripts. [filename]`/etc/tuned/`:: -If you need to customize a profile, copy the profile directory into the directory, which is used for custom profiles. If there are two profiles of the same name, the custom profile located in [filename]`/etc/tuned/` is used. +If you need to customize a profile, copy the profile directory into the [filename]`/etc/tuned/` directory, which is used for custom profiles, and then adjust it. If there is a system profile and a custom profile of the same name, the custom profile located in [filename]`/etc/tuned/` is used. + +.User-defined profile directories +==== +If you want to make TuneD load profiles from a directory other than [filename]`/usr/lib/tuned/` and [filename]`/etc/tuned/`, you can list it in [filename]`/etc/tuned/tuned-main.conf` as follows: +---- +profile_dirs=/usr/lib/tuned,/etc/tuned,/my/custom/profiles +---- +In this example, profiles are loaded also from [filename]`/my/custom/profiles/`. If two directories contain profiles with the same names, the one that is listed later takes precedence. +==== [role="_additional-resources"] .Additional resources diff --git a/tuned-adm.py b/tuned-adm.py index 712e7da98..83ed56984 100755 --- a/tuned-adm.py +++ b/tuned-adm.py @@ -124,10 +124,11 @@ def check_log_level(value): log_level = options.pop("loglevel") result = False + profile_dirs = config.get_list(consts.CFG_PROFILE_DIRS, consts.CFG_DEF_PROFILE_DIRS) dbus = config.get_bool(consts.CFG_DAEMON, consts.CFG_DEF_DAEMON) try: - admin = tuned.admin.Admin(dbus, debug, asynco, timeout, log_level) + admin = tuned.admin.Admin(profile_dirs, dbus, debug, asynco, timeout, log_level) result = admin.action(action_name, **options) except: diff --git a/tuned-gui.py b/tuned-gui.py index da591e78b..5a2b8664d 100755 --- a/tuned-gui.py +++ b/tuned-gui.py @@ -130,7 +130,7 @@ def __init__(self): return self.manager = tuned.gtk.gui_profile_loader.GuiProfileLoader( - tuned.consts.LOAD_DIRECTORIES) + self.config.get_list(consts.CFG_PROFILE_DIRS, consts.CFG_DEF_PROFILE_DIRS)) self.plugin_loader = tuned.gtk.gui_plugin_loader.GuiPluginLoader() @@ -373,8 +373,8 @@ def execute_remove_profile(self, button): try: self.manager.remove_profile(profile, is_admin=self.is_admin) - except ManagerException: - self.error_dialog('failed to authorize', '') + except ManagerException as ex: + self.error_dialog('Removing profile failed', ex.__str__()) return for item in self.treestore_profiles: @@ -591,8 +591,8 @@ def execute_update_profile(self, data): copied_profile.name = self.editing_profile_name + '-modified' try: self.manager.save_profile(copied_profile) - except ManagerException: - self.error_dialog('failed to authorize', '') + except ManagerException as ex: + self.error_dialog('Error saving profile', ex.__str__()) return else: if not TunedDialog('System profile can not be modified ' diff --git a/tuned-main.conf b/tuned-main.conf index 9cec833d2..86bca4d02 100644 --- a/tuned-main.conf +++ b/tuned-main.conf @@ -82,3 +82,8 @@ log_file_max_size = 1MB # - not_on_exit: rollbacks are always performed on a profile # switch, but not on any kind of TuneD process exit # rollback = auto + +# Directories to search for profiles separated by , or ; +# In case of conflicts in profile names, the later directory +# takes precedence +# profile_dirs = /usr/lib/tuned,/etc/tuned diff --git a/tuned/admin/admin.py b/tuned/admin/admin.py index b9ef94b94..8f8682f39 100644 --- a/tuned/admin/admin.py +++ b/tuned/admin/admin.py @@ -15,15 +15,15 @@ import logging class Admin(object): - def __init__(self, dbus = True, debug = False, asynco = False, - timeout = consts.ADMIN_TIMEOUT, + def __init__(self, profile_dirs, dbus = True, debug = False, + asynco = False, timeout = consts.ADMIN_TIMEOUT, log_level = logging.ERROR): self._dbus = dbus self._debug = debug self._async = asynco self._timeout = timeout self._cmd = commands(debug) - self._profiles_locator = profiles_locator(consts.LOAD_DIRECTORIES) + self._profiles_locator = profiles_locator(profile_dirs) self._daemon_action_finished = threading.Event() self._daemon_action_profile = "" self._daemon_action_result = True diff --git a/tuned/consts.py b/tuned/consts.py index 37493635c..965068a3b 100644 --- a/tuned/consts.py +++ b/tuned/consts.py @@ -13,7 +13,7 @@ DBUS_OBJECT = "/Tuned" DEFAULT_PROFILE = "balanced" DEFAULT_STORAGE_FILE = "/run/tuned/save.pickle" -LOAD_DIRECTORIES = ["/usr/lib/tuned", "/etc/tuned"] +SYSTEM_PROFILE_DIR = "/usr/lib/tuned" PERSISTENT_STORAGE_DIR = "/var/lib/tuned" PLUGIN_MAIN_UNIT_NAME = "main" # Magic section header because ConfigParser does not support "headerless" config @@ -122,6 +122,7 @@ CFG_UNIX_SOCKET_CONNECTIONS_BACKLOG = "connections_backlog" CFG_CPU_EPP_FLAG = "hwp_epp" CFG_ROLLBACK = "rollback" +CFG_PROFILE_DIRS = "profile_dirs" # no_daemon mode CFG_DEF_DAEMON = True @@ -171,6 +172,8 @@ CFG_FUNC_UNIX_SOCKET_CONNECTIONS_BACKLOG = "getint" # default rollback strategy CFG_DEF_ROLLBACK = "auto" +# default profile directories +CFG_DEF_PROFILE_DIRS = [SYSTEM_PROFILE_DIR, "/etc/tuned"] PATH_CPU_DMA_LATENCY = "/dev/cpu_dma_latency" diff --git a/tuned/daemon/application.py b/tuned/daemon/application.py index 607f3498a..a7400cfee 100644 --- a/tuned/daemon/application.py +++ b/tuned/daemon/application.py @@ -50,7 +50,7 @@ def __init__(self, profile_name = None, config = None): profile_factory = profiles.Factory() profile_merger = profiles.Merger() - profile_locator = profiles.Locator(consts.LOAD_DIRECTORIES) + profile_locator = profiles.Locator(self.config.get_list(consts.CFG_PROFILE_DIRS, consts.CFG_DEF_PROFILE_DIRS)) profile_loader = profiles.Loader(profile_locator, profile_factory, profile_merger, self.config, self.variables) self._daemon = daemon.Daemon(unit_manager, profile_loader, profile_name, self.config, self) diff --git a/tuned/gtk/gui_profile_loader.py b/tuned/gtk/gui_profile_loader.py index 00df17e48..9f65ae092 100644 --- a/tuned/gtk/gui_profile_loader.py +++ b/tuned/gtk/gui_profile_loader.py @@ -60,7 +60,7 @@ def set_raw_profile(self, profile_name, config): profilePath = self._locate_profile_path(profile_name) - if profilePath == tuned.consts.LOAD_DIRECTORIES[1]: + if profilePath != tuned.consts.SYSTEM_PROFILE_DIR: file_path = profilePath + '/' + profile_name + '/' + tuned.consts.PROFILE_FILE config_parser = ConfigParser(delimiters=('='), inline_comment_prefixes=('#'), strict=False) config_parser.optionxform = str @@ -127,7 +127,15 @@ def _refresh_profiles(self): self._load_all_profiles() def save_profile(self, profile): - path = tuned.consts.LOAD_DIRECTORIES[1] + '/' + profile.name + # save the new profile to a non-system directory with the highest priority + path = None + for d in reversed(self.directories): + if d != tuned.consts.SYSTEM_PROFILE_DIR: + path = os.path.join(d, profile.name) + break + if path is None: + raise managerException.ManagerException('Cannot save profile to a system directory') + config = { 'main': collections.OrderedDict(), 'filename': path + '/' + tuned.consts.PROFILE_FILE, @@ -160,7 +168,7 @@ def update_profile( raise managerException.ManagerException('Profile: ' + old_profile_name + ' is not in profiles') - path = tuned.consts.LOAD_DIRECTORIES[1] + '/' + profile.name + path = os.path.join(self._locate_profile_path(old_profile_name), profile.name) if old_profile_name != profile.name: self.remove_profile(old_profile_name, is_admin=is_admin) @@ -207,20 +215,11 @@ def remove_profile(self, profile_name, is_admin): + ' profile is stored in ' + profile_path) def is_profile_removable(self, profile_name): - - # profile is in /etc/profile - - profile_path = self._locate_profile_path(profile_name) - if profile_path == tuned.consts.LOAD_DIRECTORIES[1]: - return True - else: - return False + return not self.is_profile_factory(profile_name) def is_profile_factory(self, profile_name): - - # profile is in /usr/lib/tuned - - return not self.is_profile_removable(profile_name) + profile_path = self._locate_profile_path(profile_name) + return profile_path == tuned.consts.SYSTEM_PROFILE_DIR def _save_profile(self, config): ec = subprocess.call(['pkexec', sys.executable, tuned.gtk.gui_profile_saver.__file__ , json.dumps(config)])