meta-virtualization.lists.yoctoproject.org archive mirror
 help / color / mirror / Atom feed
* [meta-virtualization][meta-cloud-services][kirkstone][PATCH 1/1]  python3-ansible: Fix CVE-2023-5764
@ 2025-04-25 10:50 ssambu
  2025-05-06  1:47 ` Bruce Ashfield
  0 siblings, 1 reply; 2+ messages in thread
From: ssambu @ 2025-04-25 10:50 UTC (permalink / raw)
  To: meta-virtualization

From: Soumya Sambu <soumya.sambu@windriver.com>

A template injection flaw was found in Ansible where a user's controller internal
templating operations may remove the unsafe designation from template data. This
issue could allow an attacker to use a specially crafted file to introduce templating
injection when supplying templating data.

References:
https://nvd.nist.gov/vuln/detail/CVE-2023-5764
https://security-tracker.debian.org/tracker/CVE-2023-5764

Upstream patch:
https://github.com/ansible/ansible/commit/7239d2d371bc6e274cbb7314e01431adce6ae25a

Signed-off-by: Soumya Sambu <soumya.sambu@windriver.com>
---
 .../python3-ansible/CVE-2023-5764.patch       | 1737 +++++++++++++++++
 .../python/python3-ansible_2.14.11.bb         |    1 +
 2 files changed, 1738 insertions(+)
 create mode 100644 recipes-devtools/python/python3-ansible/CVE-2023-5764.patch

diff --git a/recipes-devtools/python/python3-ansible/CVE-2023-5764.patch b/recipes-devtools/python/python3-ansible/CVE-2023-5764.patch
new file mode 100644
index 00000000..cfc7e21e
--- /dev/null
+++ b/recipes-devtools/python/python3-ansible/CVE-2023-5764.patch
@@ -0,0 +1,1737 @@
+From 7239d2d371bc6e274cbb7314e01431adce6ae25a Mon Sep 17 00:00:00 2001
+From: Matt Martz <matt@sivel.net>
+Date: Mon, 27 Nov 2023 12:27:58 -0600
+Subject: [PATCH] Ensure that unsafe is more difficult to lose [stable-2.14]
+ (#82295)
+
+* Ensure that unsafe is more difficult to lose
+
+* Add Task.untemplated_args, and switch assert over to use it
+* Don't use re in first_found, switch to using native string methods
+* If nested templating results in unsafe, just error, don't continue
+
+(cherry picked from commit 586f1924512b01305f896d9ae4732773023013a3)
+
+* ci_complete
+
+CVE: CVE-2023-5764
+
+Upstream-Status: Backport [https://github.com/ansible/ansible/commit/7239d2d371bc6e274cbb7314e01431adce6ae25a]
+
+Signed-off-by: Soumya Sambu <soumya.sambu@windriver.com>
+---
+ changelogs/fragments/cve-2023-5764.yml        |   6 +
+ lib/ansible/module_utils/common/json.py       |   4 +-
+ lib/ansible/parsing/yaml/dumper.py            |   6 +-
+ lib/ansible/playbook/conditional.py           |   9 +-
+ lib/ansible/playbook/task.py                  |  24 ++
+ lib/ansible/plugins/action/assert.py          |  23 +-
+ lib/ansible/plugins/callback/__init__.py      |   4 +-
+ lib/ansible/plugins/filter/core.py            |   5 +
+ lib/ansible/plugins/lookup/first_found.py     |  15 +-
+ lib/ansible/template/__init__.py              |  15 +-
+ lib/ansible/utils/unsafe_proxy.py             | 265 +++++++++++++++++-
+ .../tasks/scm_dependency_deduplication.yml    |  16 +-
+ .../roles/test_vault_embedded/tasks/main.yml  |   2 +-
+ .../tasks/main.yml                            |   2 +-
+ .../targets/apt_repository/tasks/apt.yml      |  10 +-
+ .../assert/assert.out.nested_tmpl.stderr      |   4 +
+ .../assert/assert.out.nested_tmpl.stdout      |  12 +
+ ...t.quiet.stderr => assert.out.quiet.stderr} |   0
+ ...t.quiet.stdout => assert.out.quiet.stdout} |   0
+ .../targets/assert/nested_tmpl.yml            |   9 +
+ test/integration/targets/assert/quiet.yml     |   4 +-
+ test/integration/targets/assert/runme.sh      |   3 +-
+ .../targets/command_shell/tasks/main.yml      |   2 +-
+ test/integration/targets/copy/tasks/tests.yml |  42 +--
+ test/integration/targets/debug/runme.sh       |   2 +
+ test/integration/targets/debug/unsafe.yml     |  13 +
+ .../targets/dnf/tasks/test_sos_removal.yml    |   2 +-
+ .../integration/targets/expect/tasks/main.yml |   2 +-
+ test/integration/targets/file/tasks/main.yml  |   2 +-
+ .../targets/file/tasks/state_link.yml         |   2 +-
+ test/integration/targets/find/tasks/main.yml  |  12 +-
+ .../gathering_facts/test_gathering_facts.yml  |   4 +-
+ test/integration/targets/git/tasks/depth.yml  |   2 +-
+ .../targets/git/tasks/localmods.yml           |   4 +-
+ .../targets/git/tasks/submodules.yml          |  14 +-
+ .../tests/cli/check_config.yaml               |   4 +-
+ .../tests/cli/deleted.yaml                    |   8 +-
+ .../tests/cli/merged.yaml                     |   8 +-
+ .../tests/cli/overridden.yaml                 |   8 +-
+ .../tests/cli/replaced.yaml                   |   8 +-
+ .../targets/include_vars/tasks/main.yml       |  28 +-
+ .../lookup_ini/test_lookup_properties.yml     |   2 +-
+ .../targets/lookup_subelements/tasks/main.yml |   6 +-
+ .../targets/loop_control/inner.yml            |   4 +-
+ .../modules_test_multiple_roles.yml           |   2 +-
+ ...ules_test_multiple_roles_reverse_order.yml |   2 +-
+ .../multiple_roles/bar/tasks/main.yml         |   2 +-
+ .../multiple_roles/foo/tasks/main.yml         |   2 +-
+ .../integration/targets/script/tasks/main.yml |   4 +-
+ test/integration/targets/slurp/tasks/main.yml |   2 +-
+ .../targets/template/tasks/main.yml           |   2 +-
+ .../unarchive/tasks/test_missing_binaries.yml |   2 +-
+ .../targets/unarchive/tasks/test_mode.yml     |   8 +-
+ .../tasks/test_unprivileged_user.yml          |   2 +-
+ .../targets/unarchive/tasks/test_zip.yml      |   2 +-
+ .../targets/wait_for/tasks/main.yml           |   8 +-
+ test/units/parsing/yaml/test_dumper.py        |   7 +-
+ 57 files changed, 519 insertions(+), 143 deletions(-)
+ create mode 100644 changelogs/fragments/cve-2023-5764.yml
+ create mode 100644 test/integration/targets/assert/assert.out.nested_tmpl.stderr
+ create mode 100644 test/integration/targets/assert/assert.out.nested_tmpl.stdout
+ rename test/integration/targets/assert/{assert_quiet.out.quiet.stderr => assert.out.quiet.stderr} (100%)
+ rename test/integration/targets/assert/{assert_quiet.out.quiet.stdout => assert.out.quiet.stdout} (100%)
+ create mode 100644 test/integration/targets/assert/nested_tmpl.yml
+ create mode 100644 test/integration/targets/debug/unsafe.yml
+
+diff --git a/changelogs/fragments/cve-2023-5764.yml b/changelogs/fragments/cve-2023-5764.yml
+new file mode 100644
+index 0000000000..c37127dac1
+--- /dev/null
++++ b/changelogs/fragments/cve-2023-5764.yml
+@@ -0,0 +1,6 @@
++security_fixes:
++- templating - Address issues where internal templating can cause unsafe
++  variables to lose their unsafe designation (CVE-2023-5764)
++breaking_changes:
++- assert - Nested templating may result in an inability for the conditional
++  to be evaluated. See the porting guide for more information.
+diff --git a/lib/ansible/module_utils/common/json.py b/lib/ansible/module_utils/common/json.py
+index 727083ca23..c4333fc157 100644
+--- a/lib/ansible/module_utils/common/json.py
++++ b/lib/ansible/module_utils/common/json.py
+@@ -30,7 +30,7 @@ def _preprocess_unsafe_encode(value):
+     Used in ``AnsibleJSONEncoder.iterencode``
+     """
+     if _is_unsafe(value):
+-        value = {'__ansible_unsafe': to_text(value, errors='surrogate_or_strict', nonstring='strict')}
++        value = {'__ansible_unsafe': to_text(value._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')}
+     elif is_sequence(value):
+         value = [_preprocess_unsafe_encode(v) for v in value]
+     elif isinstance(value, Mapping):
+@@ -63,7 +63,7 @@ class AnsibleJSONEncoder(json.JSONEncoder):
+                 value = {'__ansible_vault': to_text(o._ciphertext, errors='surrogate_or_strict', nonstring='strict')}
+         elif getattr(o, '__UNSAFE__', False):
+             # unsafe object, this will never be triggered, see ``AnsibleJSONEncoder.iterencode``
+-            value = {'__ansible_unsafe': to_text(o, errors='surrogate_or_strict', nonstring='strict')}
++            value = {'__ansible_unsafe': to_text(o._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')}
+         elif isinstance(o, Mapping):
+             # hostvars and other objects
+             value = dict(o)
+diff --git a/lib/ansible/parsing/yaml/dumper.py b/lib/ansible/parsing/yaml/dumper.py
+index 8701bb8196..bf2c0843c2 100644
+--- a/lib/ansible/parsing/yaml/dumper.py
++++ b/lib/ansible/parsing/yaml/dumper.py
+@@ -24,7 +24,7 @@ import yaml
+ from ansible.module_utils.six import text_type, binary_type
+ from ansible.module_utils.common.yaml import SafeDumper
+ from ansible.parsing.yaml.objects import AnsibleUnicode, AnsibleSequence, AnsibleMapping, AnsibleVaultEncryptedUnicode
+-from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText
++from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText, _is_unsafe
+ from ansible.template import AnsibleUndefined
+ from ansible.vars.hostvars import HostVars, HostVarsVars
+ from ansible.vars.manager import VarsWithSources
+@@ -47,10 +47,14 @@ def represent_vault_encrypted_unicode(self, data):
+
+
+ def represent_unicode(self, data):
++    if _is_unsafe(data):
++        data = data._strip_unsafe()
+     return yaml.representer.SafeRepresenter.represent_str(self, text_type(data))
+
+
+ def represent_binary(self, data):
++    if _is_unsafe(data):
++        data = data._strip_unsafe()
+     return yaml.representer.SafeRepresenter.represent_binary(self, binary_type(data))
+
+
+diff --git a/lib/ansible/playbook/conditional.py b/lib/ansible/playbook/conditional.py
+index fe07358cbd..d994f8f49d 100644
+--- a/lib/ansible/playbook/conditional.py
++++ b/lib/ansible/playbook/conditional.py
+@@ -26,7 +26,7 @@ from jinja2.compiler import generate
+ from jinja2.exceptions import UndefinedError
+
+ from ansible import constants as C
+-from ansible.errors import AnsibleError, AnsibleUndefinedVariable
++from ansible.errors import AnsibleError, AnsibleUndefinedVariable, AnsibleTemplateError
+ from ansible.module_utils.six import text_type
+ from ansible.module_utils._text import to_native, to_text
+ from ansible.playbook.attribute import FieldAttribute
+@@ -138,9 +138,10 @@ class Conditional:
+             if not isinstance(conditional, text_type) or conditional == "":
+                 return conditional
+
+-            # update the lookups flag, as the string returned above may now be unsafe
+-            # and we don't want future templating calls to do unsafe things
+-            disable_lookups |= hasattr(conditional, '__UNSAFE__')
++            # If the result of the first-pass template render (to resolve inline templates) is marked unsafe,
++            # explicitly fail since the next templating operation would never evaluate
++            if hasattr(conditional, '__UNSAFE__'):
++                raise AnsibleTemplateError('Conditional is marked as unsafe, and cannot be evaluated.')
+
+             # First, we do some low-level jinja2 parsing involving the AST format of the
+             # statement to ensure we don't do anything unsafe (using the disable_lookup flag above)
+diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py
+index ba35fcf0e3..a1a1162bf2 100644
+--- a/lib/ansible/playbook/task.py
++++ b/lib/ansible/playbook/task.py
+@@ -290,6 +290,30 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
+
+         super(Task, self).post_validate(templar)
+
++    def _post_validate_args(self, attr, value, templar):
++        # smuggle an untemplated copy of the task args for actions that need more control over the templating of their
++        # input (eg, debug's var/msg, assert's "that" conditional expressions)
++        self.untemplated_args = value
++
++        # now recursively template the args dict
++        args = templar.template(value)
++
++        # FIXME: could we just nuke this entirely and/or wrap it up in ModuleArgsParser or something?
++        if '_variable_params' in args:
++            variable_params = args.pop('_variable_params')
++            if isinstance(variable_params, dict):
++                if C.INJECT_FACTS_AS_VARS:
++                    display.warning("Using a variable for a task's 'args' is unsafe in some situations "
++                                    "(see https://docs.ansible.com/ansible/devel/reference_appendices/faq.html#argsplat-unsafe)")
++                variable_params.update(args)
++                args = variable_params
++            else:
++                # if we didn't get a dict, it means there's garbage remaining after k=v parsing, just give up
++                # see https://github.com/ansible/ansible/issues/79862
++                raise AnsibleError(f"invalid or malformed argument: '{variable_params}'")
++
++        return args
++
+     def _post_validate_loop(self, attr, value, templar):
+         '''
+         Override post validation for the loop field, which is templated
+diff --git a/lib/ansible/plugins/action/assert.py b/lib/ansible/plugins/action/assert.py
+index 7721a6b47c..e8ab6a9a4f 100644
+--- a/lib/ansible/plugins/action/assert.py
++++ b/lib/ansible/plugins/action/assert.py
+@@ -63,8 +63,29 @@ class ActionModule(ActionBase):
+
+         quiet = boolean(self._task.args.get('quiet', False), strict=False)
+
++        # directly access 'that' via untemplated args from the task so we can intelligently trust embedded
++        # templates and preserve the original inputs/locations for better messaging on assert failures and
++        # errors.
++        # FIXME: even in devel, things like `that: item` don't always work properly (truthy string value
++        # is not really an embedded expression)
++        # we could fix that by doing direct var lookups on the inputs
++        # FIXME: some form of this code should probably be shared between debug, assert, and
++        # Task.post_validate, since they
++        # have a lot of overlapping needs
++        try:
++            thats = self._task.untemplated_args['that']
++        except KeyError:
++            # in the case of "we got our entire args dict from a template", we can just consult the
++            # post-templated dict (the damage has likely already been done for embedded templates anyway)
++            thats = self._task.args['that']
++
++        # FIXME: this is a case where we only want to resolve indirections, NOT recurse containers
++        # (and even then, the leaf-most expression being wrapped is at least suboptimal
++        # (since its expression will be "eaten").
++        if isinstance(thats, str):
++            thats = self._templar.template(thats)
++
+         # make sure the 'that' items are a list
+-        thats = self._task.args['that']
+         if not isinstance(thats, list):
+             thats = [thats]
+
+diff --git a/lib/ansible/plugins/callback/__init__.py b/lib/ansible/plugins/callback/__init__.py
+index d4fc347d03..7646d2939f 100644
+--- a/lib/ansible/plugins/callback/__init__.py
++++ b/lib/ansible/plugins/callback/__init__.py
+@@ -38,7 +38,7 @@ from ansible.parsing.yaml.objects import AnsibleUnicode
+ from ansible.plugins import AnsiblePlugin
+ from ansible.utils.color import stringc
+ from ansible.utils.display import Display
+-from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText
++from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText, _is_unsafe
+ from ansible.vars.clean import strip_internal_keys, module_response_deepcopy
+
+ import yaml
+@@ -113,6 +113,8 @@ def _munge_data_for_lossy_yaml(scalar):
+
+ def _pretty_represent_str(self, data):
+     """Uses block style for multi-line strings"""
++    if _is_unsafe(data):
++        data = data._strip_unsafe()
+     data = text_type(data)
+     if _should_use_block(data):
+         style = '|'
+diff --git a/lib/ansible/plugins/filter/core.py b/lib/ansible/plugins/filter/core.py
+index 52a2cd108e..b7e2c11ec8 100644
+--- a/lib/ansible/plugins/filter/core.py
++++ b/lib/ansible/plugins/filter/core.py
+@@ -37,6 +37,7 @@ from ansible.utils.display import Display
+ from ansible.utils.encrypt import passlib_or_crypt
+ from ansible.utils.hashing import md5s, checksum_s
+ from ansible.utils.unicode import unicode_wrap
++from ansible.utils.unsafe_proxy import _is_unsafe
+ from ansible.utils.vars import merge_hash
+
+ display = Display()
+@@ -215,6 +216,8 @@ def from_yaml(data):
+         # The ``text_type`` call here strips any custom
+         # string wrapper class, so that CSafeLoader can
+         # read the data
++        if _is_unsafe(data):
++            data = data._strip_unsafe()
+         return yaml_load(text_type(to_text(data, errors='surrogate_or_strict')))
+     return data
+
+@@ -224,6 +227,8 @@ def from_yaml_all(data):
+         # The ``text_type`` call here strips any custom
+         # string wrapper class, so that CSafeLoader can
+         # read the data
++        if _is_unsafe(data):
++            data = data._strip_unsafe()
+         return yaml_load_all(text_type(to_text(data, errors='surrogate_or_strict')))
+     return data
+
+diff --git a/lib/ansible/plugins/lookup/first_found.py b/lib/ansible/plugins/lookup/first_found.py
+index 5b94b103a4..a882db017b 100644
+--- a/lib/ansible/plugins/lookup/first_found.py
++++ b/lib/ansible/plugins/lookup/first_found.py
+@@ -136,7 +136,6 @@ RETURN = """
+     elements: path
+ """
+ import os
+-import re
+
+ from collections.abc import Mapping, Sequence
+
+@@ -147,10 +146,22 @@ from ansible.module_utils.six import string_types
+ from ansible.plugins.lookup import LookupBase
+
+
++def _splitter(value, chars):
++    chars = set(chars)
++    v = ''
++    for c in value:
++        if c in chars:
++            yield v
++            v = ''
++            continue
++        v += c
++    yield v
++
++
+ def _split_on(terms, spliters=','):
+     termlist = []
+     if isinstance(terms, string_types):
+-        termlist = re.split(r'[%s]' % ''.join(map(re.escape, spliters)), terms)
++        termlist = list(_splitter(terms, spliters))
+     else:
+         # added since options will already listify
+         for t in terms:
+diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py
+index baa85ed793..c45cfe350e 100644
+--- a/lib/ansible/template/__init__.py
++++ b/lib/ansible/template/__init__.py
+@@ -31,7 +31,7 @@ from contextlib import contextmanager
+ from numbers import Number
+ from traceback import format_exc
+
+-from jinja2.exceptions import TemplateSyntaxError, UndefinedError
++from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError
+ from jinja2.loaders import FileSystemLoader
+ from jinja2.nativetypes import NativeEnvironment
+ from jinja2.runtime import Context, StrictUndefined
+@@ -55,7 +55,7 @@ from ansible.template.vars import AnsibleJ2Vars
+ from ansible.utils.display import Display
+ from ansible.utils.listify import listify_lookup_plugin_terms
+ from ansible.utils.native_jinja import NativeJinjaText
+-from ansible.utils.unsafe_proxy import wrap_var
++from ansible.utils.unsafe_proxy import wrap_var, AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText
+
+ display = Display()
+
+@@ -332,10 +332,21 @@ class AnsibleContext(Context):
+     flag is checked post-templating, and (when set) will result in the
+     final templated result being wrapped in AnsibleUnsafe.
+     '''
++    _disallowed_callables = frozenset({
++        AnsibleUnsafeText._strip_unsafe.__qualname__,
++        AnsibleUnsafeBytes._strip_unsafe.__qualname__,
++        NativeJinjaUnsafeText._strip_unsafe.__qualname__,
++    })
++
+     def __init__(self, *args, **kwargs):
+         super(AnsibleContext, self).__init__(*args, **kwargs)
+         self.unsafe = False
+
++    def call(self, obj, *args, **kwargs):
++        if getattr(obj, '__qualname__', None) in self._disallowed_callables or obj in self._disallowed_callables:
++            raise SecurityError(f"{obj!r} is not safely callable")
++        return super().call(obj, *args, **kwargs)
++
+     def _is_unsafe(self, val):
+         '''
+         Our helper function, which will also recursively check dict and
+diff --git a/lib/ansible/utils/unsafe_proxy.py b/lib/ansible/utils/unsafe_proxy.py
+index d78ebf6e8d..7da74dcfb2 100644
+--- a/lib/ansible/utils/unsafe_proxy.py
++++ b/lib/ansible/utils/unsafe_proxy.py
+@@ -69,15 +69,264 @@ class AnsibleUnsafe(object):
+
+
+ class AnsibleUnsafeBytes(binary_type, AnsibleUnsafe):
+-    def decode(self, *args, **kwargs):
+-        """Wrapper method to ensure type conversions maintain unsafe context"""
+-        return AnsibleUnsafeText(super(AnsibleUnsafeBytes, self).decode(*args, **kwargs))
++    def _strip_unsafe(self):
++        return super().__bytes__()
++
++    def __str__(self, /):  # pylint: disable=invalid-str-returned
++        return self.encode()
++
++    def __bytes__(self, /):  # pylint: disable=invalid-bytes-returned
++        return self
++
++    def __repr__(self, /):  # pylint: disable=invalid-repr-returned
++        return AnsibleUnsafeText(super().__repr__())
++
++    def __format__(self, format_spec, /):  # pylint: disable=invalid-format-returned
++        return self.__class__(super().__format__(format_spec))
++
++    def __getitem__(self, key, /):
++        return self.__class__(super().__getitem__(key))
++
++    def __iter__(self, /):
++        cls = self.__class__
++        return (cls(c) for c in super().__iter__())
++
++    def __reversed__(self, /):
++        return self[::-1]
++
++    def __add__(self, value, /):
++        return self.__class__(super().__add__(value))
++
++    def __radd__(self, value, /):
++        return self.__class__(value.__add__(self))
++
++    def __mul__(self, value, /):
++        return self.__class__(super().__mul__(value))
++
++    __rmul__ = __mul__
++
++    def __mod__(self, value, /):
++        return self.__class__(super().__mod__(value))
++
++    def __rmod__(self, value, /):
++        return self.__class__(super().__rmod__(value))
++
++    def capitalize(self, /):
++        return self.__class__(super().capitalize())
++
++    def casefold(self, /):
++        return self.__class__(super().casefold())
++
++    def center(self, width, fillchar=b' ', /):
++        return self.__class__(super().center(width, fillchar))
++
++    def decode(self, /, encoding='utf-8', errors='strict'):
++        return AnsibleUnsafeText(super().decode(encoding=encoding, errors=errors))
++
++    def removeprefix(self, prefix, /):
++        return self.__class__(super().removeprefix(prefix))
++
++    def removesuffix(self, suffix, /):
++        return self.__class__(super().removesuffix(suffix))
++
++    def expandtabs(self, /, tabsize=8):
++        return self.__class__(super().expandtabs(tabsize))
++
++    def format(self, /, *args, **kwargs):
++        return self.__class__(super().format(*args, **kwargs))
++
++    def format_map(self, mapping, /):
++        return self.__class__(super().format_map(mapping))
++
++    def join(self, iterable_of_bytes, /):
++        return self.__class__(super().join(iterable_of_bytes))
++
++    def ljust(self, width, fillchar=b' ', /):
++        return self.__class__(super().ljust(width, fillchar))
++
++    def lower(self, /):
++        return self.__class__(super().lower())
++
++    def lstrip(self, bytes=None, /):
++        return self.__class__(super().lstrip(bytes))
++
++    def partition(self, sep, /):
++        cls = self.__class__
++        return tuple(cls(e) for e in super().partition(sep))
++
++    def replace(self, old, new, count=-1, /):
++        return self.__class__(super().replace(old, new, count))
++
++    def rjust(self, width, fillchar=b' ', /):
++        return self.__class__(super().rjust(width, fillchar))
++
++    def rpartition(self, sep, /):
++        cls = self.__class__
++        return tuple(cls(e) for e in super().rpartition(sep))
++
++    def rstrip(self, bytes=None, /):
++        return self.__class__(super().rstrip(bytes))
++
++    def split(self, /, sep=None, maxsplit=-1):
++        cls = self.__class__
++        return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)]
++
++    def rsplit(self, /, sep=None, maxsplit=-1):
++        cls = self.__class__
++        return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)]
++
++    def splitlines(self, /, keepends=False):
++        cls = self.__class__
++        return [cls(e) for e in super().splitlines(keepends=keepends)]
++
++    def strip(self, bytes=None, /):
++        return self.__class__(super().strip(bytes))
++
++    def swapcase(self, /):
++        return self.__class__(super().swapcase())
++
++    def title(self, /):
++        return self.__class__(super().title())
++
++    def translate(self, table, /, delete=b''):
++        return self.__class__(super().translate(table, delete=delete))
++
++    def upper(self, /):
++        return self.__class__(super().upper())
++
++    def zfill(self, width, /):
++        return self.__class__(super().zfill(width))
+
+
+ class AnsibleUnsafeText(text_type, AnsibleUnsafe):
+-    def encode(self, *args, **kwargs):
+-        """Wrapper method to ensure type conversions maintain unsafe context"""
+-        return AnsibleUnsafeBytes(super(AnsibleUnsafeText, self).encode(*args, **kwargs))
++    # def __getattribute__(self, name):
++    #     print(f'attr: {name}')
++    #     return object.__getattribute__(self, name)
++
++    def _strip_unsafe(self, /):
++        return super().__str__()
++
++    def __str__(self, /):  # pylint: disable=invalid-str-returned
++        return self
++
++    def __repr__(self, /):  # pylint: disable=invalid-repr-returned
++        return self.__class__(super().__repr__())
++
++    def __format__(self, format_spec, /):  # pylint: disable=invalid-format-returned
++        return self.__class__(super().__format__(format_spec))
++
++    def __getitem__(self, key, /):
++        return self.__class__(super().__getitem__(key))
++
++    def __iter__(self, /):
++        cls = self.__class__
++        return (cls(c) for c in super().__iter__())
++
++    def __reversed__(self, /):
++        return self[::-1]
++
++    def __add__(self, value, /):
++        return self.__class__(super().__add__(value))
++
++    def __radd__(self, value, /):
++        return self.__class__(value.__add__(self))
++
++    def __mul__(self, value, /):
++        return self.__class__(super().__mul__(value))
++
++    __rmul__ = __mul__
++
++    def __mod__(self, value, /):
++        return self.__class__(super().__mod__(value))
++
++    def __rmod__(self, value, /):
++        return self.__class__(super().__rmod__(value))
++
++    def capitalize(self, /):
++        return self.__class__(super().capitalize())
++
++    def casefold(self, /):
++        return self.__class__(super().casefold())
++
++    def center(self, width, fillchar=' ', /):
++        return self.__class__(super().center(width, fillchar))
++
++    def encode(self, /, encoding='utf-8', errors='strict'):
++        return AnsibleUnsafeBytes(super().encode(encoding=encoding, errors=errors))
++
++    def removeprefix(self, prefix, /):
++        return self.__class__(super().removeprefix(prefix))
++
++    def removesuffix(self, suffix, /):
++        return self.__class__(super().removesuffix(suffix))
++
++    def expandtabs(self, /, tabsize=8):
++        return self.__class__(super().expandtabs(tabsize))
++
++    def format(self, /, *args, **kwargs):
++        return self.__class__(super().format(*args, **kwargs))
++
++    def format_map(self, mapping, /):
++        return self.__class__(super().format_map(mapping))
++
++    def join(self, iterable, /):
++        return self.__class__(super().join(iterable))
++
++    def ljust(self, width, fillchar=' ', /):
++        return self.__class__(super().ljust(width, fillchar))
++
++    def lower(self, /):
++        return self.__class__(super().lower())
++
++    def lstrip(self, chars=None, /):
++        return self.__class__(super().lstrip(chars))
++
++    def partition(self, sep, /):
++        cls = self.__class__
++        return tuple(cls(e) for e in super().partition(sep))
++
++    def replace(self, old, new, count=-1, /):
++        return self.__class__(super().replace(old, new, count))
++
++    def rjust(self, width, fillchar=' ', /):
++        return self.__class__(super().rjust(width, fillchar))
++
++    def rpartition(self, sep, /):
++        cls = self.__class__
++        return tuple(cls(e) for e in super().rpartition(sep))
++
++    def rstrip(self, chars=None, /):
++        return self.__class__(super().rstrip(chars))
++
++    def split(self, /, sep=None, maxsplit=-1):
++        cls = self.__class__
++        return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)]
++
++    def rsplit(self, /, sep=None, maxsplit=-1):
++        cls = self.__class__
++        return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)]
++
++    def splitlines(self, /, keepends=False):
++        cls = self.__class__
++        return [cls(e) for e in super().splitlines(keepends=keepends)]
++
++    def strip(self, chars=None, /):
++        return self.__class__(super().strip(chars))
++
++    def swapcase(self, /):
++        return self.__class__(super().swapcase())
++
++    def title(self, /):
++        return self.__class__(super().title())
++
++    def translate(self, table, /):
++        return self.__class__(super().translate(table))
++
++    def upper(self, /):
++        return self.__class__(super().upper())
++
++    def zfill(self, width, /):
++        return self.__class__(super().zfill(width))
+
+
+ class NativeJinjaUnsafeText(NativeJinjaText, AnsibleUnsafeText):
+@@ -126,3 +375,7 @@ def to_unsafe_bytes(*args, **kwargs):
+
+ def to_unsafe_text(*args, **kwargs):
+     return wrap_var(to_text(*args, **kwargs))
++
++
++def _is_unsafe(obj):
++    return getattr(obj, '__UNSAFE__', False) is True
+diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml
+index f200be1803..e084752494 100644
+--- a/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml
++++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml
+@@ -13,22 +13,22 @@
+         in command.stdout_lines
+       - >-
+         "Installing 'namespace_1.collection_1:1.0.0' to
+-        '{{ install_path }}/namespace_1/collection_1'"
++        '" ~ install_path ~ "/namespace_1/collection_1'"
+         in command.stdout_lines
+       - >-
+         'Created collection for namespace_1.collection_1:1.0.0 at
+-        {{ install_path }}/namespace_1/collection_1'
++        ' ~ install_path ~ '/namespace_1/collection_1'
+         in command.stdout_lines
+       - >-
+         'namespace_1.collection_1:1.0.0 was installed successfully'
+         in command.stdout_lines
+       - >-
+         "Installing 'namespace_2.collection_2:1.0.0' to
+-        '{{ install_path }}/namespace_2/collection_2'"
++        '" ~ install_path ~ "/namespace_2/collection_2'"
+         in command.stdout_lines
+       - >-
+         'Created collection for namespace_2.collection_2:1.0.0 at
+-        {{ install_path }}/namespace_2/collection_2'
++        ' ~ install_path ~ '/namespace_2/collection_2'
+         in command.stdout_lines
+       - >-
+         'namespace_2.collection_2:1.0.0 was installed successfully'
+@@ -58,22 +58,22 @@
+         in command.stdout_lines
+       - >-
+         "Installing 'namespace_1.collection_1:1.0.0' to
+-        '{{ install_path }}/namespace_1/collection_1'"
++        '" ~ install_path ~ "/namespace_1/collection_1'"
+         in command.stdout_lines
+       - >-
+         'Created collection for namespace_1.collection_1:1.0.0 at
+-        {{ install_path }}/namespace_1/collection_1'
++        ' ~ install_path ~ '/namespace_1/collection_1'
+         in command.stdout_lines
+       - >-
+         'namespace_1.collection_1:1.0.0 was installed successfully'
+         in command.stdout_lines
+       - >-
+         "Installing 'namespace_2.collection_2:1.0.0' to
+-        '{{ install_path }}/namespace_2/collection_2'"
++        '" ~ install_path ~ "/namespace_2/collection_2'"
+         in command.stdout_lines
+       - >-
+         'Created collection for namespace_2.collection_2:1.0.0 at
+-        {{ install_path }}/namespace_2/collection_2'
++        ' ~ install_path ~ '/namespace_2/collection_2'
+         in command.stdout_lines
+       - >-
+         'namespace_2.collection_2:1.0.0 was installed successfully'
+diff --git a/test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml b/test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml
+index eba938966d..98ef751b86 100644
+--- a/test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml
++++ b/test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml
+@@ -2,7 +2,7 @@
+ - name: Assert that a embedded vault of a string with no newline works
+   assert:
+     that:
+-      - '"{{ vault_encrypted_one_line_var }}" == "Setec Astronomy"'
++      - 'vault_encrypted_one_line_var == "Setec Astronomy"'
+
+ - name: Assert that a multi line embedded vault works, including new line
+   assert:
+diff --git a/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml b/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
+index e09004a1d9..107e65cb11 100644
+--- a/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
++++ b/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
+@@ -2,7 +2,7 @@
+ - name: Assert that a vault encrypted file with embedded vault of a string with no newline works
+   assert:
+     that:
+-      - '"{{ vault_file_encrypted_with_encrypted_one_line_var }}" == "Setec Astronomy"'
++      - 'vault_file_encrypted_with_encrypted_one_line_var == "Setec Astronomy"'
+
+ - name: Assert that a vault encrypted file with multi line embedded vault works, including new line
+   assert:
+diff --git a/test/integration/targets/apt_repository/tasks/apt.yml b/test/integration/targets/apt_repository/tasks/apt.yml
+index 0dc25afd59..9c15e64765 100644
+--- a/test/integration/targets/apt_repository/tasks/apt.yml
++++ b/test/integration/targets/apt_repository/tasks/apt.yml
+@@ -50,7 +50,7 @@
+     that:
+       - 'result.changed'
+       - 'result.state == "present"'
+-      - 'result.repo == "{{test_ppa_name}}"'
++      - 'result.repo == test_ppa_name'
+
+ - name: 'examine apt cache mtime'
+   stat: path='/var/cache/apt/pkgcache.bin'
+@@ -81,7 +81,7 @@
+     that:
+       - 'result.changed'
+       - 'result.state == "present"'
+-      - 'result.repo == "{{test_ppa_name}}"'
++      - 'result.repo == test_ppa_name'
+
+ - name: 'examine apt cache mtime'
+   stat: path='/var/cache/apt/pkgcache.bin'
+@@ -112,7 +112,7 @@
+     that:
+       - 'result.changed'
+       - 'result.state == "present"'
+-      - 'result.repo == "{{test_ppa_name}}"'
++      - 'result.repo == test_ppa_name'
+
+ - name: 'examine apt cache mtime'
+   stat: path='/var/cache/apt/pkgcache.bin'
+@@ -151,7 +151,7 @@
+     that:
+       - 'result.changed'
+       - 'result.state == "present"'
+-      - 'result.repo == "{{test_ppa_spec}}"'
++      - 'result.repo == test_ppa_spec'
+       - result_cache is not changed
+
+ - name: 'examine apt cache mtime'
+@@ -191,7 +191,7 @@
+     that:
+       - 'result.changed'
+       - 'result.state == "present"'
+-      - 'result.repo == "{{test_ppa_spec}}"'
++      - 'result.repo == test_ppa_spec'
+
+ - name: 'examine source file'
+   stat: path='/etc/apt/sources.list.d/{{test_ppa_filename}}.list'
+diff --git a/test/integration/targets/assert/assert.out.nested_tmpl.stderr b/test/integration/targets/assert/assert.out.nested_tmpl.stderr
+new file mode 100644
+index 0000000000..ea208a41c7
+--- /dev/null
++++ b/test/integration/targets/assert/assert.out.nested_tmpl.stderr
+@@ -0,0 +1,4 @@
+++ ansible-playbook -i localhost, -c local nested_tmpl.yml
++++ set +x
++[WARNING]: conditional statements should not include jinja2 templating
++delimiters such as {{ }} or {% %}. Found: "{{ foo }}" == "bar"
+diff --git a/test/integration/targets/assert/assert.out.nested_tmpl.stdout b/test/integration/targets/assert/assert.out.nested_tmpl.stdout
+new file mode 100644
+index 0000000000..8ca3fb76d4
+--- /dev/null
++++ b/test/integration/targets/assert/assert.out.nested_tmpl.stdout
+@@ -0,0 +1,12 @@
++
++PLAY [localhost] ***************************************************************
++
++TASK [assert] ******************************************************************
++ok: [localhost] => {
++    "changed": false,
++    "msg": "All assertions passed"
++}
++
++PLAY RECAP *********************************************************************
++localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
++
+diff --git a/test/integration/targets/assert/assert_quiet.out.quiet.stderr b/test/integration/targets/assert/assert.out.quiet.stderr
+similarity index 100%
+rename from test/integration/targets/assert/assert_quiet.out.quiet.stderr
+rename to test/integration/targets/assert/assert.out.quiet.stderr
+diff --git a/test/integration/targets/assert/assert_quiet.out.quiet.stdout b/test/integration/targets/assert/assert.out.quiet.stdout
+similarity index 100%
+rename from test/integration/targets/assert/assert_quiet.out.quiet.stdout
+rename to test/integration/targets/assert/assert.out.quiet.stdout
+diff --git a/test/integration/targets/assert/nested_tmpl.yml b/test/integration/targets/assert/nested_tmpl.yml
+new file mode 100644
+index 0000000000..3da4b1d80e
+--- /dev/null
++++ b/test/integration/targets/assert/nested_tmpl.yml
+@@ -0,0 +1,9 @@
++- hosts: localhost
++  gather_facts: False
++  tasks:
++    - assert:
++        that:
++          - '"{{ foo }}" == "bar"'
++          - foo == "bar"
++      vars:
++        foo: bar
+diff --git a/test/integration/targets/assert/quiet.yml b/test/integration/targets/assert/quiet.yml
+index 6834712c2c..1c425cb5ba 100644
+--- a/test/integration/targets/assert/quiet.yml
++++ b/test/integration/targets/assert/quiet.yml
+@@ -5,12 +5,12 @@
+     item_A: yes
+   tasks:
+   - assert:
+-      that: "{{ item }} is defined"
++      that: "item is defined"
+       quiet: True
+     with_items:
+       - item_A
+   - assert:
+-      that: "{{ item }} is defined"
++      that: "item is defined"
+       quiet: False
+     with_items:
+       - item_A
+diff --git a/test/integration/targets/assert/runme.sh b/test/integration/targets/assert/runme.sh
+index ca0a858726..b79072813d 100755
+--- a/test/integration/targets/assert/runme.sh
++++ b/test/integration/targets/assert/runme.sh
+@@ -45,7 +45,7 @@ cleanup() {
+    fi
+ }
+
+-BASEFILE=assert_quiet.out
++BASEFILE=assert.out
+
+ ORIGFILE="${BASEFILE}"
+ OUTFILE="${BASEFILE}.new"
+@@ -69,3 +69,4 @@ export ANSIBLE_NOCOLOR=1
+ export ANSIBLE_RETRY_FILES_ENABLED=0
+
+ run_test quiet
++run_test nested_tmpl
+diff --git a/test/integration/targets/command_shell/tasks/main.yml b/test/integration/targets/command_shell/tasks/main.yml
+index 12a944c48c..1f4aa5d75e 100644
+--- a/test/integration/targets/command_shell/tasks/main.yml
++++ b/test/integration/targets/command_shell/tasks/main.yml
+@@ -296,7 +296,7 @@
+   assert:
+     that:
+     - shell_result0 is changed
+-    - shell_result0.cmd == '{{ remote_tmp_dir_test }}/test.sh'
++    - shell_result0.cmd == remote_tmp_dir_test ~ '/test.sh'
+     - shell_result0.rc == 0
+     - shell_result0.stderr == ''
+     - shell_result0.stdout == 'win'
+diff --git a/test/integration/targets/copy/tasks/tests.yml b/test/integration/targets/copy/tasks/tests.yml
+index 7220356332..d6c8e63c9a 100644
+--- a/test/integration/targets/copy/tasks/tests.yml
++++ b/test/integration/targets/copy/tasks/tests.yml
+@@ -1176,7 +1176,7 @@
+   assert:
+     that:
+       - "copy_result6.changed"
+-      - "copy_result6.dest == '{{remote_dir_expanded}}/multiline.txt'"
++      - "copy_result6.dest == remote_dir_expanded ~ '/multiline.txt'"
+       - "copy_result6.checksum == '9cd0697c6a9ff6689f0afb9136fa62e0b3fee903'"
+
+ # test overwriting a file as an unprivileged user (pull request #8624)
+@@ -2079,26 +2079,26 @@
+     assert:
+       that:
+       - testcase5 is changed
+-      - "stat_new_dir_with_chown.stat.uid == {{ ansible_copy_test_user.uid }}"
+-      - "stat_new_dir_with_chown.stat.gid == {{ ansible_copy_test_group.gid }}"
+-      - "stat_new_dir_with_chown.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
+-      - "stat_new_dir_with_chown.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
+-      - "stat_new_dir_with_chown_file1.stat.uid == {{ ansible_copy_test_user.uid }}"
+-      - "stat_new_dir_with_chown_file1.stat.gid == {{ ansible_copy_test_group.gid }}"
+-      - "stat_new_dir_with_chown_file1.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
+-      - "stat_new_dir_with_chown_file1.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
+-      - "stat_new_dir_with_chown_subdir.stat.uid == {{ ansible_copy_test_user.uid }}"
+-      - "stat_new_dir_with_chown_subdir.stat.gid == {{ ansible_copy_test_group.gid }}"
+-      - "stat_new_dir_with_chown_subdir.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
+-      - "stat_new_dir_with_chown_subdir.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
+-      - "stat_new_dir_with_chown_subdir_file12.stat.uid == {{ ansible_copy_test_user.uid }}"
+-      - "stat_new_dir_with_chown_subdir_file12.stat.gid == {{ ansible_copy_test_group.gid }}"
+-      - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
+-      - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
+-      - "stat_new_dir_with_chown_link_file12.stat.uid == {{ ansible_copy_test_user.uid }}"
+-      - "stat_new_dir_with_chown_link_file12.stat.gid == {{ ansible_copy_test_group.gid }}"
+-      - "stat_new_dir_with_chown_link_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
+-      - "stat_new_dir_with_chown_link_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
++      - "stat_new_dir_with_chown.stat.uid == ansible_copy_test_user.uid"
++      - "stat_new_dir_with_chown.stat.gid == ansible_copy_test_group.gid"
++      - "stat_new_dir_with_chown.stat.pw_name == ansible_copy_test_user_name"
++      - "stat_new_dir_with_chown.stat.gr_name == ansible_copy_test_user_name"
++      - "stat_new_dir_with_chown_file1.stat.uid == ansible_copy_test_user.uid"
++      - "stat_new_dir_with_chown_file1.stat.gid == ansible_copy_test_group.gid"
++      - "stat_new_dir_with_chown_file1.stat.pw_name == ansible_copy_test_user_name"
++      - "stat_new_dir_with_chown_file1.stat.gr_name == ansible_copy_test_user_name"
++      - "stat_new_dir_with_chown_subdir.stat.uid == ansible_copy_test_user.uid"
++      - "stat_new_dir_with_chown_subdir.stat.gid == ansible_copy_test_group.gid"
++      - "stat_new_dir_with_chown_subdir.stat.pw_name == ansible_copy_test_user_name"
++      - "stat_new_dir_with_chown_subdir.stat.gr_name == ansible_copy_test_user_name"
++      - "stat_new_dir_with_chown_subdir_file12.stat.uid == ansible_copy_test_user.uid"
++      - "stat_new_dir_with_chown_subdir_file12.stat.gid == ansible_copy_test_group.gid"
++      - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == ansible_copy_test_user_name"
++      - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == ansible_copy_test_user_name"
++      - "stat_new_dir_with_chown_link_file12.stat.uid == ansible_copy_test_user.uid"
++      - "stat_new_dir_with_chown_link_file12.stat.gid == ansible_copy_test_group.gid"
++      - "stat_new_dir_with_chown_link_file12.stat.pw_name == ansible_copy_test_user_name"
++      - "stat_new_dir_with_chown_link_file12.stat.gr_name == ansible_copy_test_user_name"
+
+   always:
+     - name: execute - remove the user for test
+diff --git a/test/integration/targets/debug/runme.sh b/test/integration/targets/debug/runme.sh
+index 5faeb782a6..dc02859d35 100755
+--- a/test/integration/targets/debug/runme.sh
++++ b/test/integration/targets/debug/runme.sh
+@@ -18,3 +18,5 @@ done
+
+ # ensure debug does not set top level vars when looking at ansible_facts
+ ansible-playbook nosetfacts.yml "$@"
++
++ansible-playbook unsafe.yml "$@"
+diff --git a/test/integration/targets/debug/unsafe.yml b/test/integration/targets/debug/unsafe.yml
+new file mode 100644
+index 0000000000..6a78af1a69
+--- /dev/null
++++ b/test/integration/targets/debug/unsafe.yml
+@@ -0,0 +1,13 @@
++- hosts: localhost
++  gather_facts: false
++  vars:
++    unsafe_var: !unsafe undef()|mandatory
++  tasks:
++    - debug:
++        var: '{{ unsafe_var }}'
++      ignore_errors: true
++      register: result
++
++    - assert:
++        that:
++          - result is successful
+diff --git a/test/integration/targets/dnf/tasks/test_sos_removal.yml b/test/integration/targets/dnf/tasks/test_sos_removal.yml
+index 40ceb62bf4..0d70cf7877 100644
+--- a/test/integration/targets/dnf/tasks/test_sos_removal.yml
++++ b/test/integration/targets/dnf/tasks/test_sos_removal.yml
+@@ -15,5 +15,5 @@
+     that:
+       - sos_rm is successful
+       - sos_rm is changed
+-      - "'Removed: sos-{{ sos_version }}-{{ sos_release }}' in sos_rm.results[0]"
++      - "'Removed: sos-' ~ sos_version ~ '-' ~ sos_release in sos_rm.results[0]"
+       - sos_rm.results|length == 1
+diff --git a/test/integration/targets/expect/tasks/main.yml b/test/integration/targets/expect/tasks/main.yml
+index d6f43f2c6a..7bf18c5e5c 100644
+--- a/test/integration/targets/expect/tasks/main.yml
++++ b/test/integration/targets/expect/tasks/main.yml
+@@ -117,7 +117,7 @@
+ - name: assert chdir works
+   assert:
+     that:
+-    - "'{{chdir_result.stdout | trim}}' == '{{remote_tmp_dir_real_path.stdout | trim}}'"
++    - "chdir_result.stdout | trim == remote_tmp_dir_real_path.stdout | trim"
+
+ - name: test timeout option
+   expect:
+diff --git a/test/integration/targets/file/tasks/main.yml b/test/integration/targets/file/tasks/main.yml
+index 17b0fae68a..a5bd68d768 100644
+--- a/test/integration/targets/file/tasks/main.yml
++++ b/test/integration/targets/file/tasks/main.yml
+@@ -927,7 +927,7 @@
+     that:
+       - "file_error3 is failed"
+       - "file_error3.msg == 'src does not exist'"
+-      - "file_error3.dest == '{{ remote_tmp_dir_test }}/hard.txt' | expanduser"
++      - "file_error3.dest == remote_tmp_dir_test | expanduser ~ '/hard.txt'"
+       - "file_error3.src == 'non-existing-file-that-does-not-exist.txt'"
+
+ - block:
+diff --git a/test/integration/targets/file/tasks/state_link.yml b/test/integration/targets/file/tasks/state_link.yml
+index 673fe6fd52..6f96cdcba9 100644
+--- a/test/integration/targets/file/tasks/state_link.yml
++++ b/test/integration/targets/file/tasks/state_link.yml
+@@ -199,7 +199,7 @@
+       - "missing_dst_no_follow_enable_force_use_mode2 is changed"
+       - "missing_dst_no_follow_enable_force_use_mode3 is not changed"
+       - "soft3_result['stat'].islnk"
+-      - "soft3_result['stat'].lnk_target == '{{ user.home }}/nonexistent'"
++      - "soft3_result['stat'].lnk_target == user.home ~ '/nonexistent'"
+
+ #
+ # Test creating a link to a directory https://github.com/ansible/ansible/issues/1369
+diff --git a/test/integration/targets/find/tasks/main.yml b/test/integration/targets/find/tasks/main.yml
+index 5381a14478..89c62b9b6f 100644
+--- a/test/integration/targets/find/tasks/main.yml
++++ b/test/integration/targets/find/tasks/main.yml
+@@ -267,7 +267,7 @@
+ - name: assert we skipped the ogg file
+   assert:
+     that:
+-      - '"{{ remote_tmp_dir_test }}/e/f/g/h/8.ogg" not in find_test3_list'
++      - 'remote_tmp_dir_test ~ "/e/f/g/h/8.ogg" not in find_test3_list'
+
+ - name: patterns with regex
+   find:
+@@ -317,7 +317,7 @@
+   assert:
+     that:
+       - result.matched == 1
+-      - '"{{ remote_tmp_dir_test }}/astest/old.txt" in astest_list'
++      - 'remote_tmp_dir_test ~ "/astest/old.txt" in astest_list'
+
+ - name: find files newer than 1 week
+   find:
+@@ -332,7 +332,7 @@
+   assert:
+     that:
+       - result.matched == 1
+-      - '"{{ remote_tmp_dir_test }}/astest/new.txt" in astest_list'
++      - 'remote_tmp_dir_test ~ "/astest/new.txt" in astest_list'
+
+ - name: add some content to the new file
+   shell: "echo hello world > {{ remote_tmp_dir_test }}/astest/new.txt"
+@@ -352,7 +352,7 @@
+   assert:
+     that:
+       - result.matched == 1
+-      - '"{{ remote_tmp_dir_test }}/astest/new.txt" in astest_list'
++      - 'remote_tmp_dir_test ~ "/astest/new.txt" in astest_list'
+       - '"checksum" in result.files[0]'
+
+ - name: find ANY item with LESS than 5 bytes, also get checksums
+@@ -371,6 +371,6 @@
+   assert:
+     that:
+       - result.matched == 2
+-      - '"{{ remote_tmp_dir_test }}/astest/old.txt" in astest_list'
+-      - '"{{ remote_tmp_dir_test }}/astest/.hidden.txt" in astest_list'
++      - 'remote_tmp_dir_test ~ "/astest/old.txt" in astest_list'
++      - 'remote_tmp_dir_test ~ "/astest/.hidden.txt" in astest_list'
+       - '"checksum" in result.files[0]'
+diff --git a/test/integration/targets/gathering_facts/test_gathering_facts.yml b/test/integration/targets/gathering_facts/test_gathering_facts.yml
+index 47027e8717..faa187b73e 100644
+--- a/test/integration/targets/gathering_facts/test_gathering_facts.yml
++++ b/test/integration/targets/gathering_facts/test_gathering_facts.yml
+@@ -433,7 +433,7 @@
+     - name: Test reading facts from default fact_path
+       assert:
+         that:
+-          - '"{{ ansible_local.testfact.fact_dir }}" == "default"'
++          - 'ansible_local.testfact.fact_dir == "default"'
+
+ - hosts: facthost9
+   tags: [ 'fact_local']
+@@ -444,7 +444,7 @@
+     - name: Test reading facts from custom fact_path
+       assert:
+         that:
+-          - '"{{ ansible_local.testfact.fact_dir }}" == "custom"'
++          - 'ansible_local.testfact.fact_dir == "custom"'
+
+ - hosts: facthost20
+   tags: [ 'fact_facter_ohai' ]
+diff --git a/test/integration/targets/git/tasks/depth.yml b/test/integration/targets/git/tasks/depth.yml
+index 547f84f7b5..e0585ca39b 100644
+--- a/test/integration/targets/git/tasks/depth.yml
++++ b/test/integration/targets/git/tasks/depth.yml
+@@ -169,7 +169,7 @@
+ - name: DEPTH | check update arrived
+   assert:
+     that:
+-      - "{{ a_file.content | b64decode | trim }} == 3"
++      - a_file.content | b64decode | trim == "3"
+       - git_fetch is changed
+
+ - name: DEPTH | clear checkout_dir
+diff --git a/test/integration/targets/git/tasks/localmods.yml b/test/integration/targets/git/tasks/localmods.yml
+index 09a1326d58..0e0cf684ed 100644
+--- a/test/integration/targets/git/tasks/localmods.yml
++++ b/test/integration/targets/git/tasks/localmods.yml
+@@ -47,7 +47,7 @@
+ - name: LOCALMODS | check update arrived
+   assert:
+     that:
+-      - "{{ a_file.content | b64decode | trim }} == 2"
++      - a_file.content | b64decode | trim == "2"
+       - git_fetch_force is changed
+
+ - name: LOCALMODS | clear checkout_dir
+@@ -105,7 +105,7 @@
+ - name: LOCALMODS | check update arrived
+   assert:
+     that:
+-      - "{{ a_file.content | b64decode | trim }} == 2"
++      - a_file.content | b64decode | trim == "2"
+       - git_fetch_force is changed
+
+ - name: LOCALMODS | clear checkout_dir
+diff --git a/test/integration/targets/git/tasks/submodules.yml b/test/integration/targets/git/tasks/submodules.yml
+index 0b311e7984..b6b02490b4 100644
+--- a/test/integration/targets/git/tasks/submodules.yml
++++ b/test/integration/targets/git/tasks/submodules.yml
+@@ -32,7 +32,7 @@
+
+ - name: SUBMODULES | Ensure submodu1 is at the appropriate commit
+   assert:
+-    that: '{{ submodule1.stdout_lines | length }} == 2'
++    that: 'submodule1.stdout_lines | length == 2'
+
+ - name: SUBMODULES | clear checkout_dir
+   file:
+@@ -53,7 +53,7 @@
+
+ - name: SUBMODULES | Ensure submodule1 is at the appropriate commit
+   assert:
+-    that: '{{ submodule1.stdout_lines | length }} == 4'
++    that: 'submodule1.stdout_lines | length == 4'
+
+ - name: SUBMODULES | Copy the checkout so we can run several different tests on it
+   command: 'cp -pr {{ checkout_dir }} {{ checkout_dir }}.bak'
+@@ -84,8 +84,8 @@
+ - name: SUBMODULES | Ensure both submodules are at the appropriate commit
+   assert:
+     that:
+-      - '{{ submodule1.stdout_lines|length }} == 4'
+-      - '{{ submodule2.stdout_lines|length }} == 2'
++      - 'submodule1.stdout_lines|length == 4'
++      - 'submodule2.stdout_lines|length == 2'
+
+
+ - name: SUBMODULES | Remove checkout dir
+@@ -112,7 +112,7 @@
+
+ - name: SUBMODULES | Ensure submodule1 is at the appropriate commit
+   assert:
+-    that: '{{ submodule1.stdout_lines | length }} == 5'
++    that: 'submodule1.stdout_lines | length == 5'
+
+
+ - name: SUBMODULES | Test that update with recursive found new submodules
+@@ -121,7 +121,7 @@
+
+ - name: SUBMODULES | Enusre submodule2 is at the appropriate commit
+   assert:
+-    that: '{{ submodule2.stdout_lines | length }} == 4'
++    that: 'submodule2.stdout_lines | length == 4'
+
+ - name: SUBMODULES | clear checkout_dir
+   file:
+@@ -147,4 +147,4 @@
+
+ - name: SUBMODULES | Ensure submodule1 is at the appropriate commit
+   assert:
+-    that: '{{ submodule1.stdout_lines | length }} == 4'
++    that: 'submodule1.stdout_lines | length == 4'
+diff --git a/test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml b/test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml
+index f1ddc71b2c..e45331a148 100644
+--- a/test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml
++++ b/test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml
+@@ -22,7 +22,7 @@
+ - name: Check that multiple duplicate lines collapse into a single commands
+   assert:
+     that:
+-      - "{{ result.commands|length }} == 1"
++      - "result.commands|length == 1"
+
+ - name: Check that set is correctly prepended
+   assert:
+@@ -58,6 +58,6 @@
+
+ - assert:
+     that:
+-      - "{{ result.filtered|length }} == 2"
++      - "result.filtered|length == 2"
+
+ - debug: msg="END cli/config_check.yaml on connection={{ ansible_connection }}"
+diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml
+index 7b2d53a340..316e91c43d 100644
+--- a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml
++++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml
+@@ -16,17 +16,17 @@
+     - name: Assert that the before dicts were correctly generated
+       assert:
+         that:
+-          - "{{ populate | symmetric_difference(result['before']) |length == 0 }}"
++          - "populate | symmetric_difference(result['before']) |length == 0"
+
+     - name: Assert that the correct set of commands were generated
+       assert:
+         that:
+-          - "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
++          - "deleted['commands'] | symmetric_difference(result['commands']) |length == 0"
+
+     - name: Assert that the after dicts were correctly generated
+       assert:
+         that:
+-          - "{{ deleted['after'] | symmetric_difference(result['after']) |length == 0 }}"
++          - "deleted['after'] | symmetric_difference(result['after']) |length == 0"
+
+     - name: Delete attributes of given interfaces (IDEMPOTENT)
+       vyos.vyos.vyos_lldp_interfaces: *deleted
+@@ -41,6 +41,6 @@
+     - name: Assert that the before dicts were correctly generated
+       assert:
+         that:
+-          - "{{ deleted['after'] | symmetric_difference(result['before']) |length == 0 }}"
++          - "deleted['after'] | symmetric_difference(result['before']) |length == 0"
+   always:
+     - include_tasks: _remove_config.yaml
+diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml
+index bf968b21de..7e0bb53d33 100644
+--- a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml
++++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml
+@@ -28,17 +28,17 @@
+
+     - name: Assert that before dicts were correctly generated
+       assert:
+-        that: "{{ merged['before'] | symmetric_difference(result['before']) |length == 0 }}"
++        that: "merged['before'] | symmetric_difference(result['before']) |length == 0"
+
+     - name: Assert that correct set of commands were generated
+       assert:
+         that:
+-          - "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
++          - "merged['commands'] | symmetric_difference(result['commands']) |length == 0"
+
+     - name: Assert that after dicts was correctly generated
+       assert:
+         that:
+-          - "{{ merged['after'] | symmetric_difference(result['after']) |length == 0 }}"
++          - "merged['after'] | symmetric_difference(result['after']) |length == 0"
+
+     - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT)
+       vyos.vyos.vyos_lldp_interfaces: *merged
+@@ -52,7 +52,7 @@
+     - name: Assert that before dicts were correctly generated
+       assert:
+         that:
+-          - "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
++          - "merged['after'] | symmetric_difference(result['before']) |length == 0"
+
+   always:
+     - include_tasks: _remove_config.yaml
+diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml
+index 8cf038c91b..ad13f39328 100644
+--- a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml
++++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml
+@@ -19,17 +19,17 @@
+     - name: Assert that before dicts were correctly generated
+       assert:
+         that:
+-          - "{{ populate_intf | symmetric_difference(result['before']) |length == 0 }}"
++          - "populate_intf | symmetric_difference(result['before']) |length == 0"
+
+     - name: Assert that correct commands were generated
+       assert:
+         that:
+-          - "{{ overridden['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
++          - "overridden['commands'] | symmetric_difference(result['commands']) |length == 0"
+
+     - name: Assert that after dicts were correctly generated
+       assert:
+         that:
+-          - "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}"
++          - "overridden['after'] | symmetric_difference(result['after']) |length == 0"
+
+     - name: Overrides all device configuration with provided configurations (IDEMPOTENT)
+       vyos.vyos.vyos_lldp_interfaces: *overridden
+@@ -43,7 +43,7 @@
+     - name: Assert that before dicts were correctly generated
+       assert:
+         that:
+-          - "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}"
++          - "overridden['after'] | symmetric_difference(result['before']) |length == 0"
+
+   always:
+     - include_tasks: _remove_config.yaml
+diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml
+index 17acf0654c..aadc379300 100644
+--- a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml
++++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml
+@@ -33,17 +33,17 @@
+     - name: Assert that correct set of commands were generated
+       assert:
+         that:
+-          - "{{ replaced['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
++          - "replaced['commands'] | symmetric_difference(result['commands']) |length == 0"
+
+     - name: Assert that before dicts are correctly generated
+       assert:
+         that:
+-          - "{{ populate | symmetric_difference(result['before']) |length == 0 }}"
++          - "populate | symmetric_difference(result['before']) |length == 0"
+
+     - name: Assert that after dict is correctly generated
+       assert:
+         that:
+-          - "{{ replaced['after'] | symmetric_difference(result['after']) |length == 0 }}"
++          - "replaced['after'] | symmetric_difference(result['after']) |length == 0"
+
+     - name: Replace device configurations of listed LLDP interfaces with provided configurarions (IDEMPOTENT)
+       vyos.vyos.vyos_lldp_interfaces: *replaced
+@@ -57,7 +57,7 @@
+     - name: Assert that before dict is correctly generated
+       assert:
+         that:
+-          - "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}"
++          - "replaced['after'] | symmetric_difference(result['before']) |length == 0"
+
+   always:
+     - include_tasks: _remove_config.yaml
+diff --git a/test/integration/targets/include_vars/tasks/main.yml b/test/integration/targets/include_vars/tasks/main.yml
+index db15ba3c5d..6fc4e85a33 100644
+--- a/test/integration/targets/include_vars/tasks/main.yml
++++ b/test/integration/targets/include_vars/tasks/main.yml
+@@ -15,7 +15,7 @@
+     that:
+       - "testing == 789"
+       - "base_dir == 'environments/development'"
+-      - "{{ included_one_file.ansible_included_var_files | length }} == 1"
++      - "included_one_file.ansible_included_var_files | length == 1"
+       - "'vars/environments/development/all.yml' in included_one_file.ansible_included_var_files[0]"
+
+ - name: include the vars/environments/development/all.yml and save results in all
+@@ -51,7 +51,7 @@
+   assert:
+     that:
+       - webapp_version is defined
+-      - "'file_without_extension' in '{{ include_without_file_extension.ansible_included_var_files | join(' ') }}'"
++      - "'file_without_extension' in include_without_file_extension.ansible_included_var_files | join(' ')"
+
+ - name: include every directory in vars
+   include_vars:
+@@ -67,7 +67,7 @@
+       - "testing == 456"
+       - "base_dir == 'services'"
+       - "webapp_containers == 10"
+-      - "{{ include_every_dir.ansible_included_var_files | length }} == 7"
++      - "include_every_dir.ansible_included_var_files | length == 7"
+       - "'vars/all/all.yml' in include_every_dir.ansible_included_var_files[0]"
+       - "'vars/environments/development/all.yml' in include_every_dir.ansible_included_var_files[1]"
+       - "'vars/environments/development/services/webapp.yml' in include_every_dir.ansible_included_var_files[2]"
+@@ -88,9 +88,9 @@
+     that:
+       - "testing == 789"
+       - "base_dir == 'environments/development'"
+-      - "{{ include_without_webapp.ansible_included_var_files | length }} == 4"
+-      - "'webapp.yml' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'"
+-      - "'file_without_extension' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'"
++      - "include_without_webapp.ansible_included_var_files | length == 4"
++      - "'webapp.yml' not in include_without_webapp.ansible_included_var_files | join(' ')"
++      - "'file_without_extension' not in include_without_webapp.ansible_included_var_files | join(' ')"
+
+ - name: include only files matching webapp.yml
+   include_vars:
+@@ -104,9 +104,9 @@
+       - "testing == 101112"
+       - "base_dir == 'development/services'"
+       - "webapp_containers == 20"
+-      - "{{ include_match_webapp.ansible_included_var_files | length }} == 1"
++      - "include_match_webapp.ansible_included_var_files | length == 1"
+       - "'vars/environments/development/services/webapp.yml' in include_match_webapp.ansible_included_var_files[0]"
+-      - "'all.yml' not in '{{ include_match_webapp.ansible_included_var_files | join(' ') }}'"
++      - "'all.yml' not in include_match_webapp.ansible_included_var_files | join(' ')"
+
+ - name: include only files matching webapp.yml and store results in webapp
+   include_vars:
+@@ -173,10 +173,10 @@
+ - name: Verify the hash variable
+   assert:
+     that:
+-      - "{{ config | length }} == 3"
++      - "config | length == 3"
+       - "config.key0 == 0"
+       - "config.key1 == 0"
+-      - "{{ config.key2 | length }} == 1"
++      - "config.key2 | length == 1"
+       - "config.key2.a == 21"
+
+ - name: Include the second file to merge the hash variable
+@@ -187,10 +187,10 @@
+ - name: Verify that the hash is merged
+   assert:
+     that:
+-      - "{{ config | length }} == 4"
++      - "config | length == 4"
+       - "config.key0 == 0"
+       - "config.key1 == 1"
+-      - "{{ config.key2 | length }} == 2"
++      - "config.key2 | length == 2"
+       - "config.key2.a == 21"
+       - "config.key2.b == 22"
+       - "config.key3 == 3"
+@@ -202,9 +202,9 @@
+ - name: Verify that the properties from the first file is cleared
+   assert:
+     that:
+-      - "{{ config | length }} == 3"
++      - "config | length == 3"
+       - "config.key1 == 1"
+-      - "{{ config.key2 | length }} == 1"
++      - "config.key2 | length == 1"
+       - "config.key2.b == 22"
+       - "config.key3 == 3"
+
+diff --git a/test/integration/targets/lookup_ini/test_lookup_properties.yml b/test/integration/targets/lookup_ini/test_lookup_properties.yml
+index a6fc0f7d7c..ed34760092 100644
+--- a/test/integration/targets/lookup_ini/test_lookup_properties.yml
++++ b/test/integration/targets/lookup_ini/test_lookup_properties.yml
+@@ -10,7 +10,7 @@
+         field_with_space: "{{lookup('ini', 'field.with.space  type=properties  file=lookup.properties')}}"
+
+     - assert:
+-        that: "{{item}} is defined"
++        that: "item is defined"
+       with_items: [ 'test1', 'test2', 'test_dot', 'field_with_space' ]
+
+     - name: "read ini value"
+diff --git a/test/integration/targets/lookup_subelements/tasks/main.yml b/test/integration/targets/lookup_subelements/tasks/main.yml
+index 9d93cf2096..7885347bb2 100644
+--- a/test/integration/targets/lookup_subelements/tasks/main.yml
++++ b/test/integration/targets/lookup_subelements/tasks/main.yml
+@@ -133,7 +133,7 @@
+
+ - assert:
+     that:
+-      - "'{{ item.0.name }}' != 'carol'"
++      - "item.0.name != 'carol'"
+   with_subelements:
+     - "{{ users }}"
+     - mysql.privs
+@@ -220,5 +220,5 @@
+
+ - assert:
+     that:
+-      - "'{{ user_alice }}' == 'localhost'"
+-      - "'{{ user_bob }}' == 'db1'"
++      - "user_alice == 'localhost'"
++      - "user_bob == 'db1'"
+diff --git a/test/integration/targets/loop_control/inner.yml b/test/integration/targets/loop_control/inner.yml
+index 1c286fa460..976f196102 100644
+--- a/test/integration/targets/loop_control/inner.yml
++++ b/test/integration/targets/loop_control/inner.yml
+@@ -3,7 +3,7 @@
+     that:
+       - ansible_loop.index == ansible_loop.index0 + 1
+       - ansible_loop.revindex == ansible_loop.revindex0 + 1
+-      - ansible_loop.first == {{ ansible_loop.index == 1 }}
+-      - ansible_loop.last == {{ ansible_loop.index == ansible_loop.length }}
++      - ansible_loop.first == (ansible_loop.index == 1)
++      - ansible_loop.last == (ansible_loop.index == ansible_loop.length)
+       - ansible_loop.length == 3
+       - ansible_loop.allitems|join(',') == 'first,second,third'
+diff --git a/test/integration/targets/module_precedence/modules_test_multiple_roles.yml b/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
+index f4bd264957..182c2158e8 100644
+--- a/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
++++ b/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
+@@ -14,4 +14,4 @@
+   - assert:
+       that:
+         - '"location" in result'
+-        - 'result["location"] == "{{ expected_location}}"'
++        - 'result["location"] == expected_location'
+diff --git a/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml b/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
+index 5403ae238c..ec5619f39e 100644
+--- a/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
++++ b/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
+@@ -13,4 +13,4 @@
+   - assert:
+       that:
+         - '"location" in result'
+-        - 'result["location"] == "{{ expected_location}}"'
++        - 'result["location"] == expected_location'
+diff --git a/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml b/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
+index 52c3402013..62b38a7cb5 100644
+--- a/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
++++ b/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
+@@ -7,4 +7,4 @@
+   assert:
+     that:
+       - '"location" in result'
+-      - 'result["location"] == "{{ expected_location }}"'
++      - 'result["location"] == expected_location'
+diff --git a/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml b/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
+index 52c3402013..62b38a7cb5 100644
+--- a/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
++++ b/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
+@@ -7,4 +7,4 @@
+   assert:
+     that:
+       - '"location" in result'
+-      - 'result["location"] == "{{ expected_location }}"'
++      - 'result["location"] == expected_location'
+diff --git a/test/integration/targets/script/tasks/main.yml b/test/integration/targets/script/tasks/main.yml
+index 989513d531..74189f817d 100644
+--- a/test/integration/targets/script/tasks/main.yml
++++ b/test/integration/targets/script/tasks/main.yml
+@@ -198,7 +198,7 @@
+   assert:
+     that:
+       - _check_mode_test2 is skipped
+-      - '_check_mode_test2.msg == "{{ remote_tmp_dir_test | expanduser }}/afile2.txt exists, matching creates option"'
++      - '_check_mode_test2.msg == remote_tmp_dir_test | expanduser ~ "/afile2.txt exists, matching creates option"'
+
+ - name: Remove afile2.txt
+   file:
+@@ -220,7 +220,7 @@
+   assert:
+     that:
+       - _check_mode_test3 is skipped
+-      - '_check_mode_test3.msg == "{{ remote_tmp_dir_test | expanduser }}/afile2.txt does not exist, matching removes option"'
++      - '_check_mode_test3.msg == remote_tmp_dir_test | expanduser ~ "/afile2.txt does not exist, matching removes option"'
+
+ # executable
+
+diff --git a/test/integration/targets/slurp/tasks/main.yml b/test/integration/targets/slurp/tasks/main.yml
+index 939859415a..f8ebb1594c 100644
+--- a/test/integration/targets/slurp/tasks/main.yml
++++ b/test/integration/targets/slurp/tasks/main.yml
+@@ -33,7 +33,7 @@
+       - 'slurp_existing.encoding == "base64"'
+       - 'slurp_existing is not changed'
+       - 'slurp_existing is not failed'
+-      - '"{{ slurp_existing.content | b64decode }}" == "We are at the café"'
++      - 'slurp_existing.content | b64decode == "We are at the café"'
+
+ - name: Create a binary file to test with
+   copy:
+diff --git a/test/integration/targets/template/tasks/main.yml b/test/integration/targets/template/tasks/main.yml
+index c0d2e11a65..3c91734b09 100644
+--- a/test/integration/targets/template/tasks/main.yml
++++ b/test/integration/targets/template/tasks/main.yml
+@@ -357,7 +357,7 @@
+ - assert:
+     that:
+       - "\"foo t'e~m\\plated\" in unusual_results.stdout_lines"
+-      - "{{unusual_results.stdout_lines| length}} == 1"
++      - "unusual_results.stdout_lines| length == 1"
+
+ - name: check that the unusual filename can be checked for changes
+   template:
+diff --git a/test/integration/targets/unarchive/tasks/test_missing_binaries.yml b/test/integration/targets/unarchive/tasks/test_missing_binaries.yml
+index 58d38f4f91..49f862b46f 100644
+--- a/test/integration/targets/unarchive/tasks/test_missing_binaries.yml
++++ b/test/integration/targets/unarchive/tasks/test_missing_binaries.yml
+@@ -66,7 +66,7 @@
+           - zip_success.changed
+           # Verify that file list is generated
+           - "'files' in zip_success"
+-          - "{{zip_success['files']| length}} == 3"
++          - "zip_success['files']| length == 3"
+           - "'foo-unarchive.txt' in zip_success['files']"
+           - "'foo-unarchive-777.txt' in zip_success['files']"
+           - "'FOO-UNAR.TXT' in zip_success['files']"
+diff --git a/test/integration/targets/unarchive/tasks/test_mode.yml b/test/integration/targets/unarchive/tasks/test_mode.yml
+index c69e3bd2b2..06fbc7b8d9 100644
+--- a/test/integration/targets/unarchive/tasks/test_mode.yml
++++ b/test/integration/targets/unarchive/tasks/test_mode.yml
+@@ -24,7 +24,7 @@
+       - "unarchive06_stat.stat.mode == '0600'"
+       # Verify that file list is generated
+       - "'files' in unarchive06"
+-      - "{{unarchive06['files']| length}} == 1"
++      - "unarchive06['files']| length == 1"
+       - "'foo-unarchive.txt' in unarchive06['files']"
+
+ - name: remove our tar.gz unarchive destination
+@@ -74,7 +74,7 @@
+       - "unarchive07.changed == false"
+       # Verify that file list is generated
+       - "'files' in unarchive07"
+-      - "{{unarchive07['files']| length}} == 1"
++      - "unarchive07['files']| length == 1"
+       - "'foo-unarchive.txt' in unarchive07['files']"
+
+ - name: remove our tar.gz unarchive destination
+@@ -108,7 +108,7 @@
+       - "unarchive08_stat.stat.mode == '0601'"
+       # Verify that file list is generated
+       - "'files' in unarchive08"
+-      - "{{unarchive08['files']| length}} == 3"
++      - "unarchive08['files']| length == 3"
+       - "'foo-unarchive.txt' in unarchive08['files']"
+       - "'foo-unarchive-777.txt' in unarchive08['files']"
+       - "'FOO-UNAR.TXT' in unarchive08['files']"
+@@ -140,7 +140,7 @@
+       - "unarchive08_stat.stat.mode == '0601'"
+       # Verify that file list is generated
+       - "'files' in unarchive08"
+-      - "{{unarchive08['files']| length}} == 3"
++      - "unarchive08['files']| length == 3"
+       - "'foo-unarchive.txt' in unarchive08['files']"
+       - "'foo-unarchive-777.txt' in unarchive08['files']"
+       - "'FOO-UNAR.TXT' in unarchive08['files']"
+diff --git a/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml b/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
+index 8ee1db49e4..9f45e4c991 100644
+--- a/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
++++ b/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
+@@ -40,7 +40,7 @@
+           - unarchive10 is changed
+           # Verify that file list is generated
+           - "'files' in unarchive10"
+-          - "{{unarchive10['files']| length}} == 1"
++          - "unarchive10['files']| length == 1"
+           - "'foo-unarchive.txt' in unarchive10['files']"
+           - archive_path.stat.exists
+
+diff --git a/test/integration/targets/unarchive/tasks/test_zip.yml b/test/integration/targets/unarchive/tasks/test_zip.yml
+index cf03946fcd..0fc5dc9ce6 100644
+--- a/test/integration/targets/unarchive/tasks/test_zip.yml
++++ b/test/integration/targets/unarchive/tasks/test_zip.yml
+@@ -17,7 +17,7 @@
+       - "unarchive03.changed == true"
+       # Verify that file list is generated
+       - "'files' in unarchive03"
+-      - "{{unarchive03['files']| length}} == 3"
++      - "unarchive03['files']| length == 3"
+       - "'foo-unarchive.txt' in unarchive03['files']"
+       - "'foo-unarchive-777.txt' in unarchive03['files']"
+       - "'FOO-UNAR.TXT' in unarchive03['files']"
+diff --git a/test/integration/targets/wait_for/tasks/main.yml b/test/integration/targets/wait_for/tasks/main.yml
+index f71ddbda6b..f81fd0f246 100644
+--- a/test/integration/targets/wait_for/tasks/main.yml
++++ b/test/integration/targets/wait_for/tasks/main.yml
+@@ -40,7 +40,7 @@
+   assert:
+     that:
+       - waitfor is successful
+-      - waitfor.path == "{{ remote_tmp_dir | expanduser }}/wait_for_file"
++      - waitfor.path == remote_tmp_dir | expanduser ~ "/wait_for_file"
+       - waitfor.elapsed >= 2
+       - waitfor.elapsed <= 15
+
+@@ -58,7 +58,7 @@
+   assert:
+     that:
+       - waitfor is successful
+-      - waitfor.path == "{{ remote_tmp_dir | expanduser }}/wait_for_file"
++      - waitfor.path == remote_tmp_dir | expanduser ~ "/wait_for_file"
+       - waitfor.elapsed >= 2
+       - waitfor.elapsed <= 15
+
+@@ -156,7 +156,7 @@
+     that:
+       - waitfor is successful
+       - waitfor is not changed
+-      - "waitfor.port == {{ http_port }}"
++      - "waitfor.port == http_port"
+
+ - name: install psutil using pip (non-Linux only)
+   pip:
+@@ -184,7 +184,7 @@
+     that:
+       - waitfor is successful
+       - waitfor is not changed
+-      - "waitfor.port == {{ http_port }}"
++      - "waitfor.port == http_port"
+
+ - name: test wait_for with delay
+   wait_for:
+diff --git a/test/units/parsing/yaml/test_dumper.py b/test/units/parsing/yaml/test_dumper.py
+index 5fbc139ba0..cbf5b45646 100644
+--- a/test/units/parsing/yaml/test_dumper.py
++++ b/test/units/parsing/yaml/test_dumper.py
+@@ -29,7 +29,6 @@ from ansible.parsing.yaml import dumper, objects
+ from ansible.parsing.yaml.loader import AnsibleLoader
+ from ansible.module_utils.six import PY2
+ from ansible.template import AnsibleUndefined
+-from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes
+
+ from units.mock.yaml_helper import YamlTestUtils
+ from units.mock.vault_helper import TextVaultSecret
+@@ -69,8 +68,7 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils):
+
+     def test_bytes(self):
+         b_text = u'tréma'.encode('utf-8')
+-        unsafe_object = AnsibleUnsafeBytes(b_text)
+-        yaml_out = self._dump_string(unsafe_object, dumper=self.dumper)
++        yaml_out = self._dump_string(b_text, dumper=self.dumper)
+
+         stream = self._build_stream(yaml_out)
+         loader = self._loader(stream)
+@@ -97,8 +95,7 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils):
+
+     def test_unicode(self):
+         u_text = u'nöel'
+-        unsafe_object = AnsibleUnsafeText(u_text)
+-        yaml_out = self._dump_string(unsafe_object, dumper=self.dumper)
++        yaml_out = self._dump_string(u_text, dumper=self.dumper)
+
+         stream = self._build_stream(yaml_out)
+         loader = self._loader(stream)
+--
+2.40.0
diff --git a/recipes-devtools/python/python3-ansible_2.14.11.bb b/recipes-devtools/python/python3-ansible_2.14.11.bb
index f3ab2377..f57ef6f8 100644
--- a/recipes-devtools/python/python3-ansible_2.14.11.bb
+++ b/recipes-devtools/python/python3-ansible_2.14.11.bb
@@ -9,4 +9,5 @@ RDEPENDS:${PN} += "python3-pyyaml \
 
 SRC_URI += " \
     file://python3-ensure-py-scripts-use-py3-for-shebang.patch \
+    file://CVE-2023-5764.patch \
 "
-- 
2.40.0



^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [meta-virtualization][meta-cloud-services][kirkstone][PATCH 1/1] python3-ansible: Fix CVE-2023-5764
  2025-04-25 10:50 [meta-virtualization][meta-cloud-services][kirkstone][PATCH 1/1] python3-ansible: Fix CVE-2023-5764 ssambu
@ 2025-05-06  1:47 ` Bruce Ashfield
  0 siblings, 0 replies; 2+ messages in thread
From: Bruce Ashfield @ 2025-05-06  1:47 UTC (permalink / raw)
  To: soumya.sambu; +Cc: meta-virtualization

When I try and apply the patch:

Applying: python3-ansible: Fix CVE-2023-5764
error: corrupt patch at line 1732
Patch failed at 0001 python3-ansible: Fix CVE-2023-5764
hint: Use 'git am --show-current-patch=diff' to see the failed patch
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".

Can you double check your patch sending configuration and resend ?

Bruce

In message: [meta-virtualization][meta-cloud-services][kirkstone][PATCH 1/1] python3-ansible: Fix CVE-2023-5764
on 25/04/2025 Soumya via lists.yoctoproject.org wrote:

> From: Soumya Sambu <soumya.sambu@windriver.com>
> 
> A template injection flaw was found in Ansible where a user's controller internal
> templating operations may remove the unsafe designation from template data. This
> issue could allow an attacker to use a specially crafted file to introduce templating
> injection when supplying templating data.
> 
> References:
> https://nvd.nist.gov/vuln/detail/CVE-2023-5764
> https://security-tracker.debian.org/tracker/CVE-2023-5764
> 
> Upstream patch:
> https://github.com/ansible/ansible/commit/7239d2d371bc6e274cbb7314e01431adce6ae25a
> 
> Signed-off-by: Soumya Sambu <soumya.sambu@windriver.com>
> ---
>  .../python3-ansible/CVE-2023-5764.patch       | 1737 +++++++++++++++++
>  .../python/python3-ansible_2.14.11.bb         |    1 +
>  2 files changed, 1738 insertions(+)
>  create mode 100644 recipes-devtools/python/python3-ansible/CVE-2023-5764.patch
> 
> diff --git a/recipes-devtools/python/python3-ansible/CVE-2023-5764.patch b/recipes-devtools/python/python3-ansible/CVE-2023-5764.patch
> new file mode 100644
> index 00000000..cfc7e21e
> --- /dev/null
> +++ b/recipes-devtools/python/python3-ansible/CVE-2023-5764.patch
> @@ -0,0 +1,1737 @@
> +From 7239d2d371bc6e274cbb7314e01431adce6ae25a Mon Sep 17 00:00:00 2001
> +From: Matt Martz <matt@sivel.net>
> +Date: Mon, 27 Nov 2023 12:27:58 -0600
> +Subject: [PATCH] Ensure that unsafe is more difficult to lose [stable-2.14]
> + (#82295)
> +
> +* Ensure that unsafe is more difficult to lose
> +
> +* Add Task.untemplated_args, and switch assert over to use it
> +* Don't use re in first_found, switch to using native string methods
> +* If nested templating results in unsafe, just error, don't continue
> +
> +(cherry picked from commit 586f1924512b01305f896d9ae4732773023013a3)
> +
> +* ci_complete
> +
> +CVE: CVE-2023-5764
> +
> +Upstream-Status: Backport [https://github.com/ansible/ansible/commit/7239d2d371bc6e274cbb7314e01431adce6ae25a]
> +
> +Signed-off-by: Soumya Sambu <soumya.sambu@windriver.com>
> +---
> + changelogs/fragments/cve-2023-5764.yml        |   6 +
> + lib/ansible/module_utils/common/json.py       |   4 +-
> + lib/ansible/parsing/yaml/dumper.py            |   6 +-
> + lib/ansible/playbook/conditional.py           |   9 +-
> + lib/ansible/playbook/task.py                  |  24 ++
> + lib/ansible/plugins/action/assert.py          |  23 +-
> + lib/ansible/plugins/callback/__init__.py      |   4 +-
> + lib/ansible/plugins/filter/core.py            |   5 +
> + lib/ansible/plugins/lookup/first_found.py     |  15 +-
> + lib/ansible/template/__init__.py              |  15 +-
> + lib/ansible/utils/unsafe_proxy.py             | 265 +++++++++++++++++-
> + .../tasks/scm_dependency_deduplication.yml    |  16 +-
> + .../roles/test_vault_embedded/tasks/main.yml  |   2 +-
> + .../tasks/main.yml                            |   2 +-
> + .../targets/apt_repository/tasks/apt.yml      |  10 +-
> + .../assert/assert.out.nested_tmpl.stderr      |   4 +
> + .../assert/assert.out.nested_tmpl.stdout      |  12 +
> + ...t.quiet.stderr => assert.out.quiet.stderr} |   0
> + ...t.quiet.stdout => assert.out.quiet.stdout} |   0
> + .../targets/assert/nested_tmpl.yml            |   9 +
> + test/integration/targets/assert/quiet.yml     |   4 +-
> + test/integration/targets/assert/runme.sh      |   3 +-
> + .../targets/command_shell/tasks/main.yml      |   2 +-
> + test/integration/targets/copy/tasks/tests.yml |  42 +--
> + test/integration/targets/debug/runme.sh       |   2 +
> + test/integration/targets/debug/unsafe.yml     |  13 +
> + .../targets/dnf/tasks/test_sos_removal.yml    |   2 +-
> + .../integration/targets/expect/tasks/main.yml |   2 +-
> + test/integration/targets/file/tasks/main.yml  |   2 +-
> + .../targets/file/tasks/state_link.yml         |   2 +-
> + test/integration/targets/find/tasks/main.yml  |  12 +-
> + .../gathering_facts/test_gathering_facts.yml  |   4 +-
> + test/integration/targets/git/tasks/depth.yml  |   2 +-
> + .../targets/git/tasks/localmods.yml           |   4 +-
> + .../targets/git/tasks/submodules.yml          |  14 +-
> + .../tests/cli/check_config.yaml               |   4 +-
> + .../tests/cli/deleted.yaml                    |   8 +-
> + .../tests/cli/merged.yaml                     |   8 +-
> + .../tests/cli/overridden.yaml                 |   8 +-
> + .../tests/cli/replaced.yaml                   |   8 +-
> + .../targets/include_vars/tasks/main.yml       |  28 +-
> + .../lookup_ini/test_lookup_properties.yml     |   2 +-
> + .../targets/lookup_subelements/tasks/main.yml |   6 +-
> + .../targets/loop_control/inner.yml            |   4 +-
> + .../modules_test_multiple_roles.yml           |   2 +-
> + ...ules_test_multiple_roles_reverse_order.yml |   2 +-
> + .../multiple_roles/bar/tasks/main.yml         |   2 +-
> + .../multiple_roles/foo/tasks/main.yml         |   2 +-
> + .../integration/targets/script/tasks/main.yml |   4 +-
> + test/integration/targets/slurp/tasks/main.yml |   2 +-
> + .../targets/template/tasks/main.yml           |   2 +-
> + .../unarchive/tasks/test_missing_binaries.yml |   2 +-
> + .../targets/unarchive/tasks/test_mode.yml     |   8 +-
> + .../tasks/test_unprivileged_user.yml          |   2 +-
> + .../targets/unarchive/tasks/test_zip.yml      |   2 +-
> + .../targets/wait_for/tasks/main.yml           |   8 +-
> + test/units/parsing/yaml/test_dumper.py        |   7 +-
> + 57 files changed, 519 insertions(+), 143 deletions(-)
> + create mode 100644 changelogs/fragments/cve-2023-5764.yml
> + create mode 100644 test/integration/targets/assert/assert.out.nested_tmpl.stderr
> + create mode 100644 test/integration/targets/assert/assert.out.nested_tmpl.stdout
> + rename test/integration/targets/assert/{assert_quiet.out.quiet.stderr => assert.out.quiet.stderr} (100%)
> + rename test/integration/targets/assert/{assert_quiet.out.quiet.stdout => assert.out.quiet.stdout} (100%)
> + create mode 100644 test/integration/targets/assert/nested_tmpl.yml
> + create mode 100644 test/integration/targets/debug/unsafe.yml
> +
> +diff --git a/changelogs/fragments/cve-2023-5764.yml b/changelogs/fragments/cve-2023-5764.yml
> +new file mode 100644
> +index 0000000000..c37127dac1
> +--- /dev/null
> ++++ b/changelogs/fragments/cve-2023-5764.yml
> +@@ -0,0 +1,6 @@
> ++security_fixes:
> ++- templating - Address issues where internal templating can cause unsafe
> ++  variables to lose their unsafe designation (CVE-2023-5764)
> ++breaking_changes:
> ++- assert - Nested templating may result in an inability for the conditional
> ++  to be evaluated. See the porting guide for more information.
> +diff --git a/lib/ansible/module_utils/common/json.py b/lib/ansible/module_utils/common/json.py
> +index 727083ca23..c4333fc157 100644
> +--- a/lib/ansible/module_utils/common/json.py
> ++++ b/lib/ansible/module_utils/common/json.py
> +@@ -30,7 +30,7 @@ def _preprocess_unsafe_encode(value):
> +     Used in ``AnsibleJSONEncoder.iterencode``
> +     """
> +     if _is_unsafe(value):
> +-        value = {'__ansible_unsafe': to_text(value, errors='surrogate_or_strict', nonstring='strict')}
> ++        value = {'__ansible_unsafe': to_text(value._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')}
> +     elif is_sequence(value):
> +         value = [_preprocess_unsafe_encode(v) for v in value]
> +     elif isinstance(value, Mapping):
> +@@ -63,7 +63,7 @@ class AnsibleJSONEncoder(json.JSONEncoder):
> +                 value = {'__ansible_vault': to_text(o._ciphertext, errors='surrogate_or_strict', nonstring='strict')}
> +         elif getattr(o, '__UNSAFE__', False):
> +             # unsafe object, this will never be triggered, see ``AnsibleJSONEncoder.iterencode``
> +-            value = {'__ansible_unsafe': to_text(o, errors='surrogate_or_strict', nonstring='strict')}
> ++            value = {'__ansible_unsafe': to_text(o._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')}
> +         elif isinstance(o, Mapping):
> +             # hostvars and other objects
> +             value = dict(o)
> +diff --git a/lib/ansible/parsing/yaml/dumper.py b/lib/ansible/parsing/yaml/dumper.py
> +index 8701bb8196..bf2c0843c2 100644
> +--- a/lib/ansible/parsing/yaml/dumper.py
> ++++ b/lib/ansible/parsing/yaml/dumper.py
> +@@ -24,7 +24,7 @@ import yaml
> + from ansible.module_utils.six import text_type, binary_type
> + from ansible.module_utils.common.yaml import SafeDumper
> + from ansible.parsing.yaml.objects import AnsibleUnicode, AnsibleSequence, AnsibleMapping, AnsibleVaultEncryptedUnicode
> +-from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText
> ++from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText, _is_unsafe
> + from ansible.template import AnsibleUndefined
> + from ansible.vars.hostvars import HostVars, HostVarsVars
> + from ansible.vars.manager import VarsWithSources
> +@@ -47,10 +47,14 @@ def represent_vault_encrypted_unicode(self, data):
> +
> +
> + def represent_unicode(self, data):
> ++    if _is_unsafe(data):
> ++        data = data._strip_unsafe()
> +     return yaml.representer.SafeRepresenter.represent_str(self, text_type(data))
> +
> +
> + def represent_binary(self, data):
> ++    if _is_unsafe(data):
> ++        data = data._strip_unsafe()
> +     return yaml.representer.SafeRepresenter.represent_binary(self, binary_type(data))
> +
> +
> +diff --git a/lib/ansible/playbook/conditional.py b/lib/ansible/playbook/conditional.py
> +index fe07358cbd..d994f8f49d 100644
> +--- a/lib/ansible/playbook/conditional.py
> ++++ b/lib/ansible/playbook/conditional.py
> +@@ -26,7 +26,7 @@ from jinja2.compiler import generate
> + from jinja2.exceptions import UndefinedError
> +
> + from ansible import constants as C
> +-from ansible.errors import AnsibleError, AnsibleUndefinedVariable
> ++from ansible.errors import AnsibleError, AnsibleUndefinedVariable, AnsibleTemplateError
> + from ansible.module_utils.six import text_type
> + from ansible.module_utils._text import to_native, to_text
> + from ansible.playbook.attribute import FieldAttribute
> +@@ -138,9 +138,10 @@ class Conditional:
> +             if not isinstance(conditional, text_type) or conditional == "":
> +                 return conditional
> +
> +-            # update the lookups flag, as the string returned above may now be unsafe
> +-            # and we don't want future templating calls to do unsafe things
> +-            disable_lookups |= hasattr(conditional, '__UNSAFE__')
> ++            # If the result of the first-pass template render (to resolve inline templates) is marked unsafe,
> ++            # explicitly fail since the next templating operation would never evaluate
> ++            if hasattr(conditional, '__UNSAFE__'):
> ++                raise AnsibleTemplateError('Conditional is marked as unsafe, and cannot be evaluated.')
> +
> +             # First, we do some low-level jinja2 parsing involving the AST format of the
> +             # statement to ensure we don't do anything unsafe (using the disable_lookup flag above)
> +diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py
> +index ba35fcf0e3..a1a1162bf2 100644
> +--- a/lib/ansible/playbook/task.py
> ++++ b/lib/ansible/playbook/task.py
> +@@ -290,6 +290,30 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
> +
> +         super(Task, self).post_validate(templar)
> +
> ++    def _post_validate_args(self, attr, value, templar):
> ++        # smuggle an untemplated copy of the task args for actions that need more control over the templating of their
> ++        # input (eg, debug's var/msg, assert's "that" conditional expressions)
> ++        self.untemplated_args = value
> ++
> ++        # now recursively template the args dict
> ++        args = templar.template(value)
> ++
> ++        # FIXME: could we just nuke this entirely and/or wrap it up in ModuleArgsParser or something?
> ++        if '_variable_params' in args:
> ++            variable_params = args.pop('_variable_params')
> ++            if isinstance(variable_params, dict):
> ++                if C.INJECT_FACTS_AS_VARS:
> ++                    display.warning("Using a variable for a task's 'args' is unsafe in some situations "
> ++                                    "(see https://docs.ansible.com/ansible/devel/reference_appendices/faq.html#argsplat-unsafe)")
> ++                variable_params.update(args)
> ++                args = variable_params
> ++            else:
> ++                # if we didn't get a dict, it means there's garbage remaining after k=v parsing, just give up
> ++                # see https://github.com/ansible/ansible/issues/79862
> ++                raise AnsibleError(f"invalid or malformed argument: '{variable_params}'")
> ++
> ++        return args
> ++
> +     def _post_validate_loop(self, attr, value, templar):
> +         '''
> +         Override post validation for the loop field, which is templated
> +diff --git a/lib/ansible/plugins/action/assert.py b/lib/ansible/plugins/action/assert.py
> +index 7721a6b47c..e8ab6a9a4f 100644
> +--- a/lib/ansible/plugins/action/assert.py
> ++++ b/lib/ansible/plugins/action/assert.py
> +@@ -63,8 +63,29 @@ class ActionModule(ActionBase):
> +
> +         quiet = boolean(self._task.args.get('quiet', False), strict=False)
> +
> ++        # directly access 'that' via untemplated args from the task so we can intelligently trust embedded
> ++        # templates and preserve the original inputs/locations for better messaging on assert failures and
> ++        # errors.
> ++        # FIXME: even in devel, things like `that: item` don't always work properly (truthy string value
> ++        # is not really an embedded expression)
> ++        # we could fix that by doing direct var lookups on the inputs
> ++        # FIXME: some form of this code should probably be shared between debug, assert, and
> ++        # Task.post_validate, since they
> ++        # have a lot of overlapping needs
> ++        try:
> ++            thats = self._task.untemplated_args['that']
> ++        except KeyError:
> ++            # in the case of "we got our entire args dict from a template", we can just consult the
> ++            # post-templated dict (the damage has likely already been done for embedded templates anyway)
> ++            thats = self._task.args['that']
> ++
> ++        # FIXME: this is a case where we only want to resolve indirections, NOT recurse containers
> ++        # (and even then, the leaf-most expression being wrapped is at least suboptimal
> ++        # (since its expression will be "eaten").
> ++        if isinstance(thats, str):
> ++            thats = self._templar.template(thats)
> ++
> +         # make sure the 'that' items are a list
> +-        thats = self._task.args['that']
> +         if not isinstance(thats, list):
> +             thats = [thats]
> +
> +diff --git a/lib/ansible/plugins/callback/__init__.py b/lib/ansible/plugins/callback/__init__.py
> +index d4fc347d03..7646d2939f 100644
> +--- a/lib/ansible/plugins/callback/__init__.py
> ++++ b/lib/ansible/plugins/callback/__init__.py
> +@@ -38,7 +38,7 @@ from ansible.parsing.yaml.objects import AnsibleUnicode
> + from ansible.plugins import AnsiblePlugin
> + from ansible.utils.color import stringc
> + from ansible.utils.display import Display
> +-from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText
> ++from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText, _is_unsafe
> + from ansible.vars.clean import strip_internal_keys, module_response_deepcopy
> +
> + import yaml
> +@@ -113,6 +113,8 @@ def _munge_data_for_lossy_yaml(scalar):
> +
> + def _pretty_represent_str(self, data):
> +     """Uses block style for multi-line strings"""
> ++    if _is_unsafe(data):
> ++        data = data._strip_unsafe()
> +     data = text_type(data)
> +     if _should_use_block(data):
> +         style = '|'
> +diff --git a/lib/ansible/plugins/filter/core.py b/lib/ansible/plugins/filter/core.py
> +index 52a2cd108e..b7e2c11ec8 100644
> +--- a/lib/ansible/plugins/filter/core.py
> ++++ b/lib/ansible/plugins/filter/core.py
> +@@ -37,6 +37,7 @@ from ansible.utils.display import Display
> + from ansible.utils.encrypt import passlib_or_crypt
> + from ansible.utils.hashing import md5s, checksum_s
> + from ansible.utils.unicode import unicode_wrap
> ++from ansible.utils.unsafe_proxy import _is_unsafe
> + from ansible.utils.vars import merge_hash
> +
> + display = Display()
> +@@ -215,6 +216,8 @@ def from_yaml(data):
> +         # The ``text_type`` call here strips any custom
> +         # string wrapper class, so that CSafeLoader can
> +         # read the data
> ++        if _is_unsafe(data):
> ++            data = data._strip_unsafe()
> +         return yaml_load(text_type(to_text(data, errors='surrogate_or_strict')))
> +     return data
> +
> +@@ -224,6 +227,8 @@ def from_yaml_all(data):
> +         # The ``text_type`` call here strips any custom
> +         # string wrapper class, so that CSafeLoader can
> +         # read the data
> ++        if _is_unsafe(data):
> ++            data = data._strip_unsafe()
> +         return yaml_load_all(text_type(to_text(data, errors='surrogate_or_strict')))
> +     return data
> +
> +diff --git a/lib/ansible/plugins/lookup/first_found.py b/lib/ansible/plugins/lookup/first_found.py
> +index 5b94b103a4..a882db017b 100644
> +--- a/lib/ansible/plugins/lookup/first_found.py
> ++++ b/lib/ansible/plugins/lookup/first_found.py
> +@@ -136,7 +136,6 @@ RETURN = """
> +     elements: path
> + """
> + import os
> +-import re
> +
> + from collections.abc import Mapping, Sequence
> +
> +@@ -147,10 +146,22 @@ from ansible.module_utils.six import string_types
> + from ansible.plugins.lookup import LookupBase
> +
> +
> ++def _splitter(value, chars):
> ++    chars = set(chars)
> ++    v = ''
> ++    for c in value:
> ++        if c in chars:
> ++            yield v
> ++            v = ''
> ++            continue
> ++        v += c
> ++    yield v
> ++
> ++
> + def _split_on(terms, spliters=','):
> +     termlist = []
> +     if isinstance(terms, string_types):
> +-        termlist = re.split(r'[%s]' % ''.join(map(re.escape, spliters)), terms)
> ++        termlist = list(_splitter(terms, spliters))
> +     else:
> +         # added since options will already listify
> +         for t in terms:
> +diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py
> +index baa85ed793..c45cfe350e 100644
> +--- a/lib/ansible/template/__init__.py
> ++++ b/lib/ansible/template/__init__.py
> +@@ -31,7 +31,7 @@ from contextlib import contextmanager
> + from numbers import Number
> + from traceback import format_exc
> +
> +-from jinja2.exceptions import TemplateSyntaxError, UndefinedError
> ++from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError
> + from jinja2.loaders import FileSystemLoader
> + from jinja2.nativetypes import NativeEnvironment
> + from jinja2.runtime import Context, StrictUndefined
> +@@ -55,7 +55,7 @@ from ansible.template.vars import AnsibleJ2Vars
> + from ansible.utils.display import Display
> + from ansible.utils.listify import listify_lookup_plugin_terms
> + from ansible.utils.native_jinja import NativeJinjaText
> +-from ansible.utils.unsafe_proxy import wrap_var
> ++from ansible.utils.unsafe_proxy import wrap_var, AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText
> +
> + display = Display()
> +
> +@@ -332,10 +332,21 @@ class AnsibleContext(Context):
> +     flag is checked post-templating, and (when set) will result in the
> +     final templated result being wrapped in AnsibleUnsafe.
> +     '''
> ++    _disallowed_callables = frozenset({
> ++        AnsibleUnsafeText._strip_unsafe.__qualname__,
> ++        AnsibleUnsafeBytes._strip_unsafe.__qualname__,
> ++        NativeJinjaUnsafeText._strip_unsafe.__qualname__,
> ++    })
> ++
> +     def __init__(self, *args, **kwargs):
> +         super(AnsibleContext, self).__init__(*args, **kwargs)
> +         self.unsafe = False
> +
> ++    def call(self, obj, *args, **kwargs):
> ++        if getattr(obj, '__qualname__', None) in self._disallowed_callables or obj in self._disallowed_callables:
> ++            raise SecurityError(f"{obj!r} is not safely callable")
> ++        return super().call(obj, *args, **kwargs)
> ++
> +     def _is_unsafe(self, val):
> +         '''
> +         Our helper function, which will also recursively check dict and
> +diff --git a/lib/ansible/utils/unsafe_proxy.py b/lib/ansible/utils/unsafe_proxy.py
> +index d78ebf6e8d..7da74dcfb2 100644
> +--- a/lib/ansible/utils/unsafe_proxy.py
> ++++ b/lib/ansible/utils/unsafe_proxy.py
> +@@ -69,15 +69,264 @@ class AnsibleUnsafe(object):
> +
> +
> + class AnsibleUnsafeBytes(binary_type, AnsibleUnsafe):
> +-    def decode(self, *args, **kwargs):
> +-        """Wrapper method to ensure type conversions maintain unsafe context"""
> +-        return AnsibleUnsafeText(super(AnsibleUnsafeBytes, self).decode(*args, **kwargs))
> ++    def _strip_unsafe(self):
> ++        return super().__bytes__()
> ++
> ++    def __str__(self, /):  # pylint: disable=invalid-str-returned
> ++        return self.encode()
> ++
> ++    def __bytes__(self, /):  # pylint: disable=invalid-bytes-returned
> ++        return self
> ++
> ++    def __repr__(self, /):  # pylint: disable=invalid-repr-returned
> ++        return AnsibleUnsafeText(super().__repr__())
> ++
> ++    def __format__(self, format_spec, /):  # pylint: disable=invalid-format-returned
> ++        return self.__class__(super().__format__(format_spec))
> ++
> ++    def __getitem__(self, key, /):
> ++        return self.__class__(super().__getitem__(key))
> ++
> ++    def __iter__(self, /):
> ++        cls = self.__class__
> ++        return (cls(c) for c in super().__iter__())
> ++
> ++    def __reversed__(self, /):
> ++        return self[::-1]
> ++
> ++    def __add__(self, value, /):
> ++        return self.__class__(super().__add__(value))
> ++
> ++    def __radd__(self, value, /):
> ++        return self.__class__(value.__add__(self))
> ++
> ++    def __mul__(self, value, /):
> ++        return self.__class__(super().__mul__(value))
> ++
> ++    __rmul__ = __mul__
> ++
> ++    def __mod__(self, value, /):
> ++        return self.__class__(super().__mod__(value))
> ++
> ++    def __rmod__(self, value, /):
> ++        return self.__class__(super().__rmod__(value))
> ++
> ++    def capitalize(self, /):
> ++        return self.__class__(super().capitalize())
> ++
> ++    def casefold(self, /):
> ++        return self.__class__(super().casefold())
> ++
> ++    def center(self, width, fillchar=b' ', /):
> ++        return self.__class__(super().center(width, fillchar))
> ++
> ++    def decode(self, /, encoding='utf-8', errors='strict'):
> ++        return AnsibleUnsafeText(super().decode(encoding=encoding, errors=errors))
> ++
> ++    def removeprefix(self, prefix, /):
> ++        return self.__class__(super().removeprefix(prefix))
> ++
> ++    def removesuffix(self, suffix, /):
> ++        return self.__class__(super().removesuffix(suffix))
> ++
> ++    def expandtabs(self, /, tabsize=8):
> ++        return self.__class__(super().expandtabs(tabsize))
> ++
> ++    def format(self, /, *args, **kwargs):
> ++        return self.__class__(super().format(*args, **kwargs))
> ++
> ++    def format_map(self, mapping, /):
> ++        return self.__class__(super().format_map(mapping))
> ++
> ++    def join(self, iterable_of_bytes, /):
> ++        return self.__class__(super().join(iterable_of_bytes))
> ++
> ++    def ljust(self, width, fillchar=b' ', /):
> ++        return self.__class__(super().ljust(width, fillchar))
> ++
> ++    def lower(self, /):
> ++        return self.__class__(super().lower())
> ++
> ++    def lstrip(self, bytes=None, /):
> ++        return self.__class__(super().lstrip(bytes))
> ++
> ++    def partition(self, sep, /):
> ++        cls = self.__class__
> ++        return tuple(cls(e) for e in super().partition(sep))
> ++
> ++    def replace(self, old, new, count=-1, /):
> ++        return self.__class__(super().replace(old, new, count))
> ++
> ++    def rjust(self, width, fillchar=b' ', /):
> ++        return self.__class__(super().rjust(width, fillchar))
> ++
> ++    def rpartition(self, sep, /):
> ++        cls = self.__class__
> ++        return tuple(cls(e) for e in super().rpartition(sep))
> ++
> ++    def rstrip(self, bytes=None, /):
> ++        return self.__class__(super().rstrip(bytes))
> ++
> ++    def split(self, /, sep=None, maxsplit=-1):
> ++        cls = self.__class__
> ++        return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)]
> ++
> ++    def rsplit(self, /, sep=None, maxsplit=-1):
> ++        cls = self.__class__
> ++        return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)]
> ++
> ++    def splitlines(self, /, keepends=False):
> ++        cls = self.__class__
> ++        return [cls(e) for e in super().splitlines(keepends=keepends)]
> ++
> ++    def strip(self, bytes=None, /):
> ++        return self.__class__(super().strip(bytes))
> ++
> ++    def swapcase(self, /):
> ++        return self.__class__(super().swapcase())
> ++
> ++    def title(self, /):
> ++        return self.__class__(super().title())
> ++
> ++    def translate(self, table, /, delete=b''):
> ++        return self.__class__(super().translate(table, delete=delete))
> ++
> ++    def upper(self, /):
> ++        return self.__class__(super().upper())
> ++
> ++    def zfill(self, width, /):
> ++        return self.__class__(super().zfill(width))
> +
> +
> + class AnsibleUnsafeText(text_type, AnsibleUnsafe):
> +-    def encode(self, *args, **kwargs):
> +-        """Wrapper method to ensure type conversions maintain unsafe context"""
> +-        return AnsibleUnsafeBytes(super(AnsibleUnsafeText, self).encode(*args, **kwargs))
> ++    # def __getattribute__(self, name):
> ++    #     print(f'attr: {name}')
> ++    #     return object.__getattribute__(self, name)
> ++
> ++    def _strip_unsafe(self, /):
> ++        return super().__str__()
> ++
> ++    def __str__(self, /):  # pylint: disable=invalid-str-returned
> ++        return self
> ++
> ++    def __repr__(self, /):  # pylint: disable=invalid-repr-returned
> ++        return self.__class__(super().__repr__())
> ++
> ++    def __format__(self, format_spec, /):  # pylint: disable=invalid-format-returned
> ++        return self.__class__(super().__format__(format_spec))
> ++
> ++    def __getitem__(self, key, /):
> ++        return self.__class__(super().__getitem__(key))
> ++
> ++    def __iter__(self, /):
> ++        cls = self.__class__
> ++        return (cls(c) for c in super().__iter__())
> ++
> ++    def __reversed__(self, /):
> ++        return self[::-1]
> ++
> ++    def __add__(self, value, /):
> ++        return self.__class__(super().__add__(value))
> ++
> ++    def __radd__(self, value, /):
> ++        return self.__class__(value.__add__(self))
> ++
> ++    def __mul__(self, value, /):
> ++        return self.__class__(super().__mul__(value))
> ++
> ++    __rmul__ = __mul__
> ++
> ++    def __mod__(self, value, /):
> ++        return self.__class__(super().__mod__(value))
> ++
> ++    def __rmod__(self, value, /):
> ++        return self.__class__(super().__rmod__(value))
> ++
> ++    def capitalize(self, /):
> ++        return self.__class__(super().capitalize())
> ++
> ++    def casefold(self, /):
> ++        return self.__class__(super().casefold())
> ++
> ++    def center(self, width, fillchar=' ', /):
> ++        return self.__class__(super().center(width, fillchar))
> ++
> ++    def encode(self, /, encoding='utf-8', errors='strict'):
> ++        return AnsibleUnsafeBytes(super().encode(encoding=encoding, errors=errors))
> ++
> ++    def removeprefix(self, prefix, /):
> ++        return self.__class__(super().removeprefix(prefix))
> ++
> ++    def removesuffix(self, suffix, /):
> ++        return self.__class__(super().removesuffix(suffix))
> ++
> ++    def expandtabs(self, /, tabsize=8):
> ++        return self.__class__(super().expandtabs(tabsize))
> ++
> ++    def format(self, /, *args, **kwargs):
> ++        return self.__class__(super().format(*args, **kwargs))
> ++
> ++    def format_map(self, mapping, /):
> ++        return self.__class__(super().format_map(mapping))
> ++
> ++    def join(self, iterable, /):
> ++        return self.__class__(super().join(iterable))
> ++
> ++    def ljust(self, width, fillchar=' ', /):
> ++        return self.__class__(super().ljust(width, fillchar))
> ++
> ++    def lower(self, /):
> ++        return self.__class__(super().lower())
> ++
> ++    def lstrip(self, chars=None, /):
> ++        return self.__class__(super().lstrip(chars))
> ++
> ++    def partition(self, sep, /):
> ++        cls = self.__class__
> ++        return tuple(cls(e) for e in super().partition(sep))
> ++
> ++    def replace(self, old, new, count=-1, /):
> ++        return self.__class__(super().replace(old, new, count))
> ++
> ++    def rjust(self, width, fillchar=' ', /):
> ++        return self.__class__(super().rjust(width, fillchar))
> ++
> ++    def rpartition(self, sep, /):
> ++        cls = self.__class__
> ++        return tuple(cls(e) for e in super().rpartition(sep))
> ++
> ++    def rstrip(self, chars=None, /):
> ++        return self.__class__(super().rstrip(chars))
> ++
> ++    def split(self, /, sep=None, maxsplit=-1):
> ++        cls = self.__class__
> ++        return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)]
> ++
> ++    def rsplit(self, /, sep=None, maxsplit=-1):
> ++        cls = self.__class__
> ++        return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)]
> ++
> ++    def splitlines(self, /, keepends=False):
> ++        cls = self.__class__
> ++        return [cls(e) for e in super().splitlines(keepends=keepends)]
> ++
> ++    def strip(self, chars=None, /):
> ++        return self.__class__(super().strip(chars))
> ++
> ++    def swapcase(self, /):
> ++        return self.__class__(super().swapcase())
> ++
> ++    def title(self, /):
> ++        return self.__class__(super().title())
> ++
> ++    def translate(self, table, /):
> ++        return self.__class__(super().translate(table))
> ++
> ++    def upper(self, /):
> ++        return self.__class__(super().upper())
> ++
> ++    def zfill(self, width, /):
> ++        return self.__class__(super().zfill(width))
> +
> +
> + class NativeJinjaUnsafeText(NativeJinjaText, AnsibleUnsafeText):
> +@@ -126,3 +375,7 @@ def to_unsafe_bytes(*args, **kwargs):
> +
> + def to_unsafe_text(*args, **kwargs):
> +     return wrap_var(to_text(*args, **kwargs))
> ++
> ++
> ++def _is_unsafe(obj):
> ++    return getattr(obj, '__UNSAFE__', False) is True
> +diff --git a/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml b/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml
> +index f200be1803..e084752494 100644
> +--- a/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml
> ++++ b/test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml
> +@@ -13,22 +13,22 @@
> +         in command.stdout_lines
> +       - >-
> +         "Installing 'namespace_1.collection_1:1.0.0' to
> +-        '{{ install_path }}/namespace_1/collection_1'"
> ++        '" ~ install_path ~ "/namespace_1/collection_1'"
> +         in command.stdout_lines
> +       - >-
> +         'Created collection for namespace_1.collection_1:1.0.0 at
> +-        {{ install_path }}/namespace_1/collection_1'
> ++        ' ~ install_path ~ '/namespace_1/collection_1'
> +         in command.stdout_lines
> +       - >-
> +         'namespace_1.collection_1:1.0.0 was installed successfully'
> +         in command.stdout_lines
> +       - >-
> +         "Installing 'namespace_2.collection_2:1.0.0' to
> +-        '{{ install_path }}/namespace_2/collection_2'"
> ++        '" ~ install_path ~ "/namespace_2/collection_2'"
> +         in command.stdout_lines
> +       - >-
> +         'Created collection for namespace_2.collection_2:1.0.0 at
> +-        {{ install_path }}/namespace_2/collection_2'
> ++        ' ~ install_path ~ '/namespace_2/collection_2'
> +         in command.stdout_lines
> +       - >-
> +         'namespace_2.collection_2:1.0.0 was installed successfully'
> +@@ -58,22 +58,22 @@
> +         in command.stdout_lines
> +       - >-
> +         "Installing 'namespace_1.collection_1:1.0.0' to
> +-        '{{ install_path }}/namespace_1/collection_1'"
> ++        '" ~ install_path ~ "/namespace_1/collection_1'"
> +         in command.stdout_lines
> +       - >-
> +         'Created collection for namespace_1.collection_1:1.0.0 at
> +-        {{ install_path }}/namespace_1/collection_1'
> ++        ' ~ install_path ~ '/namespace_1/collection_1'
> +         in command.stdout_lines
> +       - >-
> +         'namespace_1.collection_1:1.0.0 was installed successfully'
> +         in command.stdout_lines
> +       - >-
> +         "Installing 'namespace_2.collection_2:1.0.0' to
> +-        '{{ install_path }}/namespace_2/collection_2'"
> ++        '" ~ install_path ~ "/namespace_2/collection_2'"
> +         in command.stdout_lines
> +       - >-
> +         'Created collection for namespace_2.collection_2:1.0.0 at
> +-        {{ install_path }}/namespace_2/collection_2'
> ++        ' ~ install_path ~ '/namespace_2/collection_2'
> +         in command.stdout_lines
> +       - >-
> +         'namespace_2.collection_2:1.0.0 was installed successfully'
> +diff --git a/test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml b/test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml
> +index eba938966d..98ef751b86 100644
> +--- a/test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml
> ++++ b/test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml
> +@@ -2,7 +2,7 @@
> + - name: Assert that a embedded vault of a string with no newline works
> +   assert:
> +     that:
> +-      - '"{{ vault_encrypted_one_line_var }}" == "Setec Astronomy"'
> ++      - 'vault_encrypted_one_line_var == "Setec Astronomy"'
> +
> + - name: Assert that a multi line embedded vault works, including new line
> +   assert:
> +diff --git a/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml b/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
> +index e09004a1d9..107e65cb11 100644
> +--- a/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
> ++++ b/test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
> +@@ -2,7 +2,7 @@
> + - name: Assert that a vault encrypted file with embedded vault of a string with no newline works
> +   assert:
> +     that:
> +-      - '"{{ vault_file_encrypted_with_encrypted_one_line_var }}" == "Setec Astronomy"'
> ++      - 'vault_file_encrypted_with_encrypted_one_line_var == "Setec Astronomy"'
> +
> + - name: Assert that a vault encrypted file with multi line embedded vault works, including new line
> +   assert:
> +diff --git a/test/integration/targets/apt_repository/tasks/apt.yml b/test/integration/targets/apt_repository/tasks/apt.yml
> +index 0dc25afd59..9c15e64765 100644
> +--- a/test/integration/targets/apt_repository/tasks/apt.yml
> ++++ b/test/integration/targets/apt_repository/tasks/apt.yml
> +@@ -50,7 +50,7 @@
> +     that:
> +       - 'result.changed'
> +       - 'result.state == "present"'
> +-      - 'result.repo == "{{test_ppa_name}}"'
> ++      - 'result.repo == test_ppa_name'
> +
> + - name: 'examine apt cache mtime'
> +   stat: path='/var/cache/apt/pkgcache.bin'
> +@@ -81,7 +81,7 @@
> +     that:
> +       - 'result.changed'
> +       - 'result.state == "present"'
> +-      - 'result.repo == "{{test_ppa_name}}"'
> ++      - 'result.repo == test_ppa_name'
> +
> + - name: 'examine apt cache mtime'
> +   stat: path='/var/cache/apt/pkgcache.bin'
> +@@ -112,7 +112,7 @@
> +     that:
> +       - 'result.changed'
> +       - 'result.state == "present"'
> +-      - 'result.repo == "{{test_ppa_name}}"'
> ++      - 'result.repo == test_ppa_name'
> +
> + - name: 'examine apt cache mtime'
> +   stat: path='/var/cache/apt/pkgcache.bin'
> +@@ -151,7 +151,7 @@
> +     that:
> +       - 'result.changed'
> +       - 'result.state == "present"'
> +-      - 'result.repo == "{{test_ppa_spec}}"'
> ++      - 'result.repo == test_ppa_spec'
> +       - result_cache is not changed
> +
> + - name: 'examine apt cache mtime'
> +@@ -191,7 +191,7 @@
> +     that:
> +       - 'result.changed'
> +       - 'result.state == "present"'
> +-      - 'result.repo == "{{test_ppa_spec}}"'
> ++      - 'result.repo == test_ppa_spec'
> +
> + - name: 'examine source file'
> +   stat: path='/etc/apt/sources.list.d/{{test_ppa_filename}}.list'
> +diff --git a/test/integration/targets/assert/assert.out.nested_tmpl.stderr b/test/integration/targets/assert/assert.out.nested_tmpl.stderr
> +new file mode 100644
> +index 0000000000..ea208a41c7
> +--- /dev/null
> ++++ b/test/integration/targets/assert/assert.out.nested_tmpl.stderr
> +@@ -0,0 +1,4 @@
> +++ ansible-playbook -i localhost, -c local nested_tmpl.yml
> ++++ set +x
> ++[WARNING]: conditional statements should not include jinja2 templating
> ++delimiters such as {{ }} or {% %}. Found: "{{ foo }}" == "bar"
> +diff --git a/test/integration/targets/assert/assert.out.nested_tmpl.stdout b/test/integration/targets/assert/assert.out.nested_tmpl.stdout
> +new file mode 100644
> +index 0000000000..8ca3fb76d4
> +--- /dev/null
> ++++ b/test/integration/targets/assert/assert.out.nested_tmpl.stdout
> +@@ -0,0 +1,12 @@
> ++
> ++PLAY [localhost] ***************************************************************
> ++
> ++TASK [assert] ******************************************************************
> ++ok: [localhost] => {
> ++    "changed": false,
> ++    "msg": "All assertions passed"
> ++}
> ++
> ++PLAY RECAP *********************************************************************
> ++localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
> ++
> +diff --git a/test/integration/targets/assert/assert_quiet.out.quiet.stderr b/test/integration/targets/assert/assert.out.quiet.stderr
> +similarity index 100%
> +rename from test/integration/targets/assert/assert_quiet.out.quiet.stderr
> +rename to test/integration/targets/assert/assert.out.quiet.stderr
> +diff --git a/test/integration/targets/assert/assert_quiet.out.quiet.stdout b/test/integration/targets/assert/assert.out.quiet.stdout
> +similarity index 100%
> +rename from test/integration/targets/assert/assert_quiet.out.quiet.stdout
> +rename to test/integration/targets/assert/assert.out.quiet.stdout
> +diff --git a/test/integration/targets/assert/nested_tmpl.yml b/test/integration/targets/assert/nested_tmpl.yml
> +new file mode 100644
> +index 0000000000..3da4b1d80e
> +--- /dev/null
> ++++ b/test/integration/targets/assert/nested_tmpl.yml
> +@@ -0,0 +1,9 @@
> ++- hosts: localhost
> ++  gather_facts: False
> ++  tasks:
> ++    - assert:
> ++        that:
> ++          - '"{{ foo }}" == "bar"'
> ++          - foo == "bar"
> ++      vars:
> ++        foo: bar
> +diff --git a/test/integration/targets/assert/quiet.yml b/test/integration/targets/assert/quiet.yml
> +index 6834712c2c..1c425cb5ba 100644
> +--- a/test/integration/targets/assert/quiet.yml
> ++++ b/test/integration/targets/assert/quiet.yml
> +@@ -5,12 +5,12 @@
> +     item_A: yes
> +   tasks:
> +   - assert:
> +-      that: "{{ item }} is defined"
> ++      that: "item is defined"
> +       quiet: True
> +     with_items:
> +       - item_A
> +   - assert:
> +-      that: "{{ item }} is defined"
> ++      that: "item is defined"
> +       quiet: False
> +     with_items:
> +       - item_A
> +diff --git a/test/integration/targets/assert/runme.sh b/test/integration/targets/assert/runme.sh
> +index ca0a858726..b79072813d 100755
> +--- a/test/integration/targets/assert/runme.sh
> ++++ b/test/integration/targets/assert/runme.sh
> +@@ -45,7 +45,7 @@ cleanup() {
> +    fi
> + }
> +
> +-BASEFILE=assert_quiet.out
> ++BASEFILE=assert.out
> +
> + ORIGFILE="${BASEFILE}"
> + OUTFILE="${BASEFILE}.new"
> +@@ -69,3 +69,4 @@ export ANSIBLE_NOCOLOR=1
> + export ANSIBLE_RETRY_FILES_ENABLED=0
> +
> + run_test quiet
> ++run_test nested_tmpl
> +diff --git a/test/integration/targets/command_shell/tasks/main.yml b/test/integration/targets/command_shell/tasks/main.yml
> +index 12a944c48c..1f4aa5d75e 100644
> +--- a/test/integration/targets/command_shell/tasks/main.yml
> ++++ b/test/integration/targets/command_shell/tasks/main.yml
> +@@ -296,7 +296,7 @@
> +   assert:
> +     that:
> +     - shell_result0 is changed
> +-    - shell_result0.cmd == '{{ remote_tmp_dir_test }}/test.sh'
> ++    - shell_result0.cmd == remote_tmp_dir_test ~ '/test.sh'
> +     - shell_result0.rc == 0
> +     - shell_result0.stderr == ''
> +     - shell_result0.stdout == 'win'
> +diff --git a/test/integration/targets/copy/tasks/tests.yml b/test/integration/targets/copy/tasks/tests.yml
> +index 7220356332..d6c8e63c9a 100644
> +--- a/test/integration/targets/copy/tasks/tests.yml
> ++++ b/test/integration/targets/copy/tasks/tests.yml
> +@@ -1176,7 +1176,7 @@
> +   assert:
> +     that:
> +       - "copy_result6.changed"
> +-      - "copy_result6.dest == '{{remote_dir_expanded}}/multiline.txt'"
> ++      - "copy_result6.dest == remote_dir_expanded ~ '/multiline.txt'"
> +       - "copy_result6.checksum == '9cd0697c6a9ff6689f0afb9136fa62e0b3fee903'"
> +
> + # test overwriting a file as an unprivileged user (pull request #8624)
> +@@ -2079,26 +2079,26 @@
> +     assert:
> +       that:
> +       - testcase5 is changed
> +-      - "stat_new_dir_with_chown.stat.uid == {{ ansible_copy_test_user.uid }}"
> +-      - "stat_new_dir_with_chown.stat.gid == {{ ansible_copy_test_group.gid }}"
> +-      - "stat_new_dir_with_chown.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
> +-      - "stat_new_dir_with_chown.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
> +-      - "stat_new_dir_with_chown_file1.stat.uid == {{ ansible_copy_test_user.uid }}"
> +-      - "stat_new_dir_with_chown_file1.stat.gid == {{ ansible_copy_test_group.gid }}"
> +-      - "stat_new_dir_with_chown_file1.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
> +-      - "stat_new_dir_with_chown_file1.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
> +-      - "stat_new_dir_with_chown_subdir.stat.uid == {{ ansible_copy_test_user.uid }}"
> +-      - "stat_new_dir_with_chown_subdir.stat.gid == {{ ansible_copy_test_group.gid }}"
> +-      - "stat_new_dir_with_chown_subdir.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
> +-      - "stat_new_dir_with_chown_subdir.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
> +-      - "stat_new_dir_with_chown_subdir_file12.stat.uid == {{ ansible_copy_test_user.uid }}"
> +-      - "stat_new_dir_with_chown_subdir_file12.stat.gid == {{ ansible_copy_test_group.gid }}"
> +-      - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
> +-      - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
> +-      - "stat_new_dir_with_chown_link_file12.stat.uid == {{ ansible_copy_test_user.uid }}"
> +-      - "stat_new_dir_with_chown_link_file12.stat.gid == {{ ansible_copy_test_group.gid }}"
> +-      - "stat_new_dir_with_chown_link_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
> +-      - "stat_new_dir_with_chown_link_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
> ++      - "stat_new_dir_with_chown.stat.uid == ansible_copy_test_user.uid"
> ++      - "stat_new_dir_with_chown.stat.gid == ansible_copy_test_group.gid"
> ++      - "stat_new_dir_with_chown.stat.pw_name == ansible_copy_test_user_name"
> ++      - "stat_new_dir_with_chown.stat.gr_name == ansible_copy_test_user_name"
> ++      - "stat_new_dir_with_chown_file1.stat.uid == ansible_copy_test_user.uid"
> ++      - "stat_new_dir_with_chown_file1.stat.gid == ansible_copy_test_group.gid"
> ++      - "stat_new_dir_with_chown_file1.stat.pw_name == ansible_copy_test_user_name"
> ++      - "stat_new_dir_with_chown_file1.stat.gr_name == ansible_copy_test_user_name"
> ++      - "stat_new_dir_with_chown_subdir.stat.uid == ansible_copy_test_user.uid"
> ++      - "stat_new_dir_with_chown_subdir.stat.gid == ansible_copy_test_group.gid"
> ++      - "stat_new_dir_with_chown_subdir.stat.pw_name == ansible_copy_test_user_name"
> ++      - "stat_new_dir_with_chown_subdir.stat.gr_name == ansible_copy_test_user_name"
> ++      - "stat_new_dir_with_chown_subdir_file12.stat.uid == ansible_copy_test_user.uid"
> ++      - "stat_new_dir_with_chown_subdir_file12.stat.gid == ansible_copy_test_group.gid"
> ++      - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == ansible_copy_test_user_name"
> ++      - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == ansible_copy_test_user_name"
> ++      - "stat_new_dir_with_chown_link_file12.stat.uid == ansible_copy_test_user.uid"
> ++      - "stat_new_dir_with_chown_link_file12.stat.gid == ansible_copy_test_group.gid"
> ++      - "stat_new_dir_with_chown_link_file12.stat.pw_name == ansible_copy_test_user_name"
> ++      - "stat_new_dir_with_chown_link_file12.stat.gr_name == ansible_copy_test_user_name"
> +
> +   always:
> +     - name: execute - remove the user for test
> +diff --git a/test/integration/targets/debug/runme.sh b/test/integration/targets/debug/runme.sh
> +index 5faeb782a6..dc02859d35 100755
> +--- a/test/integration/targets/debug/runme.sh
> ++++ b/test/integration/targets/debug/runme.sh
> +@@ -18,3 +18,5 @@ done
> +
> + # ensure debug does not set top level vars when looking at ansible_facts
> + ansible-playbook nosetfacts.yml "$@"
> ++
> ++ansible-playbook unsafe.yml "$@"
> +diff --git a/test/integration/targets/debug/unsafe.yml b/test/integration/targets/debug/unsafe.yml
> +new file mode 100644
> +index 0000000000..6a78af1a69
> +--- /dev/null
> ++++ b/test/integration/targets/debug/unsafe.yml
> +@@ -0,0 +1,13 @@
> ++- hosts: localhost
> ++  gather_facts: false
> ++  vars:
> ++    unsafe_var: !unsafe undef()|mandatory
> ++  tasks:
> ++    - debug:
> ++        var: '{{ unsafe_var }}'
> ++      ignore_errors: true
> ++      register: result
> ++
> ++    - assert:
> ++        that:
> ++          - result is successful
> +diff --git a/test/integration/targets/dnf/tasks/test_sos_removal.yml b/test/integration/targets/dnf/tasks/test_sos_removal.yml
> +index 40ceb62bf4..0d70cf7877 100644
> +--- a/test/integration/targets/dnf/tasks/test_sos_removal.yml
> ++++ b/test/integration/targets/dnf/tasks/test_sos_removal.yml
> +@@ -15,5 +15,5 @@
> +     that:
> +       - sos_rm is successful
> +       - sos_rm is changed
> +-      - "'Removed: sos-{{ sos_version }}-{{ sos_release }}' in sos_rm.results[0]"
> ++      - "'Removed: sos-' ~ sos_version ~ '-' ~ sos_release in sos_rm.results[0]"
> +       - sos_rm.results|length == 1
> +diff --git a/test/integration/targets/expect/tasks/main.yml b/test/integration/targets/expect/tasks/main.yml
> +index d6f43f2c6a..7bf18c5e5c 100644
> +--- a/test/integration/targets/expect/tasks/main.yml
> ++++ b/test/integration/targets/expect/tasks/main.yml
> +@@ -117,7 +117,7 @@
> + - name: assert chdir works
> +   assert:
> +     that:
> +-    - "'{{chdir_result.stdout | trim}}' == '{{remote_tmp_dir_real_path.stdout | trim}}'"
> ++    - "chdir_result.stdout | trim == remote_tmp_dir_real_path.stdout | trim"
> +
> + - name: test timeout option
> +   expect:
> +diff --git a/test/integration/targets/file/tasks/main.yml b/test/integration/targets/file/tasks/main.yml
> +index 17b0fae68a..a5bd68d768 100644
> +--- a/test/integration/targets/file/tasks/main.yml
> ++++ b/test/integration/targets/file/tasks/main.yml
> +@@ -927,7 +927,7 @@
> +     that:
> +       - "file_error3 is failed"
> +       - "file_error3.msg == 'src does not exist'"
> +-      - "file_error3.dest == '{{ remote_tmp_dir_test }}/hard.txt' | expanduser"
> ++      - "file_error3.dest == remote_tmp_dir_test | expanduser ~ '/hard.txt'"
> +       - "file_error3.src == 'non-existing-file-that-does-not-exist.txt'"
> +
> + - block:
> +diff --git a/test/integration/targets/file/tasks/state_link.yml b/test/integration/targets/file/tasks/state_link.yml
> +index 673fe6fd52..6f96cdcba9 100644
> +--- a/test/integration/targets/file/tasks/state_link.yml
> ++++ b/test/integration/targets/file/tasks/state_link.yml
> +@@ -199,7 +199,7 @@
> +       - "missing_dst_no_follow_enable_force_use_mode2 is changed"
> +       - "missing_dst_no_follow_enable_force_use_mode3 is not changed"
> +       - "soft3_result['stat'].islnk"
> +-      - "soft3_result['stat'].lnk_target == '{{ user.home�}}/nonexistent'"
> ++      - "soft3_result['stat'].lnk_target == user.home�~ '/nonexistent'"
> +
> + #
> + # Test creating a link to a directory https://github.com/ansible/ansible/issues/1369
> +diff --git a/test/integration/targets/find/tasks/main.yml b/test/integration/targets/find/tasks/main.yml
> +index 5381a14478..89c62b9b6f 100644
> +--- a/test/integration/targets/find/tasks/main.yml
> ++++ b/test/integration/targets/find/tasks/main.yml
> +@@ -267,7 +267,7 @@
> + - name: assert we skipped the ogg file
> +   assert:
> +     that:
> +-      - '"{{ remote_tmp_dir_test }}/e/f/g/h/8.ogg" not in find_test3_list'
> ++      - 'remote_tmp_dir_test ~ "/e/f/g/h/8.ogg" not in find_test3_list'
> +
> + - name: patterns with regex
> +   find:
> +@@ -317,7 +317,7 @@
> +   assert:
> +     that:
> +       - result.matched == 1
> +-      - '"{{ remote_tmp_dir_test }}/astest/old.txt" in astest_list'
> ++      - 'remote_tmp_dir_test ~ "/astest/old.txt" in astest_list'
> +
> + - name: find files newer than 1 week
> +   find:
> +@@ -332,7 +332,7 @@
> +   assert:
> +     that:
> +       - result.matched == 1
> +-      - '"{{ remote_tmp_dir_test }}/astest/new.txt" in astest_list'
> ++      - 'remote_tmp_dir_test ~ "/astest/new.txt" in astest_list'
> +
> + - name: add some content to the new file
> +   shell: "echo hello world > {{ remote_tmp_dir_test }}/astest/new.txt"
> +@@ -352,7 +352,7 @@
> +   assert:
> +     that:
> +       - result.matched == 1
> +-      - '"{{ remote_tmp_dir_test }}/astest/new.txt" in astest_list'
> ++      - 'remote_tmp_dir_test ~ "/astest/new.txt" in astest_list'
> +       - '"checksum" in result.files[0]'
> +
> + - name: find ANY item with LESS than 5 bytes, also get checksums
> +@@ -371,6 +371,6 @@
> +   assert:
> +     that:
> +       - result.matched == 2
> +-      - '"{{ remote_tmp_dir_test }}/astest/old.txt" in astest_list'
> +-      - '"{{ remote_tmp_dir_test }}/astest/.hidden.txt" in astest_list'
> ++      - 'remote_tmp_dir_test ~ "/astest/old.txt" in astest_list'
> ++      - 'remote_tmp_dir_test ~ "/astest/.hidden.txt" in astest_list'
> +       - '"checksum" in result.files[0]'
> +diff --git a/test/integration/targets/gathering_facts/test_gathering_facts.yml b/test/integration/targets/gathering_facts/test_gathering_facts.yml
> +index 47027e8717..faa187b73e 100644
> +--- a/test/integration/targets/gathering_facts/test_gathering_facts.yml
> ++++ b/test/integration/targets/gathering_facts/test_gathering_facts.yml
> +@@ -433,7 +433,7 @@
> +     - name: Test reading facts from default fact_path
> +       assert:
> +         that:
> +-          - '"{{ ansible_local.testfact.fact_dir }}" == "default"'
> ++          - 'ansible_local.testfact.fact_dir == "default"'
> +
> + - hosts: facthost9
> +   tags: [ 'fact_local']
> +@@ -444,7 +444,7 @@
> +     - name: Test reading facts from custom fact_path
> +       assert:
> +         that:
> +-          - '"{{ ansible_local.testfact.fact_dir }}" == "custom"'
> ++          - 'ansible_local.testfact.fact_dir == "custom"'
> +
> + - hosts: facthost20
> +   tags: [ 'fact_facter_ohai' ]
> +diff --git a/test/integration/targets/git/tasks/depth.yml b/test/integration/targets/git/tasks/depth.yml
> +index 547f84f7b5..e0585ca39b 100644
> +--- a/test/integration/targets/git/tasks/depth.yml
> ++++ b/test/integration/targets/git/tasks/depth.yml
> +@@ -169,7 +169,7 @@
> + - name: DEPTH | check update arrived
> +   assert:
> +     that:
> +-      - "{{ a_file.content | b64decode | trim }} == 3"
> ++      - a_file.content | b64decode | trim == "3"
> +       - git_fetch is changed
> +
> + - name: DEPTH | clear checkout_dir
> +diff --git a/test/integration/targets/git/tasks/localmods.yml b/test/integration/targets/git/tasks/localmods.yml
> +index 09a1326d58..0e0cf684ed 100644
> +--- a/test/integration/targets/git/tasks/localmods.yml
> ++++ b/test/integration/targets/git/tasks/localmods.yml
> +@@ -47,7 +47,7 @@
> + - name: LOCALMODS | check update arrived
> +   assert:
> +     that:
> +-      - "{{ a_file.content | b64decode | trim }} == 2"
> ++      - a_file.content | b64decode | trim == "2"
> +       - git_fetch_force is changed
> +
> + - name: LOCALMODS | clear checkout_dir
> +@@ -105,7 +105,7 @@
> + - name: LOCALMODS | check update arrived
> +   assert:
> +     that:
> +-      - "{{ a_file.content | b64decode | trim }} == 2"
> ++      - a_file.content | b64decode | trim == "2"
> +       - git_fetch_force is changed
> +
> + - name: LOCALMODS | clear checkout_dir
> +diff --git a/test/integration/targets/git/tasks/submodules.yml b/test/integration/targets/git/tasks/submodules.yml
> +index 0b311e7984..b6b02490b4 100644
> +--- a/test/integration/targets/git/tasks/submodules.yml
> ++++ b/test/integration/targets/git/tasks/submodules.yml
> +@@ -32,7 +32,7 @@
> +
> + - name: SUBMODULES | Ensure submodu1 is at the appropriate commit
> +   assert:
> +-    that: '{{ submodule1.stdout_lines | length }} == 2'
> ++    that: 'submodule1.stdout_lines | length == 2'
> +
> + - name: SUBMODULES | clear checkout_dir
> +   file:
> +@@ -53,7 +53,7 @@
> +
> + - name: SUBMODULES | Ensure submodule1 is at the appropriate commit
> +   assert:
> +-    that: '{{ submodule1.stdout_lines | length }} == 4'
> ++    that: 'submodule1.stdout_lines | length == 4'
> +
> + - name: SUBMODULES | Copy the checkout so we can run several different tests on it
> +   command: 'cp -pr {{ checkout_dir }} {{ checkout_dir }}.bak'
> +@@ -84,8 +84,8 @@
> + - name: SUBMODULES | Ensure both submodules are at the appropriate commit
> +   assert:
> +     that:
> +-      - '{{ submodule1.stdout_lines|length }} == 4'
> +-      - '{{ submodule2.stdout_lines|length }} == 2'
> ++      - 'submodule1.stdout_lines|length == 4'
> ++      - 'submodule2.stdout_lines|length == 2'
> +
> +
> + - name: SUBMODULES | Remove checkout dir
> +@@ -112,7 +112,7 @@
> +
> + - name: SUBMODULES | Ensure submodule1 is at the appropriate commit
> +   assert:
> +-    that: '{{ submodule1.stdout_lines | length }} == 5'
> ++    that: 'submodule1.stdout_lines | length == 5'
> +
> +
> + - name: SUBMODULES | Test that update with recursive found new submodules
> +@@ -121,7 +121,7 @@
> +
> + - name: SUBMODULES | Enusre submodule2 is at the appropriate commit
> +   assert:
> +-    that: '{{ submodule2.stdout_lines | length }} == 4'
> ++    that: 'submodule2.stdout_lines | length == 4'
> +
> + - name: SUBMODULES | clear checkout_dir
> +   file:
> +@@ -147,4 +147,4 @@
> +
> + - name: SUBMODULES | Ensure submodule1 is at the appropriate commit
> +   assert:
> +-    that: '{{ submodule1.stdout_lines | length }} == 4'
> ++    that: 'submodule1.stdout_lines | length == 4'
> +diff --git a/test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml b/test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml
> +index f1ddc71b2c..e45331a148 100644
> +--- a/test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml
> ++++ b/test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml
> +@@ -22,7 +22,7 @@
> + - name: Check that multiple duplicate lines collapse into a single commands
> +   assert:
> +     that:
> +-      - "{{ result.commands|length }} == 1"
> ++      - "result.commands|length == 1"
> +
> + - name: Check that set is correctly prepended
> +   assert:
> +@@ -58,6 +58,6 @@
> +
> + - assert:
> +     that:
> +-      - "{{ result.filtered|length }} == 2"
> ++      - "result.filtered|length == 2"
> +
> + - debug: msg="END cli/config_check.yaml on connection={{ ansible_connection }}"
> +diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml
> +index 7b2d53a340..316e91c43d 100644
> +--- a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml
> ++++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml
> +@@ -16,17 +16,17 @@
> +     - name: Assert that the before dicts were correctly generated
> +       assert:
> +         that:
> +-          - "{{ populate | symmetric_difference(result['before']) |length == 0 }}"
> ++          - "populate | symmetric_difference(result['before']) |length == 0"
> +
> +     - name: Assert that the correct set of commands were generated
> +       assert:
> +         that:
> +-          - "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
> ++          - "deleted['commands'] | symmetric_difference(result['commands']) |length == 0"
> +
> +     - name: Assert that the after dicts were correctly generated
> +       assert:
> +         that:
> +-          - "{{ deleted['after'] | symmetric_difference(result['after']) |length == 0 }}"
> ++          - "deleted['after'] | symmetric_difference(result['after']) |length == 0"
> +
> +     - name: Delete attributes of given interfaces (IDEMPOTENT)
> +       vyos.vyos.vyos_lldp_interfaces: *deleted
> +@@ -41,6 +41,6 @@
> +     - name: Assert that the before dicts were correctly generated
> +       assert:
> +         that:
> +-          - "{{ deleted['after'] | symmetric_difference(result['before']) |length == 0 }}"
> ++          - "deleted['after'] | symmetric_difference(result['before']) |length == 0"
> +   always:
> +     - include_tasks: _remove_config.yaml
> +diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml
> +index bf968b21de..7e0bb53d33 100644
> +--- a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml
> ++++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml
> +@@ -28,17 +28,17 @@
> +
> +     - name: Assert that before dicts were correctly generated
> +       assert:
> +-        that: "{{ merged['before'] | symmetric_difference(result['before']) |length == 0 }}"
> ++        that: "merged['before'] | symmetric_difference(result['before']) |length == 0"
> +
> +     - name: Assert that correct set of commands were generated
> +       assert:
> +         that:
> +-          - "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
> ++          - "merged['commands'] | symmetric_difference(result['commands']) |length == 0"
> +
> +     - name: Assert that after dicts was correctly generated
> +       assert:
> +         that:
> +-          - "{{ merged['after'] | symmetric_difference(result['after']) |length == 0 }}"
> ++          - "merged['after'] | symmetric_difference(result['after']) |length == 0"
> +
> +     - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT)
> +       vyos.vyos.vyos_lldp_interfaces: *merged
> +@@ -52,7 +52,7 @@
> +     - name: Assert that before dicts were correctly generated
> +       assert:
> +         that:
> +-          - "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
> ++          - "merged['after'] | symmetric_difference(result['before']) |length == 0"
> +
> +   always:
> +     - include_tasks: _remove_config.yaml
> +diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml
> +index 8cf038c91b..ad13f39328 100644
> +--- a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml
> ++++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml
> +@@ -19,17 +19,17 @@
> +     - name: Assert that before dicts were correctly generated
> +       assert:
> +         that:
> +-          - "{{ populate_intf | symmetric_difference(result['before']) |length == 0 }}"
> ++          - "populate_intf | symmetric_difference(result['before']) |length == 0"
> +
> +     - name: Assert that correct commands were generated
> +       assert:
> +         that:
> +-          - "{{ overridden['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
> ++          - "overridden['commands'] | symmetric_difference(result['commands']) |length == 0"
> +
> +     - name: Assert that after dicts were correctly generated
> +       assert:
> +         that:
> +-          - "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}"
> ++          - "overridden['after'] | symmetric_difference(result['after']) |length == 0"
> +
> +     - name: Overrides all device configuration with provided configurations (IDEMPOTENT)
> +       vyos.vyos.vyos_lldp_interfaces: *overridden
> +@@ -43,7 +43,7 @@
> +     - name: Assert that before dicts were correctly generated
> +       assert:
> +         that:
> +-          - "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}"
> ++          - "overridden['after'] | symmetric_difference(result['before']) |length == 0"
> +
> +   always:
> +     - include_tasks: _remove_config.yaml
> +diff --git a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml
> +index 17acf0654c..aadc379300 100644
> +--- a/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml
> ++++ b/test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml
> +@@ -33,17 +33,17 @@
> +     - name: Assert that correct set of commands were generated
> +       assert:
> +         that:
> +-          - "{{ replaced['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
> ++          - "replaced['commands'] | symmetric_difference(result['commands']) |length == 0"
> +
> +     - name: Assert that before dicts are correctly generated
> +       assert:
> +         that:
> +-          - "{{ populate | symmetric_difference(result['before']) |length == 0 }}"
> ++          - "populate | symmetric_difference(result['before']) |length == 0"
> +
> +     - name: Assert that after dict is correctly generated
> +       assert:
> +         that:
> +-          - "{{ replaced['after'] | symmetric_difference(result['after']) |length == 0 }}"
> ++          - "replaced['after'] | symmetric_difference(result['after']) |length == 0"
> +
> +     - name: Replace device configurations of listed LLDP interfaces with provided configurarions (IDEMPOTENT)
> +       vyos.vyos.vyos_lldp_interfaces: *replaced
> +@@ -57,7 +57,7 @@
> +     - name: Assert that before dict is correctly generated
> +       assert:
> +         that:
> +-          - "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}"
> ++          - "replaced['after'] | symmetric_difference(result['before']) |length == 0"
> +
> +   always:
> +     - include_tasks: _remove_config.yaml
> +diff --git a/test/integration/targets/include_vars/tasks/main.yml b/test/integration/targets/include_vars/tasks/main.yml
> +index db15ba3c5d..6fc4e85a33 100644
> +--- a/test/integration/targets/include_vars/tasks/main.yml
> ++++ b/test/integration/targets/include_vars/tasks/main.yml
> +@@ -15,7 +15,7 @@
> +     that:
> +       - "testing == 789"
> +       - "base_dir == 'environments/development'"
> +-      - "{{ included_one_file.ansible_included_var_files�| length }} == 1"
> ++      - "included_one_file.ansible_included_var_files�| length == 1"
> +       - "'vars/environments/development/all.yml' in included_one_file.ansible_included_var_files[0]"
> +
> + - name: include the vars/environments/development/all.yml and save results in all
> +@@ -51,7 +51,7 @@
> +   assert:
> +     that:
> +       - webapp_version is defined
> +-      - "'file_without_extension' in '{{ include_without_file_extension.ansible_included_var_files | join(' ') }}'"
> ++      - "'file_without_extension' in include_without_file_extension.ansible_included_var_files | join(' ')"
> +
> + - name: include every directory in vars
> +   include_vars:
> +@@ -67,7 +67,7 @@
> +       - "testing == 456"
> +       - "base_dir == 'services'"
> +       - "webapp_containers == 10"
> +-      - "{{ include_every_dir.ansible_included_var_files�| length }} == 7"
> ++      - "include_every_dir.ansible_included_var_files�| length == 7"
> +       - "'vars/all/all.yml' in include_every_dir.ansible_included_var_files[0]"
> +       - "'vars/environments/development/all.yml' in include_every_dir.ansible_included_var_files[1]"
> +       - "'vars/environments/development/services/webapp.yml' in include_every_dir.ansible_included_var_files[2]"
> +@@ -88,9 +88,9 @@
> +     that:
> +       - "testing == 789"
> +       - "base_dir == 'environments/development'"
> +-      - "{{ include_without_webapp.ansible_included_var_files�| length }} == 4"
> +-      - "'webapp.yml' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'"
> +-      - "'file_without_extension' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'"
> ++      - "include_without_webapp.ansible_included_var_files�| length == 4"
> ++      - "'webapp.yml' not in include_without_webapp.ansible_included_var_files | join(' ')"
> ++      - "'file_without_extension' not in include_without_webapp.ansible_included_var_files | join(' ')"
> +
> + - name: include only files matching webapp.yml
> +   include_vars:
> +@@ -104,9 +104,9 @@
> +       - "testing == 101112"
> +       - "base_dir == 'development/services'"
> +       - "webapp_containers == 20"
> +-      - "{{ include_match_webapp.ansible_included_var_files�| length }} == 1"
> ++      - "include_match_webapp.ansible_included_var_files�| length == 1"
> +       - "'vars/environments/development/services/webapp.yml' in include_match_webapp.ansible_included_var_files[0]"
> +-      - "'all.yml' not in '{{ include_match_webapp.ansible_included_var_files | join(' ') }}'"
> ++      - "'all.yml' not in include_match_webapp.ansible_included_var_files | join(' ')"
> +
> + - name: include only files matching webapp.yml and store results in webapp
> +   include_vars:
> +@@ -173,10 +173,10 @@
> + - name: Verify the hash variable
> +   assert:
> +     that:
> +-      - "{{ config | length }} == 3"
> ++      - "config | length == 3"
> +       - "config.key0 == 0"
> +       - "config.key1 == 0"
> +-      - "{{ config.key2 | length }} == 1"
> ++      - "config.key2 | length == 1"
> +       - "config.key2.a == 21"
> +
> + - name: Include the second file to merge the hash variable
> +@@ -187,10 +187,10 @@
> + - name: Verify that the hash is merged
> +   assert:
> +     that:
> +-      - "{{ config | length }} == 4"
> ++      - "config | length == 4"
> +       - "config.key0 == 0"
> +       - "config.key1 == 1"
> +-      - "{{ config.key2 | length }} == 2"
> ++      - "config.key2 | length == 2"
> +       - "config.key2.a == 21"
> +       - "config.key2.b == 22"
> +       - "config.key3 == 3"
> +@@ -202,9 +202,9 @@
> + - name: Verify that the properties from the first file is cleared
> +   assert:
> +     that:
> +-      - "{{ config | length }} == 3"
> ++      - "config | length == 3"
> +       - "config.key1 == 1"
> +-      - "{{ config.key2 | length }} == 1"
> ++      - "config.key2 | length == 1"
> +       - "config.key2.b == 22"
> +       - "config.key3 == 3"
> +
> +diff --git a/test/integration/targets/lookup_ini/test_lookup_properties.yml b/test/integration/targets/lookup_ini/test_lookup_properties.yml
> +index a6fc0f7d7c..ed34760092 100644
> +--- a/test/integration/targets/lookup_ini/test_lookup_properties.yml
> ++++ b/test/integration/targets/lookup_ini/test_lookup_properties.yml
> +@@ -10,7 +10,7 @@
> +         field_with_space: "{{lookup('ini', 'field.with.space  type=properties  file=lookup.properties')}}"
> +
> +     - assert:
> +-        that: "{{item}} is defined"
> ++        that: "item is defined"
> +       with_items: [ 'test1', 'test2', 'test_dot', 'field_with_space' ]
> +
> +     - name: "read ini value"
> +diff --git a/test/integration/targets/lookup_subelements/tasks/main.yml b/test/integration/targets/lookup_subelements/tasks/main.yml
> +index 9d93cf2096..7885347bb2 100644
> +--- a/test/integration/targets/lookup_subelements/tasks/main.yml
> ++++ b/test/integration/targets/lookup_subelements/tasks/main.yml
> +@@ -133,7 +133,7 @@
> +
> + - assert:
> +     that:
> +-      - "'{{ item.0.name }}' != 'carol'"
> ++      - "item.0.name != 'carol'"
> +   with_subelements:
> +     - "{{ users }}"
> +     - mysql.privs
> +@@ -220,5 +220,5 @@
> +
> + - assert:
> +     that:
> +-      - "'{{ user_alice }}' == 'localhost'"
> +-      - "'{{ user_bob }}' == 'db1'"
> ++      - "user_alice == 'localhost'"
> ++      - "user_bob == 'db1'"
> +diff --git a/test/integration/targets/loop_control/inner.yml b/test/integration/targets/loop_control/inner.yml
> +index 1c286fa460..976f196102 100644
> +--- a/test/integration/targets/loop_control/inner.yml
> ++++ b/test/integration/targets/loop_control/inner.yml
> +@@ -3,7 +3,7 @@
> +     that:
> +       - ansible_loop.index == ansible_loop.index0 + 1
> +       - ansible_loop.revindex == ansible_loop.revindex0 + 1
> +-      - ansible_loop.first == {{ ansible_loop.index == 1 }}
> +-      - ansible_loop.last == {{ ansible_loop.index == ansible_loop.length }}
> ++      - ansible_loop.first == (ansible_loop.index == 1)
> ++      - ansible_loop.last == (ansible_loop.index == ansible_loop.length)
> +       - ansible_loop.length == 3
> +       - ansible_loop.allitems|join(',') == 'first,second,third'
> +diff --git a/test/integration/targets/module_precedence/modules_test_multiple_roles.yml b/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
> +index f4bd264957..182c2158e8 100644
> +--- a/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
> ++++ b/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
> +@@ -14,4 +14,4 @@
> +   - assert:
> +       that:
> +         - '"location" in result'
> +-        - 'result["location"] == "{{ expected_location}}"'
> ++        - 'result["location"] == expected_location'
> +diff --git a/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml b/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
> +index 5403ae238c..ec5619f39e 100644
> +--- a/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
> ++++ b/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
> +@@ -13,4 +13,4 @@
> +   - assert:
> +       that:
> +         - '"location" in result'
> +-        - 'result["location"] == "{{ expected_location}}"'
> ++        - 'result["location"] == expected_location'
> +diff --git a/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml b/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
> +index 52c3402013..62b38a7cb5 100644
> +--- a/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
> ++++ b/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
> +@@ -7,4 +7,4 @@
> +   assert:
> +     that:
> +       - '"location" in result'
> +-      - 'result["location"] == "{{ expected_location }}"'
> ++      - 'result["location"] == expected_location'
> +diff --git a/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml b/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
> +index 52c3402013..62b38a7cb5 100644
> +--- a/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
> ++++ b/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
> +@@ -7,4 +7,4 @@
> +   assert:
> +     that:
> +       - '"location" in result'
> +-      - 'result["location"] == "{{ expected_location }}"'
> ++      - 'result["location"] == expected_location'
> +diff --git a/test/integration/targets/script/tasks/main.yml b/test/integration/targets/script/tasks/main.yml
> +index 989513d531..74189f817d 100644
> +--- a/test/integration/targets/script/tasks/main.yml
> ++++ b/test/integration/targets/script/tasks/main.yml
> +@@ -198,7 +198,7 @@
> +   assert:
> +     that:
> +       - _check_mode_test2 is skipped
> +-      - '_check_mode_test2.msg == "{{ remote_tmp_dir_test | expanduser }}/afile2.txt exists, matching creates option"'
> ++      - '_check_mode_test2.msg == remote_tmp_dir_test | expanduser ~ "/afile2.txt exists, matching creates option"'
> +
> + - name: Remove afile2.txt
> +   file:
> +@@ -220,7 +220,7 @@
> +   assert:
> +     that:
> +       - _check_mode_test3 is skipped
> +-      - '_check_mode_test3.msg == "{{ remote_tmp_dir_test | expanduser }}/afile2.txt does not exist, matching removes option"'
> ++      - '_check_mode_test3.msg == remote_tmp_dir_test | expanduser ~ "/afile2.txt does not exist, matching removes option"'
> +
> + # executable
> +
> +diff --git a/test/integration/targets/slurp/tasks/main.yml b/test/integration/targets/slurp/tasks/main.yml
> +index 939859415a..f8ebb1594c 100644
> +--- a/test/integration/targets/slurp/tasks/main.yml
> ++++ b/test/integration/targets/slurp/tasks/main.yml
> +@@ -33,7 +33,7 @@
> +       - 'slurp_existing.encoding == "base64"'
> +       - 'slurp_existing is not changed'
> +       - 'slurp_existing is not failed'
> +-      - '"{{ slurp_existing.content | b64decode }}" == "We are at the caf�"'
> ++      - 'slurp_existing.content | b64decode == "We are at the caf�"'
> +
> + - name: Create a binary file to test with
> +   copy:
> +diff --git a/test/integration/targets/template/tasks/main.yml b/test/integration/targets/template/tasks/main.yml
> +index c0d2e11a65..3c91734b09 100644
> +--- a/test/integration/targets/template/tasks/main.yml
> ++++ b/test/integration/targets/template/tasks/main.yml
> +@@ -357,7 +357,7 @@
> + - assert:
> +     that:
> +       - "\"foo t'e~m\\plated\" in unusual_results.stdout_lines"
> +-      - "{{unusual_results.stdout_lines| length}} == 1"
> ++      - "unusual_results.stdout_lines| length == 1"
> +
> + - name: check that the unusual filename can be checked for changes
> +   template:
> +diff --git a/test/integration/targets/unarchive/tasks/test_missing_binaries.yml b/test/integration/targets/unarchive/tasks/test_missing_binaries.yml
> +index 58d38f4f91..49f862b46f 100644
> +--- a/test/integration/targets/unarchive/tasks/test_missing_binaries.yml
> ++++ b/test/integration/targets/unarchive/tasks/test_missing_binaries.yml
> +@@ -66,7 +66,7 @@
> +           - zip_success.changed
> +           # Verify that file list is generated
> +           - "'files' in zip_success"
> +-          - "{{zip_success['files']| length}} == 3"
> ++          - "zip_success['files']| length == 3"
> +           - "'foo-unarchive.txt' in zip_success['files']"
> +           - "'foo-unarchive-777.txt' in zip_success['files']"
> +           - "'FOO-UNAR.TXT' in zip_success['files']"
> +diff --git a/test/integration/targets/unarchive/tasks/test_mode.yml b/test/integration/targets/unarchive/tasks/test_mode.yml
> +index c69e3bd2b2..06fbc7b8d9 100644
> +--- a/test/integration/targets/unarchive/tasks/test_mode.yml
> ++++ b/test/integration/targets/unarchive/tasks/test_mode.yml
> +@@ -24,7 +24,7 @@
> +       - "unarchive06_stat.stat.mode == '0600'"
> +       # Verify that file list is generated
> +       - "'files' in unarchive06"
> +-      - "{{unarchive06['files']| length}} == 1"
> ++      - "unarchive06['files']| length == 1"
> +       - "'foo-unarchive.txt' in unarchive06['files']"
> +
> + - name: remove our tar.gz unarchive destination
> +@@ -74,7 +74,7 @@
> +       - "unarchive07.changed == false"
> +       # Verify that file list is generated
> +       - "'files' in unarchive07"
> +-      - "{{unarchive07['files']| length}} == 1"
> ++      - "unarchive07['files']| length == 1"
> +       - "'foo-unarchive.txt' in unarchive07['files']"
> +
> + - name: remove our tar.gz unarchive destination
> +@@ -108,7 +108,7 @@
> +       - "unarchive08_stat.stat.mode == '0601'"
> +       # Verify that file list is generated
> +       - "'files' in unarchive08"
> +-      - "{{unarchive08['files']| length}} == 3"
> ++      - "unarchive08['files']| length == 3"
> +       - "'foo-unarchive.txt' in unarchive08['files']"
> +       - "'foo-unarchive-777.txt' in unarchive08['files']"
> +       - "'FOO-UNAR.TXT' in unarchive08['files']"
> +@@ -140,7 +140,7 @@
> +       - "unarchive08_stat.stat.mode == '0601'"
> +       # Verify that file list is generated
> +       - "'files' in unarchive08"
> +-      - "{{unarchive08['files']| length}} == 3"
> ++      - "unarchive08['files']| length == 3"
> +       - "'foo-unarchive.txt' in unarchive08['files']"
> +       - "'foo-unarchive-777.txt' in unarchive08['files']"
> +       - "'FOO-UNAR.TXT' in unarchive08['files']"
> +diff --git a/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml b/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
> +index 8ee1db49e4..9f45e4c991 100644
> +--- a/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
> ++++ b/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
> +@@ -40,7 +40,7 @@
> +           - unarchive10 is changed
> +           # Verify that file list is generated
> +           - "'files' in unarchive10"
> +-          - "{{unarchive10['files']| length}} == 1"
> ++          - "unarchive10['files']| length == 1"
> +           - "'foo-unarchive.txt' in unarchive10['files']"
> +           - archive_path.stat.exists
> +
> +diff --git a/test/integration/targets/unarchive/tasks/test_zip.yml b/test/integration/targets/unarchive/tasks/test_zip.yml
> +index cf03946fcd..0fc5dc9ce6 100644
> +--- a/test/integration/targets/unarchive/tasks/test_zip.yml
> ++++ b/test/integration/targets/unarchive/tasks/test_zip.yml
> +@@ -17,7 +17,7 @@
> +       - "unarchive03.changed == true"
> +       # Verify that file list is generated
> +       - "'files' in unarchive03"
> +-      - "{{unarchive03['files']| length}} == 3"
> ++      - "unarchive03['files']| length == 3"
> +       - "'foo-unarchive.txt' in unarchive03['files']"
> +       - "'foo-unarchive-777.txt' in unarchive03['files']"
> +       - "'FOO-UNAR.TXT' in unarchive03['files']"
> +diff --git a/test/integration/targets/wait_for/tasks/main.yml b/test/integration/targets/wait_for/tasks/main.yml
> +index f71ddbda6b..f81fd0f246 100644
> +--- a/test/integration/targets/wait_for/tasks/main.yml
> ++++ b/test/integration/targets/wait_for/tasks/main.yml
> +@@ -40,7 +40,7 @@
> +   assert:
> +     that:
> +       - waitfor is successful
> +-      - waitfor.path == "{{ remote_tmp_dir | expanduser }}/wait_for_file"
> ++      - waitfor.path == remote_tmp_dir | expanduser ~ "/wait_for_file"
> +       - waitfor.elapsed >= 2
> +       - waitfor.elapsed <= 15
> +
> +@@ -58,7 +58,7 @@
> +   assert:
> +     that:
> +       - waitfor is successful
> +-      - waitfor.path == "{{ remote_tmp_dir | expanduser }}/wait_for_file"
> ++      - waitfor.path == remote_tmp_dir | expanduser ~ "/wait_for_file"
> +       - waitfor.elapsed >= 2
> +       - waitfor.elapsed <= 15
> +
> +@@ -156,7 +156,7 @@
> +     that:
> +       - waitfor is successful
> +       - waitfor is not changed
> +-      - "waitfor.port == {{ http_port }}"
> ++      - "waitfor.port == http_port"
> +
> + - name: install psutil using pip (non-Linux only)
> +   pip:
> +@@ -184,7 +184,7 @@
> +     that:
> +       - waitfor is successful
> +       - waitfor is not changed
> +-      - "waitfor.port == {{ http_port }}"
> ++      - "waitfor.port == http_port"
> +
> + - name: test wait_for with delay
> +   wait_for:
> +diff --git a/test/units/parsing/yaml/test_dumper.py b/test/units/parsing/yaml/test_dumper.py
> +index 5fbc139ba0..cbf5b45646 100644
> +--- a/test/units/parsing/yaml/test_dumper.py
> ++++ b/test/units/parsing/yaml/test_dumper.py
> +@@ -29,7 +29,6 @@ from ansible.parsing.yaml import dumper, objects
> + from ansible.parsing.yaml.loader import AnsibleLoader
> + from ansible.module_utils.six import PY2
> + from ansible.template import AnsibleUndefined
> +-from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes
> +
> + from units.mock.yaml_helper import YamlTestUtils
> + from units.mock.vault_helper import TextVaultSecret
> +@@ -69,8 +68,7 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils):
> +
> +     def test_bytes(self):
> +         b_text = u'tr�ma'.encode('utf-8')
> +-        unsafe_object = AnsibleUnsafeBytes(b_text)
> +-        yaml_out = self._dump_string(unsafe_object, dumper=self.dumper)
> ++        yaml_out = self._dump_string(b_text, dumper=self.dumper)
> +
> +         stream = self._build_stream(yaml_out)
> +         loader = self._loader(stream)
> +@@ -97,8 +95,7 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils):
> +
> +     def test_unicode(self):
> +         u_text = u'n�el'
> +-        unsafe_object = AnsibleUnsafeText(u_text)
> +-        yaml_out = self._dump_string(unsafe_object, dumper=self.dumper)
> ++        yaml_out = self._dump_string(u_text, dumper=self.dumper)
> +
> +         stream = self._build_stream(yaml_out)
> +         loader = self._loader(stream)
> +--
> +2.40.0
> diff --git a/recipes-devtools/python/python3-ansible_2.14.11.bb b/recipes-devtools/python/python3-ansible_2.14.11.bb
> index f3ab2377..f57ef6f8 100644
> --- a/recipes-devtools/python/python3-ansible_2.14.11.bb
> +++ b/recipes-devtools/python/python3-ansible_2.14.11.bb
> @@ -9,4 +9,5 @@ RDEPENDS:${PN} += "python3-pyyaml \
>  
>  SRC_URI += " \
>      file://python3-ensure-py-scripts-use-py3-for-shebang.patch \
> +    file://CVE-2023-5764.patch \
>  "
> -- 
> 2.40.0
> 

> 
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#9236): https://lists.yoctoproject.org/g/meta-virtualization/message/9236
> Mute This Topic: https://lists.yoctoproject.org/mt/112448815/1050810
> Group Owner: meta-virtualization+owner@lists.yoctoproject.org
> Unsubscribe: https://lists.yoctoproject.org/g/meta-virtualization/unsub [bruce.ashfield@gmail.com]
> -=-=-=-=-=-=-=-=-=-=-=-
> 



^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2025-05-06  1:47 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-04-25 10:50 [meta-virtualization][meta-cloud-services][kirkstone][PATCH 1/1] python3-ansible: Fix CVE-2023-5764 ssambu
2025-05-06  1:47 ` Bruce Ashfield

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).