1
0
Fork 0

tuned: added support for built-in functions

Built-in functions are defined as plugins in tuned/profiles/functions.
Each function (plugin) is python code named function_NAME.py, where
NAME is the name of the built-in function defined. For example
how to implement own function see provided functions.

Functions are expanded in tuned profile configuration file during profile
load. Functions are expanded after variables. The syntax for function call is
${f:NAME:ARG1:ARG2...}. The function NAME will be called and it's result
substituted. It is possible to escape $ by \$ and : by \:. Functions are
loaded and executed on demand. Function which is once loaded during profile
application is not reloaded on multiple executions. Variables
in profile arguments are supported, but function calls aren't.

The following functions have been implemented so far:

exec:ARG1:...:ARGN - execute external command with arguments and subtitute
                     its output.
kb2s:ARG           - converts kbytes specified as ARG to sectors.
s2kb:ARG           - converts sectors specified as ARG to kbytes.

Example:

[variables]
cmd = date
curr_date = ${f:exec:${cmd}}

This commit also changed escaping of variables to be consistent with
functions. They can be now escaped by \, i.e. \${VAR} will not be expanded.
Escaping by double '$' is no more supported.

This commit also removes handler for all exceptions during profile loading.
This is not to mask real errors and ease development.

Resolves: rhbz#1225135

Signed-off-by: Jaroslav Škarvada <jskarvad@redhat.com>
This commit is contained in:
Jaroslav Škarvada 2015-06-05 15:53:59 +02:00
parent 8f618c92ea
commit 8968280a4f
12 changed files with 202 additions and 5 deletions

View file

@ -30,6 +30,8 @@ LOG_FILE_MAXBYTES = 100*1000
LOG_FILE = "/var/log/tuned/tuned.log"
PID_FILE = "/run/tuned/tuned.pid"
SYSTEM_RELEASE_FILE = "/etc/system-release-cpe"
# prefix for functions plugins
FUNCTION_PREFIX = "function_"
# prefix for exported environment variables when calling scripts
ENV_PREFIX = "TUNED_"

View file

@ -3,6 +3,7 @@ import errno
import threading
import tuned.logs
from tuned.exceptions import TunedException
from tuned.profiles.exceptions import InvalidProfileException
import tuned.consts as consts
from tuned.utils.commands import commands
@ -68,7 +69,7 @@ class Daemon(object):
else:
try:
self._profile = self._profile_loader.load(profile_name)
except:
except InvalidProfileException:
raise TunedException("Cannot load profile '%s'." % profile_name)
if save_instantly:

View file

@ -5,3 +5,4 @@ from tuned.profiles.unit import *
from tuned.profiles.exceptions import *
from tuned.profiles.factory import *
from tuned.profiles.merger import *
import functions

View file

@ -0,0 +1 @@
from repository import Repository

View file

@ -0,0 +1,34 @@
import os
import tuned.logs
from tuned.utils.commands import commands
log = tuned.logs.get()
class Function(object):
"""
Built-in function
"""
def __init__(self, name, nargs_max, nargs_min = None):
self._name = name
self._nargs_max = nargs_max
self._nargs_min = nargs_min
self._cmd = commands()
# checks arguments
# nargs_max - maximal number of arguments, there mustn't be more arguments,
# if nargs_max is 0, number of arguments is unlimited
# nargs_min - minimal number of arguments, if not None there must
# be the same number of arguments or more
@classmethod
def _check_args(cls, args, nargs_max, nargs_min = None):
if args is None or nargs_max is None:
return False
la = len(args)
return (nargs_max == 0 or nargs_max == la) and (nargs_min is None or nargs_min <= la)
def execute(self, args):
if self._check_args(args, self._nargs_max, self._nargs_min):
return True
else:
log.error("invalid number of arguments for builtin function '%s'" % self._name)
return False

View file

@ -0,0 +1,20 @@
import os
import tuned.logs
import base
from tuned.utils.commands import commands
class s2kb(base.Function):
"""
Conversion function: sectors to kbytes
"""
def __init__(self):
# unlimited number of arguments, min 1 argument (the name of executable)
super(self.__class__, self).__init__("s2kb", 0, 1)
def execute(self, args):
if not super(self.__class__, self).execute(args):
return None
(ret, out) = self._cmd.execute(args)
if ret == 0:
return out
return None

View file

@ -0,0 +1,20 @@
import os
import tuned.logs
import base
from tuned.utils.commands import commands
class kb2s(base.Function):
"""
Conversion function: kbytes to sectors
"""
def __init__(self):
# 1 argument
super(self.__class__, self).__init__("kb2s", 1)
def execute(self, args):
if not super(self.__class__, self).execute(args):
return None
try:
return str(int(args[0]) * 2)
except ValueError:
return None

View file

@ -0,0 +1,20 @@
import os
import tuned.logs
import base
from tuned.utils.commands import commands
class s2kb(base.Function):
"""
Conversion function: sectors to kbytes
"""
def __init__(self):
# 1 argument
super(self.__class__, self).__init__("s2kb", 1)
def execute(self, args):
if not super(self.__class__, self).execute(args):
return None
try:
return str(int(args[0]) / 2)
except ValueError:
return None

View file

@ -0,0 +1,48 @@
import os
import re
import glob
import repository
import tuned.logs
import tuned.consts as consts
from tuned.utils.commands import commands
log = tuned.logs.get()
cmd = commands()
class Functions():
"""
Built-in functions
"""
def __init__(self):
self._repository = repository.Repository()
def sub_func(self, mo):
sorig = mo.string[mo.start():mo.end()]
if mo.lastindex != 1:
return sorig
s = mo.string[mo.start(1):mo.end(1)]
if len(s) == 0:
return sorig
sl = re.split(r'(?<!\\):', s)
sl = map(lambda v: str(v).replace("\:", ":"), sl)
if not re.match(r'\w+$', sl[0]):
log.error("invalid function name '%s'" % sl[0])
return sorig
try:
f = self._repository.load_func(sl[0])
except ImportError:
log.error("function '%s' not implemented" % sl[0])
return sorig
s = f.execute(sl[1:])
if s is None:
return sorig
return s
def expand(self, s):
if s is None:
return s
r = re.compile(r'(?<!\\)\${f:([\w:\\]+)}')
# expand functions and convert all \${f:*} to ${f:*} (unescape)
return re.sub(r'\\(\${f:[\w:\\]+})', r'\1', r.sub(self.sub_func, s))

View file

@ -0,0 +1,43 @@
from tuned.utils.plugin_loader import PluginLoader
import base
import tuned.logs
import tuned.consts as consts
from tuned.utils.commands import commands
log = tuned.logs.get()
class Repository(PluginLoader):
def __init__(self):
super(self.__class__, self).__init__()
self._functions = {}
@property
def functions(self):
return self._functions
def _set_loader_parameters(self):
self._namespace = "tuned.profiles.functions"
self._prefix = consts.FUNCTION_PREFIX
self._interface = tuned.profiles.functions.base.Function
def create(self, function_name):
log.debug("creating function %s" % function_name)
function_cls = self.load_plugin(function_name)
function_instance = function_cls()
self._functions[function_name] = function_instance
return function_instance
# loads function from plugin file and return it
# if it is already loaded, just return it, it is not loaded again
def load_func(self, function_name):
if not function_name in self._functions:
return self.create(function_name)
return self._functions[function_name]
def delete(self, function):
assert isinstance(function, self._interface)
log.debug("removing function %s" % function)
for k, v in self._functions.items():
if v == function:
del self._functions[k]

View file

@ -5,7 +5,6 @@ import os.path
import collections
import tuned.logs
import re
from tuned.profiles.exceptions import InvalidProfileException
log = tuned.logs.get()

View file

@ -1,6 +1,7 @@
import os
import re
import tuned.logs
import functions.functions as functions
import tuned.consts as consts
from tuned.utils.commands import commands
from configobj import ConfigObj
@ -16,6 +17,7 @@ class Variables():
self._cmd = commands()
self._lookup_re = {}
self._lookup_env = {}
self._functions = functions.Functions()
def _add_env_prefix(self, s, prefix):
if s.find(prefix) == 0:
@ -35,7 +37,7 @@ class Variables():
v = self.expand(value)
# variables referenced by ${VAR}, $ can be escaped by two $,
# i.e. the following will not expand: $${VAR}
self._lookup_re[r'(?<!\$)\${' + re.escape(s) + r'}'] = v
self._lookup_re[r'(?<!\\)\${' + re.escape(s) + r'}'] = v
self._lookup_env[self._add_env_prefix(s, consts.ENV_PREFIX)] = v
def add_dict(self, d):
@ -64,11 +66,17 @@ class Variables():
else:
self.add_variable(item, cfg[item])
# expand static variables (no functions)
def expand_static(self, value):
return re.sub(r'\\(\${\w+})', r'\1', self._cmd.multiple_re_replace(self._lookup_re, value))
def expand(self, value):
if value is None:
return None
# expand variables and finally convert all $${VAR} to ${VAR} (unescape)
return re.sub(r'\$(\${\w+})', r'\1', self._cmd.multiple_re_replace(self._lookup_re, str(value)))
# expand variables and convert all \${VAR} to ${VAR} (unescape)
s = self.expand_static(str(value))
# expand built-in functions
return self._functions.expand(s)
def get_env(self):
return self._lookup_env