Commit 4098e828 by Brian Coca

several fixes to template

- now obeys global undefined var setting and allows override (mostly for with_ )
- moved environment instanciation to init instead of each template call
- removed hardcoded template token matching and now use actually configured tokens, now it won't break if someone changes default configs in ansible.cfg
- made reenetrant template calls now pass the same data it got, dictionary and lists were loosing existing and new params
- moved fail_on_undeinfed parameter to template call, as it should only realky be set to false on specific templates and not globally
- added overrides, which will allow template to implement jinja2 header override features
- added filter list to overrides to disallow possibly insecure ones, TODO: check if this is still needed as facts should not be templated anymore
- TODO: actually implement jinja2 header overrides
parent 7291f9e9
...@@ -40,20 +40,19 @@ __all__ = ['Templar'] ...@@ -40,20 +40,19 @@ __all__ = ['Templar']
# A regex for checking to see if a variable we're trying to # A regex for checking to see if a variable we're trying to
# expand is just a single variable name. # expand is just a single variable name.
SINGLE_VAR = re.compile(r"^{{\s*(\w*)\s*}}$")
# Primitive Types which we don't want Jinja to convert to strings. # Primitive Types which we don't want Jinja to convert to strings.
NON_TEMPLATED_TYPES = ( bool, Number ) NON_TEMPLATED_TYPES = ( bool, Number )
JINJA2_OVERRIDE = '#jinja2:' JINJA2_OVERRIDE = '#jinja2:'
JINJA2_ALLOWED_OVERRIDES = ['trim_blocks', 'lstrip_blocks', 'newline_sequence', 'keep_trailing_newline'] JINJA2_ALLOWED_OVERRIDES = frozenset(['trim_blocks', 'lstrip_blocks', 'newline_sequence', 'keep_trailing_newline'])
class Templar: class Templar:
''' '''
The main class for templating, with the main entry-point of template(). The main class for templating, with the main entry-point of template().
''' '''
def __init__(self, loader, shared_loader_obj=None, variables=dict(), fail_on_undefined=C.DEFAULT_UNDEFINED_VAR_BEHAVIOR): def __init__(self, loader, shared_loader_obj=None, variables=dict()):
self._loader = loader self._loader = loader
self._basedir = loader.get_basedir() self._basedir = loader.get_basedir()
self._filters = None self._filters = None
...@@ -70,7 +69,12 @@ class Templar: ...@@ -70,7 +69,12 @@ class Templar:
# should result in fatal errors being raised # should result in fatal errors being raised
self._fail_on_lookup_errors = True self._fail_on_lookup_errors = True
self._fail_on_filter_errors = True self._fail_on_filter_errors = True
self._fail_on_undefined_errors = fail_on_undefined self._fail_on_undefined_errors = C.DEFAULT_UNDEFINED_VAR_BEHAVIOR
self.environment = Environment(trim_blocks=True, undefined=StrictUndefined, extensions=self._get_extensions(), finalize=self._finalize)
self.environment.template_class = AnsibleJ2Template
self.SINGLE_VAR = re.compile(r"^%s\s*(\w*)\s*%s$" % (self.environment.variable_start_string, self.environment.variable_end_string))
def _count_newlines_from_end(self, in_str): def _count_newlines_from_end(self, in_str):
''' '''
...@@ -129,7 +133,7 @@ class Templar: ...@@ -129,7 +133,7 @@ class Templar:
assert isinstance(variables, dict) assert isinstance(variables, dict)
self._available_variables = variables.copy() self._available_variables = variables.copy()
def template(self, variable, convert_bare=False, preserve_trailing_newlines=False): def template(self, variable, convert_bare=False, preserve_trailing_newlines=False, fail_on_undefined=None, overrides=None):
''' '''
Templates (possibly recursively) any given data as input. If convert_bare is Templates (possibly recursively) any given data as input. If convert_bare is
set to True, the given data will be wrapped as a jinja2 variable ('{{foo}}') set to True, the given data will be wrapped as a jinja2 variable ('{{foo}}')
...@@ -147,7 +151,7 @@ class Templar: ...@@ -147,7 +151,7 @@ class Templar:
# Check to see if the string we are trying to render is just referencing a single # Check to see if the string we are trying to render is just referencing a single
# var. In this case we don't want to accidentally change the type of the variable # var. In this case we don't want to accidentally change the type of the variable
# to a string by using the jinja template renderer. We just want to pass it. # to a string by using the jinja template renderer. We just want to pass it.
only_one = SINGLE_VAR.match(variable) only_one = self.SINGLE_VAR.match(variable)
if only_one: if only_one:
var_name = only_one.group(1) var_name = only_one.group(1)
if var_name in self._available_variables: if var_name in self._available_variables:
...@@ -155,10 +159,10 @@ class Templar: ...@@ -155,10 +159,10 @@ class Templar:
if isinstance(resolved_val, NON_TEMPLATED_TYPES): if isinstance(resolved_val, NON_TEMPLATED_TYPES):
return resolved_val return resolved_val
result = self._do_template(variable, preserve_trailing_newlines=preserve_trailing_newlines) result = self._do_template(variable, preserve_trailing_newlines=preserve_trailing_newlines, fail_on_undefined=fail_on_undefined, overrides=overrides)
# if this looks like a dictionary or list, convert it to such using the safe_eval method # if this looks like a dictionary or list, convert it to such using the safe_eval method
if (result.startswith("{") and not result.startswith("{{")) or result.startswith("["): if (result.startswith("{") and not result.startswith(self.environment.variable_start_string)) or result.startswith("["):
eval_results = safe_eval(result, locals=self._available_variables, include_exceptions=True) eval_results = safe_eval(result, locals=self._available_variables, include_exceptions=True)
if eval_results[1] is None: if eval_results[1] is None:
result = eval_results[0] result = eval_results[0]
...@@ -169,11 +173,11 @@ class Templar: ...@@ -169,11 +173,11 @@ class Templar:
return result return result
elif isinstance(variable, (list, tuple)): elif isinstance(variable, (list, tuple)):
return [self.template(v, convert_bare=convert_bare) for v in variable] return [self.template(v, convert_bare=convert_bare, preserve_trailing_newlines=preserve_trailing_newlines, fail_on_undefined=fail_on_undefined, overrides=overrides) for v in variable]
elif isinstance(variable, dict): elif isinstance(variable, dict):
d = {} d = {}
for (k, v) in variable.iteritems(): for (k, v) in variable.iteritems():
d[k] = self.template(v, convert_bare=convert_bare) d[k] = self.template(v, convert_bare=convert_bare, preserve_trailing_newlines=preserve_trailing_newlines, fail_on_undefined=fail_on_undefined, overrides=overrides)
return d return d
else: else:
return variable return variable
...@@ -188,7 +192,7 @@ class Templar: ...@@ -188,7 +192,7 @@ class Templar:
''' '''
returns True if the data contains a variable pattern returns True if the data contains a variable pattern
''' '''
return "$" in data or "{{" in data or '{%' in data return self.environment.block_start_string in data or self.environment.variable_start_string in data
def _convert_bare_variable(self, variable): def _convert_bare_variable(self, variable):
''' '''
...@@ -198,8 +202,8 @@ class Templar: ...@@ -198,8 +202,8 @@ class Templar:
if isinstance(variable, basestring): if isinstance(variable, basestring):
first_part = variable.split(".")[0].split("[")[0] first_part = variable.split(".")[0].split("[")[0]
if first_part in self._available_variables and '{{' not in variable and '$' not in variable: if first_part in self._available_variables and self.environment.variable_start_string not in variable:
return "{{%s}}" % variable return "%s%s%s" % (self.environment.variable_start_string, variable, self.environment.variable_end_string)
# the variable didn't meet the conditions to be converted, # the variable didn't meet the conditions to be converted,
# so just return it as-is # so just return it as-is
...@@ -230,16 +234,24 @@ class Templar: ...@@ -230,16 +234,24 @@ class Templar:
else: else:
raise AnsibleError("lookup plugin (%s) not found" % name) raise AnsibleError("lookup plugin (%s) not found" % name)
def _do_template(self, data, preserve_trailing_newlines=False): def _do_template(self, data, preserve_trailing_newlines=False, fail_on_undefined=None, overrides=None):
if fail_on_undefined is None:
fail_on_undefined = self._fail_on_undefined_errors
try: try:
# allows template header overrides to change jinja2 options.
if overrides is None:
myenv = self.environment.overlay()
else:
overrides = JINJA2_ALLOWED_OVERRIDES.intersection(set(overrides))
myenv = self.environment.overlay(overrides)
environment = Environment(trim_blocks=True, undefined=StrictUndefined, extensions=self._get_extensions(), finalize=self._finalize) #FIXME: add tests
environment.filters.update(self._get_filters()) myenv.filters.update(self._get_filters())
environment.template_class = AnsibleJ2Template
try: try:
t = environment.from_string(data) t = myenv.from_string(data)
except TemplateSyntaxError, e: except TemplateSyntaxError, e:
raise AnsibleError("template error while templating string: %s" % str(e)) raise AnsibleError("template error while templating string: %s" % str(e))
except Exception, e: except Exception, e:
...@@ -280,8 +292,9 @@ class Templar: ...@@ -280,8 +292,9 @@ class Templar:
return res return res
except (UndefinedError, AnsibleUndefinedVariable), e: except (UndefinedError, AnsibleUndefinedVariable), e:
if self._fail_on_undefined_errors: if fail_on_undefined:
raise raise
else: else:
#TODO: return warning about undefined var
return data return data
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment