diff options
35 files changed, 378 insertions, 99 deletions
@@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: ansible-core -Version: 2.17.0 +Version: 2.17.1 Summary: Radically simple IT automation Home-page: https://ansible.com/ Author: Ansible, Inc. diff --git a/changelogs/CHANGELOG-v2.17.rst b/changelogs/CHANGELOG-v2.17.rst index dd46da0..65bd411 100644 --- a/changelogs/CHANGELOG-v2.17.rst +++ b/changelogs/CHANGELOG-v2.17.rst @@ -4,6 +4,30 @@ ansible-core 2.17 "Gallows Pole" Release Notes .. contents:: Topics +v2.17.1 +======= + +Release Summary +--------------- + +| Release Date: 2024-06-17 +| `Porting Guide <https://docs.ansible.com/ansible-core/2.17/porting_guides/porting_guide_core_2.17.html>`__ + +Minor Changes +------------- + +- ansible-test - Update ``pypi-test-container`` to version 3.1.0. + +Bugfixes +-------- + +- Fix rapid memory usage growth when notifying handlers using the ``listen`` keyword (https://github.com/ansible/ansible/issues/83392) +- Fix the task attribute ``resolved_action`` to show the FQCN instead of ``None`` when ``action`` or ``local_action`` is used in the playbook. +- Fix using ``module_defaults`` with ``local_action``/``action`` (https://github.com/ansible/ansible/issues/81905). +- fixed unit test test_borken_cowsay to address mock not been properly applied when existing unix system already have cowsay installed. +- powershell - Implement more robust deletion mechanism for C# code compilation temporary files. This should avoid scenarios where the underlying temporary directory may be temporarily locked by antivirus tools or other IO problems. A failure to delete one of these temporary directories will result in a warning rather than an outright failure. +- shell plugin - properly quote all needed components of shell commands (https://github.com/ansible/ansible/issues/82535) + v2.17.0 ======= diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index f2e7e54..3020a6f 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -687,3 +687,47 @@ releases: - psrp-version-req.yml - uri_follow_redirect_bool.yml release_date: '2024-05-13' + 2.17.1: + changes: + minor_changes: + - ansible-test - Update ``pypi-test-container`` to version 3.1.0. + release_summary: '| Release Date: 2024-06-17 + + | `Porting Guide <https://docs.ansible.com/ansible-core/2.17/porting_guides/porting_guide_core_2.17.html>`__ + + ' + codename: Gallows Pole + fragments: + - 2.17.1_summary.yaml + - ansible-test-pypi-test-container-update.yml + release_date: '2024-06-17' + 2.17.1rc1: + changes: + bugfixes: + - Fix rapid memory usage growth when notifying handlers using the ``listen`` + keyword (https://github.com/ansible/ansible/issues/83392) + - Fix the task attribute ``resolved_action`` to show the FQCN instead of ``None`` + when ``action`` or ``local_action`` is used in the playbook. + - Fix using ``module_defaults`` with ``local_action``/``action`` (https://github.com/ansible/ansible/issues/81905). + - fixed unit test test_borken_cowsay to address mock not been properly applied + when existing unix system already have cowsay installed. + - powershell - Implement more robust deletion mechanism for C# code compilation + temporary files. This should avoid scenarios where the underlying temporary + directory may be temporarily locked by antivirus tools or other IO problems. + A failure to delete one of these temporary directories will result in a warning + rather than an outright failure. + - shell plugin - properly quote all needed components of shell commands (https://github.com/ansible/ansible/issues/82535) + release_summary: '| Release Date: 2024-06-10 + + | `Porting Guide <https://docs.ansible.com/ansible-core/2.17/porting_guides/porting_guide_core_2.17.html>`__ + + ' + codename: Gallows Pole + fragments: + - 2.17.1rc1_summary.yaml + - 82535-properly-quote-shell.yml + - 83327.yml + - 83392-fix-memory-issues-handlers.yml + - PowerShell-AddType-temp.yml + - correct-callback-fqcn-old-style-action-invocation.yml + release_date: '2024-06-10' diff --git a/lib/ansible/executor/powershell/exec_wrapper.ps1 b/lib/ansible/executor/powershell/exec_wrapper.ps1 index 0f97bdf..cce99ab 100644 --- a/lib/ansible/executor/powershell/exec_wrapper.ps1 +++ b/lib/ansible/executor/powershell/exec_wrapper.ps1 @@ -178,6 +178,7 @@ $($ErrorRecord.InvocationInfo.PositionMessage) Write-AnsibleLog "INFO - converting json raw to a payload" "exec_wrapper" $payload = ConvertFrom-AnsibleJson -InputObject $json_raw + $payload.module_args._ansible_exec_wrapper_warnings = [System.Collections.Generic.List[string]]@() # TODO: handle binary modules # TODO: handle persistence diff --git a/lib/ansible/executor/powershell/module_powershell_wrapper.ps1 b/lib/ansible/executor/powershell/module_powershell_wrapper.ps1 index c35c84c..f79dd6f 100644 --- a/lib/ansible/executor/powershell/module_powershell_wrapper.ps1 +++ b/lib/ansible/executor/powershell/module_powershell_wrapper.ps1 @@ -29,7 +29,18 @@ if ($csharp_utils.Count -gt 0) { # add any C# references so the module does not have to do so $new_tmp = [System.Environment]::ExpandEnvironmentVariables($Payload.module_args["_ansible_remote_tmp"]) - Add-CSharpType -References $csharp_utils -TempPath $new_tmp -IncludeDebugInfo + + # We use a fake module object to capture warnings + $fake_module = [PSCustomObject]@{ + Tmpdir = $new_tmp + Verbosity = 3 + } + $warning_func = New-Object -TypeName System.Management.Automation.PSScriptMethod -ArgumentList Warn, { + param($message) + $Payload.module_args._ansible_exec_wrapper_warnings.Add($message) + } + $fake_module.PSObject.Members.Add($warning_func) + Add-CSharpType -References $csharp_utils -AnsibleModule $fake_module } if ($Payload.ContainsKey("coverage") -and $null -ne $host.Runspace -and $null -ne $host.Runspace.Debugger) { diff --git a/lib/ansible/module_utils/ansible_release.py b/lib/ansible/module_utils/ansible_release.py index 88f7515..7ed9fdf 100644 --- a/lib/ansible/module_utils/ansible_release.py +++ b/lib/ansible/module_utils/ansible_release.py @@ -17,6 +17,6 @@ from __future__ import annotations -__version__ = '2.17.0' +__version__ = '2.17.1' __author__ = 'Ansible, Inc.' __codename__ = "Gallows Pole" diff --git a/lib/ansible/module_utils/csharp/Ansible.Basic.cs b/lib/ansible/module_utils/csharp/Ansible.Basic.cs index a042af8..0859582 100644 --- a/lib/ansible/module_utils/csharp/Ansible.Basic.cs +++ b/lib/ansible/module_utils/csharp/Ansible.Basic.cs @@ -1025,7 +1025,16 @@ namespace Ansible.Basic foreach (DictionaryEntry entry in param) { string paramKey = (string)entry.Key; - if (!legalInputs.Contains(paramKey, StringComparer.OrdinalIgnoreCase)) + if (paramKey == "_ansible_exec_wrapper_warnings") + { + // Special key used in module_powershell_wrapper to pass + // along any warnings that should be returned back to + // Ansible. + removedParameters.Add(paramKey); + foreach (string warning in (IList<string>)entry.Value) + Warn(warning); + } + else if (!legalInputs.Contains(paramKey, StringComparer.OrdinalIgnoreCase)) unsupportedParameters.Add(paramKey); else if (!legalInputs.Contains(paramKey)) // For backwards compatibility we do not care about the case but we need to warn the users as this will diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 index f40c338..b18a9a1 100644 --- a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 @@ -75,7 +75,7 @@ Function Add-CSharpType { [Switch]$IgnoreWarnings, [Switch]$PassThru, [Parameter(Mandatory = $true, ParameterSetName = "Module")][Object]$AnsibleModule, - [Parameter(ParameterSetName = "Manual")][String]$TempPath = $env:TMP, + [Parameter(ParameterSetName = "Manual")][String]$TempPath, [Parameter(ParameterSetName = "Manual")][Switch]$IncludeDebugInfo, [String[]]$CompileSymbols = @() ) @@ -280,9 +280,11 @@ Function Add-CSharpType { $include_debug = $AnsibleModule.Verbosity -ge 3 } else { - $temp_path = $TempPath + $temp_path = [System.IO.Path]::GetTempPath() $include_debug = $IncludeDebugInfo.IsPresent } + $temp_path = Join-Path -Path $temp_path -ChildPath ([Guid]::NewGuid().Guid) + $compiler_options = [System.Collections.ArrayList]@("/optimize") if ($defined_symbols.Count -gt 0) { $compiler_options.Add("/define:" + ([String]::Join(";", $defined_symbols.ToArray()))) > $null @@ -304,8 +306,12 @@ Function Add-CSharpType { ) # create a code snippet for each reference and check if we need - # to reference any extra assemblies - $ignore_warnings = [System.Collections.ArrayList]@() + # to reference any extra assemblies. + # CS1610 is a warning when csc.exe failed to delete temporary files. + # We use our own temp dir deletion mechanism so this doesn't become a + # fatal error. + # https://github.com/ansible-collections/ansible.windows/issues/598 + $ignore_warnings = [System.Collections.ArrayList]@('1610') $compile_units = [System.Collections.Generic.List`1[System.CodeDom.CodeSnippetCompileUnit]]@() foreach ($reference in $References) { # scan through code and add any assemblies that match @@ -373,7 +379,26 @@ Function Add-CSharpType { } } - $compile = $provider.CompileAssemblyFromDom($compile_parameters, $compile_units) + $null = New-Item -Path $temp_path -ItemType Directory -Force + try { + $compile = $provider.CompileAssemblyFromDom($compile_parameters, $compile_units) + } + finally { + # Try to delete the temp path, if this fails and we are running + # with a module object write a warning instead of failing. + try { + [System.IO.Directory]::Delete($temp_path, $true) + } + catch { + $msg = "Failed to cleanup temporary directory '$temp_path' used for compiling C# code." + if ($AnsibleModule) { + $AnsibleModule.Warn("$msg Files may still be present after the task is complete. Error: $_") + } + else { + throw "$msg Error: $_" + } + } + } } finally { foreach ($kvp in $originalEnv.GetEnumerator()) { diff --git a/lib/ansible/parsing/mod_args.py b/lib/ansible/parsing/mod_args.py index 56e7e98..fb982f5 100644 --- a/lib/ansible/parsing/mod_args.py +++ b/lib/ansible/parsing/mod_args.py @@ -53,6 +53,17 @@ BUILTIN_TASKS = frozenset(add_internal_fqcns(( ))) +def _get_action_context(action_or_module, collection_list): + module_context = module_loader.find_plugin_with_context(action_or_module, collection_list=collection_list) + if module_context and module_context.resolved and module_context.action_plugin: + action_or_module = module_context.action_plugin + + context = action_loader.find_plugin_with_context(action_or_module, collection_list=collection_list) + if not context or not context.resolved: + context = module_context + return context + + class ModuleArgsParser: """ @@ -291,6 +302,11 @@ class ModuleArgsParser: delegate_to = 'localhost' action, args = self._normalize_parameters(thing, action=action, additional_args=additional_args) + if action is not None and not skip_action_validation: + context = _get_action_context(action, self._collection_list) + if context is not None and context.resolved: + self.resolved_action = context.resolved_fqcn + # module: <stuff> is the more new-style invocation # filter out task attributes so we're only querying unrecognized keys as actions/modules @@ -306,9 +322,7 @@ class ModuleArgsParser: is_action_candidate = True else: try: - context = action_loader.find_plugin_with_context(item, collection_list=self._collection_list) - if not context.resolved: - context = module_loader.find_plugin_with_context(item, collection_list=self._collection_list) + context = _get_action_context(item, self._collection_list) except AnsibleError as e: if e.obj is None: e.obj = self._task_ds diff --git a/lib/ansible/playbook/handler.py b/lib/ansible/playbook/handler.py index 09f122e..a7b4a53 100644 --- a/lib/ansible/playbook/handler.py +++ b/lib/ansible/playbook/handler.py @@ -37,6 +37,16 @@ class Handler(Task): ''' returns a human-readable representation of the handler ''' return "HANDLER: %s" % self.get_name() + def _validate_listen(self, attr, name, value): + new_value = self.get_validated_value(name, attr, value, None) + if self._role is not None: + for listener in new_value.copy(): + new_value.extend([ + f"{self._role.get_name(include_role_fqcn=True)} : {listener}", + f"{self._role.get_name(include_role_fqcn=False)} : {listener}", + ]) + setattr(self, name, new_value) + @staticmethod def load(data, block=None, role=None, task_include=None, variable_manager=None, loader=None): t = Handler(block=block, role=role, task_include=task_include) diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index 7f54639..8345869 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -208,6 +208,7 @@ class Task(Base, Conditional, Taggable, CollectionSearch, Notifiable, Delegatabl # But if it wasn't, we can add the yaml object now to get more detail raise AnsibleParserError(to_native(e), obj=ds, orig_exc=e) else: + # Set the resolved action plugin (or if it does not exist, module) for callbacks. self.resolved_action = args_parser.resolved_action # the command/shell/script modules used to support the `cmd` arg, diff --git a/lib/ansible/plugins/filter/splitext.yml b/lib/ansible/plugins/filter/splitext.yml index 5f94692..56d5c30 100644 --- a/lib/ansible/plugins/filter/splitext.yml +++ b/lib/ansible/plugins/filter/splitext.yml @@ -14,14 +14,14 @@ DOCUMENTATION: EXAMPLES: | - # gobble => [ '/etc/make', 'conf' ] + # gobble => [ '/etc/make', '.conf' ] gobble: "{{ '/etc/make.conf' | splitext }}" - # file_n_ext => [ 'ansible', 'cfg' ] + # file_n_ext => [ 'ansible', '.cfg' ] file_n_ext: "{{ 'ansible.cfg' | splitext }}" - # hoax => ['/etc/hoasdf', ''] - hoax: '{{ "/etc//hoasdf/" | splitext }}' + # hoax => [ '/etc/hoasdf', '' ] + hoax: "{{ '/etc/hoasdf' | splitext }}" RETURN: _value: diff --git a/lib/ansible/plugins/filter/win_splitdrive.yml b/lib/ansible/plugins/filter/win_splitdrive.yml index 828d1dd..d6ad422 100644 --- a/lib/ansible/plugins/filter/win_splitdrive.yml +++ b/lib/ansible/plugins/filter/win_splitdrive.yml @@ -5,6 +5,8 @@ DOCUMENTATION: short_description: Split a Windows path by the drive letter description: - Returns a list with the first component being the drive letter and the second, the rest of the path. + - If the path contains a drive letter, drive will contain everything up to and including the colon. + - If the path contains a UNC (Universal Naming Convention) path, drive will contain the host name and share, up to but not including the fourth separator. options: _input: description: A Windows path. @@ -13,17 +15,27 @@ DOCUMENTATION: EXAMPLES: | - # To get the last name of a file Windows path, like ['C', '\Users\asdf\foo.txt'] out of 'C:\Users\asdf\foo.txt' + # To get the last name of a file Windows path, like ['C:', '\Users\asdf\foo.txt'] out of 'C:\Users\asdf\foo.txt' {{ mypath | win_splitdrive }} - # just the drive letter + # To get path from UNC (Universal Naming Convention) path, like ['//host/computer', '/dir/a'] out of '//host/computer/dir/a' + + # just the drive letter, like ['C:'] out of 'C:\Users\asdf\foo.txt' + {{ mypath | win_splitdrive | first }} + + # path w/o drive letter, like ['\Users\asdf\foo.txt'] out of 'C:\Users\asdf\foo.txt' + {{ mypath | win_splitdrive | last }} + + # just the hostname and share, like ['//host/computer'] out of '//host/computer/dir/a' {{ mypath | win_splitdrive | first }} - # path w/o drive letter + # path w/o hostname and share, like ['/dir/a'] out of '//host/computer/dir/a' {{ mypath | win_splitdrive | last }} RETURN: _value: - description: List in which the first element is the drive letter and the second the rest of the path. + description: + - List in which the first element is the drive letter with colon and the second the rest of the path. + - In case of UNC path, first element is the hostname and share and the second the rest of the path. type: list elements: str diff --git a/lib/ansible/plugins/shell/__init__.py b/lib/ansible/plugins/shell/__init__.py index 5aa0a1b..56be533 100644 --- a/lib/ansible/plugins/shell/__init__.py +++ b/lib/ansible/plugins/shell/__init__.py @@ -85,7 +85,7 @@ class ShellBase(AnsiblePlugin): return 'ansible-tmp-%s-%s-%s' % (time.time(), os.getpid(), random.randint(0, 2**48)) def env_prefix(self, **kwargs): - return ' '.join(['%s=%s' % (k, shlex.quote(text_type(v))) for k, v in kwargs.items()]) + return ' '.join(['%s=%s' % (k, self.quote(text_type(v))) for k, v in kwargs.items()]) def join_path(self, *args): return os.path.join(*args) @@ -101,41 +101,33 @@ class ShellBase(AnsiblePlugin): def chmod(self, paths, mode): cmd = ['chmod', mode] cmd.extend(paths) - cmd = [shlex.quote(c) for c in cmd] - - return ' '.join(cmd) + return shlex.join(cmd) def chown(self, paths, user): cmd = ['chown', user] cmd.extend(paths) - cmd = [shlex.quote(c) for c in cmd] - - return ' '.join(cmd) + return shlex.join(cmd) def chgrp(self, paths, group): cmd = ['chgrp', group] cmd.extend(paths) - cmd = [shlex.quote(c) for c in cmd] - - return ' '.join(cmd) + return shlex.join(cmd) def set_user_facl(self, paths, user, mode): """Only sets acls for users as that's really all we need""" cmd = ['setfacl', '-m', 'u:%s:%s' % (user, mode)] cmd.extend(paths) - cmd = [shlex.quote(c) for c in cmd] - - return ' '.join(cmd) + return shlex.join(cmd) def remove(self, path, recurse=False): - path = shlex.quote(path) + path = self.quote(path) cmd = 'rm -f ' if recurse: cmd += '-r ' return cmd + "%s %s" % (path, self._SHELL_REDIRECT_ALLNULL) def exists(self, path): - cmd = ['test', '-e', shlex.quote(path)] + cmd = ['test', '-e', self.quote(path)] return ' '.join(cmd) def mkdtemp(self, basefile=None, system=False, mode=0o700, tmpdir=None): @@ -194,8 +186,7 @@ class ShellBase(AnsiblePlugin): # Check that the user_path to expand is safe if user_home_path != '~': if not _USER_HOME_PATH_RE.match(user_home_path): - # shlex.quote will make the shell return the string verbatim - user_home_path = shlex.quote(user_home_path) + user_home_path = self.quote(user_home_path) elif username: # if present the user name is appended to resolve "that user's home" user_home_path += username @@ -207,20 +198,20 @@ class ShellBase(AnsiblePlugin): return 'echo %spwd%s' % (self._SHELL_SUB_LEFT, self._SHELL_SUB_RIGHT) def build_module_command(self, env_string, shebang, cmd, arg_path=None): - # don't quote the cmd if it's an empty string, because this will break pipelining mode - if cmd.strip() != '': - cmd = shlex.quote(cmd) + env_string = env_string.strip() + if env_string: + env_string += ' ' - cmd_parts = [] - if shebang: - shebang = shebang.replace("#!", "").strip() - else: - shebang = "" - cmd_parts.extend([env_string.strip(), shebang, cmd]) - if arg_path is not None: - cmd_parts.append(arg_path) - new_cmd = " ".join(cmd_parts) - return new_cmd + if shebang is None: + shebang = '' + + cmd_parts = [ + shebang.removeprefix('#!').strip(), + cmd.strip(), + arg_path, + ] + + return f'{env_string}%s' % shlex.join(cps for cp in cmd_parts if cp and (cps := cp.strip())) def append_command(self, cmd, cmd_to_append): """Append an additional command if supported by the shell""" diff --git a/lib/ansible/plugins/shell/sh.py b/lib/ansible/plugins/shell/sh.py index e0412b7..ef85596 100644 --- a/lib/ansible/plugins/shell/sh.py +++ b/lib/ansible/plugins/shell/sh.py @@ -13,8 +13,6 @@ extends_documentation_fragment: - shell_common ''' -import shlex - from ansible.plugins.shell import ShellBase @@ -66,7 +64,7 @@ class ShellModule(ShellBase): # Quoting gets complex here. We're writing a python string that's # used by a variety of shells on the remote host to invoke a python # "one-liner". - shell_escaped_path = shlex.quote(path) + shell_escaped_path = self.quote(path) test = "rc=flag; [ -r %(p)s ] %(shell_or)s rc=2; [ -f %(p)s ] %(shell_or)s rc=1; [ -d %(p)s ] %(shell_and)s rc=3; %(i)s -V 2>/dev/null %(shell_or)s rc=4; [ x\"$rc\" != \"xflag\" ] %(shell_and)s echo \"${rc} \"%(p)s %(shell_and)s exit 0" % dict(p=shell_escaped_path, i=python_interp, shell_and=self._SHELL_AND, shell_or=self._SHELL_OR) # NOQA csums = [ u"({0} -c 'import hashlib; BLOCKSIZE = 65536; hasher = hashlib.sha1();{2}afile = open(\"'{1}'\", \"rb\"){2}buf = afile.read(BLOCKSIZE){2}while len(buf) > 0:{2}\thasher.update(buf){2}\tbuf = afile.read(BLOCKSIZE){2}afile.close(){2}print(hasher.hexdigest())' 2>/dev/null)".format(python_interp, shell_escaped_path, self._SHELL_EMBEDDED_PY_EOL), # NOQA Python > 2.4 (including python3) diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py index efd69ef..a5822c3 100644 --- a/lib/ansible/plugins/strategy/__init__.py +++ b/lib/ansible/plugins/strategy/__init__.py @@ -549,27 +549,13 @@ class StrategyBase: yield handler break - templar.available_variables = {} - seen = [] + seen = set() for handler in handlers: - if listeners := handler.listen: - listeners = handler.get_validated_value( - 'listen', - handler.fattributes.get('listen'), - listeners, - templar, - ) - if handler._role is not None: - for listener in listeners.copy(): - listeners.extend([ - handler._role.get_name(include_role_fqcn=True) + ' : ' + listener, - handler._role.get_name(include_role_fqcn=False) + ' : ' + listener - ]) - if notification in listeners: - if handler.name and handler.name in seen: - continue - seen.append(handler.name) - yield handler + if notification in handler.listen: + if handler.name and handler.name in seen: + continue + seen.add(handler.name) + yield handler @debug_closure def _process_pending_results(self, iterator, one_pass=False, max_passes=None): diff --git a/lib/ansible/release.py b/lib/ansible/release.py index 88f7515..7ed9fdf 100644 --- a/lib/ansible/release.py +++ b/lib/ansible/release.py @@ -17,6 +17,6 @@ from __future__ import annotations -__version__ = '2.17.0' +__version__ = '2.17.1' __author__ = 'Ansible, Inc.' __codename__ = "Gallows Pole" diff --git a/lib/ansible_core.egg-info/PKG-INFO b/lib/ansible_core.egg-info/PKG-INFO index 90ea773..5cefb3a 100644 --- a/lib/ansible_core.egg-info/PKG-INFO +++ b/lib/ansible_core.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: ansible-core -Version: 2.17.0 +Version: 2.17.1 Summary: Radically simple IT automation Home-page: https://ansible.com/ Author: Ansible, Inc. diff --git a/lib/ansible_core.egg-info/SOURCES.txt b/lib/ansible_core.egg-info/SOURCES.txt index 343b795..6aa9d9d 100644 --- a/lib/ansible_core.egg-info/SOURCES.txt +++ b/lib/ansible_core.egg-info/SOURCES.txt @@ -3452,9 +3452,12 @@ test/integration/targets/setup_win_printargv/files/PrintArgv.cs test/integration/targets/setup_win_printargv/meta/main.yml test/integration/targets/setup_win_printargv/tasks/main.yml test/integration/targets/shell/aliases +test/integration/targets/shell/test-command-building-playbook.yml test/integration/targets/shell/action_plugins/test_shell.py test/integration/targets/shell/connection_plugins/test_connection_default.py test/integration/targets/shell/connection_plugins/test_connection_override.py +test/integration/targets/shell/meta/main.yml +test/integration/targets/shell/tasks/command-building.yml test/integration/targets/shell/tasks/main.yml test/integration/targets/slurp/aliases test/integration/targets/slurp/files/bar.bin diff --git a/test/integration/targets/any_errors_fatal/80981.yml b/test/integration/targets/any_errors_fatal/80981.yml index 51cf8df..5654aa1 100644 --- a/test/integration/targets/any_errors_fatal/80981.yml +++ b/test/integration/targets/any_errors_fatal/80981.yml @@ -11,7 +11,7 @@ rescue: - name: Rescues both hosts debug: - msg: rescue + msg: rescuedd - name: You can recover from fatal errors by adding a rescue section to the block. debug: msg: recovered diff --git a/test/integration/targets/any_errors_fatal/runme.sh b/test/integration/targets/any_errors_fatal/runme.sh index 58f0ddf..55381a7 100755 --- a/test/integration/targets/any_errors_fatal/runme.sh +++ b/test/integration/targets/any_errors_fatal/runme.sh @@ -15,8 +15,6 @@ if [ "${res}" -eq 0 ] ; then exit 1 fi -set -ux - ansible-playbook -i inventory "$@" always_block.yml | tee out.txt | grep 'any_errors_fatal_always_block_start' res=$? cat out.txt @@ -25,8 +23,6 @@ if [ "${res}" -ne 0 ] ; then exit 1 fi -set -ux - for test_name in test_include_role test_include_tasks; do ansible-playbook -i inventory "$@" -e test_name=$test_name 50897.yml | tee out.txt | grep 'any_errors_fatal_this_should_never_be_reached' res=$? @@ -36,6 +32,8 @@ for test_name in test_include_role test_include_tasks; do fi done +set -e + ansible-playbook -i inventory "$@" 31543.yml | tee out.txt [ "$(grep -c 'SHOULD NOT HAPPEN' out.txt)" -eq 0 ] @@ -47,5 +45,5 @@ ansible-playbook -i inventory "$@" 73246.yml | tee out.txt ansible-playbook -i inventory "$@" 80981.yml | tee out.txt [ "$(grep -c 'SHOULD NOT HAPPEN' out.txt)" -eq 0 ] -[ "$(grep -c 'rescue' out.txt)" -eq 2 ] +[ "$(grep -c 'rescuedd' out.txt)" -eq 2 ] [ "$(grep -c 'recovered' out.txt)" -eq 2 ] diff --git a/test/integration/targets/collections/test_task_resolved_plugin/unqualified_and_collections_kw.yml b/test/integration/targets/collections/test_task_resolved_plugin/unqualified_and_collections_kw.yml index 5af4eda..ac70778 100644 --- a/test/integration/targets/collections/test_task_resolved_plugin/unqualified_and_collections_kw.yml +++ b/test/integration/targets/collections/test_task_resolved_plugin/unqualified_and_collections_kw.yml @@ -10,5 +10,7 @@ - ping: - collection_action: - collection_module: - - formerly_action: - - formerly_module: + - local_action: + module: formerly_action + - action: + module: formerly_module diff --git a/test/integration/targets/json_cleanup/library/bad_json b/test/integration/targets/json_cleanup/library/bad_json index 1df8c72..e84de04 100644 --- a/test/integration/targets/json_cleanup/library/bad_json +++ b/test/integration/targets/json_cleanup/library/bad_json @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash set -eu diff --git a/test/integration/targets/json_cleanup/module_output_cleaning.yml b/test/integration/targets/json_cleanup/module_output_cleaning.yml index 165352a..94ae9c1 100644 --- a/test/integration/targets/json_cleanup/module_output_cleaning.yml +++ b/test/integration/targets/json_cleanup/module_output_cleaning.yml @@ -2,10 +2,17 @@ hosts: localhost gather_facts: false tasks: + - name: where is bash + shell: command -v bash + register: bash + changed_when: false + - name: call module that spews extra stuff bad_json: register: clean_json ignore_errors: true + vars: + ansible_bash_interpreter: '{{ bash.stdout }}' - name: all expected is there assert: diff --git a/test/integration/targets/module_defaults/tasks/main.yml b/test/integration/targets/module_defaults/tasks/main.yml index 747c2f9..0483278 100644 --- a/test/integration/targets/module_defaults/tasks/main.yml +++ b/test/integration/targets/module_defaults/tasks/main.yml @@ -10,16 +10,23 @@ - debug: register: foo + - local_action: + module: debug + register: local_action_foo + - name: test that 'debug' task used default 'msg' param assert: - that: foo.msg == "test default" + that: + - foo.msg == "test default" + - local_action_foo.msg == "test default" - name: remove test file file: state: absent - name: touch test file - file: + local_action: + module: file state: touch - name: stat test file diff --git a/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 b/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 index 625cb21..27b0d10 100644 --- a/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 +++ b/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 @@ -1323,6 +1323,37 @@ test_no_log - Invoked with: $actual | Assert-DictionaryEqual -Expected $expected } + "Run with exec wrapper warnings" = { + Set-Variable -Name complex_args -Scope Global -Value @{ + _ansible_exec_wrapper_warnings = [System.Collections.Generic.List[string]]@( + 'Warning 1', + 'Warning 2' + ) + } + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + $m.Warn("Warning 3") + + $failed = $false + try { + $m.ExitJson() + } + catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equal -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_.Exception.InnerException.Output) + } + $failed | Assert-Equal -Expected $true + + $expected = @{ + changed = $false + invocation = @{ + module_args = @{} + } + warnings = @("Warning 1", "Warning 2", "Warning 3") + } + $actual | Assert-DictionaryEqual -Expected $expected + } + "FailJson with message" = { $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) diff --git a/test/integration/targets/shell/meta/main.yml b/test/integration/targets/shell/meta/main.yml new file mode 100644 index 0000000..cb6005d --- /dev/null +++ b/test/integration/targets/shell/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - prepare_tests + - setup_remote_tmp_dir diff --git a/test/integration/targets/shell/tasks/command-building.yml b/test/integration/targets/shell/tasks/command-building.yml new file mode 100644 index 0000000..bd45261 --- /dev/null +++ b/test/integration/targets/shell/tasks/command-building.yml @@ -0,0 +1,53 @@ +- vars: + atd: '{{ remote_tmp_dir }}/shell/t m p' + api: '{{ remote_tmp_dir }}/shell/p y t h o n' + block: + - name: create test dir + file: + path: '{{ atd|dirname }}' + state: directory + + - name: create tempdir with spaces + file: + path: '{{ atd }}' + state: directory + + - name: create symlink for ansible_python_interpreter to file with spaces + file: + dest: '{{ api }}' + src: '{{ ansible_facts.python.executable }}' + state: link + + - name: run simple test playbook + command: >- + ansible-playbook -vvv -i inventory + -e 'ansible_python_interpreter="{{ api }}"' + -e 'ansible_pipelining=0' + "{{ role_path }}/test-command-building-playbook.yml" + environment: + ANSIBLE_REMOTE_TMP: '{{ atd }}' + ANSIBLE_NOCOLOR: "1" + ANSIBLE_FORCE_COLOR: "0" + register: command_building + delegate_to: localhost + + - debug: + var: command_building.stdout_lines + + - block: + - debug: + var: py_cmd + + - debug: + var: tmp_dir + + - assert: + that: + - py_cmd in exec_line + - tmp_dir in exec_line + vars: + exec_line: '{{ command_building.stdout_lines | select("search", "EXEC.*p y t h o n") | first }}' + py_cmd: >- + '"'"'{{ api }}'"'"' + tmp_dir: >- + '"'"'{{ atd }}/ansible-tmp- diff --git a/test/integration/targets/shell/tasks/main.yml b/test/integration/targets/shell/tasks/main.yml index d6f2a2b..7a442f4 100644 --- a/test/integration/targets/shell/tasks/main.yml +++ b/test/integration/targets/shell/tasks/main.yml @@ -34,3 +34,5 @@ with_items: - powershell - sh + +- import_tasks: command-building.yml diff --git a/test/integration/targets/shell/test-command-building-playbook.yml b/test/integration/targets/shell/test-command-building-playbook.yml new file mode 100644 index 0000000..f77cf4c --- /dev/null +++ b/test/integration/targets/shell/test-command-building-playbook.yml @@ -0,0 +1,4 @@ +- hosts: testhost + gather_facts: false + tasks: + - ping: diff --git a/test/integration/targets/win_exec_wrapper/tasks/main.yml b/test/integration/targets/win_exec_wrapper/tasks/main.yml index f1342c4..75da3d6 100644 --- a/test/integration/targets/win_exec_wrapper/tasks/main.yml +++ b/test/integration/targets/win_exec_wrapper/tasks/main.yml @@ -194,16 +194,33 @@ become_test_username: ansible_become_test gen_pw: "{{ 'password123!' + lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}" -- name: create unprivileged user - win_user: - name: "{{ become_test_username }}" - password: "{{ gen_pw }}" - update_password: always - groups: Users - register: become_test_user_result - - name: execute tests and ensure that test user is deleted regardless of success/failure block: + - name: create unprivileged user + win_user: + name: "{{ become_test_username }}" + password: "{{ gen_pw }}" + update_password: always + groups: Users + register: become_test_user_result + + - name: create tempdir for test user + win_file: + path: C:\Windows\TEMP\test-dir + state: directory + + - name: deny delete permissions on new temp dir for test user + win_acl: + path: C:\Windows\TEMP\test-dir + user: '{{ become_test_user_result.sid }}' + type: '{{ item.type }}' + rights: '{{ item.rights }}' + loop: + - type: allow + rights: ListDirectory, CreateFiles, CreateDirectories, ReadAttributes, ReadExtendedAttributes, WriteData, WriteAttributes, WriteExtendedAttributes, Synchronize + - type: deny + rights: DeleteSubdirectoriesAndFiles, Delete + - name: ensure current user is not the become user win_shell: whoami register: whoami_out @@ -238,6 +255,21 @@ - become_system is successful - become_system.output == become_test_user_result.sid + - name: run module with tempdir with no delete access + win_ping: + register: temp_deletion_warning + vars: + <<: *become_vars + ansible_remote_tmp: C:\Windows\TEMP\test-dir + + - name: assert warning about tmpdir deletion is present + assert: + that: + - temp_deletion_warning.warnings | count == 1 + - >- + temp_deletion_warning.warnings[0] is + regex("(?i).*Failed to cleanup temporary directory 'C:\\\\Windows\\\\TEMP\\\\test-dir\\\\.*' used for compiling C# code\\. Files may still be present after the task is complete\\..*") + always: - name: ensure test user is deleted win_user: @@ -249,7 +281,12 @@ win_shell: rmdir /S /Q {{ profile_dir_out.stdout_lines[0] }} args: executable: cmd.exe - when: become_test_username in profile_dir_out.stdout_lines[0] + when: become_test_username in profile_dir_out.stdout_lines[0] | default("") + + - name: remove test tempdir + win_file: + path: C:\Windows\TEMP\test-dir + state: absent - name: test common functions in exec test_common_functions: diff --git a/test/lib/ansible_test/_internal/pypi_proxy.py b/test/lib/ansible_test/_internal/pypi_proxy.py index d119efa..1c9a9aa 100644 --- a/test/lib/ansible_test/_internal/pypi_proxy.py +++ b/test/lib/ansible_test/_internal/pypi_proxy.py @@ -69,7 +69,7 @@ def run_pypi_proxy(args: EnvironmentConfig, targets_use_pypi: bool) -> None: display.warning('Unable to use the PyPI proxy because Docker is not available. Installation of packages using `pip` may fail.') return - image = 'quay.io/ansible/pypi-test-container:2.0.0' + image = 'quay.io/ansible/pypi-test-container:3.1.0' port = 3141 run_support_container( diff --git a/test/units/playbook/test_block.py b/test/units/playbook/test_block.py index aac5f71..3c4dfb1 100644 --- a/test/units/playbook/test_block.py +++ b/test/units/playbook/test_block.py @@ -20,6 +20,10 @@ from __future__ import annotations import unittest from ansible.playbook.block import Block from ansible.playbook.task import Task +from ansible.plugins.loader import init_plugin_loader + + +init_plugin_loader() class TestBlock(unittest.TestCase): diff --git a/test/units/playbook/test_helpers.py b/test/units/playbook/test_helpers.py index 2977b0d..fb6170c 100644 --- a/test/units/playbook/test_helpers.py +++ b/test/units/playbook/test_helpers.py @@ -24,13 +24,16 @@ from unittest.mock import MagicMock from units.mock.loader import DictDataLoader from ansible import errors +from ansible.playbook import helpers from ansible.playbook.block import Block from ansible.playbook.handler import Handler from ansible.playbook.task import Task from ansible.playbook.task_include import TaskInclude from ansible.playbook.role.include import RoleInclude +from ansible.plugins.loader import init_plugin_loader -from ansible.playbook import helpers + +init_plugin_loader() class MixinForMocks(object): diff --git a/test/units/utils/display/test_broken_cowsay.py b/test/units/utils/display/test_broken_cowsay.py index 854b78b..50691c2 100644 --- a/test/units/utils/display/test_broken_cowsay.py +++ b/test/units/utils/display/test_broken_cowsay.py @@ -10,13 +10,12 @@ from unittest.mock import MagicMock def test_display_with_fake_cowsay_binary(capsys, mocker): - display = Display() mocker.patch("ansible.constants.ANSIBLE_COW_PATH", "./cowsay.sh") - mock_popen = MagicMock() mock_popen.return_value.returncode = 1 mocker.patch("subprocess.Popen", mock_popen) + display = Display() assert not hasattr(display, "cows_available") assert display.b_cowsay is None |