From bc8ffcfb2396fae8f015e263d0158bbc65e58e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Lyson=C4=9Bk?= Date: Thu, 3 Aug 2017 14:58:43 +0200 Subject: [PATCH] Introduce the concept of manually vs automatically set profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A profile can now be set in two modes: automatic and manual. When the automatic mode is active, Tuned always activates the recommended profile upon startup. This mode can be set using the following command: tuned-adm auto_profile When the manual mode is active, the previously applied profile is activated upon startup. This mode can be set by switching to the desired profile, e.g. tuned-adm profile throughput-performance The mode is saved on the second line of /etc/tuned/active_profile. When the file /etc/tuned/active_profile does not exist, or is empty, automatic mode is enabled. When the file has only one line, i.e. it was generated by a previous Tuned version, manual mode is enabled so that the upgrade doesn't cause unexpected active profile changes. Related: rhbz#1459146 Signed-off-by: Ondřej Lysoněk --- com.redhat.tuned.policy | 10 ++++++ tuned-adm.py | 3 ++ tuned/admin/admin.py | 61 ++++++++++++++++++++++++++-------- tuned/admin/dbus_controller.py | 3 ++ tuned/consts.py | 5 +++ tuned/daemon/controller.py | 25 +++++++++----- tuned/daemon/daemon.py | 55 +++++++++++++++++++++--------- 7 files changed, 125 insertions(+), 37 deletions(-) diff --git a/com.redhat.tuned.policy b/com.redhat.tuned.policy index 1f9e6a7..1db1bf2 100644 --- a/com.redhat.tuned.policy +++ b/com.redhat.tuned.policy @@ -117,6 +117,16 @@ + + Enable automatic profile selection mode + Authentication is required to change profile selection mode + + auth_admin + auth_admin + yes + + + Verify Tuned profile Authentication is required to verify Tuned profile diff --git a/tuned-adm.py b/tuned-adm.py index 9dc6f42..c56dc68 100755 --- a/tuned-adm.py +++ b/tuned-adm.py @@ -71,6 +71,9 @@ if __name__ == "__main__": parser_verify.set_defaults(action="verify_profile") parser_verify.add_argument("--ignore-missing", "-i", action="store_true", help="do not treat missing/non-supported tunings as errors") + parser_auto_profile = subparsers.add_parser("auto_profile", help="enable automatic profile selection mode, switch to the recommended profile") + parser_auto_profile.set_defaults(action="auto_profile") + args = parser.parse_args(sys.argv[1:]) options = vars(args) diff --git a/tuned/admin/admin.py b/tuned/admin/admin.py index 040f5cd..b751ff0 100644 --- a/tuned/admin/admin.py +++ b/tuned/admin/admin.py @@ -112,7 +112,12 @@ class Admin(object): def _get_active_profile(self): profile_name = None - profile_name = str.strip(self._cmd.read_file(consts.ACTIVE_PROFILE_FILE, None)) + contents = str.strip(self._cmd.read_file(consts.ACTIVE_PROFILE_FILE)) + if contents == '': + profile_name = '' + else: + arr = contents.split('\n') + profile_name = arr[0] if profile_name == "": profile_name = None return profile_name @@ -161,6 +166,8 @@ class Admin(object): return True return self._print_profile_name(profile_name) + # TODO action profile mode - auto/manual + def _profile_print_status(self, ret, msg): if ret: if not self._controller.is_running() and not self._controller.start(): @@ -197,20 +204,23 @@ class Admin(object): self._controller.set_action(self._action_dbus_wait_profile, profile_name) return self._profile_print_status(ret, msg) - def _action_profile(self, profiles): - if len(profiles) == 0: - return self._action_list() - profile_name = " ".join(profiles) - if profile_name == "": - return False + def _restart_tuned(self): + print("Trying to (re)start tuned...") + (ret, msg) = self._cmd.execute(["service", "tuned", "restart"]) + if ret == 0: + print("Tuned (re)started, changes applied.") + else: + print("Tuned (re)start failed, you need to (re)start tuned by hand for changes to apply.") + + def _set_profile(self, profile_name, manual): if profile_name in self._profiles_locator.get_known_names(): - if self._cmd.write_to_file(consts.ACTIVE_PROFILE_FILE, profile_name): - print("Trying to (re)start tuned...") - (ret, msg) = self._cmd.execute(["service", "tuned", "restart"]) - if ret == 0: - print("Tuned (re)started, changes applied.") - else: - print("Tuned (re)start failed, you need to (re)start tuned by hand for changes to apply.") + s = profile_name + '\n' + if manual: + s += consts.ACTIVE_PROFILE_MANUAL + '\n' + else: + s += consts.ACTIVE_PROFILE_AUTO + '\n' + if self._cmd.write_to_file(consts.ACTIVE_PROFILE_FILE, s): + self._restart_tuned() return True else: self._error("Unable to switch profile, do you have enough permissions?") @@ -219,6 +229,29 @@ class Admin(object): self._error("Requested profile '%s' doesn't exist." % profile_name) return False + def _action_profile(self, profiles): + if len(profiles) == 0: + return self._action_list() + profile_name = " ".join(profiles) + if profile_name == "": + return False + return self._set_profile(profile_name, True) + + def _action_dbus_auto_profile(self): + profile_name = self._controller.recommend_profile() + self._daemon_action_finished.clear() + (ret, msg) = self._controller.auto_profile() + if self._async or not ret: + return self._controller.exit(self._profile_print_status(ret, msg)) + else: + self._timestamp = time.time() + self._controller.set_action(self._action_dbus_wait_profile, profile_name) + return self._profile_print_status(ret, msg) + + def _action_auto_profile(self): + profile_name = self._cmd.recommend_profile() + return self._set_profile(profile_name, False) + def _action_dbus_recommend_profile(self): print(self._controller.recommend_profile()) return self._controller.exit(True) diff --git a/tuned/admin/dbus_controller.py b/tuned/admin/dbus_controller.py index 2721ecb..322b8c0 100644 --- a/tuned/admin/dbus_controller.py +++ b/tuned/admin/dbus_controller.py @@ -104,6 +104,9 @@ class DBusController(object): return (False, "No profile specified") return self._call("switch_profile", new_profile) + def auto_profile(self): + return self._call("auto_profile") + def recommend_profile(self): return self._call("recommend_profile") diff --git a/tuned/consts.py b/tuned/consts.py index d557608..467ddfa 100644 --- a/tuned/consts.py +++ b/tuned/consts.py @@ -99,3 +99,8 @@ STR_VERIFY_PROFILE_FAIL = "verify: failed: '%s'" # timout for tuned-adm operations in seconds ADMIN_TIMEOUT = 600 + +# Strings for /etc/tuned/active_profile specifying if the active profile +# was set automatically or manually +ACTIVE_PROFILE_AUTO = "auto" +ACTIVE_PROFILE_MANUAL = "manual" diff --git a/tuned/daemon/controller.py b/tuned/daemon/controller.py index 5addcee..d626bf5 100644 --- a/tuned/daemon/controller.py +++ b/tuned/daemon/controller.py @@ -79,19 +79,15 @@ class Controller(tuned.exports.interfaces.ExportableInterface): else: return self.stop() and self.start() - @exports.export("s", "(bs)") - def switch_profile(self, profile_name, caller = None): - if caller == "": - return (False, "Unauthorized") + def _switch_profile(self, profile_name, manual): was_running = self._daemon.is_running() msg = "OK" success = True reapply = False try: if was_running: - # stop(switch_profile = True), due to profile switch - self._daemon.stop(True) - self._daemon.set_profile(profile_name) + self._daemon.stop(profile_switch = True) + self._daemon.set_profile(profile_name, manual) except tuned.exceptions.TunedException as e: success = False msg = str(e) @@ -110,6 +106,19 @@ class Controller(tuned.exports.interfaces.ExportableInterface): return (success, msg) + @exports.export("s", "(bs)") + def switch_profile(self, profile_name, caller = None): + if caller == "": + return (False, "Unauthorized") + return self._switch_profile(profile_name, True) + + @exports.export("", "(bs)") + def auto_profile(self, caller = None): + if caller == "": + return (False, "Unauthorized") + profile_name = self.recommend_profile() + return self._switch_profile(profile_name, False) + @exports.export("", "s") def active_profile(self, caller = None): if caller == "": @@ -126,7 +135,7 @@ class Controller(tuned.exports.interfaces.ExportableInterface): if self._daemon.is_running(): self._daemon.stop() if self._daemon.is_enabled(): - self._daemon.set_profile(None, save_instantly=True) + self._daemon.set_profile(None, True, save_instantly=True) return True @exports.export("", "b") diff --git a/tuned/daemon/daemon.py b/tuned/daemon/daemon.py index 8432294..55f66d8 100644 --- a/tuned/daemon/daemon.py +++ b/tuned/daemon/daemon.py @@ -57,31 +57,34 @@ class Daemon(object): self._profile_applied = threading.Event() def _init_profile(self, profile_name): + manual = True if profile_name is None: - profile_name = self._get_active_profile() + (profile_name, manual) = self._get_startup_profile() self._profile = None - self.set_profile(profile_name) + self._manual = None + self.set_profile(profile_name, manual) - def set_profile(self, profile_name, save_instantly=False): + def set_profile(self, profile_name, manual, save_instantly=False): if self.is_running(): raise TunedException(self._notify_profile_changed(profile_name, False, "Cannot set profile while the daemon is running.")) if profile_name == "" or profile_name is None: self._profile = None + self._manual = None elif profile_name not in self.profile_loader.profile_locator.get_known_names(): - raise TunedException(self._notify_profile_changed(profile_name, False, "Requested profile '%s' doesn't exist." % profile_name)) else: try: self._profile = self._profile_loader.load(profile_name) + self._manual = manual except InvalidProfileException as e: raise TunedException(self._notify_profile_changed(profile_name, False, "Cannot load profile '%s': %s" % (profile_name, e))) if save_instantly: if profile_name is None: profile_name = "" - self._save_active_profile(profile_name) + self._save_active_profile(profile_name, manual) @property def profile(self): @@ -103,7 +106,7 @@ class Daemon(object): raise TunedException("Cannot start the daemon without setting a profile.") self._unit_manager.create(self._profile.units) - self._save_active_profile(self._profile.name) + self._save_active_profile(self._profile.name, self._manual) self._unit_manager.start_tuning() self._profile_applied.set() log.info("static tuning from profile '%s' applied" % self._profile.name) @@ -154,10 +157,15 @@ class Daemon(object): self._unit_manager.stop_tuning(full_rollback) self._unit_manager.destroy_all() - def _save_active_profile(self, profile_name): + def _save_active_profile(self, profile_name, manual): try: with open(consts.ACTIVE_PROFILE_FILE, "w") as f: - f.write(profile_name + "\n") + if len(profile_name) > 0: + f.write(profile_name + "\n") + if manual: + f.write(consts.ACTIVE_PROFILE_MANUAL + "\n") + else: + f.write(consts.ACTIVE_PROFILE_AUTO + "\n") except (OSError,IOError) as e: log.error("Cannot write active profile into %s: %s" % (consts.ACTIVE_PROFILE_FILE, str(e))) @@ -165,16 +173,33 @@ class Daemon(object): log.info("no profile preset, checking what is recommended for your configuration") profile = self._cmd.recommend_profile(hardcoded = not self._recommend_command) log.info("using '%s' profile and setting it as active" % profile) - self._save_active_profile(profile) + self._save_active_profile(profile, False) return profile - def _get_active_profile(self): + def _get_startup_profile(self): + manual = False try: with open(consts.ACTIVE_PROFILE_FILE, "r") as f: - profile = f.read().strip() - if profile == "": + content = f.read().strip() + if content == "": profile = self._set_recommended_profile() - return profile + else: + arr = content.split('\n') + if len(arr) > 2 or (len(arr) == 2 and arr[1] != consts.ACTIVE_PROFILE_AUTO and arr[1] != consts.ACTIVE_PROFILE_MANUAL): + profile = self._set_recommended_profile() + log.error("cannot read active profile from '%s': bad format. Falling back to '%s' profile." + % consts.ACTIVE_PROFILE_FILE, profile) + else: + profile = arr[0] + if len(arr) == 2: + manual = arr[1] == consts.ACTIVE_PROFILE_MANUAL + if not manual: + profile = self._set_recommended_profile() + else: + # The file has only one line - generated by previous Tuned version. + # Treat the profile as manually set. + manual = True + return (profile, manual) except IOError as e: if e.errno == errno.ENOENT: # No such file or directory @@ -182,10 +207,10 @@ class Daemon(object): else: profile = consts.DEFAULT_PROFILE log.error("error reading active profile from '%s', falling back to '%s' profile" % (consts.ACTIVE_PROFILE_FILE, profile)) - return profile + return (profile, manual) except (OSError, EOFError) as e: log.error("cannot read active profile, falling back to '%s' profile" % consts.DEFAULT_PROFILE) - return consts.DEFAULT_PROFILE + return (consts.DEFAULT_PROFILE, manual) def is_enabled(self): return self._profile is not None