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)
+