Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-15-SP2:Update
ansible
0001-Ensure-that-unsafe-is-more-difficult-to-lo...
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File 0001-Ensure-that-unsafe-is-more-difficult-to-lose-stable-.patch of Package ansible
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) * 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
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor