ansible/CVE-2023-5764.patch
wk333 c2c9243c74 Fix CVE-2023-5764
(cherry picked from commit 8729e01684c95cc9a9372bd609a4caab3bfcb5ad)
2025-04-02 09:40:36 +08:00

2011 lines
81 KiB
Diff
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

From f374897940ad8f388c273af91db4863c51799c6d Mon Sep 17 00:00:00 2001
From: Abhijeet Kasurde <akasurde@redhat.com>
Date: Tue, 7 Sep 2021 10:21:47 +0530
Subject: [PATCH 1/2] Ensure that unsafe is more difficult to lose
[stable-2.14] (#82295)
Origin: https://build.opensuse.org/projects/SUSE:SLE-15-SP3:Update/packages/ansible/files/0001-Ensure-that-unsafe-is-more-difficult-to-lose-stable-.patch?expand=1
* Ensure that unsafe is more difficult to lose
* Add Task.untemplated_args, and switch assert over to use it
* Don't use re in first_found, switch to using native string methods
* If nested templating results in unsafe, just error, don't continue
(cherry picked from commit 586f1924512b01305f896d9ae4732773023013a3)
* ci_complete
yaml dumper: Add YAML respresenter for AnsibleUndefined (#75078)
Fixes: #75072
Signed-off-by: Abhijeet Kasurde <akasurde@redhat.com>
Ensure single vaulted values aren't counted as sequences. Fixes #70784 (#70786)
AnsibleVaultEncryptedUnicode should be considered a string (#71609)
* AnsibleVaultEncryptedUnicode should be considered a string
* linting fix
* clog frag
2.12: Add YAML representers for NativeJinjaUnsafeText and NativeJinjaText (#77299)
* Add a YAML representer for NativeJinjaUnsafeText (#76186)
(cherry picked from commit dd220ddc2faf9510bdfedacf8b755798038591d9)
* Add a YAML representer for NativeJinjaText (#77282)
Fixes #77280
(cherry picked from commit c9db73f04e7a5fae7bbbdff8efbd585d15971d31)
---
.../fragments/70784-vault-is-string.yml | 3 +
.../fragments/71609-is_string-vault.yml | 3 +
changelogs/fragments/75072_undefined_yaml.yml | 3 +
changelogs/fragments/cve-2023-5764.yml | 6 +
.../nativejinjatext-yaml-representer.yml | 2 +
...nativejinjaunsafetext-yaml-representer.yml | 2 +
.../module_utils/common/collections.py | 3 +-
lib/ansible/module_utils/common/json.py | 14 +-
lib/ansible/parsing/yaml/dumper.py | 50 +++-
lib/ansible/playbook/conditional.py | 9 +-
lib/ansible/playbook/task.py | 24 ++
lib/ansible/plugins/action/assert.py | 23 +-
lib/ansible/plugins/callback/__init__.py | 90 +++++-
lib/ansible/plugins/filter/core.py | 25 +-
lib/ansible/plugins/lookup/first_found.py | 25 ++
lib/ansible/template/__init__.py | 23 +-
lib/ansible/utils/unsafe_proxy.py | 263 +++++++++++++++++-
.../targets/apt_repository/tasks/apt.yml | 11 +-
.../assert/assert.out.nested_tmpl.stderr | 4 +
.../assert/assert.out.nested_tmpl.stdout | 12 +
...t.quiet.stderr => assert.out.quiet.stderr} | 0
...t.quiet.stdout => assert.out.quiet.stdout} | 0
.../targets/assert/nested_tmpl.yml | 9 +
test/integration/targets/assert/quiet.yml | 4 +-
test/integration/targets/assert/runme.sh | 3 +-
.../targets/command_shell/tasks/main.yml | 2 +-
test/integration/targets/copy/tasks/tests.yml | 42 +--
test/integration/targets/debug/runme.sh | 2 +
test/integration/targets/debug/unsafe.yml | 13 +
.../integration/targets/expect/tasks/main.yml | 7 +-
.../targets/file/tasks/state_link.yml | 2 +-
test/integration/targets/find/tasks/main.yml | 88 +++++-
.../gathering_facts/test_gathering_facts.yml | 4 +-
test/integration/targets/git/tasks/depth.yml | 2 +-
.../targets/git/tasks/localmods.yml | 4 +-
.../targets/git/tasks/submodules.yml | 25 +-
.../targets/include_vars/tasks/main.yml | 20 +-
.../tests/cli/merged.yaml | 9 +-
.../tests/cli/replaced.yaml | 10 +-
.../test_lookup_properties.yml | 2 +-
.../modules_test_multiple_roles.yml | 2 +-
...ules_test_multiple_roles_reverse_order.yml | 2 +-
.../multiple_roles/bar/tasks/main.yml | 2 +-
.../multiple_roles/foo/tasks/main.yml | 2 +-
.../integration/targets/script/tasks/main.yml | 4 +-
test/integration/targets/slurp/tasks/main.yml | 2 +-
.../targets/template/tasks/main.yml | 2 +-
.../targets/unarchive/tasks/test_mode.yml | 8 +-
.../tasks/test_unprivileged_user.yml | 2 +-
.../targets/unarchive/tasks/test_zip.yml | 2 +-
.../roles/test_vault_embedded/tasks/main.yml | 2 +-
.../tasks/main.yml | 2 +-
.../vyos_config/tests/cli/check_config.yaml | 4 +-
.../vyos_interfaces/tests/cli/deleted.yaml | 9 +-
.../vyos_interfaces/tests/cli/overridden.yaml | 10 +-
.../targets/wait_for/tasks/main.yml | 20 +-
.../module_utils/common/test_collections.py | 15 +-
test/units/parsing/test_ajson.py | 1 +
test/units/parsing/yaml/test_dumper.py | 20 +-
59 files changed, 828 insertions(+), 126 deletions(-)
create mode 100644 changelogs/fragments/70784-vault-is-string.yml
create mode 100644 changelogs/fragments/71609-is_string-vault.yml
create mode 100644 changelogs/fragments/75072_undefined_yaml.yml
create mode 100644 changelogs/fragments/cve-2023-5764.yml
create mode 100644 changelogs/fragments/nativejinjatext-yaml-representer.yml
create mode 100644 changelogs/fragments/nativejinjaunsafetext-yaml-representer.yml
create mode 100644 test/integration/targets/assert/assert.out.nested_tmpl.stderr
create mode 100644 test/integration/targets/assert/assert.out.nested_tmpl.stdout
rename test/integration/targets/assert/{assert_quiet.out.quiet.stderr => assert.out.quiet.stderr} (100%)
rename test/integration/targets/assert/{assert_quiet.out.quiet.stdout => assert.out.quiet.stdout} (100%)
create mode 100644 test/integration/targets/assert/nested_tmpl.yml
create mode 100644 test/integration/targets/debug/unsafe.yml
diff --git a/changelogs/fragments/70784-vault-is-string.yml b/changelogs/fragments/70784-vault-is-string.yml
new file mode 100644
index 0000000000..8dc1164a85
--- /dev/null
+++ b/changelogs/fragments/70784-vault-is-string.yml
@@ -0,0 +1,3 @@
+bugfixes:
+- JSON Encoder - Ensure we treat single vault encrypted values as strings
+ (https://github.com/ansible/ansible/issues/70784)
diff --git a/changelogs/fragments/71609-is_string-vault.yml b/changelogs/fragments/71609-is_string-vault.yml
new file mode 100644
index 0000000000..89ddd91913
--- /dev/null
+++ b/changelogs/fragments/71609-is_string-vault.yml
@@ -0,0 +1,3 @@
+bugfixes:
+- is_string/vault - Ensure the is_string helper properly identifies AnsibleVaultEncryptedUnicode
+ as a string (https://github.com/ansible/ansible/pull/71609)
diff --git a/changelogs/fragments/75072_undefined_yaml.yml b/changelogs/fragments/75072_undefined_yaml.yml
new file mode 100644
index 0000000000..227c24de1b
--- /dev/null
+++ b/changelogs/fragments/75072_undefined_yaml.yml
@@ -0,0 +1,3 @@
+---
+minor_changes:
+- yaml dumper - YAML representer for AnsibleUndefined (https://github.com/ansible/ansible/issues/75072).
diff --git a/changelogs/fragments/cve-2023-5764.yml b/changelogs/fragments/cve-2023-5764.yml
new file mode 100644
index 0000000000..c37127dac1
--- /dev/null
+++ b/changelogs/fragments/cve-2023-5764.yml
@@ -0,0 +1,6 @@
+security_fixes:
+- templating - Address issues where internal templating can cause unsafe
+ variables to lose their unsafe designation (CVE-2023-5764)
+breaking_changes:
+- assert - Nested templating may result in an inability for the conditional
+ to be evaluated. See the porting guide for more information.
diff --git a/changelogs/fragments/nativejinjatext-yaml-representer.yml b/changelogs/fragments/nativejinjatext-yaml-representer.yml
new file mode 100644
index 0000000000..ef2f460a09
--- /dev/null
+++ b/changelogs/fragments/nativejinjatext-yaml-representer.yml
@@ -0,0 +1,2 @@
+bugfixes:
+ - Add a YAML representer for ``NativeJinjaText``
diff --git a/changelogs/fragments/nativejinjaunsafetext-yaml-representer.yml b/changelogs/fragments/nativejinjaunsafetext-yaml-representer.yml
new file mode 100644
index 0000000000..e13486fb30
--- /dev/null
+++ b/changelogs/fragments/nativejinjaunsafetext-yaml-representer.yml
@@ -0,0 +1,2 @@
+bugfixes:
+ - Add a YAML representer for ``NativeJinjaUnsafeText``
diff --git a/lib/ansible/module_utils/common/collections.py b/lib/ansible/module_utils/common/collections.py
index 0a166cd4cf..123ee8354b 100644
--- a/lib/ansible/module_utils/common/collections.py
+++ b/lib/ansible/module_utils/common/collections.py
@@ -58,7 +58,8 @@ class ImmutableDict(Hashable, Mapping):
def is_string(seq):
"""Identify whether the input has a string-like type (inclding bytes)."""
- return isinstance(seq, (text_type, binary_type))
+ # AnsibleVaultEncryptedUnicode inherits from Sequence, but is expected to be a string like object
+ return isinstance(seq, (text_type, binary_type)) or getattr(seq, '__ENCRYPTED__', False)
def is_iterable(seq, include_strings=False):
diff --git a/lib/ansible/module_utils/common/json.py b/lib/ansible/module_utils/common/json.py
index 3018e9e238..41f9b85fb7 100644
--- a/lib/ansible/module_utils/common/json.py
+++ b/lib/ansible/module_utils/common/json.py
@@ -15,14 +15,22 @@ from ansible.module_utils.common._collections_compat import Mapping
from ansible.module_utils.common.collections import is_sequence
+def _is_unsafe(value):
+ return getattr(value, '__UNSAFE__', False) and not getattr(value, '__ENCRYPTED__', False)
+
+
+def _is_vault(value):
+ return getattr(value, '__ENCRYPTED__', False)
+
+
def _preprocess_unsafe_encode(value):
"""Recursively preprocess a data structure converting instances of ``AnsibleUnsafe``
into their JSON dict representations
Used in ``AnsibleJSONEncoder.iterencode``
"""
- if getattr(value, '__UNSAFE__', False) and not getattr(value, '__ENCRYPTED__', False):
- value = {'__ansible_unsafe': to_text(value, errors='surrogate_or_strict', nonstring='strict')}
+ if _is_unsafe(value):
+ 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):
@@ -51,7 +59,7 @@ class AnsibleJSONEncoder(json.JSONEncoder):
value = {'__ansible_vault': to_text(o._ciphertext, errors='surrogate_or_strict', nonstring='strict')}
elif getattr(o, '__UNSAFE__', False):
# unsafe object, this will never be triggered, see ``AnsibleJSONEncoder.iterencode``
- value = {'__ansible_unsafe': to_text(o, errors='surrogate_or_strict', nonstring='strict')}
+ value = {'__ansible_unsafe': to_text(o._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')}
elif isinstance(o, Mapping):
# hostvars and other objects
value = dict(o)
diff --git a/lib/ansible/parsing/yaml/dumper.py b/lib/ansible/parsing/yaml/dumper.py
index 67a2efb36d..ddb1363a9e 100644
--- a/lib/ansible/parsing/yaml/dumper.py
+++ b/lib/ansible/parsing/yaml/dumper.py
@@ -21,9 +21,10 @@ __metaclass__ = type
import yaml
-from ansible.module_utils.six import PY3
+from ansible.module_utils.six import PY3, text_type
from ansible.parsing.yaml.objects import AnsibleUnicode, AnsibleSequence, AnsibleMapping, AnsibleVaultEncryptedUnicode
-from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes
+from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText, _is_unsafe
+from ansible.template import AnsibleUndefined
from ansible.vars.hostvars import HostVars, HostVarsVars
@@ -44,12 +45,30 @@ def represent_vault_encrypted_unicode(self, data):
return self.represent_scalar(u'!vault', data._ciphertext.decode(), style='|')
-if PY3:
- represent_unicode = yaml.representer.SafeRepresenter.represent_str
- represent_binary = yaml.representer.SafeRepresenter.represent_binary
-else:
- represent_unicode = yaml.representer.SafeRepresenter.represent_unicode
- represent_binary = yaml.representer.SafeRepresenter.represent_str
+def represent_unicode(self, data):
+ if _is_unsafe(data):
+ data = data._strip_unsafe()
+ if PY3:
+ return yaml.representer.SafeRepresenter.represent_str(self, text_type(data))
+ else:
+ return yaml.representer.SafeRepresenter.represent_unicode(self, text_type(data))
+
+
+def represent_binary(self, data):
+ if _is_unsafe(data):
+ data = data._strip_unsafe()
+ if PY3:
+ return yaml.representer.SafeRepresenter.represent_binary(self, binary_type(data))
+ else:
+ return yaml.representer.SafeRepresenter.represent_str(self, binary_type(data))
+
+
+def represent_undefined(self, data):
+ # Here bool will ensure _fail_with_undefined_error happens
+ # if the value is Undefined.
+ # This happens because Jinja sets __bool__ on StrictUndefined
+ return bool(data)
+
AnsibleDumper.add_representer(
AnsibleUnicode,
@@ -90,3 +109,18 @@ AnsibleDumper.add_representer(
AnsibleVaultEncryptedUnicode,
represent_vault_encrypted_unicode,
)
+
+AnsibleDumper.add_representer(
+ AnsibleUndefined,
+ represent_undefined,
+)
+
+AnsibleDumper.add_representer(
+ NativeJinjaUnsafeText,
+ represent_unicode,
+)
+
+AnsibleDumper.add_representer(
+ NativeJinjaText,
+ represent_unicode,
+)
diff --git a/lib/ansible/playbook/conditional.py b/lib/ansible/playbook/conditional.py
index 2fadf77487..ac4fc0c568 100644
--- a/lib/ansible/playbook/conditional.py
+++ b/lib/ansible/playbook/conditional.py
@@ -26,7 +26,7 @@ from jinja2.compiler import generate
from jinja2.exceptions import UndefinedError
from ansible import constants as C
-from ansible.errors import AnsibleError, AnsibleUndefinedVariable
+from ansible.errors import AnsibleError, AnsibleUndefinedVariable, AnsibleTemplateError
from ansible.module_utils.six import text_type
from ansible.module_utils._text import to_native
from ansible.playbook.attribute import FieldAttribute
@@ -138,9 +138,10 @@ class Conditional:
if not isinstance(conditional, text_type) or conditional == "":
return conditional
- # update the lookups flag, as the string returned above may now be unsafe
- # and we don't want future templating calls to do unsafe things
- disable_lookups |= hasattr(conditional, '__UNSAFE__')
+ # If the result of the first-pass template render (to resolve inline templates) is marked unsafe,
+ # explicitly fail since the next templating operation would never evaluate
+ if hasattr(conditional, '__UNSAFE__'):
+ raise AnsibleTemplateError('Conditional is marked as unsafe, and cannot be evaluated.')
# First, we do some low-level jinja2 parsing involving the AST format of the
# statement to ensure we don't do anything unsafe (using the disable_lookup flag above)
diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py
index 7fd480c895..e24d27ba8a 100644
--- a/lib/ansible/playbook/task.py
+++ b/lib/ansible/playbook/task.py
@@ -298,6 +298,30 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
super(Task, self).post_validate(templar)
+ def _post_validate_args(self, attr, value, templar):
+ # smuggle an untemplated copy of the task args for actions that need more control over the templating of their
+ # input (eg, debug's var/msg, assert's "that" conditional expressions)
+ self.untemplated_args = value
+
+ # now recursively template the args dict
+ args = templar.template(value)
+
+ # FIXME: could we just nuke this entirely and/or wrap it up in ModuleArgsParser or something?
+ if '_variable_params' in args:
+ variable_params = args.pop('_variable_params')
+ if isinstance(variable_params, dict):
+ if C.INJECT_FACTS_AS_VARS:
+ display.warning("Using a variable for a task's 'args' is unsafe in some situations "
+ "(see https://docs.ansible.com/ansible/devel/reference_appendices/faq.html#argsplat-unsafe)")
+ variable_params.update(args)
+ args = variable_params
+ else:
+ # if we didn't get a dict, it means there's garbage remaining after k=v parsing, just give up
+ # see https://github.com/ansible/ansible/issues/79862
+ raise AnsibleError(f"invalid or malformed argument: '{variable_params}'")
+
+ return args
+
def _post_validate_loop(self, attr, value, templar):
'''
Override post validation for the loop field, which is templated
diff --git a/lib/ansible/plugins/action/assert.py b/lib/ansible/plugins/action/assert.py
index 7721a6b47c..e8ab6a9a4f 100644
--- a/lib/ansible/plugins/action/assert.py
+++ b/lib/ansible/plugins/action/assert.py
@@ -63,8 +63,29 @@ class ActionModule(ActionBase):
quiet = boolean(self._task.args.get('quiet', False), strict=False)
+ # directly access 'that' via untemplated args from the task so we can intelligently trust embedded
+ # templates and preserve the original inputs/locations for better messaging on assert failures and
+ # errors.
+ # FIXME: even in devel, things like `that: item` don't always work properly (truthy string value
+ # is not really an embedded expression)
+ # we could fix that by doing direct var lookups on the inputs
+ # FIXME: some form of this code should probably be shared between debug, assert, and
+ # Task.post_validate, since they
+ # have a lot of overlapping needs
+ try:
+ thats = self._task.untemplated_args['that']
+ except KeyError:
+ # in the case of "we got our entire args dict from a template", we can just consult the
+ # post-templated dict (the damage has likely already been done for embedded templates anyway)
+ thats = self._task.args['that']
+
+ # FIXME: this is a case where we only want to resolve indirections, NOT recurse containers
+ # (and even then, the leaf-most expression being wrapped is at least suboptimal
+ # (since its expression will be "eaten").
+ if isinstance(thats, str):
+ thats = self._templar.template(thats)
+
# make sure the 'that' items are a list
- thats = self._task.args['that']
if not isinstance(thats, list):
thats = [thats]
diff --git a/lib/ansible/plugins/callback/__init__.py b/lib/ansible/plugins/callback/__init__.py
index 71287f8b5e..08b3b4cb29 100644
--- a/lib/ansible/plugins/callback/__init__.py
+++ b/lib/ansible/plugins/callback/__init__.py
@@ -22,6 +22,7 @@ __metaclass__ = type
import difflib
import json
import os
+import re
import sys
import warnings
@@ -29,12 +30,15 @@ from copy import deepcopy
from ansible import constants as C
from ansible.module_utils.common._collections_compat import MutableMapping
-from ansible.module_utils.six import PY3
+from ansible.module_utils.six import PY3, text_type
from ansible.module_utils._text import to_text
from ansible.parsing.ajson import AnsibleJSONEncoder
+from ansible.parsing.yaml.dumper import AnsibleDumper
+from ansible.parsing.yaml.objects import AnsibleUnicode
from ansible.plugins import AnsiblePlugin, get_plugin_class
from ansible.utils.color import stringc
from ansible.utils.display import Display
+from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText, _is_unsafe
from ansible.vars.clean import strip_internal_keys, module_response_deepcopy
if PY3:
@@ -51,6 +55,90 @@ __all__ = ["CallbackBase"]
_DEBUG_ALLOWED_KEYS = frozenset(('msg', 'exception', 'warnings', 'deprecations'))
+_YAML_TEXT_TYPES = (text_type, AnsibleUnicode, AnsibleUnsafeText, NativeJinjaUnsafeText)
+# Characters that libyaml/pyyaml consider breaks
+_YAML_BREAK_CHARS = '\n\x85\u2028\u2029' # NL, NEL, LS, PS
+# regex representation of libyaml/pyyaml of a space followed by a break character
+_SPACE_BREAK_RE = re.compile(fr' +([{_YAML_BREAK_CHARS}])')
+
+
+class _AnsibleCallbackDumper(AnsibleDumper):
+ def __init__(self, lossy=False):
+ self._lossy = lossy
+
+ def __call__(self, *args, **kwargs):
+ # pyyaml expects that we are passing an object that can be instantiated, but to
+ # smuggle the ``lossy`` configuration, we do that in ``__init__`` and then
+ # define this ``__call__`` that will mimic the ability for pyyaml to instantiate class
+ super().__init__(*args, **kwargs)
+ return self
+
+
+def _should_use_block(scalar):
+ """Returns true if string should be in block format based on the existence of various newline separators"""
+ # This method of searching is faster than using a regex
+ for ch in _YAML_BREAK_CHARS:
+ if ch in scalar:
+ return True
+ return False
+
+
+class _SpecialCharacterTranslator:
+ def __getitem__(self, ch):
+ # "special character" logic from pyyaml yaml.emitter.Emitter.analyze_scalar, translated to decimal
+ # for perf w/ str.translate
+ if (ch == 10 or
+ 32 <= ch <= 126 or
+ ch == 133 or
+ 160 <= ch <= 55295 or
+ 57344 <= ch <= 65533 or
+ 65536 <= ch < 1114111)\
+ and ch != 65279:
+ return ch
+ return None
+
+
+def _filter_yaml_special(scalar):
+ """Filter a string removing any character that libyaml/pyyaml declare as special"""
+ return scalar.translate(_SpecialCharacterTranslator())
+
+
+def _munge_data_for_lossy_yaml(scalar):
+ """Modify a string so that analyze_scalar in libyaml/pyyaml will allow block formatting"""
+ # we care more about readability than accuracy, so...
+ # ...libyaml/pyyaml does not permit trailing spaces for block scalars
+ scalar = scalar.rstrip()
+ # ...libyaml/pyyaml does not permit tabs for block scalars
+ scalar = scalar.expandtabs()
+ # ...libyaml/pyyaml only permits special characters for double quoted scalars
+ scalar = _filter_yaml_special(scalar)
+ # ...libyaml/pyyaml only permits spaces followed by breaks for double quoted scalars
+ return _SPACE_BREAK_RE.sub(r'\1', 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 = '|'
+ if self._lossy:
+ data = _munge_data_for_lossy_yaml(data)
+ else:
+ style = self.default_style
+
+ node = yaml.representer.ScalarNode('tag:yaml.org,2002:str', data, style=style)
+ if self.alias_key is not None:
+ self.represented_objects[self.alias_key] = node
+ return node
+
+
+for data_type in _YAML_TEXT_TYPES:
+ _AnsibleCallbackDumper.add_representer(
+ data_type,
+ _pretty_represent_str
+ )
class CallbackBase(AnsiblePlugin):
diff --git a/lib/ansible/plugins/filter/core.py b/lib/ansible/plugins/filter/core.py
index 1fc6790a4d..d39d8426bc 100644
--- a/lib/ansible/plugins/filter/core.py
+++ b/lib/ansible/plugins/filter/core.py
@@ -53,6 +53,7 @@ from ansible.utils.display import Display
from ansible.utils.encrypt import passlib_or_crypt
from ansible.utils.hashing import md5s, checksum_s
from ansible.utils.unicode import unicode_wrap
+from ansible.utils.unsafe_proxy import _is_unsafe
from ansible.utils.vars import merge_hash
display = Display()
@@ -63,13 +64,19 @@ UUID_NAMESPACE_ANSIBLE = uuid.UUID('361E6D51-FAEC-444A-9079-341386DA8E2E')
def to_yaml(a, *args, **kw):
'''Make verbose, human readable yaml'''
default_flow_style = kw.pop('default_flow_style', None)
- transformed = yaml.dump(a, Dumper=AnsibleDumper, allow_unicode=True, default_flow_style=default_flow_style, **kw)
+ try:
+ transformed = yaml.dump(a, Dumper=AnsibleDumper, allow_unicode=True, default_flow_style=default_flow_style, **kw)
+ except Exception as e:
+ raise AnsibleFilterError("to_yaml - %s" % to_native(e), orig_exc=e)
return to_text(transformed)
def to_nice_yaml(a, indent=4, *args, **kw):
'''Make verbose, human readable yaml'''
- transformed = yaml.dump(a, Dumper=AnsibleDumper, indent=indent, allow_unicode=True, default_flow_style=False, **kw)
+ try:
+ transformed = yaml.dump(a, Dumper=AnsibleDumper, indent=indent, allow_unicode=True, default_flow_style=False, **kw)
+ except Exception as e:
+ raise AnsibleFilterError("to_nice_yaml - %s" % to_native(e), orig_exc=e)
return to_text(transformed)
@@ -207,13 +214,23 @@ def regex_escape(string, re_type='python'):
def from_yaml(data):
if isinstance(data, string_types):
- return yaml.safe_load(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
def from_yaml_all(data):
if isinstance(data, string_types):
- return yaml.safe_load_all(data)
+ # The ``text_type`` call here strips any custom
+ # string wrapper class, so that CSafeLoader can
+ # read the data
+ if _is_unsafe(data):
+ data = data._strip_unsafe()
+ return yaml_load_all(text_type(to_text(data, errors='surrogate_or_strict')))
return data
diff --git a/lib/ansible/plugins/lookup/first_found.py b/lib/ansible/plugins/lookup/first_found.py
index a1828e6b9c..39a2f1aac3 100644
--- a/lib/ansible/plugins/lookup/first_found.py
+++ b/lib/ansible/plugins/lookup/first_found.py
@@ -102,6 +102,8 @@ RETURN = """
"""
import os
+from collections.abc import Mapping, Sequence
+
from jinja2.exceptions import UndefinedError
from ansible.errors import AnsibleFileNotFound, AnsibleLookupError, AnsibleUndefinedVariable
@@ -110,6 +112,29 @@ from ansible.module_utils.parsing.convert_bool import boolean
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 = list(_splitter(terms, spliters))
+ else:
+ # added since options will already listify
+ for t in terms:
+ termlist.extend(_split_on(t, spliters))
+ return termlist
+
+
class LookupModule(LookupBase):
def run(self, terms, variables, **kwargs):
diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py
index a20b1bae68..94ab31e58d 100644
--- a/lib/ansible/template/__init__.py
+++ b/lib/ansible/template/__init__.py
@@ -35,7 +35,7 @@ try:
except ImportError:
from sha import sha as sha1
-from jinja2.exceptions import TemplateSyntaxError, UndefinedError
+from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError
from jinja2.loaders import FileSystemLoader
from jinja2.runtime import Context, StrictUndefined
@@ -53,6 +53,9 @@ from ansible.template.vars import AnsibleJ2Vars
from ansible.utils.collection_loader import AnsibleCollectionRef
from ansible.utils.display import Display
from ansible.utils.unsafe_proxy import wrap_var
+from ansible.utils.listify import listify_lookup_plugin_terms
+from ansible.utils.native_jinja import NativeJinjaText
+from ansible.utils.unsafe_proxy import wrap_var, AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText
display = Display()
@@ -259,10 +262,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
@@ -874,8 +888,11 @@ class Templar:
rf = t.root_render_func(new_context)
try:
- res = j2_concat(rf)
- unsafe = getattr(new_context, 'unsafe', False)
+ if self.jinja2_native:
+ res = ansible_native_concat(rf)
+ else:
+ res = j2_concat(rf)
+ unsafe = getattr(self.cur_context, 'unsafe', False)
if unsafe:
res = wrap_var(res)
except TypeError as te:
diff --git a/lib/ansible/utils/unsafe_proxy.py b/lib/ansible/utils/unsafe_proxy.py
index ffb5f37503..0bfd6340ff 100644
--- a/lib/ansible/utils/unsafe_proxy.py
+++ b/lib/ansible/utils/unsafe_proxy.py
@@ -67,10 +67,267 @@ class AnsibleUnsafe(object):
class AnsibleUnsafeBytes(binary_type, AnsibleUnsafe):
- pass
+ 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 __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):
pass
@@ -133,3 +390,7 @@ def to_unsafe_bytes(*args, **kwargs):
def to_unsafe_text(*args, **kwargs):
return wrap_var(to_text(*args, **kwargs))
+
+
+def _is_unsafe(obj):
+ return getattr(obj, '__UNSAFE__', False) is True
diff --git a/test/integration/targets/apt_repository/tasks/apt.yml b/test/integration/targets/apt_repository/tasks/apt.yml
index 941335f2c6..8d0f4ad896 100644
--- a/test/integration/targets/apt_repository/tasks/apt.yml
+++ b/test/integration/targets/apt_repository/tasks/apt.yml
@@ -50,7 +50,7 @@
that:
- 'result.changed'
- 'result.state == "present"'
- - 'result.repo == "{{test_ppa_name}}"'
+ - 'result.repo == test_ppa_name'
- name: 'examine apt cache mtime'
stat: path='/var/cache/apt/pkgcache.bin'
@@ -81,7 +81,7 @@
that:
- 'result.changed'
- 'result.state == "present"'
- - 'result.repo == "{{test_ppa_name}}"'
+ - 'result.repo == test_ppa_name'
- name: 'examine apt cache mtime'
stat: path='/var/cache/apt/pkgcache.bin'
@@ -112,7 +112,7 @@
that:
- 'result.changed'
- 'result.state == "present"'
- - 'result.repo == "{{test_ppa_name}}"'
+ - 'result.repo == test_ppa_name'
- name: 'examine apt cache mtime'
stat: path='/var/cache/apt/pkgcache.bin'
@@ -146,7 +146,8 @@
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'
stat: path='/var/cache/apt/pkgcache.bin'
@@ -181,7 +182,7 @@
that:
- 'result.changed'
- 'result.state == "present"'
- - 'result.repo == "{{test_ppa_spec}}"'
+ - 'result.repo == test_ppa_spec'
- name: 'examine source file'
stat: path='/etc/apt/sources.list.d/{{test_ppa_filename}}.list'
diff --git a/test/integration/targets/assert/assert.out.nested_tmpl.stderr b/test/integration/targets/assert/assert.out.nested_tmpl.stderr
new file mode 100644
index 0000000000..ea208a41c7
--- /dev/null
+++ b/test/integration/targets/assert/assert.out.nested_tmpl.stderr
@@ -0,0 +1,4 @@
++ ansible-playbook -i localhost, -c local nested_tmpl.yml
+++ set +x
+[WARNING]: conditional statements should not include jinja2 templating
+delimiters such as {{ }} or {% %}. Found: "{{ foo }}" == "bar"
diff --git a/test/integration/targets/assert/assert.out.nested_tmpl.stdout b/test/integration/targets/assert/assert.out.nested_tmpl.stdout
new file mode 100644
index 0000000000..8ca3fb76d4
--- /dev/null
+++ b/test/integration/targets/assert/assert.out.nested_tmpl.stdout
@@ -0,0 +1,12 @@
+
+PLAY [localhost] ***************************************************************
+
+TASK [assert] ******************************************************************
+ok: [localhost] => {
+ "changed": false,
+ "msg": "All assertions passed"
+}
+
+PLAY RECAP *********************************************************************
+localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+
diff --git a/test/integration/targets/assert/assert_quiet.out.quiet.stderr b/test/integration/targets/assert/assert.out.quiet.stderr
similarity index 100%
rename from test/integration/targets/assert/assert_quiet.out.quiet.stderr
rename to test/integration/targets/assert/assert.out.quiet.stderr
diff --git a/test/integration/targets/assert/assert_quiet.out.quiet.stdout b/test/integration/targets/assert/assert.out.quiet.stdout
similarity index 100%
rename from test/integration/targets/assert/assert_quiet.out.quiet.stdout
rename to test/integration/targets/assert/assert.out.quiet.stdout
diff --git a/test/integration/targets/assert/nested_tmpl.yml b/test/integration/targets/assert/nested_tmpl.yml
new file mode 100644
index 0000000000..3da4b1d80e
--- /dev/null
+++ b/test/integration/targets/assert/nested_tmpl.yml
@@ -0,0 +1,9 @@
+- hosts: localhost
+ gather_facts: False
+ tasks:
+ - assert:
+ that:
+ - '"{{ foo }}" == "bar"'
+ - foo == "bar"
+ vars:
+ foo: bar
diff --git a/test/integration/targets/assert/quiet.yml b/test/integration/targets/assert/quiet.yml
index 6834712c2c..1c425cb5ba 100644
--- a/test/integration/targets/assert/quiet.yml
+++ b/test/integration/targets/assert/quiet.yml
@@ -5,12 +5,12 @@
item_A: yes
tasks:
- assert:
- that: "{{ item }} is defined"
+ that: "item is defined"
quiet: True
with_items:
- item_A
- assert:
- that: "{{ item }} is defined"
+ that: "item is defined"
quiet: False
with_items:
- item_A
diff --git a/test/integration/targets/assert/runme.sh b/test/integration/targets/assert/runme.sh
index ca0a858726..b79072813d 100755
--- a/test/integration/targets/assert/runme.sh
+++ b/test/integration/targets/assert/runme.sh
@@ -45,7 +45,7 @@ cleanup() {
fi
}
-BASEFILE=assert_quiet.out
+BASEFILE=assert.out
ORIGFILE="${BASEFILE}"
OUTFILE="${BASEFILE}.new"
@@ -69,3 +69,4 @@ export ANSIBLE_NOCOLOR=1
export ANSIBLE_RETRY_FILES_ENABLED=0
run_test quiet
+run_test nested_tmpl
diff --git a/test/integration/targets/command_shell/tasks/main.yml b/test/integration/targets/command_shell/tasks/main.yml
index 9dc6c9beb6..d685443b55 100644
--- a/test/integration/targets/command_shell/tasks/main.yml
+++ b/test/integration/targets/command_shell/tasks/main.yml
@@ -286,7 +286,7 @@
assert:
that:
- shell_result0 is changed
- - shell_result0.cmd == '{{ output_dir_test }}/test.sh'
+ - shell_result0.cmd == output_dir_test ~ '/test.sh'
- shell_result0.rc == 0
- shell_result0.stderr == ''
- shell_result0.stdout == 'win'
diff --git a/test/integration/targets/copy/tasks/tests.yml b/test/integration/targets/copy/tasks/tests.yml
index 900f86a66a..2a150c4217 100644
--- a/test/integration/targets/copy/tasks/tests.yml
+++ b/test/integration/targets/copy/tasks/tests.yml
@@ -1176,7 +1176,7 @@
assert:
that:
- "copy_result6.changed"
- - "copy_result6.dest == '{{remote_dir_expanded}}/multiline.txt'"
+ - "copy_result6.dest == remote_dir_expanded ~ '/multiline.txt'"
- "copy_result6.checksum == '9cd0697c6a9ff6689f0afb9136fa62e0b3fee903'"
# test overwriting a file as an unprivileged user (pull request #8624)
@@ -2060,26 +2060,26 @@
assert:
that:
- testcase5 is changed
- - "stat_new_dir_with_chown.stat.uid == {{ ansible_copy_test_user.uid }}"
- - "stat_new_dir_with_chown.stat.gid == {{ ansible_copy_test_group.gid }}"
- - "stat_new_dir_with_chown.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
- - "stat_new_dir_with_chown.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
- - "stat_new_dir_with_chown_file1.stat.uid == {{ ansible_copy_test_user.uid }}"
- - "stat_new_dir_with_chown_file1.stat.gid == {{ ansible_copy_test_group.gid }}"
- - "stat_new_dir_with_chown_file1.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
- - "stat_new_dir_with_chown_file1.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
- - "stat_new_dir_with_chown_subdir.stat.uid == {{ ansible_copy_test_user.uid }}"
- - "stat_new_dir_with_chown_subdir.stat.gid == {{ ansible_copy_test_group.gid }}"
- - "stat_new_dir_with_chown_subdir.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
- - "stat_new_dir_with_chown_subdir.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
- - "stat_new_dir_with_chown_subdir_file12.stat.uid == {{ ansible_copy_test_user.uid }}"
- - "stat_new_dir_with_chown_subdir_file12.stat.gid == {{ ansible_copy_test_group.gid }}"
- - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
- - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
- - "stat_new_dir_with_chown_link_file12.stat.uid == {{ ansible_copy_test_user.uid }}"
- - "stat_new_dir_with_chown_link_file12.stat.gid == {{ ansible_copy_test_group.gid }}"
- - "stat_new_dir_with_chown_link_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
- - "stat_new_dir_with_chown_link_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
+ - "stat_new_dir_with_chown.stat.uid == ansible_copy_test_user.uid"
+ - "stat_new_dir_with_chown.stat.gid == ansible_copy_test_group.gid"
+ - "stat_new_dir_with_chown.stat.pw_name == ansible_copy_test_user_name"
+ - "stat_new_dir_with_chown.stat.gr_name == ansible_copy_test_user_name"
+ - "stat_new_dir_with_chown_file1.stat.uid == ansible_copy_test_user.uid"
+ - "stat_new_dir_with_chown_file1.stat.gid == ansible_copy_test_group.gid"
+ - "stat_new_dir_with_chown_file1.stat.pw_name == ansible_copy_test_user_name"
+ - "stat_new_dir_with_chown_file1.stat.gr_name == ansible_copy_test_user_name"
+ - "stat_new_dir_with_chown_subdir.stat.uid == ansible_copy_test_user.uid"
+ - "stat_new_dir_with_chown_subdir.stat.gid == ansible_copy_test_group.gid"
+ - "stat_new_dir_with_chown_subdir.stat.pw_name == ansible_copy_test_user_name"
+ - "stat_new_dir_with_chown_subdir.stat.gr_name == ansible_copy_test_user_name"
+ - "stat_new_dir_with_chown_subdir_file12.stat.uid == ansible_copy_test_user.uid"
+ - "stat_new_dir_with_chown_subdir_file12.stat.gid == ansible_copy_test_group.gid"
+ - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == ansible_copy_test_user_name"
+ - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == ansible_copy_test_user_name"
+ - "stat_new_dir_with_chown_link_file12.stat.uid == ansible_copy_test_user.uid"
+ - "stat_new_dir_with_chown_link_file12.stat.gid == ansible_copy_test_group.gid"
+ - "stat_new_dir_with_chown_link_file12.stat.pw_name == ansible_copy_test_user_name"
+ - "stat_new_dir_with_chown_link_file12.stat.gr_name == ansible_copy_test_user_name"
always:
- name: execute - remove the user for test
diff --git a/test/integration/targets/debug/runme.sh b/test/integration/targets/debug/runme.sh
index 5ccb1bfda6..05357c2a37 100755
--- a/test/integration/targets/debug/runme.sh
+++ b/test/integration/targets/debug/runme.sh
@@ -15,3 +15,5 @@ for i in 1 2 3; do
grep "ok: \[localhost\] => (item=$i)" out
grep "\"item\": $i" out
done
+
+ansible-playbook unsafe.yml "$@"
diff --git a/test/integration/targets/debug/unsafe.yml b/test/integration/targets/debug/unsafe.yml
new file mode 100644
index 0000000000..6a78af1a69
--- /dev/null
+++ b/test/integration/targets/debug/unsafe.yml
@@ -0,0 +1,13 @@
+- hosts: localhost
+ gather_facts: false
+ vars:
+ unsafe_var: !unsafe undef()|mandatory
+ tasks:
+ - debug:
+ var: '{{ unsafe_var }}'
+ ignore_errors: true
+ register: result
+
+ - assert:
+ that:
+ - result is successful
diff --git a/test/integration/targets/expect/tasks/main.yml b/test/integration/targets/expect/tasks/main.yml
index 0c408d282d..b46474367f 100644
--- a/test/integration/targets/expect/tasks/main.yml
+++ b/test/integration/targets/expect/tasks/main.yml
@@ -109,10 +109,15 @@
foo: bar
register: chdir_result
+- name: get output_dir real path
+ raw: >
+ {{ ansible_python_interpreter }} -c 'import os; os.chdir("{{output_dir}}"); print(os.getcwd())'
+ register: output_dir_real_path
+
- name: assert chdir works
assert:
that:
- - "'{{chdir_result.stdout |expanduser | realpath }}' == '{{output_dir | expanduser | realpath}}'"
+ - "chdir_result.stdout | trim == output_dir_real_path.stdout | trim"
- name: test timeout option
expect:
diff --git a/test/integration/targets/file/tasks/state_link.yml b/test/integration/targets/file/tasks/state_link.yml
index d9ac96740c..9e13eccbd1 100644
--- a/test/integration/targets/file/tasks/state_link.yml
+++ b/test/integration/targets/file/tasks/state_link.yml
@@ -196,7 +196,7 @@
- "missing_dst_no_follow_enable_force_use_mode2 is changed"
- "missing_dst_no_follow_enable_force_use_mode3 is not changed"
- "soft3_result['stat'].islnk"
- - "soft3_result['stat'].lnk_target == '{{ user.home }}/nonexistent'"
+ - "soft3_result['stat'].lnk_target == user.home ~ '/nonexistent'"
#
# Test creating a link to a directory https://github.com/ansible/ansible/issues/1369
diff --git a/test/integration/targets/find/tasks/main.yml b/test/integration/targets/find/tasks/main.yml
index 028d3cb45c..f8ea1a45f6 100644
--- a/test/integration/targets/find/tasks/main.yml
+++ b/test/integration/targets/find/tasks/main.yml
@@ -116,4 +116,90 @@
- name: assert we skipped the ogg file
assert:
that:
- - '"{{ output_dir_test }}/e/f/g/h/8.ogg" not in find_test3_list'
\ No newline at end of file
+ - 'output_dir_test ~ "/e/f/g/h/8.ogg" not in find_test3_list'
+
+- name: patterns with regex
+ find:
+ paths: "{{ output_dir_test }}"
+ recurse: yes
+ use_regex: true
+ patterns: .*\.ogg
+ register: find_test4
+
+- name: assert we matched the ogg file
+ assert:
+ that:
+ - output_dir_test ~ "/e/f/g/h/8.ogg" in find_test4.files|map(attribute="path")
+
+- name: create our age/size testing sub-directory
+ file:
+ path: "{{ output_dir_test }}/astest"
+ state: directory
+
+- name: create test file with old timestamps
+ file:
+ path: "{{ output_dir_test }}/astest/old.txt"
+ state: touch
+ modification_time: "202001011200.0"
+
+- name: create test file with current timestamps
+ file:
+ path: "{{ output_dir_test }}/astest/new.txt"
+ state: touch
+
+- name: create hidden test file with current timestamps
+ file:
+ path: "{{ output_dir_test }}/astest/.hidden.txt"
+ state: touch
+
+- name: find files older than 1 week
+ find:
+ path: "{{ output_dir_test }}/astest"
+ age: 1w
+ hidden: true
+ register: result
+
+- set_fact:
+ astest_list: "{{ result.files|map(attribute='path') }}"
+
+- name: assert we only find the old file
+ assert:
+ that:
+ - result.matched == 1
+ - 'output_dir_test ~ "/astest/old.txt" in astest_list'
+
+- name: find files newer than 1 week
+ find:
+ path: "{{ output_dir_test }}/astest"
+ age: -1w
+ register: result
+
+- set_fact:
+ astest_list: "{{ result.files|map(attribute='path') }}"
+
+- name: assert we only find the current file
+ assert:
+ that:
+ - result.matched == 1
+ - 'output_dir_test ~ "/astest/new.txt" in astest_list'
+
+- name: add some content to the new file
+ shell: "echo hello world > {{ output_dir_test }}/astest/new.txt"
+
+- name: find files with MORE than 5 bytes, also get checksums
+ find:
+ path: "{{ output_dir_test }}/astest"
+ size: 5
+ hidden: true
+ get_checksum: true
+ register: result
+
+- set_fact:
+ astest_list: "{{ result.files|map(attribute='path') }}"
+
+- name: assert we only find the hello world file
+ assert:
+ that:
+ - result.matched == 1
+ - 'output_dir_test ~ "/astest/new.txt" in astest_list'
+ - '"checksum" in result.files[0]'
diff --git a/test/integration/targets/gathering_facts/test_gathering_facts.yml b/test/integration/targets/gathering_facts/test_gathering_facts.yml
index 5924a15649..c32bb3c8b8 100644
--- a/test/integration/targets/gathering_facts/test_gathering_facts.yml
+++ b/test/integration/targets/gathering_facts/test_gathering_facts.yml
@@ -371,7 +371,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']
@@ -382,7 +382,7 @@
- name: Test reading facts from custom fact_path
assert:
that:
- - '"{{ ansible_local.testfact.fact_dir }}" == "custom"'
+ - 'ansible_local.testfact.fact_dir == "custom"'
- hosts: facthost20
tags: [ 'fact_facter_ohai' ]
diff --git a/test/integration/targets/git/tasks/depth.yml b/test/integration/targets/git/tasks/depth.yml
index 547f84f7b5..e0585ca39b 100644
--- a/test/integration/targets/git/tasks/depth.yml
+++ b/test/integration/targets/git/tasks/depth.yml
@@ -169,7 +169,7 @@
- name: DEPTH | check update arrived
assert:
that:
- - "{{ a_file.content | b64decode | trim }} == 3"
+ - a_file.content | b64decode | trim == "3"
- git_fetch is changed
- name: DEPTH | clear checkout_dir
diff --git a/test/integration/targets/git/tasks/localmods.yml b/test/integration/targets/git/tasks/localmods.yml
index 09a1326d58..0e0cf684ed 100644
--- a/test/integration/targets/git/tasks/localmods.yml
+++ b/test/integration/targets/git/tasks/localmods.yml
@@ -47,7 +47,7 @@
- name: LOCALMODS | check update arrived
assert:
that:
- - "{{ a_file.content | b64decode | trim }} == 2"
+ - a_file.content | b64decode | trim == "2"
- git_fetch_force is changed
- name: LOCALMODS | clear checkout_dir
@@ -105,7 +105,7 @@
- name: LOCALMODS | check update arrived
assert:
that:
- - "{{ a_file.content | b64decode | trim }} == 2"
+ - a_file.content | b64decode | trim == "2"
- git_fetch_force is changed
- name: LOCALMODS | clear checkout_dir
diff --git a/test/integration/targets/git/tasks/submodules.yml b/test/integration/targets/git/tasks/submodules.yml
index 647d1e23b4..1ba84afbde 100644
--- a/test/integration/targets/git/tasks/submodules.yml
+++ b/test/integration/targets/git/tasks/submodules.yml
@@ -32,7 +32,7 @@
- name: SUBMODULES | Ensure submodu1 is at the appropriate commit
assert:
- that: '{{ submodule1.stdout_lines | length }} == 2'
+ that: 'submodule1.stdout_lines | length == 2'
- name: SUBMODULES | clear checkout_dir
file:
@@ -53,7 +53,7 @@
- name: SUBMODULES | Ensure submodule1 is at the appropriate commit
assert:
- that: '{{ submodule1.stdout_lines | length }} == 4'
+ that: 'submodule1.stdout_lines | length == 4'
- name: SUBMODULES | Copy the checkout so we can run several different tests on it
command: 'cp -pr {{ checkout_dir }} {{ checkout_dir }}.bak'
@@ -84,8 +84,8 @@
- name: SUBMODULES | Ensure both submodules are at the appropriate commit
assert:
that:
- - '{{ submodule1.stdout_lines|length }} == 4'
- - '{{ submodule2.stdout_lines|length }} == 2'
+ - 'submodule1.stdout_lines|length == 4'
+ - 'submodule2.stdout_lines|length == 2'
- name: SUBMODULES | Remove checkout dir
@@ -112,7 +112,7 @@
- name: SUBMODULES | Ensure submodule1 is at the appropriate commit
assert:
- that: '{{ submodule1.stdout_lines | length }} == 5'
+ that: 'submodule1.stdout_lines | length == 5'
- name: SUBMODULES | Test that update with recursive found new submodules
@@ -121,4 +121,17 @@
- 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:
+ state: absent
+ path: "{{ checkout_dir }}"
+
+
+- name: SUBMODULES | Clone main submodule repository
+ git:
+ repo: "{{ repo_submodules }}"
+ dest: "{{ checkout_dir }}/test.gitdir"
+ version: 45c6c07ef10fd9e453d90207e63da1ce5bd3ae1e
+ recursive: yes
diff --git a/test/integration/targets/include_vars/tasks/main.yml b/test/integration/targets/include_vars/tasks/main.yml
index 799d7b26a6..3c5816b0d4 100644
--- a/test/integration/targets/include_vars/tasks/main.yml
+++ b/test/integration/targets/include_vars/tasks/main.yml
@@ -15,7 +15,7 @@
that:
- "testing == 789"
- "base_dir == 'environments/development'"
- - "{{ included_one_file.ansible_included_var_files | length }} == 1"
+ - "included_one_file.ansible_included_var_files | length == 1"
- "'vars/environments/development/all.yml' in included_one_file.ansible_included_var_files[0]"
- name: include the vars/environments/development/all.yml and save results in all
@@ -51,7 +51,7 @@
assert:
that:
- webapp_version is defined
- - "'file_without_extension' in '{{ include_without_file_extension.ansible_included_var_files | join(' ') }}'"
+ - "'file_without_extension' in include_without_file_extension.ansible_included_var_files | join(' ')"
- name: include every directory in vars
include_vars:
@@ -65,7 +65,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]"
@@ -85,9 +85,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:
@@ -101,9 +101,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:
@@ -162,3 +162,7 @@
that:
- "'my_custom_service' == service_name_fqcn"
- "'my_custom_service' == service_name_tmpl_fqcn"
+
+- assert:
+ that:
+ - baz.ansible_facts.foo|type_debug != "AnsibleUnsafeText"
diff --git a/test/integration/targets/iosxr_lacp_interfaces/tests/cli/merged.yaml b/test/integration/targets/iosxr_lacp_interfaces/tests/cli/merged.yaml
index be5134091b..a2947d2959 100644
--- a/test/integration/targets/iosxr_lacp_interfaces/tests/cli/merged.yaml
+++ b/test/integration/targets/iosxr_lacp_interfaces/tests/cli/merged.yaml
@@ -24,17 +24,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)
iosxr_lacp_interfaces: *merged
@@ -49,6 +49,7 @@
- name: Assert that before dicts were correctly generated
assert:
that:
- - "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
+ - "merged['after'] | symmetric_difference(result['before']) |length == 0"
+
always:
- include_tasks: _remove_config.yaml
diff --git a/test/integration/targets/iosxr_lacp_interfaces/tests/cli/replaced.yaml b/test/integration/targets/iosxr_lacp_interfaces/tests/cli/replaced.yaml
index 0dcb8505e0..ccf9d803b2 100644
--- a/test/integration/targets/iosxr_lacp_interfaces/tests/cli/replaced.yaml
+++ b/test/integration/targets/iosxr_lacp_interfaces/tests/cli/replaced.yaml
@@ -21,17 +21,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 interfaces with provided configurarions (IDEMPOTENT)
iosxr_lacp_interfaces: *replaced
@@ -46,7 +46,7 @@
- name: Assert that before dict is correctly generated
assert:
that:
- - "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}"
-
+ - "replaced['after'] | symmetric_difference(result['before']) |length == 0"
+
always:
- include_tasks: _remove_config.yaml
diff --git a/test/integration/targets/lookup_properties/test_lookup_properties.yml b/test/integration/targets/lookup_properties/test_lookup_properties.yml
index a8cad9de48..7c33a70b0b 100644
--- a/test/integration/targets/lookup_properties/test_lookup_properties.yml
+++ b/test/integration/targets/lookup_properties/test_lookup_properties.yml
@@ -10,7 +10,7 @@
test_dot: "{{lookup('ini', 'value.dot type=properties file=lookup.properties')}}"
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"
set_fact:
diff --git a/test/integration/targets/module_precedence/modules_test_multiple_roles.yml b/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
index f4bd264957..182c2158e8 100644
--- a/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
+++ b/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
@@ -14,4 +14,4 @@
- assert:
that:
- '"location" in result'
- - 'result["location"] == "{{ expected_location}}"'
+ - 'result["location"] == expected_location'
diff --git a/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml b/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
index 5403ae238c..ec5619f39e 100644
--- a/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
+++ b/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
@@ -13,4 +13,4 @@
- assert:
that:
- '"location" in result'
- - 'result["location"] == "{{ expected_location}}"'
+ - 'result["location"] == expected_location'
diff --git a/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml b/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
index 52c3402013..62b38a7cb5 100644
--- a/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
+++ b/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
@@ -7,4 +7,4 @@
assert:
that:
- '"location" in result'
- - 'result["location"] == "{{ expected_location }}"'
+ - 'result["location"] == expected_location'
diff --git a/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml b/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
index 52c3402013..62b38a7cb5 100644
--- a/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
+++ b/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
@@ -7,4 +7,4 @@
assert:
that:
- '"location" in result'
- - 'result["location"] == "{{ expected_location }}"'
+ - 'result["location"] == expected_location'
diff --git a/test/integration/targets/script/tasks/main.yml b/test/integration/targets/script/tasks/main.yml
index f1746f7c48..3a10a1fec4 100644
--- a/test/integration/targets/script/tasks/main.yml
+++ b/test/integration/targets/script/tasks/main.yml
@@ -197,7 +197,7 @@
assert:
that:
- _check_mode_test2 is skipped
- - '_check_mode_test2.msg == "{{ output_dir_test | expanduser }}/afile2.txt exists, matching creates option"'
+ - '_check_mode_test2.msg == output_dir_test | expanduser ~ "/afile2.txt exists, matching creates option"'
- name: Remove afile2.txt
file:
@@ -219,7 +219,7 @@
assert:
that:
- _check_mode_test3 is skipped
- - '_check_mode_test3.msg == "{{ output_dir_test | expanduser }}/afile2.txt does not exist, matching removes option"'
+ - '_check_mode_test3.msg == output_dir_test | expanduser ~ "/afile2.txt does not exist, matching removes option"'
# executable
diff --git a/test/integration/targets/slurp/tasks/main.yml b/test/integration/targets/slurp/tasks/main.yml
index 4f3556fad4..fd61b7f4bc 100644
--- a/test/integration/targets/slurp/tasks/main.yml
+++ b/test/integration/targets/slurp/tasks/main.yml
@@ -33,7 +33,7 @@
- 'slurp_existing.encoding == "base64"'
- 'slurp_existing is not changed'
- 'slurp_existing is not failed'
- - '"{{ slurp_existing.content | b64decode }}" == "We are at the café"'
+ - 'slurp_existing.content | b64decode == "We are at the café"'
- name: Create a binary file to test with
copy:
diff --git a/test/integration/targets/template/tasks/main.yml b/test/integration/targets/template/tasks/main.yml
index da80343686..daed110855 100644
--- a/test/integration/targets/template/tasks/main.yml
+++ b/test/integration/targets/template/tasks/main.yml
@@ -356,7 +356,7 @@
- assert:
that:
- "\"foo t'e~m\\plated\" in unusual_results.stdout_lines"
- - "{{unusual_results.stdout_lines| length}} == 1"
+ - "unusual_results.stdout_lines| length == 1"
- name: check that the unusual filename can be checked for changes
template:
diff --git a/test/integration/targets/unarchive/tasks/test_mode.yml b/test/integration/targets/unarchive/tasks/test_mode.yml
index c69e3bd2b2..06fbc7b8d9 100644
--- a/test/integration/targets/unarchive/tasks/test_mode.yml
+++ b/test/integration/targets/unarchive/tasks/test_mode.yml
@@ -24,7 +24,7 @@
- "unarchive06_stat.stat.mode == '0600'"
# Verify that file list is generated
- "'files' in unarchive06"
- - "{{unarchive06['files']| length}} == 1"
+ - "unarchive06['files']| length == 1"
- "'foo-unarchive.txt' in unarchive06['files']"
- name: remove our tar.gz unarchive destination
@@ -74,7 +74,7 @@
- "unarchive07.changed == false"
# Verify that file list is generated
- "'files' in unarchive07"
- - "{{unarchive07['files']| length}} == 1"
+ - "unarchive07['files']| length == 1"
- "'foo-unarchive.txt' in unarchive07['files']"
- name: remove our tar.gz unarchive destination
@@ -108,7 +108,7 @@
- "unarchive08_stat.stat.mode == '0601'"
# Verify that file list is generated
- "'files' in unarchive08"
- - "{{unarchive08['files']| length}} == 3"
+ - "unarchive08['files']| length == 3"
- "'foo-unarchive.txt' in unarchive08['files']"
- "'foo-unarchive-777.txt' in unarchive08['files']"
- "'FOO-UNAR.TXT' in unarchive08['files']"
@@ -140,7 +140,7 @@
- "unarchive08_stat.stat.mode == '0601'"
# Verify that file list is generated
- "'files' in unarchive08"
- - "{{unarchive08['files']| length}} == 3"
+ - "unarchive08['files']| length == 3"
- "'foo-unarchive.txt' in unarchive08['files']"
- "'foo-unarchive-777.txt' in unarchive08['files']"
- "'FOO-UNAR.TXT' in unarchive08['files']"
diff --git a/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml b/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
index 6181e3bd62..b3653c0872 100644
--- a/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
+++ b/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
@@ -48,7 +48,7 @@
- unarchive10 is changed
# Verify that file list is generated
- "'files' in unarchive10"
- - "{{unarchive10['files']| length}} == 1"
+ - "unarchive10['files']| length == 1"
- "'foo-unarchive.txt' in unarchive10['files']"
- archive_path.stat.exists
diff --git a/test/integration/targets/unarchive/tasks/test_zip.yml b/test/integration/targets/unarchive/tasks/test_zip.yml
index aae57d8ec9..d11c5f7223 100644
--- a/test/integration/targets/unarchive/tasks/test_zip.yml
+++ b/test/integration/targets/unarchive/tasks/test_zip.yml
@@ -17,7 +17,7 @@
- "unarchive03.changed == true"
# Verify that file list is generated
- "'files' in unarchive03"
- - "{{unarchive03['files']| length}} == 3"
+ - "unarchive03['files']| length == 3"
- "'foo-unarchive.txt' in unarchive03['files']"
- "'foo-unarchive-777.txt' in unarchive03['files']"
- "'FOO-UNAR.TXT' in unarchive03['files']"
diff --git a/test/integration/targets/vault/roles/test_vault_embedded/tasks/main.yml b/test/integration/targets/vault/roles/test_vault_embedded/tasks/main.yml
index eba938966d..98ef751b86 100644
--- a/test/integration/targets/vault/roles/test_vault_embedded/tasks/main.yml
+++ b/test/integration/targets/vault/roles/test_vault_embedded/tasks/main.yml
@@ -2,7 +2,7 @@
- name: Assert that a embedded vault of a string with no newline works
assert:
that:
- - '"{{ vault_encrypted_one_line_var }}" == "Setec Astronomy"'
+ - 'vault_encrypted_one_line_var == "Setec Astronomy"'
- name: Assert that a multi line embedded vault works, including new line
assert:
diff --git a/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml b/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
index e09004a1d9..107e65cb11 100644
--- a/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
+++ b/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
@@ -2,7 +2,7 @@
- name: Assert that a vault encrypted file with embedded vault of a string with no newline works
assert:
that:
- - '"{{ vault_file_encrypted_with_encrypted_one_line_var }}" == "Setec Astronomy"'
+ - 'vault_file_encrypted_with_encrypted_one_line_var == "Setec Astronomy"'
- name: Assert that a vault encrypted file with multi line embedded vault works, including new line
assert:
diff --git a/test/integration/targets/vyos_config/tests/cli/check_config.yaml b/test/integration/targets/vyos_config/tests/cli/check_config.yaml
index 65076b3c54..30c43599af 100644
--- a/test/integration/targets/vyos_config/tests/cli/check_config.yaml
+++ b/test/integration/targets/vyos_config/tests/cli/check_config.yaml
@@ -22,7 +22,7 @@
- name: Check that multiple duplicate lines collapse into a single commands
assert:
that:
- - "{{ result.commands|length }} == 1"
+ - "result.commands|length == 1"
- name: Check that set is correctly prepended
assert:
@@ -58,6 +58,6 @@
- assert:
that:
- - "{{ result.filtered|length }} == 2"
+ - "result.filtered|length == 2"
- debug: msg="END cli/config_check.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/vyos_interfaces/tests/cli/deleted.yaml b/test/integration/targets/vyos_interfaces/tests/cli/deleted.yaml
index 5b08ea95f8..b2aa51fc77 100644
--- a/test/integration/targets/vyos_interfaces/tests/cli/deleted.yaml
+++ b/test/integration/targets/vyos_interfaces/tests/cli/deleted.yaml
@@ -16,17 +16,17 @@
- name: Assert that the before dicts were correctly generated
assert:
that:
- - "{{ populate | symmetric_difference(result['before']) |length == 0 }}"
+ - "populate | symmetric_difference(result['before']) |length == 0"
- name: Assert that the correct set of commands were generated
assert:
that:
- - "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
+ - "deleted['commands'] | symmetric_difference(result['commands']) |length == 0"
- name: Assert that the after dicts were correctly generated
assert:
that:
- - "{{ deleted['after'] | symmetric_difference(result['after']) |length == 0 }}"
+ - "deleted['after'] | symmetric_difference(result['after']) |length == 0"
- name: Delete attributes of given interfaces (IDEMPOTENT)
vyos_interfaces: *deleted
@@ -40,7 +40,6 @@
- name: Assert that the before dicts were correctly generated
assert:
that:
- - "{{ deleted['after'] | symmetric_difference(result['before']) |length == 0 }}"
-
+ - "deleted['after'] | symmetric_difference(result['before']) |length == 0"
always:
- include_tasks: _remove_config.yaml
diff --git a/test/integration/targets/vyos_interfaces/tests/cli/overridden.yaml b/test/integration/targets/vyos_interfaces/tests/cli/overridden.yaml
index 43040c1e67..4f2e323b36 100644
--- a/test/integration/targets/vyos_interfaces/tests/cli/overridden.yaml
+++ b/test/integration/targets/vyos_interfaces/tests/cli/overridden.yaml
@@ -22,17 +22,17 @@
- name: Assert that before dicts were correctly generated
assert:
that:
- - "{{ populate | 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_interfaces: *overridden
@@ -46,7 +46,7 @@
- name: Assert that before dicts were correctly generated
assert:
that:
- - "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}"
-
+ - "overridden['after'] | symmetric_difference(result['before']) |length == 0"
+
always:
- include_tasks: _remove_config.yaml
diff --git a/test/integration/targets/wait_for/tasks/main.yml b/test/integration/targets/wait_for/tasks/main.yml
index 1898fd1253..7bdbdd951a 100644
--- a/test/integration/targets/wait_for/tasks/main.yml
+++ b/test/integration/targets/wait_for/tasks/main.yml
@@ -29,7 +29,7 @@
assert:
that:
- waitfor is successful
- - waitfor.path == "{{ output_dir | expanduser }}/wait_for_file"
+ - waitfor.path == output_dir | expanduser ~ "/wait_for_file"
- waitfor.elapsed >= 2
- waitfor.elapsed <= 15
@@ -47,7 +47,7 @@
assert:
that:
- waitfor is successful
- - waitfor.path == "{{ output_dir | expanduser }}/wait_for_file"
+ - waitfor.path == output_dir | expanduser ~ "/wait_for_file"
- waitfor.elapsed >= 2
- waitfor.elapsed <= 15
@@ -135,7 +135,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:
@@ -163,4 +163,16 @@
that:
- waitfor is successful
- waitfor is not changed
- - "waitfor.port == {{ http_port }}"
+ - "waitfor.port == http_port"
+
+- name: test wait_for with delay
+ wait_for:
+ timeout: 2
+ delay: 2
+ register: waitfor
+
+- name: verify test wait_for with delay
+ assert:
+ that:
+ - waitfor is successful
+ - waitfor.elapsed >= 4
diff --git a/test/units/module_utils/common/test_collections.py b/test/units/module_utils/common/test_collections.py
index eb6e376a2c..95b2a402f2 100644
--- a/test/units/module_utils/common/test_collections.py
+++ b/test/units/module_utils/common/test_collections.py
@@ -35,8 +35,21 @@ class IterableStub:
return IteratorStub()
+class FakeAnsibleVaultEncryptedUnicode(Sequence):
+ __ENCRYPTED__ = True
+
+ def __init__(self, data):
+ self.data = data
+
+ def __getitem__(self, index):
+ return self.data[index]
+
+ def __len__(self):
+ return len(self.data)
+
+
TEST_STRINGS = u'he', u'Україна', u'Česká republika'
-TEST_STRINGS = TEST_STRINGS + tuple(s.encode('utf-8') for s in TEST_STRINGS)
+TEST_STRINGS = TEST_STRINGS + tuple(s.encode('utf-8') for s in TEST_STRINGS) + (FakeAnsibleVaultEncryptedUnicode(u'foo'),)
TEST_ITEMS_NON_SEQUENCES = (
{}, object(), frozenset(),
diff --git a/test/units/parsing/test_ajson.py b/test/units/parsing/test_ajson.py
index 929d19966d..c38f43ea57 100644
--- a/test/units/parsing/test_ajson.py
+++ b/test/units/parsing/test_ajson.py
@@ -158,6 +158,7 @@ class TestAnsibleJSONEncoder:
Test for passing AnsibleVaultEncryptedUnicode to AnsibleJSONEncoder.default().
"""
assert ansible_json_encoder.default(test_input) == {'__ansible_vault': expected}
+ assert json.dumps(test_input, cls=AnsibleJSONEncoder, preprocess_unsafe=True) == '{"__ansible_vault": "%s"}' % expected.replace('\n', '\\n')
@pytest.mark.parametrize(
'test_input,expected',
diff --git a/test/units/parsing/yaml/test_dumper.py b/test/units/parsing/yaml/test_dumper.py
index 8129ca3ab2..ee9ea8b07a 100644
--- a/test/units/parsing/yaml/test_dumper.py
+++ b/test/units/parsing/yaml/test_dumper.py
@@ -19,13 +19,16 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import io
+import yaml
+
+from jinja2.exceptions import UndefinedError
from units.compat import unittest
from ansible.parsing import vault
from ansible.parsing.yaml import dumper, objects
from ansible.parsing.yaml.loader import AnsibleLoader
from ansible.module_utils.six import PY2
-from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes
+from ansible.template import AnsibleUndefined
from units.mock.yaml_helper import YamlTestUtils
from units.mock.vault_helper import TextVaultSecret
@@ -64,8 +67,7 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils):
def test_bytes(self):
b_text = u'tréma'.encode('utf-8')
- unsafe_object = AnsibleUnsafeBytes(b_text)
- yaml_out = self._dump_string(unsafe_object, dumper=self.dumper)
+ yaml_out = self._dump_string(b_text, dumper=self.dumper)
stream = self._build_stream(yaml_out)
loader = self._loader(stream)
@@ -92,8 +94,7 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils):
def test_unicode(self):
u_text = u'nöel'
- unsafe_object = AnsibleUnsafeText(u_text)
- yaml_out = self._dump_string(unsafe_object, dumper=self.dumper)
+ yaml_out = self._dump_string(u_text, dumper=self.dumper)
stream = self._build_stream(yaml_out)
loader = self._loader(stream)
@@ -101,3 +102,12 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils):
data_from_yaml = loader.get_single_data()
self.assertEqual(u_text, data_from_yaml)
+
+ def test_undefined(self):
+ undefined_object = AnsibleUndefined()
+ try:
+ yaml_out = self._dump_string(undefined_object, dumper=self.dumper)
+ except UndefinedError:
+ yaml_out = None
+
+ self.assertIsNone(yaml_out)
--
2.44.0