VYPR
Moderate severityNVD Advisory· Published Dec 12, 2023· Updated Nov 20, 2025

Ansible: template injection

CVE-2023-5764

Description

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.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A template injection flaw in Ansible allows specially crafted data to bypass unsafe designation, enabling template injection.

What is the vulnerability?

CVE-2023-5764 is a template injection flaw in Ansible where internal templating operations can remove the unsafe designation from template data. This occurs when user-supplied data is processed through templating, and the unsafe marker is lost, allowing the data to be interpreted as a template instead of safe content [1][2]. The root cause lies in the handling of unsafe objects during serialization, where the unsafe designation was not properly preserved [3][4].

How is it exploited?

An attacker can exploit this vulnerability by supplying a specially crafted file that contains template data. When Ansible processes this data through its templating engine, the unsafe designation may be stripped, causing it to be executed as a Jinja2 template. This requires the attacker to have the ability to provide template data to the Ansible controller, which could be through a playbook, variable file, or other templating inputs. No authentication is required to trigger the vulnerability if the attacker can inject data that is later processed by the templating engine [1][2].

Impact

If successfully exploited, an attacker can achieve template injection, potentially leading to arbitrary code execution within the Ansible controller context. This could allow the attacker to execute arbitrary commands, exfiltrate sensitive data, or perform other malicious actions depending on the privileges of the Ansible process. The vulnerability is rated as High severity with a CVSS score of 8.8, indicating significant risk [1][2].

Mitigation

Red Hat and the Ansible project have released patches for Ansible Automation Platform and Ansible Engine. The fix involves modifying the serialization code to properly strip unsafe markers when encoding objects, ensuring the unsafe designation is not inadvertently removed [3][4]. Users are advised to update to the latest versions as specified in the Red Hat errata RHSA-2023:7773 [1]. There are no known workarounds for this vulnerability, and it is not currently listed on the Known Exploited Vulnerabilities (KEV) catalog.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
ansible-corePyPI
>= 2.16.0, < 2.16.12.16.1
ansible-corePyPI
>= 2.15.0, < 2.15.82.15.8
ansible-corePyPI
< 2.14.122.14.12

Affected products

35

Patches

3
7239d2d371bc

Ensure that unsafe is more difficult to lose [stable-2.14] (#82295)

https://github.com/ansible/ansibleMatt MartzNov 27, 2023via ghsa
57 files changed · +519 143
  • changelogs/fragments/cve-2023-5764.yml+6 0 added
    @@ -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.
    
  • lib/ansible/module_utils/common/json.py+2 2 modified
    @@ -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 @@ def default(self, o):
                     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)
    
  • lib/ansible/parsing/yaml/dumper.py+5 1 modified
    @@ -24,7 +24,7 @@
     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))
     
     
    
  • lib/ansible/playbook/conditional.py+5 4 modified
    @@ -26,7 +26,7 @@
     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 @@ def _check_conditional(self, conditional, templar, all_vars):
                 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)
    
  • lib/ansible/playbook/task.py+24 0 modified
    @@ -290,6 +290,30 @@ def post_validate(self, templar):
     
             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
    
  • lib/ansible/plugins/action/assert.py+22 1 modified
    @@ -63,8 +63,29 @@ def run(self, tmp=None, task_vars=None):
     
             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]
     
    
  • lib/ansible/plugins/callback/__init__.py+3 1 modified
    @@ -38,7 +38,7 @@
     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 = '|'
    
  • lib/ansible/plugins/filter/core.py+5 0 modified
    @@ -37,6 +37,7 @@
     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
     
    
  • lib/ansible/plugins/lookup/first_found.py+13 2 modified
    @@ -136,7 +136,6 @@
         elements: path
     """
     import os
    -import re
     
     from collections.abc import Mapping, Sequence
     
    @@ -147,10 +146,22 @@
     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:
    
  • lib/ansible/template/__init__.py+13 2 modified
    @@ -31,7 +31,7 @@
     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.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
    
  • lib/ansible/utils/unsafe_proxy.py+259 6 modified
    @@ -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
    
  • test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml+8 8 modified
    @@ -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'
    
  • test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/apt_repository/tasks/apt.yml+5 5 modified
    @@ -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'
    
  • test/integration/targets/assert/assert.out.nested_tmpl.stderr+4 0 added
    @@ -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"
    
  • test/integration/targets/assert/assert.out.nested_tmpl.stdout+12 0 added
    @@ -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
    +
    
  • test/integration/targets/assert/assert.out.quiet.stderr+0 0 renamed
  • test/integration/targets/assert/assert.out.quiet.stdout+0 0 renamed
  • test/integration/targets/assert/nested_tmpl.yml+9 0 added
    @@ -0,0 +1,9 @@
    +- hosts: localhost
    +  gather_facts: False
    +  tasks:
    +    - assert:
    +        that:
    +          - '"{{ foo }}" == "bar"'
    +          - foo == "bar"
    +      vars:
    +        foo: bar
    
  • test/integration/targets/assert/quiet.yml+2 2 modified
    @@ -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
    
  • test/integration/targets/assert/runme.sh+2 1 modified
    @@ -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
    
  • test/integration/targets/command_shell/tasks/main.yml+1 1 modified
    @@ -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'
    
  • test/integration/targets/copy/tasks/tests.yml+21 21 modified
    @@ -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
    
  • test/integration/targets/debug/runme.sh+2 0 modified
    @@ -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 "$@"
    
  • test/integration/targets/debug/unsafe.yml+13 0 added
    @@ -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
    
  • test/integration/targets/dnf/tasks/test_sos_removal.yml+1 1 modified
    @@ -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
    
  • test/integration/targets/expect/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/file/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/file/tasks/state_link.yml+1 1 modified
    @@ -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
    
  • test/integration/targets/find/tasks/main.yml+6 6 modified
    @@ -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]'
    
  • test/integration/targets/gathering_facts/test_gathering_facts.yml+2 2 modified
    @@ -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' ]
    
  • test/integration/targets/git/tasks/depth.yml+1 1 modified
    @@ -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
    
  • test/integration/targets/git/tasks/localmods.yml+2 2 modified
    @@ -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
    
  • test/integration/targets/git/tasks/submodules.yml+7 7 modified
    @@ -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'
    
  • test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml+2 2 modified
    @@ -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 }}"
    
  • test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml+4 4 modified
    @@ -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
    
  • test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml+4 4 modified
    @@ -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
    
  • test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml+4 4 modified
    @@ -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
    
  • test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml+4 4 modified
    @@ -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
    
  • test/integration/targets/include_vars/tasks/main.yml+14 14 modified
    @@ -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"
     
    
  • test/integration/targets/lookup_ini/test_lookup_properties.yml+1 1 modified
    @@ -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"
    
  • test/integration/targets/lookup_subelements/tasks/main.yml+3 3 modified
    @@ -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'"
    
  • test/integration/targets/loop_control/inner.yml+2 2 modified
    @@ -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'
    
  • test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml+1 1 modified
    @@ -13,4 +13,4 @@
       - assert:
           that:
             - '"location" in result'
    -        - 'result["location"] == "{{ expected_location}}"'
    +        - 'result["location"] == expected_location'
    
  • test/integration/targets/module_precedence/modules_test_multiple_roles.yml+1 1 modified
    @@ -14,4 +14,4 @@
       - assert:
           that:
             - '"location" in result'
    -        - 'result["location"] == "{{ expected_location}}"'
    +        - 'result["location"] == expected_location'
    
  • test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml+1 1 modified
    @@ -7,4 +7,4 @@
       assert:
         that:
           - '"location" in result'
    -      - 'result["location"] == "{{ expected_location }}"'
    +      - 'result["location"] == expected_location'
    
  • test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml+1 1 modified
    @@ -7,4 +7,4 @@
       assert:
         that:
           - '"location" in result'
    -      - 'result["location"] == "{{ expected_location }}"'
    +      - 'result["location"] == expected_location'
    
  • test/integration/targets/script/tasks/main.yml+2 2 modified
    @@ -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
     
    
  • test/integration/targets/slurp/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/template/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/unarchive/tasks/test_missing_binaries.yml+1 1 modified
    @@ -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']"
    
  • test/integration/targets/unarchive/tasks/test_mode.yml+4 4 modified
    @@ -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']"
    
  • test/integration/targets/unarchive/tasks/test_unprivileged_user.yml+1 1 modified
    @@ -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
     
    
  • test/integration/targets/unarchive/tasks/test_zip.yml+1 1 modified
    @@ -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']"
    
  • test/integration/targets/wait_for/tasks/main.yml+4 4 modified
    @@ -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:
    
  • test/units/parsing/yaml/test_dumper.py+2 5 modified
    @@ -29,7 +29,6 @@
     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 @@ def test_ansible_vault_encrypted_unicode(self):
     
         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 @@ def test_bytes(self):
     
         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)
    
fea130480d26

Ensure that unsafe is more difficult to lose [stable-2.15] (#82294)

https://github.com/ansible/ansibleMatt MartzNov 27, 2023via ghsa
56 files changed · +522 146
  • changelogs/fragments/cve-2023-5764.yml+6 0 added
    @@ -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.
    
  • lib/ansible/module_utils/common/json.py+2 2 modified
    @@ -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 @@ def default(self, o):
                     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)
    
  • lib/ansible/parsing/yaml/dumper.py+5 1 modified
    @@ -24,7 +24,7 @@
     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))
     
     
    
  • lib/ansible/playbook/conditional.py+6 5 modified
    @@ -25,7 +25,7 @@
     from jinja2.compiler import generate
     from jinja2.exceptions import UndefinedError
     
    -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
     from ansible.playbook.attribute import FieldAttribute
    @@ -132,9 +132,10 @@ def _check_conditional(self, conditional, templar, all_vars):
                 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)
    @@ -178,7 +179,7 @@ def generic_visit(self, node, inside_call=False, inside_yield=False):
                 # NOTE The spaces around True and False are intentional to short-circuit literal_eval for
                 #      jinja2_native=False and avoid its expensive calls.
                 presented = "{%% if %s %%} True {%% else %%} False {%% endif %%}" % conditional
    -            val = templar.template(presented, disable_lookups=disable_lookups).strip()
    +            val = templar.template(presented).strip()
                 if val == "True":
                     return True
                 elif val == "False":
    
  • lib/ansible/playbook/task.py+24 0 modified
    @@ -289,6 +289,30 @@ def post_validate(self, templar):
     
             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
    
  • lib/ansible/plugins/action/assert.py+22 1 modified
    @@ -63,8 +63,29 @@ def run(self, tmp=None, task_vars=None):
     
             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]
     
    
  • lib/ansible/plugins/callback/__init__.py+3 1 modified
    @@ -38,7 +38,7 @@
     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 = '|'
    
  • lib/ansible/plugins/filter/core.py+5 0 modified
    @@ -37,6 +37,7 @@
     from ansible.utils.encrypt import passlib_or_crypt, PASSLIB_AVAILABLE
     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
     
    
  • lib/ansible/plugins/lookup/first_found.py+13 2 modified
    @@ -136,7 +136,6 @@
         elements: path
     """
     import os
    -import re
     
     from collections.abc import Mapping, Sequence
     
    @@ -147,10 +146,22 @@
     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:
    
  • lib/ansible/template/__init__.py+13 2 modified
    @@ -31,7 +31,7 @@
     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.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()
     
    @@ -365,10 +365,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
    
  • lib/ansible/utils/unsafe_proxy.py+259 6 modified
    @@ -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
    
  • test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml+8 8 modified
    @@ -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'
    
  • test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/apt_repository/tasks/apt.yml+6 6 modified
    @@ -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'
           - '"sources_added" in result'
           - 'result.sources_added | length == 1'
           - '"git" in result.sources_added[0]'
    @@ -176,7 +176,7 @@
         that:
           - 'result.changed'
           - 'result.state == "absent"'
    -      - 'result.repo == "{{test_ppa_spec}}"'
    +      - 'result.repo == test_ppa_spec'
           - '"sources_added" in result'
           - 'result.sources_added | length == 0'
           - '"sources_removed" in result'
    @@ -207,7 +207,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'
    
  • test/integration/targets/assert/assert.out.nested_tmpl.stderr+4 0 added
    @@ -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"
    
  • test/integration/targets/assert/assert.out.nested_tmpl.stdout+12 0 added
    @@ -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
    +
    
  • test/integration/targets/assert/assert.out.quiet.stderr+0 0 renamed
  • test/integration/targets/assert/assert.out.quiet.stdout+0 0 renamed
  • test/integration/targets/assert/nested_tmpl.yml+9 0 added
    @@ -0,0 +1,9 @@
    +- hosts: localhost
    +  gather_facts: False
    +  tasks:
    +    - assert:
    +        that:
    +          - '"{{ foo }}" == "bar"'
    +          - foo == "bar"
    +      vars:
    +        foo: bar
    
  • test/integration/targets/assert/quiet.yml+2 2 modified
    @@ -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
    
  • test/integration/targets/assert/runme.sh+2 1 modified
    @@ -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
    
  • test/integration/targets/command_shell/tasks/main.yml+1 1 modified
    @@ -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'
    
  • test/integration/targets/copy/tasks/tests.yml+21 21 modified
    @@ -1250,7 +1250,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)
    @@ -2153,26 +2153,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
    
  • test/integration/targets/debug/runme.sh+2 0 modified
    @@ -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 "$@"
    
  • test/integration/targets/debug/unsafe.yml+13 0 added
    @@ -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
    
  • test/integration/targets/expect/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/file/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/file/tasks/state_link.yml+1 1 modified
    @@ -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
    
  • test/integration/targets/find/tasks/main.yml+6 6 modified
    @@ -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]'
    
  • test/integration/targets/gathering_facts/test_gathering_facts.yml+2 2 modified
    @@ -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' ]
    
  • test/integration/targets/git/tasks/depth.yml+1 1 modified
    @@ -172,7 +172,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
    
  • test/integration/targets/git/tasks/localmods.yml+2 2 modified
    @@ -58,7 +58,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
    @@ -127,7 +127,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
    
  • test/integration/targets/git/tasks/submodules.yml+7 7 modified
    @@ -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'
    
  • test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml+2 2 modified
    @@ -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 }}"
    
  • test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml+4 4 modified
    @@ -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
    
  • test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml+4 4 modified
    @@ -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
    
  • test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml+4 4 modified
    @@ -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
    
  • test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml+4 4 modified
    @@ -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
    
  • test/integration/targets/include_vars/tasks/main.yml+16 16 modified
    @@ -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"
     
    @@ -216,10 +216,10 @@
     - name: Verify that the hash is merged after vars files are accumulated
       assert:
         that:
    -      - "{{ config | length }} == 3"
    +      - "config | length == 3"
           - "config.key0 is undefined"
           - "config.key1 == 1"
    -      - "{{ config.key2 | length }} == 1"
    +      - "config.key2 | length == 1"
           - "config.key2.b == 22"
           - "config.key3 == 3"
     
    
  • test/integration/targets/lookup_ini/test_lookup_properties.yml+1 1 modified
    @@ -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"
    
  • test/integration/targets/lookup_subelements/tasks/main.yml+3 3 modified
    @@ -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'"
    
  • test/integration/targets/loop_control/inner.yml+2 2 modified
    @@ -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'
    
  • test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml+1 1 modified
    @@ -13,4 +13,4 @@
       - assert:
           that:
             - '"location" in result'
    -        - 'result["location"] == "{{ expected_location}}"'
    +        - 'result["location"] == expected_location'
    
  • test/integration/targets/module_precedence/modules_test_multiple_roles.yml+1 1 modified
    @@ -14,4 +14,4 @@
       - assert:
           that:
             - '"location" in result'
    -        - 'result["location"] == "{{ expected_location}}"'
    +        - 'result["location"] == expected_location'
    
  • test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml+1 1 modified
    @@ -7,4 +7,4 @@
       assert:
         that:
           - '"location" in result'
    -      - 'result["location"] == "{{ expected_location }}"'
    +      - 'result["location"] == expected_location'
    
  • test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml+1 1 modified
    @@ -7,4 +7,4 @@
       assert:
         that:
           - '"location" in result'
    -      - 'result["location"] == "{{ expected_location }}"'
    +      - 'result["location"] == expected_location'
    
  • test/integration/targets/script/tasks/main.yml+2 2 modified
    @@ -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
     
    
  • test/integration/targets/slurp/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/template/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/unarchive/tasks/test_missing_binaries.yml+1 1 modified
    @@ -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']"
    
  • test/integration/targets/unarchive/tasks/test_mode.yml+4 4 modified
    @@ -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']"
    
  • test/integration/targets/unarchive/tasks/test_unprivileged_user.yml+1 1 modified
    @@ -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
     
    
  • test/integration/targets/unarchive/tasks/test_zip.yml+1 1 modified
    @@ -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']"
    
  • test/integration/targets/wait_for/tasks/main.yml+4 4 modified
    @@ -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
     
    @@ -165,7 +165,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:
    @@ -193,7 +193,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:
    
  • test/units/parsing/yaml/test_dumper.py+2 5 modified
    @@ -29,7 +29,6 @@
     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 @@ def test_ansible_vault_encrypted_unicode(self):
     
         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 @@ def test_bytes(self):
     
         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)
    
270b39f6ff02

Ensure that unsafe is more difficult to lose [stable-2.16] (#82293)

https://github.com/ansible/ansibleMatt MartzNov 27, 2023via ghsa
58 files changed · +526 151
  • changelogs/fragments/cve-2023-5764.yml+6 0 added
    @@ -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.
    
  • lib/ansible/module_utils/common/json.py+2 2 modified
    @@ -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 @@ def default(self, o):
                     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)
    
  • lib/ansible/parsing/yaml/dumper.py+5 1 modified
    @@ -24,7 +24,7 @@
     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))
     
     
    
  • lib/ansible/playbook/conditional.py+5 5 modified
    @@ -21,7 +21,7 @@
     
     import typing as t
     
    -from ansible.errors import AnsibleError, AnsibleUndefinedVariable
    +from ansible.errors import AnsibleError, AnsibleUndefinedVariable, AnsibleTemplateError
     from ansible.module_utils.common.text.converters import to_native
     from ansible.playbook.attribute import FieldAttribute
     from ansible.template import Templar
    @@ -102,14 +102,14 @@ def _check_conditional(self, conditional: str, templar: Templar, all_vars: dict[
                         return False
     
                 # If the result of the first-pass template render (to resolve inline templates) is marked unsafe,
    -            # explicitly disable lookups on the final pass to prevent evaluation of untrusted content in the
    -            # constructed template.
    -            disable_lookups = hasattr(conditional, '__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.')
     
                 # NOTE The spaces around True and False are intentional to short-circuit literal_eval for
                 #      jinja2_native=False and avoid its expensive calls.
                 return templar.template(
                     "{%% if %s %%} True {%% else %%} False {%% endif %%}" % conditional,
    -                disable_lookups=disable_lookups).strip() == "True"
    +            ).strip() == "True"
             except AnsibleUndefinedVariable as e:
                 raise AnsibleUndefinedVariable("error while evaluating conditional (%s): %s" % (original, e))
    
  • lib/ansible/playbook/task.py+24 0 modified
    @@ -289,6 +289,30 @@ def post_validate(self, templar):
     
             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
    
  • lib/ansible/plugins/action/assert.py+22 1 modified
    @@ -64,8 +64,29 @@ def run(self, tmp=None, task_vars=None):
     
             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]
     
    
  • lib/ansible/plugins/callback/__init__.py+3 1 modified
    @@ -38,7 +38,7 @@
     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 = '|'
    
  • lib/ansible/plugins/filter/core.py+5 0 modified
    @@ -37,6 +37,7 @@
     from ansible.utils.encrypt import do_encrypt, PASSLIB_AVAILABLE
     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
     
    
  • lib/ansible/plugins/lookup/first_found.py+13 2 modified
    @@ -139,7 +139,6 @@
         elements: path
     """
     import os
    -import re
     
     from collections.abc import Mapping, Sequence
     
    @@ -150,10 +149,22 @@
     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:
    
  • lib/ansible/template/__init__.py+13 2 modified
    @@ -31,7 +31,7 @@
     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.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 to_unsafe_text, wrap_var
    +from ansible.utils.unsafe_proxy import to_unsafe_text, wrap_var, AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText
     
     display = Display()
     
    @@ -365,10 +365,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
    
  • lib/ansible/utils/unsafe_proxy.py+259 6 modified
    @@ -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
    
  • test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml+8 8 modified
    @@ -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'
    
  • test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/apt_repository/tasks/apt.yml+6 6 modified
    @@ -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'
           - '"sources_added" in result'
           - 'result.sources_added | length == 1'
           - '"git" in result.sources_added[0]'
    @@ -176,7 +176,7 @@
         that:
           - 'result.changed'
           - 'result.state == "absent"'
    -      - 'result.repo == "{{test_ppa_spec}}"'
    +      - 'result.repo == test_ppa_spec'
           - '"sources_added" in result'
           - 'result.sources_added | length == 0'
           - '"sources_removed" in result'
    @@ -207,7 +207,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'
    
  • test/integration/targets/assert/assert.out.nested_tmpl.stderr+4 0 added
    @@ -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"
    
  • test/integration/targets/assert/assert.out.nested_tmpl.stdout+12 0 added
    @@ -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
    +
    
  • test/integration/targets/assert/assert.out.quiet.stderr+0 0 renamed
  • test/integration/targets/assert/assert.out.quiet.stdout+0 0 renamed
  • test/integration/targets/assert/nested_tmpl.yml+9 0 added
    @@ -0,0 +1,9 @@
    +- hosts: localhost
    +  gather_facts: False
    +  tasks:
    +    - assert:
    +        that:
    +          - '"{{ foo }}" == "bar"'
    +          - foo == "bar"
    +      vars:
    +        foo: bar
    
  • test/integration/targets/assert/quiet.yml+2 2 modified
    @@ -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
    
  • test/integration/targets/assert/runme.sh+2 1 modified
    @@ -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
    
  • test/integration/targets/command_shell/tasks/main.yml+1 1 modified
    @@ -320,7 +320,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'
    
  • test/integration/targets/copy/tasks/tests.yml+21 21 modified
    @@ -1250,7 +1250,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)
    @@ -2153,26 +2153,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
    
  • test/integration/targets/debug/runme.sh+2 0 modified
    @@ -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 "$@"
    
  • test/integration/targets/debug/unsafe.yml+13 0 added
    @@ -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
    
  • test/integration/targets/expect/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/file/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/file/tasks/state_link.yml+1 1 modified
    @@ -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
    
  • test/integration/targets/find/tasks/main.yml+6 6 modified
    @@ -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,8 +371,8 @@
       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]'
     
     - name: Run mode tests
    
  • test/integration/targets/find/tasks/mode.yml+1 1 modified
    @@ -61,7 +61,7 @@
     - assert:
         that:
           - exact_mode_0644.files == exact_mode_0644_symbolic.files
    -      - exact_mode_0644.files[0].path == '{{ remote_tmp_dir_test }}/mode_0644'
    +      - exact_mode_0644.files[0].path == remote_tmp_dir_test ~ '/mode_0644'
           - user_readable_octal.files == user_readable_symbolic.files
           - user_readable_octal.files|map(attribute='path')|map('basename')|sort == ['mode_0400', 'mode_0444', 'mode_0644', 'mode_0666', 'mode_0700']
           - other_readable_octal.files == other_readable_symbolic.files
    
  • test/integration/targets/gathering_facts/test_gathering_facts.yml+2 2 modified
    @@ -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' ]
    
  • test/integration/targets/git/tasks/depth.yml+1 1 modified
    @@ -172,7 +172,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
    
  • test/integration/targets/git/tasks/localmods.yml+2 2 modified
    @@ -58,7 +58,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
    @@ -127,7 +127,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
    
  • test/integration/targets/git/tasks/submodules.yml+7 7 modified
    @@ -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'
    
  • test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml+2 2 modified
    @@ -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 }}"
    
  • test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml+4 4 modified
    @@ -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
    
  • test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml+4 4 modified
    @@ -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
    
  • test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml+4 4 modified
    @@ -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
    
  • test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml+4 4 modified
    @@ -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
    
  • test/integration/targets/include_vars/tasks/main.yml+16 16 modified
    @@ -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"
     
    @@ -216,10 +216,10 @@
     - name: Verify that the hash is merged after vars files are accumulated
       assert:
         that:
    -      - "{{ config | length }} == 3"
    +      - "config | length == 3"
           - "config.key0 is undefined"
           - "config.key1 == 1"
    -      - "{{ config.key2 | length }} == 1"
    +      - "config.key2 | length == 1"
           - "config.key2.b == 22"
           - "config.key3 == 3"
     
    
  • test/integration/targets/lookup_first_found/tasks/main.yml+4 4 modified
    @@ -109,8 +109,8 @@
       - name: Load variables specific for OS family
         assert:
           that:
    -        - "{{item|quote}} is file"
    -        - "{{item|basename == 'itworks.yml'}}"
    +        - "item is file"
    +        - "item|basename == 'itworks.yml'"
         with_first_found:
           - files:
               - "{{ansible_id}}-{{ansible_lsb.major_release}}.yml"  # invalid var, should be skipped
    @@ -124,8 +124,8 @@
       - name: Load variables specific for OS family, but now as list of dicts, same options as above
         assert:
           that:
    -        - "{{item|quote}} is file"
    -        - "{{item|basename == 'itworks.yml'}}"
    +        - "item is file"
    +        - "item|basename == 'itworks.yml'"
         with_first_found:
           - files:
               - "{{ansible_id}}-{{ansible_lsb.major_release}}.yml"
    
  • test/integration/targets/lookup_ini/test_lookup_properties.yml+1 1 modified
    @@ -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"
    
  • test/integration/targets/lookup_subelements/tasks/main.yml+3 3 modified
    @@ -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'"
    
  • test/integration/targets/loop_control/inner.yml+2 2 modified
    @@ -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'
    
  • test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml+1 1 modified
    @@ -13,4 +13,4 @@
       - assert:
           that:
             - '"location" in result'
    -        - 'result["location"] == "{{ expected_location}}"'
    +        - 'result["location"] == expected_location'
    
  • test/integration/targets/module_precedence/modules_test_multiple_roles.yml+1 1 modified
    @@ -14,4 +14,4 @@
       - assert:
           that:
             - '"location" in result'
    -        - 'result["location"] == "{{ expected_location}}"'
    +        - 'result["location"] == expected_location'
    
  • test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml+1 1 modified
    @@ -7,4 +7,4 @@
       assert:
         that:
           - '"location" in result'
    -      - 'result["location"] == "{{ expected_location }}"'
    +      - 'result["location"] == expected_location'
    
  • test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml+1 1 modified
    @@ -7,4 +7,4 @@
       assert:
         that:
           - '"location" in result'
    -      - 'result["location"] == "{{ expected_location }}"'
    +      - 'result["location"] == expected_location'
    
  • test/integration/targets/script/tasks/main.yml+2 2 modified
    @@ -209,7 +209,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:
    @@ -231,7 +231,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
     
    
  • test/integration/targets/slurp/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/template/tasks/main.yml+1 1 modified
    @@ -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:
    
  • test/integration/targets/unarchive/tasks/test_missing_binaries.yml+1 1 modified
    @@ -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']"
    
  • test/integration/targets/unarchive/tasks/test_mode.yml+4 4 modified
    @@ -47,7 +47,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
    @@ -97,7 +97,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
    @@ -131,7 +131,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']"
    @@ -163,7 +163,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']"
    
  • test/integration/targets/unarchive/tasks/test_unprivileged_user.yml+1 1 modified
    @@ -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
     
    
  • test/integration/targets/unarchive/tasks/test_zip.yml+1 1 modified
    @@ -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']"
    
  • test/integration/targets/wait_for/tasks/main.yml+4 4 modified
    @@ -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
     
    @@ -165,7 +165,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:
    @@ -193,7 +193,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:
    
  • test/units/parsing/yaml/test_dumper.py+2 5 modified
    @@ -27,7 +27,6 @@
     from ansible.parsing.yaml import dumper, objects
     from ansible.parsing.yaml.loader import AnsibleLoader
     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
    @@ -67,8 +66,7 @@ def test_ansible_vault_encrypted_unicode(self):
     
         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)
    @@ -81,8 +79,7 @@ def test_bytes(self):
     
         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)
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

9

News mentions

0

No linked articles in our index yet.