Skip to content

Commit

Permalink
Merge pull request redhat-performance#579 from zacikpa/ppd-daemon
Browse files Browse the repository at this point in the history
PPD-to-TuneD API translation daemon
  • Loading branch information
yarda authored Feb 7, 2024
2 parents b15de12 + 3e03b8b commit 11c6c57
Show file tree
Hide file tree
Showing 17 changed files with 528 additions and 5 deletions.
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ install: install-dirs
install -dD $(DESTDIR)$(DATADIR)/applications
desktop-file-install --dir=$(DESTDIR)$(DATADIR)/applications tuned-gui.desktop

install-ppd: install
$(call install_python_script,tuned-ppd.py,$(DESTDIR)/usr/sbin/tuned-ppd)
install -Dpm 0644 tuned/ppd/tuned-ppd.service $(DESTDIR)$(UNITDIR)/tuned-ppd.service
install -Dpm 0644 tuned/ppd/tuned-ppd.dbus.service $(DESTDIR)$(DATADIR)/dbus-1/system-services/net.hadess.PowerProfiles.service
install -Dpm 0644 tuned/ppd/dbus.conf $(DESTDIR)$(DATADIR)/dbus-1/system.d/net.hadess.PowerProfiles.conf
install -Dpm 0644 tuned/ppd/tuned-ppd.policy $(DESTDIR)$(DATADIR)/polkit-1/actions/net.hadess.PowerProfiles.policy
install -Dpm 0644 tuned/ppd/ppd.conf $(DESTDIR)$(SYSCONFDIR)/tuned/ppd.conf

clean: clean-html
find -name "*.pyc" | xargs rm -f
rm -rf $(VERSIONED_NAME) rpm-build-dir
Expand Down
44 changes: 44 additions & 0 deletions tuned-ppd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/python3 -Es
import sys
import os
import dbus
import signal
from dbus.mainloop.glib import DBusGMainLoop
from tuned import exports
from tuned.ppd import controller
import tuned.consts as consts


def handle_signal(signal_number, handler):
def handler_wrapper(_signal_number, _frame):
if signal_number == _signal_number:
handler()
signal.signal(signal_number, handler_wrapper)

if __name__ == "__main__":
if os.geteuid() != 0:
print("Superuser permissions are required to run the daemon.", file=sys.stderr)
sys.exit(1)

DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
try:
tuned_object = bus.get_object(consts.DBUS_BUS, consts.DBUS_OBJECT)
except dbus.exceptions.DBusException:
print("TuneD not found on the DBus, ensure that it is running.", file=sys.stderr)
sys.exit(1)
tuned_iface = dbus.Interface(tuned_object, consts.DBUS_INTERFACE)

controller = controller.Controller(bus, tuned_iface)

handle_signal(signal.SIGINT, controller.terminate)
handle_signal(signal.SIGTERM, controller.terminate)
handle_signal(signal.SIGHUP, controller.load_config)

dbus_exporter = exports.dbus_with_properties.DBusExporterWithProperties(
consts.PPD_DBUS_BUS, consts.PPD_DBUS_INTERFACE, consts.PPD_DBUS_OBJECT, consts.PPD_NAMESPACE
)

exports.register_exporter(dbus_exporter)
exports.register_object(controller)
controller.run()
2 changes: 1 addition & 1 deletion tuned.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def error(message):
args.no_socket = True

if not args.no_dbus:
app.attach_to_dbus(consts.DBUS_BUS, consts.DBUS_OBJECT, consts.DBUS_INTERFACE)
app.attach_to_dbus(consts.DBUS_BUS, consts.DBUS_OBJECT, consts.DBUS_INTERFACE, consts.NAMESPACE)

if not args.no_socket:
app.attach_to_unix_socket()
Expand Down
20 changes: 20 additions & 0 deletions tuned.spec
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,17 @@ Requires: %{name} = %{version}
%description profiles-openshift
Additional TuneD profile(s) optimized for OpenShift.

%package ppd
Summary: PPD compatibility daemon
Requires: %{name} = %{version}
# The compatibility daemon is swappable for power-profiles-daemon
Provides: ppd-service
Conflicts: ppd-service

%description ppd
An API translation daemon that allows applications to easily transition
to TuneD from power-profiles-daemon (PPD).

%prep
%autosetup -p1 -n %{name}-%{version}%{?prerel2}

Expand All @@ -276,6 +287,7 @@ make html %{make_python_arg}

%install
make install DESTDIR=%{buildroot} DOCDIR=%{docdir} %{make_python_arg}
make install-ppd DESTDIR=%{buildroot} DOCDIR=%{docdir} %{make_python_arg}
%if 0%{?rhel}
sed -i 's/\(dynamic_tuning[ \t]*=[ \t]*\).*/\10/' %{buildroot}%{_sysconfdir}/tuned/tuned-main.conf
%endif
Expand Down Expand Up @@ -556,6 +568,14 @@ fi
%{_prefix}/lib/tuned/openshift-node
%{_mandir}/man7/tuned-profiles-openshift.7*

%files ppd
%{_sbindir}/tuned-ppd
%{_unitdir}/tuned-ppd.service
%{_datadir}/dbus-1/system-services/net.hadess.PowerProfiles.service
%{_datadir}/dbus-1/system.d/net.hadess.PowerProfiles.conf
%{_datadir}/polkit-1/actions/net.hadess.PowerProfiles.policy
%config(noreplace) %{_sysconfdir}/tuned/ppd.conf

%changelog
* Tue Aug 29 2023 Jaroslav Škarvada <[email protected]> - 2.21.0-1
- new release
Expand Down
7 changes: 7 additions & 0 deletions tuned/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@
PREFIX_PROFILE_FACTORY = "System"
PREFIX_PROFILE_USER = "User"

# PPD-to-tuned API translation daemon configuration
PPD_NAMESPACE = "net.hadess.PowerProfiles"
PPD_DBUS_BUS = PPD_NAMESPACE
PPD_DBUS_OBJECT = "/net/hadess/PowerProfiles"
PPD_DBUS_INTERFACE = PPD_DBUS_BUS
PPD_CONFIG_FILE = "/etc/tuned/ppd.conf"

# 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
Expand Down
4 changes: 2 additions & 2 deletions tuned/daemon/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ def _init_signals(self):
self._handle_signal(signal.SIGINT, self._controller.terminate)
self._handle_signal(signal.SIGTERM, self._controller.terminate)

def attach_to_dbus(self, bus_name, object_name, interface_name):
def attach_to_dbus(self, bus_name, object_name, interface_name, namespace):
if self._dbus_exporter is not None:
raise TunedException("DBus interface is already initialized.")

self._dbus_exporter = exports.dbus.DBusExporter(bus_name, interface_name, object_name)
self._dbus_exporter = exports.dbus.DBusExporter(bus_name, interface_name, object_name, namespace)
exports.register_exporter(self._dbus_exporter)

def attach_to_unix_socket(self):
Expand Down
19 changes: 19 additions & 0 deletions tuned/exports/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from . import interfaces
from . import controller
from . import dbus_exporter as dbus
from . import dbus_exporter_with_properties as dbus_with_properties
from . import unix_socket_exporter as unix_socket

def export(*args, **kwargs):
Expand All @@ -17,6 +18,24 @@ def wrapper(method):
return method
return wrapper

def property_setter(*args, **kwargs):
"""Decorator, use to mark setters of exportable properties."""
def wrapper(method):
method.property_set_params = [ args, kwargs ]
return method
return wrapper

def property_getter(*args, **kwargs):
"""Decorator, use to mark getters of exportable properties."""
def wrapper(method):
method.property_get_params = [ args, kwargs ]
return method
return wrapper

def property_changed(*args, **kwargs):
ctl = controller.ExportsController.get_instance()
return ctl.property_changed(*args, **kwargs)

def register_exporter(instance):
if not isinstance(instance, interfaces.ExporterInterface):
raise Exception()
Expand Down
30 changes: 30 additions & 0 deletions tuned/exports/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ def _is_exportable_signal(self, method):
"""Check if method was marked with @exports.signal wrapper."""
return inspect.ismethod(method) and hasattr(method, "signal_params")

def _is_exportable_getter(self, method):
"""Check if method was marked with @exports.get_property wrapper."""
return inspect.ismethod(method) and hasattr(method, "property_get_params")

def _is_exportable_setter(self, method):
"""Check if method was marked with @exports.set_property wrapper."""
return inspect.ismethod(method) and hasattr(method, "property_set_params")

def _export_method(self, method):
"""Register method to all exporters."""
for exporter in self._exporters:
Expand All @@ -43,11 +51,29 @@ def _export_signal(self, method):
kwargs = method.signal_params[1]
exporter.signal(method, *args, **kwargs)

def _export_getter(self, method):
"""Register property getter to all exporters."""
for exporter in self._exporters:
args = method.property_get_params[0]
kwargs = method.property_get_params[1]
exporter.property_getter(method, *args, **kwargs)

def _export_setter(self, method):
"""Register property setter to all exporters."""
for exporter in self._exporters:
args = method.property_set_params[0]
kwargs = method.property_set_params[1]
exporter.property_setter(method, *args, **kwargs)

def send_signal(self, signal, *args, **kwargs):
"""Register signal to all exporters."""
for exporter in self._exporters:
exporter.send_signal(signal, *args, **kwargs)

def property_changed(self, *args, **kwargs):
for exporter in self._exporters:
exporter.property_changed(*args, **kwargs)

def period_check(self):
"""Allows to perform checks on exporters without special thread."""
for exporter in self._exporters:
Expand All @@ -62,6 +88,10 @@ def _initialize_exports(self):
self._export_method(method)
for name, method in inspect.getmembers(instance, self._is_exportable_signal):
self._export_signal(method)
for name, method in inspect.getmembers(instance, self._is_exportable_getter):
self._export_getter(method)
for name, method in inspect.getmembers(instance, self._is_exportable_setter):
self._export_setter(method)

self._exports_initialized = True

Expand Down
5 changes: 3 additions & 2 deletions tuned/exports/dbus_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class DBusExporter(interfaces.ExporterInterface):
to an object we dynamically construct.
"""

def __init__(self, bus_name, interface_name, object_name):
def __init__(self, bus_name, interface_name, object_name, namespace):
# Monkey patching of the D-Bus library _method_reply_error() to reply
# tracebacks via D-Bus only if in the debug mode. It doesn't seem there is a
# more simple way how to cover all possible exceptions that could occur in
Expand All @@ -82,6 +82,7 @@ def __init__(self, bus_name, interface_name, object_name):
self._bus_name = bus_name
self._interface_name = interface_name
self._object_name = object_name
self._namespace = namespace
self._thread = None
self._bus_object = None
self._polkit = polkit()
Expand Down Expand Up @@ -130,7 +131,7 @@ def export(self, method, in_signature, out_signature):
raise Exception("Method with this name is already exported.")

def wrapper(owner, *args, **kwargs):
action_id = consts.NAMESPACE + "." + method.__name__
action_id = self._namespace + "." + method.__name__
caller = args[-1]
log.debug("checking authorization for action '%s' requested by caller '%s'" % (action_id, caller))
ret = self._polkit.check_authorization(caller, action_id)
Expand Down
60 changes: 60 additions & 0 deletions tuned/exports/dbus_exporter_with_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from inspect import ismethod
from dbus.service import method, signal
from dbus import PROPERTIES_IFACE
from dbus.exceptions import DBusException
from tuned.exports.dbus_exporter import DBusExporter


class DBusExporterWithProperties(DBusExporter):
def __init__(self, bus_name, interface_name, object_name, namespace):
super(DBusExporterWithProperties, self).__init__(bus_name, interface_name, object_name, namespace)
self._property_setters = {}
self._property_getters = {}

def Get(_, interface_name, property_name):
if interface_name != self._interface_name:
raise DBusException("Unknown interface: %s" % interface_name)
if property_name not in self._property_getters:
raise DBusException("No such property: %s" % property_name)
getter = self._property_getters[property_name]
return getter()

def Set(_, interface_name, property_name, value):
if interface_name != self._interface_name:
raise DBusException("Unknown interface: %s" % interface_name)
if property_name not in self._property_setters:
raise DBusException("No such property: %s" % property_name)
setter = self._property_setters[property_name]
setter(value)

def GetAll(_, interface_name):
if interface_name != self._interface_name:
raise DBusException("Unknown interface: %s" % interface_name)
return {name: getter() for name, getter in self._property_getters.items()}

def PropertiesChanged(_, interface_name, changed_properties, invalidated_properties):
if interface_name != self._interface_name:
raise DBusException("Unknown interface: %s" % interface_name)

self._dbus_methods["Get"] = method(PROPERTIES_IFACE, in_signature="ss", out_signature="v")(Get)
self._dbus_methods["Set"] = method(PROPERTIES_IFACE, in_signature="ssv")(Set)
self._dbus_methods["GetAll"] = method(PROPERTIES_IFACE, in_signature="s", out_signature="a{sv}")(GetAll)
self._dbus_methods["PropertiesChanged"] = signal(PROPERTIES_IFACE, signature="sa{sv}as")(PropertiesChanged)
self._signals.add("PropertiesChanged")

def property_changed(self, property_name, value):
self.send_signal("PropertiesChanged", self._interface_name, {property_name: value}, {})

def property_getter(self, method, property_name):
if not ismethod(method):
raise Exception("Only bound methods can be exported.")
if property_name in self._property_getters:
raise Exception("A getter for this property is already registered.")
self._property_getters[property_name] = method

def property_setter(self, method, property_name):
if not ismethod(method):
raise Exception("Only bound methods can be exported.")
if property_name in self._property_setters:
raise Exception("A setter for this property is already registered.")
self._property_setters[property_name] = method
61 changes: 61 additions & 0 deletions tuned/ppd/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from tuned.utils.config_parser import ConfigParser, Error
from tuned.exceptions import TunedException
import os

PPD_POWER_SAVER = "power-saver"
PPD_PERFORMANCE = "performance"

MAIN_SECTION = "main"
PROFILES_SECTION = "profiles"
DEFAULT_PROFILE_OPTION = "default"


class PPDConfig:
def __init__(self, config_file):
self.load_from_file(config_file)

@property
def default_profile(self):
return self._default_profile

@property
def ppd_to_tuned(self):
return self._ppd_to_tuned

@property
def tuned_to_ppd(self):
return self._tuned_to_ppd

def load_from_file(self, config_file):
cfg = ConfigParser()

if not os.path.isfile(config_file):
raise TunedException("Configuration file '%s' does not exist" % config_file)
try:
cfg.read(config_file)
except Error:
raise TunedException("Error parsing the configuration file '%s'" % config_file)

if PROFILES_SECTION not in cfg:
raise TunedException("Missing profiles section in the configuration file '%s'" % config_file)
self._ppd_to_tuned = dict(cfg[PROFILES_SECTION])

if not all(isinstance(mapped_profile, str) for mapped_profile in self._ppd_to_tuned.values()):
raise TunedException("Invalid profile mapping in the configuration file '%s'" % config_file)

if len(set(self._ppd_to_tuned.values())) != len(self._ppd_to_tuned):
raise TunedException("Duplicate profile mapping in the configuration file '%s'" % config_file)
self._tuned_to_ppd = {v: k for k, v in self._ppd_to_tuned.items()}

if PPD_POWER_SAVER not in self._ppd_to_tuned:
raise TunedException("Missing power-saver profile in the configuration file '%s'" % config_file)

if PPD_PERFORMANCE not in self._ppd_to_tuned:
raise TunedException("Missing performance profile in the configuration file '%s'" % config_file)

if MAIN_SECTION not in cfg or DEFAULT_PROFILE_OPTION not in cfg[MAIN_SECTION]:
raise TunedException("Missing default profile in the configuration file '%s'" % config_file)

self._default_profile = cfg[MAIN_SECTION][DEFAULT_PROFILE_OPTION]
if self._default_profile not in self._ppd_to_tuned:
raise TunedException("Unknown default profile '%s'" % self._default_profile)
Loading

0 comments on commit 11c6c57

Please sign in to comment.