summaryrefslogtreecommitdiff
path: root/lib/ansible/template
diff options
context:
space:
mode:
authorMartin Krizek <martin.krizek@gmail.com>2020-09-03 09:01:34 +0200
committerGitHub <noreply@github.com>2020-09-03 09:01:34 +0200
commita3b954e5c96bbdb48241e4d4ed632cb445750461 (patch)
tree4b63bdb99a6d6a9c23731c0f439cd5df3ebc4aad /lib/ansible/template
parent3d769f3a76a867ea61df16eee328bd40fc91a950 (diff)
downloadansible-a3b954e5c96bbdb48241e4d4ed632cb445750461.tar.gz
Force template module to use non-native Jinja2 (#68560)
Fixes #46169
Diffstat (limited to 'lib/ansible/template')
-rw-r--r--lib/ansible/template/__init__.py114
-rw-r--r--lib/ansible/template/native_helpers.py5
2 files changed, 90 insertions, 29 deletions
diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py
index e19c91ff6a..bfc0251408 100644
--- a/lib/ansible/template/__init__.py
+++ b/lib/ansible/template/__init__.py
@@ -72,13 +72,16 @@ NON_TEMPLATED_TYPES = (bool, Number)
JINJA2_OVERRIDE = '#jinja2:'
from jinja2 import __version__ as j2_version
+from jinja2 import Environment
+from jinja2.utils import concat as j2_concat
+
USE_JINJA2_NATIVE = False
if C.DEFAULT_JINJA2_NATIVE:
try:
- from jinja2.nativetypes import NativeEnvironment as Environment
- from ansible.template.native_helpers import ansible_native_concat as j2_concat
- from ansible.template.native_helpers import NativeJinjaText
+ from jinja2.nativetypes import NativeEnvironment
+ from ansible.template.native_helpers import ansible_native_concat
+ from ansible.utils.native_jinja import NativeJinjaText
USE_JINJA2_NATIVE = True
except ImportError:
from jinja2 import Environment
@@ -87,9 +90,6 @@ if C.DEFAULT_JINJA2_NATIVE:
'jinja2_native requires Jinja 2.10 and above. '
'Version detected: %s. Falling back to default.' % j2_version
)
-else:
- from jinja2 import Environment
- from jinja2.utils import concat as j2_concat
JINJA2_BEGIN_TOKENS = frozenset(('variable_begin', 'block_begin', 'comment_begin', 'raw_begin'))
@@ -398,10 +398,11 @@ class AnsibleContext(Context):
class JinjaPluginIntercept(MutableMapping):
- def __init__(self, delegatee, pluginloader, *args, **kwargs):
+ def __init__(self, delegatee, pluginloader, jinja2_native, *args, **kwargs):
super(JinjaPluginIntercept, self).__init__(*args, **kwargs)
self._delegatee = delegatee
self._pluginloader = pluginloader
+ self._jinja2_native = jinja2_native
if self._pluginloader.class_name == 'FilterModule':
self._method_map_name = 'filters'
@@ -509,7 +510,7 @@ class JinjaPluginIntercept(MutableMapping):
for func_name, func in iteritems(method_map()):
fq_name = '.'.join((parent_prefix, func_name))
# FIXME: detect/warn on intra-collection function name collisions
- if USE_JINJA2_NATIVE and func_name in C.STRING_TYPE_FILTERS:
+ if self._jinja2_native and func_name in C.STRING_TYPE_FILTERS:
self._collection_jinja_func_cache[fq_name] = _wrap_native_text(func)
else:
self._collection_jinja_func_cache[fq_name] = _unroll_iterator(func)
@@ -544,6 +545,9 @@ class AnsibleEnvironment(Environment):
'''
Our custom environment, which simply allows us to override the class-level
values for the Template and Context classes used by jinja2 internally.
+
+ NOTE: Any changes to this class must be reflected in
+ :class:`AnsibleNativeEnvironment` as well.
'''
context_class = AnsibleContext
template_class = AnsibleJ2Template
@@ -551,8 +555,27 @@ class AnsibleEnvironment(Environment):
def __init__(self, *args, **kwargs):
super(AnsibleEnvironment, self).__init__(*args, **kwargs)
- self.filters = JinjaPluginIntercept(self.filters, filter_loader)
- self.tests = JinjaPluginIntercept(self.tests, test_loader)
+ self.filters = JinjaPluginIntercept(self.filters, filter_loader, jinja2_native=False)
+ self.tests = JinjaPluginIntercept(self.tests, test_loader, jinja2_native=False)
+
+
+if USE_JINJA2_NATIVE:
+ class AnsibleNativeEnvironment(NativeEnvironment):
+ '''
+ Our custom environment, which simply allows us to override the class-level
+ values for the Template and Context classes used by jinja2 internally.
+
+ NOTE: Any changes to this class must be reflected in
+ :class:`AnsibleEnvironment` as well.
+ '''
+ context_class = AnsibleContext
+ template_class = AnsibleJ2Template
+
+ def __init__(self, *args, **kwargs):
+ super(AnsibleNativeEnvironment, self).__init__(*args, **kwargs)
+
+ self.filters = JinjaPluginIntercept(self.filters, filter_loader, jinja2_native=True)
+ self.tests = JinjaPluginIntercept(self.tests, test_loader, jinja2_native=True)
class Templar:
@@ -589,7 +612,9 @@ class Templar:
self._fail_on_filter_errors = True
self._fail_on_undefined_errors = C.DEFAULT_UNDEFINED_VAR_BEHAVIOR
- self.environment = AnsibleEnvironment(
+ environment_class = AnsibleNativeEnvironment if USE_JINJA2_NATIVE else AnsibleEnvironment
+
+ self.environment = environment_class(
trim_blocks=True,
undefined=AnsibleUndefined,
extensions=self._get_extensions(),
@@ -609,17 +634,50 @@ class Templar:
# the current rendering context under which the templar class is working
self.cur_context = None
+ # FIXME these regular expressions should be re-compiled each time variable_start_string and variable_end_string are changed
self.SINGLE_VAR = re.compile(r"^%s\s*(\w*)\s*%s$" % (self.environment.variable_start_string, self.environment.variable_end_string))
-
- self._clean_regex = re.compile(r'(?:%s|%s|%s|%s)' % (
- self.environment.variable_start_string,
- self.environment.block_start_string,
- self.environment.block_end_string,
- self.environment.variable_end_string
- ))
self._no_type_regex = re.compile(r'.*?\|\s*(?:%s)(?:\([^\|]*\))?\s*\)?\s*(?:%s)' %
('|'.join(C.STRING_TYPE_FILTERS), self.environment.variable_end_string))
+ @property
+ def jinja2_native(self):
+ return not isinstance(self.environment, AnsibleEnvironment)
+
+ def copy_with_new_env(self, environment_class=AnsibleEnvironment, **kwargs):
+ r"""Creates a new copy of Templar with a new environment. The new environment is based on
+ given environment class and kwargs.
+
+ :kwarg environment_class: Environment class used for creating a new environment.
+ :kwarg \*\*kwargs: Optional arguments for the new environment that override existing
+ environment attributes.
+
+ :returns: Copy of Templar with updated environment.
+ """
+ # We need to use __new__ to skip __init__, mainly not to create a new
+ # environment there only to override it below
+ new_env = object.__new__(environment_class)
+ new_env.__dict__.update(self.environment.__dict__)
+
+ new_templar = object.__new__(Templar)
+ new_templar.__dict__.update(self.__dict__)
+ new_templar.environment = new_env
+
+ mapping = {
+ 'available_variables': new_templar,
+ 'searchpath': new_env.loader,
+ }
+
+ for key, value in kwargs.items():
+ obj = mapping.get(key, new_env)
+ try:
+ if value is not None:
+ setattr(obj, key, value)
+ except AttributeError:
+ # Ignore invalid attrs, lstrip_blocks was added in jinja2==2.7
+ pass
+
+ return new_templar
+
def _get_filters(self):
'''
Returns filter plugins, after loading and caching them if need be
@@ -633,7 +691,7 @@ class Templar:
for fp in self._filter_loader.all():
self._filters.update(fp.filters())
- if USE_JINJA2_NATIVE:
+ if self.jinja2_native:
for string_filter in C.STRING_TYPE_FILTERS:
try:
orig_filter = self._filters[string_filter]
@@ -793,7 +851,7 @@ class Templar:
disable_lookups=disable_lookups,
)
- if not USE_JINJA2_NATIVE:
+ if not self.jinja2_native:
unsafe = hasattr(result, '__UNSAFE__')
if convert_data and not self._no_type_regex.match(variable):
# if this looks like a dictionary or list, convert it to such using the safe_eval method
@@ -916,7 +974,7 @@ class Templar:
# unncessary
return list(thing)
- if USE_JINJA2_NATIVE:
+ if self.jinja2_native:
return thing
return thing if thing is not None else ''
@@ -973,7 +1031,10 @@ class Templar:
ran = wrap_var(ran)
else:
try:
- ran = wrap_var(",".join(ran))
+ if self.jinja2_native and isinstance(ran[0], NativeJinjaText):
+ ran = wrap_var(NativeJinjaText(",".join(ran)))
+ else:
+ ran = wrap_var(",".join(ran))
except TypeError:
# Lookup Plugins should always return lists. Throw an error if that's not
# the case:
@@ -995,7 +1056,7 @@ class Templar:
raise AnsibleError("lookup plugin (%s) not found" % name)
def do_template(self, data, preserve_trailing_newlines=True, escape_backslashes=True, fail_on_undefined=None, overrides=None, disable_lookups=False):
- if USE_JINJA2_NATIVE and not isinstance(data, string_types):
+ if self.jinja2_native and not isinstance(data, string_types):
return data
# For preserving the number of input newlines in the output (used
@@ -1051,7 +1112,10 @@ class Templar:
rf = t.root_render_func(new_context)
try:
- res = j2_concat(rf)
+ if self.jinja2_native:
+ res = ansible_native_concat(rf)
+ else:
+ res = j2_concat(rf)
if getattr(new_context, 'unsafe', False):
res = wrap_var(res)
except TypeError as te:
@@ -1063,7 +1127,7 @@ class Templar:
display.debug("failing because of a type error, template data is: %s" % to_text(data))
raise AnsibleError("Unexpected templating type error occurred on (%s): %s" % (to_native(data), to_native(te)))
- if USE_JINJA2_NATIVE and not isinstance(res, string_types):
+ if self.jinja2_native and not isinstance(res, string_types):
return res
if preserve_trailing_newlines:
diff --git a/lib/ansible/template/native_helpers.py b/lib/ansible/template/native_helpers.py
index 81bef436d9..4eb630614e 100644
--- a/lib/ansible/template/native_helpers.py
+++ b/lib/ansible/template/native_helpers.py
@@ -17,10 +17,7 @@ from ansible.module_utils.common.collections import is_sequence, Mapping
from ansible.module_utils.common.text.converters import container_to_text
from ansible.module_utils.six import PY2, text_type
from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
-
-
-class NativeJinjaText(text_type):
- pass
+from ansible.utils.native_jinja import NativeJinjaText
def _fail_on_undefined(data):