diff --git a/Makefile b/Makefile
index e3da864..ee63e91 100644
--- a/Makefile
+++ b/Makefile
@@ -31,6 +31,7 @@ DATADIR = /usr/share
DOCDIR = $(DATADIR)/doc/$(NAME)
PYTHON_SITELIB = $(shell python -c 'from distutils.sysconfig import get_python_lib; print get_python_lib();' || echo /usr/lib/python2.7/site-packages)
TUNED_PROFILESDIR = /usr/lib/tuned
+TUNED_RECOMMEND_DIR = $(TUNED_PROFILESDIR)/recommend.d
BASH_COMPLETIONS = $(DATADIR)/bash-completion/completions
release-dir:
@@ -100,6 +101,7 @@ install-dirs:
mkdir -p $(DESTDIR)/var/log/tuned
mkdir -p $(DESTDIR)/run/tuned
mkdir -p $(DESTDIR)$(DOCDIR)
+ mkdir -p $(DESTDIR)$(TUNED_RECOMMEND_DIR)
install: install-dirs
# library
@@ -135,7 +137,7 @@ install: install-dirs
$(DESTDIR)/etc/tuned/realtime-virtual-host-variables.conf
mv $(DESTDIR)$(TUNED_PROFILESDIR)/cpu-partitioning/cpu-partitioning-variables.conf \
$(DESTDIR)/etc/tuned/cpu-partitioning-variables.conf
- install -pm 0644 recommend.conf $(DESTDIR)$(TUNED_PROFILESDIR)/recommend.conf
+ install -pm 0644 recommend.conf $(DESTDIR)$(TUNED_RECOMMEND_DIR)/50-tuned-recommend.conf
# bash completion
install -Dpm 0644 tuned-adm.bash $(DESTDIR)$(BASH_COMPLETIONS)/tuned-adm
diff --git a/com.redhat.tuned.policy b/com.redhat.tuned.policy
index 1f9e6a7..b34fdab 100644
--- a/com.redhat.tuned.policy
+++ b/com.redhat.tuned.policy
@@ -17,6 +17,16 @@
+
+ Show current profile selection mode
+ Authentication is required to show current profile selection mode
+
+ yes
+ yes
+ yes
+
+
+
Disable Tuned
Authentication is required to disable Tuned
@@ -117,6 +127,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..a5b6b39 100755
--- a/tuned-adm.py
+++ b/tuned-adm.py
@@ -71,6 +71,12 @@ 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")
+
+ parser_profile_mode = subparsers.add_parser("profile_mode", help="show current profile selection mode")
+ parser_profile_mode.set_defaults(action="profile_mode")
+
args = parser.parse_args(sys.argv[1:])
options = vars(args)
diff --git a/tuned/admin/admin.py b/tuned/admin/admin.py
index 040f5cd..fd96b0a 100644
--- a/tuned/admin/admin.py
+++ b/tuned/admin/admin.py
@@ -112,11 +112,29 @@ 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
+ def _get_profile_mode(self):
+ contents = str.strip(self._cmd.read_file(consts.ACTIVE_PROFILE_FILE))
+ if contents == '':
+ mode = consts.ACTIVE_PROFILE_AUTO
+ else:
+ arr = contents.split('\n')
+ if len(arr) == 1:
+ # The file was generated by old Tuned -> manual mode
+ mode = consts.ACTIVE_PROFILE_MANUAL
+ else:
+ mode = arr[1]
+ return mode
+
def _print_profile_info(self, profile, profile_info):
if profile_info[0] == True:
print("Profile name:")
@@ -161,6 +179,19 @@ class Admin(object):
return True
return self._print_profile_name(profile_name)
+ def _print_profile_mode(self, mode):
+ print("Profile selection mode: " + mode)
+
+ def _action_dbus_profile_mode(self):
+ mode = self._controller.profile_mode()
+ self._print_profile_mode(mode)
+ return self._controller.exit(True)
+
+ def _action_profile_mode(self):
+ mode = self._get_profile_mode()
+ self._print_profile_mode(mode)
+ return True
+
def _profile_print_status(self, ret, msg):
if ret:
if not self._controller.is_running() and not self._controller.start():
@@ -197,20 +228,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 +253,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..fa46a0d 100644
--- a/tuned/admin/dbus_controller.py
+++ b/tuned/admin/dbus_controller.py
@@ -99,11 +99,17 @@ class DBusController(object):
def active_profile(self):
return self._call("active_profile")
+ def profile_mode(self):
+ return self._call("profile_mode")
+
def switch_profile(self, new_profile):
if new_profile == "":
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..aa3f38a 100644
--- a/tuned/consts.py
+++ b/tuned/consts.py
@@ -1,7 +1,7 @@
GLOBAL_CONFIG_FILE = "/etc/tuned/tuned-main.conf"
ACTIVE_PROFILE_FILE = "/etc/tuned/active_profile"
PROFILE_FILE = "tuned.conf"
-AUTODETECT_FILE = "recommend.conf"
+RECOMMEND_CONF_FILE = "/etc/tuned/recommend.conf"
DAEMONIZE_PARENT_TIMEOUT = 5
NAMESPACE = "com.redhat.tuned"
DBUS_BUS = NAMESPACE
@@ -12,6 +12,7 @@ DEFAULT_STORAGE_FILE = "/run/tuned/save.pickle"
LOAD_DIRECTORIES = ["/usr/lib/tuned", "/etc/tuned"]
PERSISTENT_STORAGE_DIR = "/var/lib/tuned"
PLUGIN_MAIN_UNIT_NAME = "main"
+RECOMMEND_DIRECTORIES = ["/usr/lib/tuned/recommend.d", "/etc/tuned/recommend.d"]
TMP_FILE_SUFFIX = ".tmp"
# max. number of consecutive errors to give up
@@ -99,3 +100,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..087380b 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 == "":
@@ -119,6 +128,15 @@ class Controller(tuned.exports.interfaces.ExportableInterface):
else:
return ""
+ @exports.export("", "s")
+ def profile_mode(self, caller = None):
+ if caller == "":
+ return ""
+ if self._daemon.manual:
+ return consts.ACTIVE_PROFILE_MANUAL
+ else:
+ return consts.ACTIVE_PROFILE_AUTO
+
@exports.export("", "b")
def disable(self, caller = None):
if caller == "":
@@ -126,7 +144,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, None, save_instantly=True)
return True
@exports.export("", "b")
diff --git a/tuned/daemon/daemon.py b/tuned/daemon/daemon.py
index 8432294..4292575 100644
--- a/tuned/daemon/daemon.py
+++ b/tuned/daemon/daemon.py
@@ -57,36 +57,44 @@ 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):
return self._profile
+ @property
+ def manual(self):
+ # manual == None means /etc/tuned/active_profile is empty -> automatic mode
+ return self._manual == True or self._manual is None
+
@property
def profile_loader(self):
return self._profile_loader
@@ -103,7 +111,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 +162,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 +178,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 +212,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
diff --git a/tuned/utils/commands.py b/tuned/utils/commands.py
index d2d2951..a876a86 100644
--- a/tuned/utils/commands.py
+++ b/tuned/utils/commands.py
@@ -364,40 +364,64 @@ class commands:
s = s.zfill(ls)
return ",".join(s[i:i + 8] for i in range(0, len(s), 8))
+ def process_recommend_file(self, fname):
+ matching_profile = None
+ try:
+ if not os.path.isfile(fname):
+ return None
+ config = ConfigObj(fname, list_values = False, interpolation = False)
+ for section in config.keys():
+ match = True
+ for option in config[section].keys():
+ value = config[section][option]
+ if value == "":
+ value = r"^$"
+ if option == "virt":
+ if not re.match(value, self.execute("virt-what")[1], re.S):
+ match = False
+ elif option == "system":
+ if not re.match(value, self.read_file(consts.SYSTEM_RELEASE_FILE), re.S):
+ match = False
+ elif option[0] == "/":
+ if not os.path.exists(option) or not re.match(value, self.read_file(option), re.S):
+ match = False
+ elif option[0:7] == "process":
+ ps = procfs.pidstats()
+ ps.reload_threads()
+ if len(ps.find_by_regex(re.compile(value))) == 0:
+ match = False
+ if match:
+ # remove the ",.*" suffix
+ r = re.compile(r",[^,]*$")
+ matching_profile = r.sub("", section)
+ break
+ except (IOError, OSError, ConfigObjError) as e:
+ log.error("error processing '%s', %s" % (fname, e))
+ return matching_profile
+
def recommend_profile(self, hardcoded = False):
profile = consts.DEFAULT_PROFILE
if hardcoded:
return profile
- r = re.compile(r",[^,]*$")
- for f in consts.LOAD_DIRECTORIES:
+ matching = self.process_recommend_file(consts.RECOMMEND_CONF_FILE)
+ if matching is not None:
+ return matching
+ files = {}
+ for directory in consts.RECOMMEND_DIRECTORIES:
+ contents = []
try:
- fname = os.path.join(f, consts.AUTODETECT_FILE)
- config = ConfigObj(fname, list_values = False, interpolation = False)
- for section in reversed(config.keys()):
- match = True
- for option in config[section].keys():
- value = config[section][option]
- if value == "":
- value = r"^$"
- if option == "virt":
- if not re.match(value, self.execute("virt-what")[1], re.S):
- match = False
- elif option == "system":
- if not re.match(value, self.read_file(consts.SYSTEM_RELEASE_FILE), re.S):
- match = False
- elif option[0] == "/":
- if not os.path.exists(option) or not re.match(value, self.read_file(option), re.S):
- match = False
- elif option[0:7] == "process":
- ps = procfs.pidstats()
- ps.reload_threads()
- if len(ps.find_by_regex(re.compile(value))) == 0:
- match = False
- if match:
- # remove the ",.*" suffix
- profile = r.sub("", section)
- except (IOError, OSError, ConfigObjError) as e:
- log.error("error parsing '%s', %s" % (fname, e))
+ contents = os.listdir(directory)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ log.error("error accessing %s: %s" % (directory, e))
+ for name in contents:
+ path = os.path.join(directory, name)
+ files[name] = path
+ for name in sorted(files.keys()):
+ path = files[name]
+ matching = self.process_recommend_file(path)
+ if matching is not None:
+ return matching
return profile
# Do not make balancing on patched Python 2 interpreter (rhbz#1028122).