diff --git a/anabot/launcher.py b/anabot/launcher.py index 90875cfb..83a39d37 100644 --- a/anabot/launcher.py +++ b/anabot/launcher.py @@ -5,6 +5,9 @@ from logging.handlers import SysLogHandler from anabot import config from anabot.variables import set_variable, get_variable, set_env_variable +import shlex + +CMDLINE_VAR_PREFIX = "anabot." def show_help(arg0): print('%s profile_name [recipe_url] [varname=value[,varname=value]]' % sys.argv[0]) @@ -107,6 +110,25 @@ def main(*args): logger.debug('Running preexec hooks') run_preexechooks() + # import variables from kernel command line after removing CMDLINE_VAR_PREFIX + with open("/proc/cmdline", "r") as c: + cmdline = c.read() + cmdline_params = shlex.split(cmdline) + var_params = [p for p in cmdline_params if p.startswith(CMDLINE_VAR_PREFIX)] + for param in var_params: + var_pair = param.split("=") + var_name = var_pair[0][len(CMDLINE_VAR_PREFIX):] # get rid of prefix + var_value = var_pair[1] + if get_variable(var_name, None): + var_message = "Redefining internal variable '%s' from command line "\ + "to '%s' (originally set to '%s')!" % \ + (var_name, var_value, get_variable(var_name)) + else: + var_message = "Setting internal variable from command line: %s = '%s'" % \ + (var_name, var_value) + set_variable(var_name, var_value) + logger.info(var_message) + # check recipe if not os.path.exists("/var/run/anabot/raw-recipe.xml"): reporter.log_error("No anabot recipe found!") diff --git a/anabot/preprocessor/sub.py b/anabot/preprocessor/sub.py index 1ac46585..21521dcb 100644 --- a/anabot/preprocessor/sub.py +++ b/anabot/preprocessor/sub.py @@ -11,4 +11,7 @@ def sub_element(element): element.setName(name) element.unsetNsProp(element.ns(), "name") for attribute in element.xpathEval("@*"): - attribute.setContent(attribute.content.format(**get_variables())) + try: + attribute.setContent(attribute.content.format(**get_variables())) + except KeyError as e: + raise Exception("Undefined variable for substitution in Anabot recipe: %s" % e.args[0]) diff --git a/doc/external_variables.rst b/doc/external_variables.rst new file mode 100644 index 00000000..6be7bc61 --- /dev/null +++ b/doc/external_variables.rst @@ -0,0 +1,75 @@ +================== +External variables +================== + +It is possible to pass external variables to Anabot, using either +kernel command line or Beaker task parameters. The main use +of externally defined variables is for enhancing recipes, where +a variable placeholder can be substituted by Anabot's preprocessor, +thus providing a greater flexibility in comparison with just static +recipes. + +.. note:: + Even though the main reason for introducing external variables + is their use in recipes, it is technically possible to use + those variables generally in Anabot's code, since those variables + are internally handled by |get_variable|_ and |set_variable|_. + + .. |get_variable| replace:: ``get_variable()`` + .. _get_variable: https://github.com/rhinstaller/anabot/blob/main/anabot/variables.py + .. |set_variable| replace:: ``set_variable()`` + .. _set_variable: https://github.com/rhinstaller/anabot/blob/main/anabot/variables.py + +.. warning:: + When defining external variables, chooose their names wisely. + As mentioned in the note above, they occupy the shared internal + variables space, which will lead to a clash when another variable with + the same name is used elsewhere in Anabot's code, leading to + undefined behaviour. + + A list of already existing variables in Anabot code using this + mechanism can be obtained for instance by running the following command: + in Anabot repository: + ``git grep -Pho "(get|set)_variable\([\"']\K[^'\"]+" | sort -u`` + +.. warning:: + If a variable is defined both on kernel command line and in Beaker + task parameter, the command line one takes precedence. However, it is best + to avoid defining the same variable using both mechanisms at the same time. + +Kernel command line +=================== +It is possible to define an external variable on kernel command line using +``anabot.variable_name=variable_value`` or ``anabot.variable_name="variable value"``. +The ``anabot.`` prefix will be removed and a variable named ``variable_name`` +with value ``variable_value`` (or ``variable value`` respectively) will be defined. + +Beaker task parameters +====================== +Another option to pass the variables is to use Beaker task parameters. +The parameters use ``ANABOT_SUB_`` prefix, which is removed and the rest +of variable name is converted to lowercase. + +For example, ```` will translate +into a variable named ``lang`` with value ``Czech``. + +Use in recipes +============== +Variable placeholders in recipes are substituted by external variables passed +to Anabot by means of Python string template formatting (``str.format()``) +in conjunction with a dedicated ```` element. This element handles +a substitution for an element specified by its ``ez:name`` attribute. + +.. note:: + It's necessary to use the ``sub`` element with the ``ez`` namespace specified, + otherwise it won't be recognized and Anabot will end up with an error. + The same notice is valid for the ``name`` attribute. + +It is strongly advised to use only **lowercase** placeholder names, since variable +names originating from Beaker parameters always get converted to lowercase. + +For example, ```` in the recipe with the +``lang`` variable set to ``Czech`` (defined by ``anabot.lang`` on kernel command +line or ``ANABOT_SUB_LANG`` Beaker task parameter) will get substituted for +````. + diff --git a/doc/index.rst b/doc/index.rst index f78492f1..e3994dbc 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -48,6 +48,7 @@ Contents 101 workflow + external_variables decorators history debugging diff --git a/examples/sub.xml b/examples/sub.xml deleted file mode 100644 index 7df5c452..00000000 --- a/examples/sub.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/examples/variable_substitution.xml b/examples/variable_substitution.xml new file mode 100644 index 00000000..da6a1c56 --- /dev/null +++ b/examples/variable_substitution.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/modules/beaker/__init__.py b/modules/beaker/__init__.py index b2adf040..68b5a1d1 100644 --- a/modules/beaker/__init__.py +++ b/modules/beaker/__init__.py @@ -25,6 +25,7 @@ from anabot.variables import set_variable, get_variable BEAKER = "https://%s" % get_variable('beaker_hub_hostname') +EXTERNAL_VAR_PREFIX = "ANABOT_SUB_" with open('/proc/cmdline', 'r') as proc_cmdline: cmdline = proc_cmdline.read() @@ -204,3 +205,23 @@ def param_value(name, default=None, empty_default=True): set_variable("beta", "1") else: rep.log_debug("BETA task param not found, empty or zero in beaker recipe") + +# get all parameter names for the current Beaker task +def get_param_names(): + param_xpath = '/job/recipeSet/recipe/task[@id="%s"]/params/param/@name' % task_id + param_names = [name.content for name in xml.xpathEval(param_xpath)] + if len(param_names) > 0: + rep.log_debug("Current Beaker task parameters: %s" % param_names) + else: + rep.log_debug("No parameters for current Beaker task '%s' found!" % task_id) + return param_names + +# use all task parameters beginning with EXTERNAL_VAR_PREFIX as Anabot variables, +# the variable name will be parameter name stripped from prefix and converted to lowercase +for param_name in [n for n in get_param_names() if n.startswith(EXTERNAL_VAR_PREFIX)]: + var_name = param_name[len(EXTERNAL_VAR_PREFIX):].lower() + var_value = param_value(param_name) + rep.log_debug("Setting internal variable based on task parameter: %s = '%s'" % + (var_name, var_value)) + set_variable(var_name, var_value) +