File 0002-Ensure-ANSIBLE_NO_LOG-is-respected-CVE-2024-0690-825.patch of Package ansible.37522
From f5cb50f79af310b917e6932a0c0d8e9a73261b7f Mon Sep 17 00:00:00 2001
From: Matt Martz <matt@sivel.net>
Date: Thu, 18 Jan 2024 17:17:23 -0600
Subject: [PATCH 2/2] [stable-2.14] Ensure ANSIBLE_NO_LOG is respected
(CVE-2024-0690) (#82565) (#82568)
(cherry picked from commit 6935c8e)
Force template module to use non-native Jinja2 (#68560)
Fixes #46169
Auto unroll generators produced by jinja filters (#68014)
* Auto unroll generators produced by jinja filters
* Unroll for native in finalize
* Fix indentation
Co-authored-by: Sam Doran <sdoran@redhat.com>
* Add changelog fragment
* ci_complete
* Always unroll regardless of jinja2
* ci_complete
Co-authored-by: Sam Doran <sdoran@redhat.com>
Skip literal_eval for string filters results in native jinja. (#70988) (#71313)
Fixes #70831
(cherry picked from commit b66d66027ece03f3f0a3fdb5fd6b8213965a2f1d)
Introduce context manager for temporary templar context changes (#60513)
* Introduce context manager for temporary templar context changes. Fixes #60106
* Rename and docstring
* Make set_temporary_context more generic, don't hardcode each thing you can set, apply to template action too
* not None
* linting fix
* Ignore invalid attrs
* Catch the right things, loop the right things
* Use set_temporary_context in a few extra action plugins
---
.../46169-non-native-template-module.yml | 2 +
.../60106-templar-contextmanager.yml | 4 +
.../68014-auto-unroll-jinja2-generators.yml | 3 +
...iteral_eval-string-filter-native-jinja.yml | 2 +
changelogs/fragments/cve-2024-0690.yml | 2 +
lib/ansible/config/base.yml | 2 +-
lib/ansible/playbook/base.py | 2 +-
lib/ansible/playbook/conditional.py | 4 +-
lib/ansible/playbook/play_context.py | 4 -
lib/ansible/plugins/action/ce_template.py | 4 +-
lib/ansible/plugins/action/network.py | 4 +-
lib/ansible/plugins/action/template.py | 32 ++-
lib/ansible/plugins/lookup/template.py | 50 ++--
lib/ansible/template/__init__.py | 227 ++++++++++++++++--
lib/ansible/template/native_helpers.py | 39 +++
lib/ansible/utils/native_jinja.py | 13 +
lib/ansible/utils/unsafe_proxy.py | 7 +
.../jinja2_native_types/test_casting.yml | 7 +
.../jinja2_native_types/test_dunder.yml | 2 +-
.../targets/no_log/no_log_config.yml | 13 +
test/integration/targets/no_log/runme.sh | 5 +
.../template_jinja2_non_native/46169.yml | 32 +++
.../template_jinja2_non_native/aliases | 1 +
.../template_jinja2_non_native/runme.sh | 7 +
.../templates/46169.json.j2 | 3 +
25 files changed, 398 insertions(+), 73 deletions(-)
create mode 100644 changelogs/fragments/46169-non-native-template-module.yml
create mode 100644 changelogs/fragments/60106-templar-contextmanager.yml
create mode 100644 changelogs/fragments/68014-auto-unroll-jinja2-generators.yml
create mode 100644 changelogs/fragments/70831-skip-literal_eval-string-filter-native-jinja.yml
create mode 100644 changelogs/fragments/cve-2024-0690.yml
create mode 100644 lib/ansible/utils/native_jinja.py
create mode 100644 test/integration/targets/no_log/no_log_config.yml
create mode 100644 test/integration/targets/template_jinja2_non_native/46169.yml
create mode 100644 test/integration/targets/template_jinja2_non_native/aliases
create mode 100755 test/integration/targets/template_jinja2_non_native/runme.sh
create mode 100644 test/integration/targets/template_jinja2_non_native/templates/46169.json.j2
diff --git a/changelogs/fragments/46169-non-native-template-module.yml b/changelogs/fragments/46169-non-native-template-module.yml
new file mode 100644
index 0000000000..7d004a6296
--- /dev/null
+++ b/changelogs/fragments/46169-non-native-template-module.yml
@@ -0,0 +1,2 @@
+minor_changes:
+ - Force the template module to use non-native Jinja2 (https://github.com/ansible/ansible/issues/46169)
diff --git a/changelogs/fragments/60106-templar-contextmanager.yml b/changelogs/fragments/60106-templar-contextmanager.yml
new file mode 100644
index 0000000000..45afc1544a
--- /dev/null
+++ b/changelogs/fragments/60106-templar-contextmanager.yml
@@ -0,0 +1,4 @@
+bugfixes:
+- template lookup - ensure changes to the templar in the lookup, do not
+ affect the templar context outside of the lookup
+ (https://github.com/ansible/ansible/issues/60106)
diff --git a/changelogs/fragments/68014-auto-unroll-jinja2-generators.yml b/changelogs/fragments/68014-auto-unroll-jinja2-generators.yml
new file mode 100644
index 0000000000..211d2fd665
--- /dev/null
+++ b/changelogs/fragments/68014-auto-unroll-jinja2-generators.yml
@@ -0,0 +1,3 @@
+minor_changes:
+- Templating - Add support to auto unroll generators produced by jinja2 filters, to prevent the need of explicit use of ``|list``
+ (https://github.com/ansible/ansible/pull/68014)
diff --git a/changelogs/fragments/70831-skip-literal_eval-string-filter-native-jinja.yml b/changelogs/fragments/70831-skip-literal_eval-string-filter-native-jinja.yml
new file mode 100644
index 0000000000..40b426e50b
--- /dev/null
+++ b/changelogs/fragments/70831-skip-literal_eval-string-filter-native-jinja.yml
@@ -0,0 +1,2 @@
+bugfixes:
+ - Skip literal_eval for string filters results in native jinja. (https://github.com/ansible/ansible/issues/70831)
diff --git a/changelogs/fragments/cve-2024-0690.yml b/changelogs/fragments/cve-2024-0690.yml
new file mode 100644
index 0000000000..0e030d8886
--- /dev/null
+++ b/changelogs/fragments/cve-2024-0690.yml
@@ -0,0 +1,2 @@
+security_fixes:
+- ANSIBLE_NO_LOG - Address issue where ANSIBLE_NO_LOG was ignored (CVE-2024-0690)
diff --git a/lib/ansible/config/base.yml b/lib/ansible/config/base.yml
index 3d3916a7fc..96d38e7f51 100644
--- a/lib/ansible/config/base.yml
+++ b/lib/ansible/config/base.yml
@@ -1757,7 +1757,7 @@ SHOW_CUSTOM_STATS:
type: bool
STRING_TYPE_FILTERS:
name: Filters to preserve strings
- default: [string, to_json, to_nice_json, to_yaml, ppretty, json]
+ default: [string, to_json, to_nice_json, to_yaml, to_nice_yaml, ppretty, json]
description:
- "This list of filters avoids 'type conversion' when templating variables"
- Useful when you want to avoid conversion into lists or dictionaries for JSON strings, for example.
diff --git a/lib/ansible/playbook/base.py b/lib/ansible/playbook/base.py
index 0f4dc4e430..172963a218 100644
--- a/lib/ansible/playbook/base.py
+++ b/lib/ansible/playbook/base.py
@@ -613,7 +613,7 @@ class Base(FieldAttributeBase):
# flags and misc. settings
_environment = FieldAttribute(isa='list', extend=True, prepend=True)
- _no_log = FieldAttribute(isa='bool')
+ _no_log = FieldAttribute(isa='bool', default=C.DEFAULT_NO_LOG)
_run_once = FieldAttribute(isa='bool')
_ignore_errors = FieldAttribute(isa='bool')
_ignore_unreachable = FieldAttribute(isa='bool')
diff --git a/lib/ansible/playbook/conditional.py b/lib/ansible/playbook/conditional.py
index ac4fc0c568..be4b75986c 100644
--- a/lib/ansible/playbook/conditional.py
+++ b/lib/ansible/playbook/conditional.py
@@ -173,8 +173,8 @@ class Conditional:
)
try:
e = templar.environment.overlay()
- e.filters.update(templar._get_filters())
- e.tests.update(templar._get_tests())
+ e.filters.update(templar.environment.filters)
+ e.tests.update(templar.environment.tests)
res = e._parse(conditional, None, None)
res = generate(res, e, None, None)
diff --git a/lib/ansible/playbook/play_context.py b/lib/ansible/playbook/play_context.py
index 10dd57aa3f..5b8b28526c 100644
--- a/lib/ansible/playbook/play_context.py
+++ b/lib/ansible/playbook/play_context.py
@@ -318,10 +318,6 @@ class PlayContext(Base):
if not new_info.connection_user:
new_info.connection_user = new_info.remote_user
- # set no_log to default if it was not previously set
- if new_info.no_log is None:
- new_info.no_log = C.DEFAULT_NO_LOG
-
if task.check_mode is not None:
new_info.check_mode = task.check_mode
diff --git a/lib/ansible/plugins/action/ce_template.py b/lib/ansible/plugins/action/ce_template.py
index 8d62b25647..4a72fbbfa8 100644
--- a/lib/ansible/plugins/action/ce_template.py
+++ b/lib/ansible/plugins/action/ce_template.py
@@ -100,5 +100,5 @@ class ActionModule(_ActionModule):
for role in dep_chain:
searchpath.append(role._role_path)
searchpath.append(os.path.dirname(source))
- self._templar.environment.loader.searchpath = searchpath
- self._task.args['src'] = self._templar.template(template_data)
+ with self._templar.set_temporary_context(searchpath=searchpath):
+ self._task.args['src'] = self._templar.template(template_data)
diff --git a/lib/ansible/plugins/action/network.py b/lib/ansible/plugins/action/network.py
index f0d0ca3ba7..d91c9b2af9 100644
--- a/lib/ansible/plugins/action/network.py
+++ b/lib/ansible/plugins/action/network.py
@@ -160,8 +160,8 @@ class ActionModule(_ActionModule):
for role in dep_chain:
searchpath.append(role._role_path)
searchpath.append(os.path.dirname(source))
- self._templar.environment.loader.searchpath = searchpath
- self._task.args['src'] = self._templar.template(template_data, convert_data=convert_data)
+ with self._templar.set_temporary_context(searchpath=searchpath):
+ self._task.args['src'] = self._templar.template(template_data, convert_data=convert_data)
def _get_network_os(self, task_vars):
if 'network_os' in self._task.args and self._task.args['network_os']:
diff --git a/lib/ansible/plugins/action/template.py b/lib/ansible/plugins/action/template.py
index 8fb7393ff9..cede680ca6 100644
--- a/lib/ansible/plugins/action/template.py
+++ b/lib/ansible/plugins/action/template.py
@@ -17,7 +17,7 @@ from ansible.module_utils._text import to_bytes, to_text, to_native
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.module_utils.six import string_types
from ansible.plugins.action import ActionBase
-from ansible.template import generate_ansible_template_vars
+from ansible.template import generate_ansible_template_vars, AnsibleEnvironment
class ActionModule(ActionBase):
@@ -127,27 +127,23 @@ class ActionModule(ActionBase):
newsearchpath.append(p)
searchpath = newsearchpath
- self._templar.environment.loader.searchpath = searchpath
- self._templar.environment.newline_sequence = newline_sequence
- if block_start_string is not None:
- self._templar.environment.block_start_string = block_start_string
- if block_end_string is not None:
- self._templar.environment.block_end_string = block_end_string
- if variable_start_string is not None:
- self._templar.environment.variable_start_string = variable_start_string
- if variable_end_string is not None:
- self._templar.environment.variable_end_string = variable_end_string
- self._templar.environment.trim_blocks = trim_blocks
- self._templar.environment.lstrip_blocks = lstrip_blocks
-
# add ansible 'template' vars
temp_vars = task_vars.copy()
temp_vars.update(generate_ansible_template_vars(source, dest))
- old_vars = self._templar.available_variables
- self._templar.available_variables = temp_vars
- resultant = self._templar.do_template(template_data, preserve_trailing_newlines=True, escape_backslashes=False)
- self._templar.available_variables = old_vars
+ # force templar to use AnsibleEnvironment to prevent issues with native types
+ # https://github.com/ansible/ansible/issues/46169
+ templar = self._templar.copy_with_new_env(environment_class=AnsibleEnvironment,
+ searchpath=searchpath,
+ newline_sequence=newline_sequence,
+ block_start_string=block_start_string,
+ block_end_string=block_end_string,
+ variable_start_string=variable_start_string,
+ variable_end_string=variable_end_string,
+ trim_blocks=trim_blocks,
+ lstrip_blocks=lstrip_blocks,
+ available_variables=temp_vars)
+ resultant = templar.do_template(template_data, preserve_trailing_newlines=True, escape_backslashes=False)
except AnsibleAction:
raise
except Exception as e:
diff --git a/lib/ansible/plugins/lookup/template.py b/lib/ansible/plugins/lookup/template.py
index 4fd3584b65..c04b5e0d6a 100644
--- a/lib/ansible/plugins/lookup/template.py
+++ b/lib/ansible/plugins/lookup/template.py
@@ -17,7 +17,9 @@ DOCUMENTATION = """
description: list of files to template
convert_data:
type: bool
- description: whether to convert YAML into data. If False, strings that are YAML will be left untouched.
+ description:
+ - Whether to convert YAML into data. If False, strings that are YAML will be left untouched.
+ - Mutually exclusive with the jinja2_native option.
variable_start_string:
description: The string marking the beginning of a print statement.
default: '{{'
@@ -28,6 +30,16 @@ DOCUMENTATION = """
default: '}}'
version_added: '2.8'
type: str
+ jinja2_native:
+ description:
+ - Controls whether to use Jinja2 native types.
+ - It is off by default even if global jinja2_native is True.
+ - Has no effect if global jinja2_native is False.
+ - This offers more flexibility than the template module which does not use Jinja2 native types at all.
+ - Mutually exclusive with the convert_data option.
+ default: False
+ version_added: '2.11'
+ type: bool
"""
EXAMPLES = """
@@ -51,24 +63,31 @@ import os
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
from ansible.module_utils._text import to_bytes, to_text
-from ansible.template import generate_ansible_template_vars
+from ansible.template import generate_ansible_template_vars, AnsibleEnvironment, USE_JINJA2_NATIVE
from ansible.utils.display import Display
+if USE_JINJA2_NATIVE:
+ from ansible.utils.native_jinja import NativeJinjaText
+
+
display = Display()
class LookupModule(LookupBase):
def run(self, terms, variables, **kwargs):
-
convert_data_p = kwargs.get('convert_data', True)
lookup_template_vars = kwargs.get('template_vars', {})
+ jinja2_native = kwargs.get('jinja2_native', False)
ret = []
variable_start_string = kwargs.get('variable_start_string', None)
variable_end_string = kwargs.get('variable_end_string', None)
- old_vars = self._templar.available_variables
+ if USE_JINJA2_NATIVE and not jinja2_native:
+ templar = self._templar.copy_with_new_env(environment_class=AnsibleEnvironment)
+ else:
+ templar = self._templar
for term in terms:
display.debug("File lookup term: %s" % term)
@@ -92,12 +111,6 @@ class LookupModule(LookupBase):
searchpath = newsearchpath
searchpath.insert(0, os.path.dirname(lookupfile))
- self._templar.environment.loader.searchpath = searchpath
- if variable_start_string is not None:
- self._templar.environment.variable_start_string = variable_start_string
- if variable_end_string is not None:
- self._templar.environment.variable_end_string = variable_end_string
-
# The template will have access to all existing variables,
# plus some added by ansible (e.g., template_{path,mtime}),
# plus anything passed to the lookup with the template_vars=
@@ -105,17 +118,20 @@ class LookupModule(LookupBase):
vars = deepcopy(variables)
vars.update(generate_ansible_template_vars(lookupfile))
vars.update(lookup_template_vars)
- self._templar.available_variables = vars
- # do the templating
- res = self._templar.template(template_data, preserve_trailing_newlines=True,
- convert_data=convert_data_p, escape_backslashes=False)
+ with templar.set_temporary_context(variable_start_string=variable_start_string,
+ variable_end_string=variable_end_string,
+ available_variables=vars, searchpath=searchpath):
+ res = templar.template(template_data, preserve_trailing_newlines=True,
+ convert_data=convert_data_p, escape_backslashes=False)
+
+ if USE_JINJA2_NATIVE and not jinja2_native:
+ # jinja2_native is true globally but off for the lookup, we need this text
+ # not to be processed by literal_eval anywhere in Ansible
+ res = NativeJinjaText(res)
ret.append(res)
else:
raise AnsibleError("the template file %s could not be found for the lookup" % term)
- # restore old variables
- self._templar.available_variables = old_vars
-
return ret
diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py
index 94ab31e58d..35c9dac194 100644
--- a/lib/ansible/template/__init__.py
+++ b/lib/ansible/template/__init__.py
@@ -28,6 +28,7 @@ import re
import time
from distutils.version import LooseVersion
+from contextlib import contextmanager
from numbers import Number
try:
@@ -42,8 +43,9 @@ from jinja2.runtime import Context, StrictUndefined
from ansible import constants as C
from ansible.errors import AnsibleError, AnsibleFilterError, AnsibleUndefinedVariable, AnsibleAssertionError
from ansible.module_utils.six import iteritems, string_types, text_type
+from ansible.module_utils.six.moves import range
from ansible.module_utils._text import to_native, to_text, to_bytes
-from ansible.module_utils.common._collections_compat import Sequence, Mapping, MutableMapping
+from ansible.module_utils.common._collections_compat import Iterator, Sequence, Mapping, MappingView, MutableMapping
from ansible.module_utils.common.collections import is_sequence
from ansible.module_utils.compat.importlib import import_module
from ansible.plugins.loader import filter_loader, lookup_loader, test_loader
@@ -71,12 +73,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 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
@@ -85,15 +91,15 @@ 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'))
JINJA2_END_TOKENS = frozenset(('variable_end', 'block_end', 'comment_end', 'raw_end'))
+RANGE_TYPE = type(range(0))
+
+
def generate_ansible_template_vars(path, dest_path=None):
b_path = to_bytes(path)
try:
@@ -230,6 +236,60 @@ def recursive_check_defined(item):
raise AnsibleFilterError("{0} is undefined".format(item))
+def _is_rolled(value):
+ """Helper method to determine if something is an unrolled generator,
+ iterator, or similar object
+ """
+ return (
+ isinstance(value, Iterator) or
+ isinstance(value, MappingView) or
+ isinstance(value, RANGE_TYPE)
+ )
+
+
+def _unroll_iterator(func):
+ """Wrapper function, that intercepts the result of a filter
+ and auto unrolls a generator, so that users are not required to
+ explicitly use ``|list`` to unroll.
+ """
+ def wrapper(*args, **kwargs):
+ ret = func(*args, **kwargs)
+ if _is_rolled(ret):
+ return list(ret)
+ return ret
+
+ return _update_wrapper(wrapper, func)
+
+
+def _update_wrapper(wrapper, func):
+ # This code is duplicated from ``functools.update_wrapper`` from Py3.7.
+ # ``functools.update_wrapper`` was failing when the func was ``functools.partial``
+ for attr in ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__'):
+ try:
+ value = getattr(func, attr)
+ except AttributeError:
+ pass
+ else:
+ setattr(wrapper, attr, value)
+ for attr in ('__dict__',):
+ getattr(wrapper, attr).update(getattr(func, attr, {}))
+ wrapper.__wrapped__ = func
+ return wrapper
+
+
+def _wrap_native_text(func):
+ """Wrapper function, that intercepts the result of a filter
+ and wraps it into NativeJinjaText which is then used
+ in ``ansible_native_concat`` to indicate that it is a text
+ which should not be passed into ``literal_eval``.
+ """
+ def wrapper(*args, **kwargs):
+ ret = func(*args, **kwargs)
+ return NativeJinjaText(ret)
+
+ return _update_wrapper(wrapper, func)
+
+
class AnsibleUndefined(StrictUndefined):
'''
A custom Undefined class, which returns further Undefined objects on access,
@@ -350,10 +410,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'
@@ -406,10 +467,13 @@ class JinjaPluginIntercept(MutableMapping):
method_map = getattr(plugin_impl, self._method_map_name)
- for f in iteritems(method_map()):
- fq_name = '.'.join((parent_prefix, f[0]))
+ for func_name, func in iteritems(method_map()):
+ fq_name = '.'.join((parent_prefix, func_name))
# FIXME: detect/warn on intra-collection function name collisions
- self._collection_jinja_func_cache[fq_name] = f[1]
+ 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)
function_impl = self._collection_jinja_func_cache[key]
return function_impl
@@ -433,6 +497,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
@@ -440,8 +507,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:
@@ -478,7 +564,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(),
@@ -489,17 +577,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
@@ -513,6 +634,17 @@ class Templar:
for fp in self._filter_loader.all():
self._filters.update(fp.filters())
+ if self.jinja2_native:
+ for string_filter in C.STRING_TYPE_FILTERS:
+ try:
+ orig_filter = self._filters[string_filter]
+ except KeyError:
+ try:
+ orig_filter = self.environment.filters[string_filter]
+ except KeyError:
+ continue
+ self._filters[string_filter] = _wrap_native_text(orig_filter)
+
return self._filters.copy()
def _get_tests(self):
@@ -570,6 +702,36 @@ class Templar:
)
self.available_variables = variables
+ @contextmanager
+ def set_temporary_context(self, **kwargs):
+ """Context manager used to set temporary templating context, without having to worry about resetting
+ original values afterward
+
+ Use a keyword that maps to the attr you are setting. Applies to ``self.environment`` by default, to
+ set context on another object, it must be in ``mapping``.
+ """
+ mapping = {
+ 'available_variables': self,
+ 'searchpath': self.environment.loader,
+ }
+ original = {}
+
+ for key, value in kwargs.items():
+ obj = mapping.get(key, self.environment)
+ try:
+ original[key] = getattr(obj, key)
+ if value is not None:
+ setattr(obj, key, value)
+ except AttributeError:
+ # Ignore invalid attrs, lstrip_blocks was added in jinja2==2.7
+ pass
+
+ yield
+
+ for key in original:
+ obj = mapping.get(key, self.environment)
+ setattr(obj, key, original[key])
+
def template(self, variable, convert_bare=False, preserve_trailing_newlines=True, escape_backslashes=True, fail_on_undefined=None, overrides=None,
convert_data=True, static_vars=None, cache=True, disable_lookups=False):
'''
@@ -632,7 +794,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
@@ -746,8 +908,18 @@ class Templar:
If using ANSIBLE_JINJA2_NATIVE we bypass this and return the actual value always
'''
- if USE_JINJA2_NATIVE:
+ if _is_rolled(thing):
+ # Auto unroll a generator, so that users are not required to
+ # explicitly use ``|list`` to unroll
+ # This only affects the scenario where the final result of templating
+ # is a generator, and not where a filter creates a generator in the middle
+ # of a template. See ``_unroll_iterator`` for the other case. This is probably
+ # unncessary
+ return list(thing)
+
+ if self.jinja2_native:
return thing
+
return thing if thing is not None else ''
def _fail_lookup(self, name, *args, **kwargs):
@@ -802,7 +974,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:
@@ -824,7 +999,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
@@ -853,6 +1028,8 @@ class Templar:
# Adds Ansible custom filters and tests
myenv.filters.update(self._get_filters())
+ for k in myenv.filters:
+ myenv.filters[k] = _unroll_iterator(myenv.filters[k])
myenv.tests.update(self._get_tests())
if escape_backslashes:
@@ -904,7 +1081,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 11c14b7fa1..84296ad9b6 100644
--- a/lib/ansible/template/native_helpers.py
+++ b/lib/ansible/template/native_helpers.py
@@ -14,6 +14,34 @@ from jinja2._compat import text_type
from jinja2.runtime import StrictUndefined
from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
+from ansible.utils.native_jinja import NativeJinjaText
+
+
+def _fail_on_undefined(data):
+ """Recursively find an undefined value in a nested data structure
+ and properly raise the undefined exception.
+ """
+ if isinstance(data, Mapping):
+ for value in data.values():
+ _fail_on_undefined(value)
+ elif is_sequence(data):
+ for item in data:
+ _fail_on_undefined(item)
+ else:
+ if isinstance(data, StrictUndefined):
+ # To actually raise the undefined exception we need to
+ # access the undefined object otherwise the exception would
+ # be raised on the next access which might not be properly
+ # handled.
+ # See https://github.com/ansible/ansible/issues/52158
+ # and StrictUndefined implementation in upstream Jinja2.
+ str(data)
+
+ return data
+
+
+class NativeJinjaText(text_type):
+ pass
def ansible_native_concat(nodes):
@@ -49,9 +77,20 @@ def ansible_native_concat(nodes):
# We do that only here because it is taken care of by text_type() in the else block below already.
str(out)
+ if isinstance(out, NativeJinjaText):
+ # Sometimes (e.g. ``| string``) we need to mark variables
+ # in a special way so that they remain strings and are not
+ # passed into literal_eval.
+ # See:
+ # https://github.com/ansible/ansible/issues/70831
+ # https://github.com/pallets/jinja/issues/1200
+ # https://github.com/ansible/ansible/issues/70831#issuecomment-664190894
+ return out
+
# short circuit literal_eval when possible
if not isinstance(out, list):
return out
+
else:
if isinstance(nodes, types.GeneratorType):
nodes = chain(head, nodes)
diff --git a/lib/ansible/utils/native_jinja.py b/lib/ansible/utils/native_jinja.py
new file mode 100644
index 0000000000..53ef14004a
--- /dev/null
+++ b/lib/ansible/utils/native_jinja.py
@@ -0,0 +1,13 @@
+# Copyright: (c) 2020, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+from ansible.module_utils.six import text_type
+
+
+class NativeJinjaText(text_type):
+ pass
diff --git a/lib/ansible/utils/unsafe_proxy.py b/lib/ansible/utils/unsafe_proxy.py
index 0bfd6340ff..54bebd177a 100644
--- a/lib/ansible/utils/unsafe_proxy.py
+++ b/lib/ansible/utils/unsafe_proxy.py
@@ -57,6 +57,7 @@ from ansible.module_utils._text import to_bytes, to_text
from ansible.module_utils.common._collections_compat import Mapping, Set
from ansible.module_utils.common.collections import is_sequence
from ansible.module_utils.six import string_types, binary_type, text_type
+from ansible.utils.native_jinja import NativeJinjaText
__all__ = ['AnsibleUnsafe', 'wrap_var']
@@ -331,6 +332,10 @@ class NativeJinjaUnsafeText(NativeJinjaText, AnsibleUnsafeText):
pass
+class NativeJinjaUnsafeText(NativeJinjaText, AnsibleUnsafeText):
+ pass
+
+
class UnsafeProxy(object):
def __new__(cls, obj, *args, **kwargs):
from ansible.utils.display import Display
@@ -376,6 +381,8 @@ def wrap_var(v):
v = _wrap_set(v)
elif is_sequence(v):
v = _wrap_sequence(v)
+ elif isinstance(v, NativeJinjaText):
+ v = NativeJinjaUnsafeText(v)
elif isinstance(v, binary_type):
v = AnsibleUnsafeBytes(v)
elif isinstance(v, text_type):
diff --git a/test/integration/targets/jinja2_native_types/test_casting.yml b/test/integration/targets/jinja2_native_types/test_casting.yml
index 5b4fe3ac0e..da06ab2e28 100644
--- a/test/integration/targets/jinja2_native_types/test_casting.yml
+++ b/test/integration/targets/jinja2_native_types/test_casting.yml
@@ -1,17 +1,22 @@
- name: cast things to other things
set_fact:
int_to_str: "{{ i_two|to_text }}"
+ int_to_str2: "{{ i_two | string }}"
str_to_int: "{{ s_two|int }}"
dict_to_str: "{{ dict_one|to_text }}"
list_to_str: "{{ list_one|to_text }}"
int_to_bool: "{{ i_one|bool }}"
str_true_to_bool: "{{ s_true|bool }}"
str_false_to_bool: "{{ s_false|bool }}"
+ list_to_json_str: "{{ list_one | to_json }}"
+ list_to_yaml_str: "{{ list_one | to_yaml }}"
- assert:
that:
- 'int_to_str == "2"'
- 'int_to_str|type_debug in ["str", "unicode"]'
+ - 'int_to_str2 == "2"'
+ - 'int_to_str2|type_debug in ["NativeJinjaText"]'
- 'str_to_int == 2'
- 'str_to_int|type_debug == "int"'
- 'dict_to_str|type_debug in ["str", "unicode"]'
@@ -22,3 +27,5 @@
- 'str_true_to_bool|type_debug == "bool"'
- 'str_false_to_bool is sameas false'
- 'str_false_to_bool|type_debug == "bool"'
+ - 'list_to_json_str|type_debug in ["NativeJinjaText"]'
+ - 'list_to_yaml_str|type_debug in ["NativeJinjaText"]'
diff --git a/test/integration/targets/jinja2_native_types/test_dunder.yml b/test/integration/targets/jinja2_native_types/test_dunder.yml
index 46fd4d0a90..df5ea9276b 100644
--- a/test/integration/targets/jinja2_native_types/test_dunder.yml
+++ b/test/integration/targets/jinja2_native_types/test_dunder.yml
@@ -20,4 +20,4 @@
- assert:
that:
- - 'const_dunder|type_debug in ["str", "unicode"]'
+ - 'const_dunder|type_debug in ["str", "unicode", "NativeJinjaText"]'
diff --git a/test/integration/targets/no_log/no_log_config.yml b/test/integration/targets/no_log/no_log_config.yml
new file mode 100644
index 0000000000..8a5088059d
--- /dev/null
+++ b/test/integration/targets/no_log/no_log_config.yml
@@ -0,0 +1,13 @@
+- hosts: testhost
+ gather_facts: false
+ tasks:
+ - debug:
+ no_log: true
+
+ - debug:
+ no_log: false
+
+ - debug:
+
+ - debug:
+ loop: '{{ range(3) }}'
diff --git a/test/integration/targets/no_log/runme.sh b/test/integration/targets/no_log/runme.sh
index bb5c048fc9..8bfe019bb9 100755
--- a/test/integration/targets/no_log/runme.sh
+++ b/test/integration/targets/no_log/runme.sh
@@ -19,3 +19,8 @@ set -eux
# test invalid data passed to a suboption
[ "$(ansible-playbook no_log_suboptions_invalid.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(SUPREME|IDIOM|MOCKUP|EDUCATED|FOOTREST|CRAFTY|FELINE|CRYSTAL|EXPECTANT|AGROUND|GOLIATH|FREEFALL)')" = "0" ]
+
+# test variations on ANSIBLE_NO_LOG
+[ "$(ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "1" ]
+[ "$(ANSIBLE_NO_LOG=0 ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "1" ]
+[ "$(ANSIBLE_NO_LOG=1 ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "6" ]
diff --git a/test/integration/targets/template_jinja2_non_native/46169.yml b/test/integration/targets/template_jinja2_non_native/46169.yml
new file mode 100644
index 0000000000..efb443eae0
--- /dev/null
+++ b/test/integration/targets/template_jinja2_non_native/46169.yml
@@ -0,0 +1,32 @@
+- hosts: localhost
+ gather_facts: no
+ tasks:
+ - set_fact:
+ output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}"
+
+ - template:
+ src: templates/46169.json.j2
+ dest: "{{ output_dir }}/result.json"
+
+ - command: "diff templates/46169.json.j2 {{ output_dir }}/result.json"
+ register: diff_result
+
+ - assert:
+ that:
+ - diff_result.stdout == ""
+
+ - block:
+ - set_fact:
+ non_native_lookup: "{{ lookup('template', 'templates/46169.json.j2') }}"
+
+ - assert:
+ that:
+ - non_native_lookup | type_debug == 'NativeJinjaUnsafeText'
+
+ - set_fact:
+ native_lookup: "{{ lookup('template', 'templates/46169.json.j2', jinja2_native=true) }}"
+
+ - assert:
+ that:
+ - native_lookup | type_debug == 'dict'
+ when: lookup('pipe', ansible_python_interpreter ~ ' -c "import jinja2; print(jinja2.__version__)"') is version('2.10', '>=')
diff --git a/test/integration/targets/template_jinja2_non_native/aliases b/test/integration/targets/template_jinja2_non_native/aliases
new file mode 100644
index 0000000000..b59832142f
--- /dev/null
+++ b/test/integration/targets/template_jinja2_non_native/aliases
@@ -0,0 +1 @@
+shippable/posix/group3
diff --git a/test/integration/targets/template_jinja2_non_native/runme.sh b/test/integration/targets/template_jinja2_non_native/runme.sh
new file mode 100755
index 0000000000..fe9d495a3e
--- /dev/null
+++ b/test/integration/targets/template_jinja2_non_native/runme.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_JINJA2_NATIVE=1
+ansible-playbook 46169.yml -v "$@"
+unset ANSIBLE_JINJA2_NATIVE
diff --git a/test/integration/targets/template_jinja2_non_native/templates/46169.json.j2 b/test/integration/targets/template_jinja2_non_native/templates/46169.json.j2
new file mode 100644
index 0000000000..a4fc3f6717
--- /dev/null
+++ b/test/integration/targets/template_jinja2_non_native/templates/46169.json.j2
@@ -0,0 +1,3 @@
+{
+ "key": "bar"
+}
--
2.44.0