summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcos <cos>2024-04-20 10:25:11 +0200
committercos <cos>2024-04-20 10:25:11 +0200
commita7de3345cf462cb3ff1bb70f13c1a032dc885489 (patch)
tree7e33268314fe23fda65d9138bf5538d2952b9831
parent5e80ff5a990b0c26652e40c7b0da666dc9fdfbc1 (diff)
downloaddebian-ansible-core-debian/second-failed-recreation-attempt.zip
-rw-r--r--debian/patches/"Unexpected-changes"47553
1 files changed, 47553 insertions, 0 deletions
diff --git a/debian/patches/"Unexpected-changes" b/debian/patches/"Unexpected-changes"
new file mode 100644
index 00000000..3b8cfd07
--- /dev/null
+++ b/debian/patches/"Unexpected-changes"
@@ -0,0 +1,47553 @@
+Description: gbp buildpackage appears to refuse building without this
+ .
+ Likely, the tool didn't love parsing past the explanation in db5b595.
+ .
+ ansible-core (2.16.5-1) unstable; urgency=medium
+ .
+ * New upstream release (Closes: #1057640)
+ - ansible-test supports python 3.12 now (Closes: #1061782, #1061781)
+ - fixes CVE-2024-0690 (Closes: #1061156)
+ * Drop dependency on python3-distutils (Closes: #1065826)
+ * Drop patches applied upstream
+ * Refreshen patches
+Author: Lee Garrett <debian@rocketjump.eu>
+Bug-Debian: https://bugs.debian.org/1057640
+Bug-Debian: https://bugs.debian.org/1061156
+Bug-Debian: https://bugs.debian.org/1061781
+Bug-Debian: https://bugs.debian.org/1061782
+Bug-Debian: https://bugs.debian.org/1065826
+
+---
+The information above should follow the Patch Tagging Guidelines, please
+checkout https://dep.debian.net/deps/dep3/ to learn about the format. Here
+are templates for supplementary fields that you might want to add:
+
+Origin: (upstream|backport|vendor|other), (<patch-url>|commit:<commit-id>)
+Bug: <upstream-bugtracker-url>
+Bug-Debian: https://bugs.debian.org/<bugnumber>
+Bug-Ubuntu: https://launchpad.net/bugs/<bugnumber>
+Forwarded: (no|not-needed|<patch-forwarded-url>)
+Applied-Upstream: <version>, (<commit-url>|commit:<commid-id>)
+Reviewed-By: <name and email of someone who approved/reviewed the patch>
+Last-Update: 2024-04-20
+
+--- ansible-core-2.16.5.orig/COPYING
++++ ansible-core-2.16.5/COPYING
+@@ -1,7 +1,7 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+- Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
++ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+@@ -645,7 +645,7 @@ the "copyright" line and a pointer to wh
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+- along with this program. If not, see <https://www.gnu.org/licenses/>.
++ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+ Also add information on how to contact you by electronic and paper mail.
+
+@@ -664,11 +664,12 @@ might be different; for a GUI interface,
+ You should also get your employer (if you work as a programmer) or school,
+ if any, to sign a "copyright disclaimer" for the program, if necessary.
+ For more information on this, and how to apply and follow the GNU GPL, see
+-<https://www.gnu.org/licenses/>.
++<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+ into proprietary programs. If your program is a subroutine library, you
+ may consider it more useful to permit linking proprietary applications with
+ the library. If this is what you want to do, use the GNU Lesser General
+ Public License instead of this License. But first, please read
+-<https://www.gnu.org/licenses/why-not-lgpl.html>.
++<http://www.gnu.org/philosophy/why-not-lgpl.html>.
++
+--- ansible-core-2.16.5.orig/MANIFEST.in
++++ ansible-core-2.16.5/MANIFEST.in
+@@ -5,6 +5,7 @@ include changelogs/changelog.yaml
+ include licenses/*.txt
+ include requirements.txt
+ recursive-include packaging *.py *.j2
++recursive-include test/ansible_test *.py Makefile
+ recursive-include test/integration *
+ recursive-include test/sanity *.in *.json *.py *.txt
+ recursive-include test/support *.py *.ps1 *.psm1 *.cs *.md
+--- ansible-core-2.16.5.orig/PKG-INFO
++++ ansible-core-2.16.5/PKG-INFO
+@@ -1,6 +1,6 @@
+ Metadata-Version: 2.1
+ Name: ansible-core
+-Version: 2.16.5
++Version: 2.14.13
+ Summary: Radically simple IT automation
+ Home-page: https://ansible.com/
+ Author: Ansible, Inc.
+@@ -21,21 +21,21 @@ Classifier: License :: OSI Approved :: G
+ Classifier: Natural Language :: English
+ Classifier: Operating System :: POSIX
+ Classifier: Programming Language :: Python :: 3
++Classifier: Programming Language :: Python :: 3.9
+ Classifier: Programming Language :: Python :: 3.10
+ Classifier: Programming Language :: Python :: 3.11
+-Classifier: Programming Language :: Python :: 3.12
+ Classifier: Programming Language :: Python :: 3 :: Only
+ Classifier: Topic :: System :: Installation/Setup
+ Classifier: Topic :: System :: Systems Administration
+ Classifier: Topic :: Utilities
+-Requires-Python: >=3.10
++Requires-Python: >=3.9
+ Description-Content-Type: text/markdown
+ License-File: COPYING
+ Requires-Dist: jinja2>=3.0.0
+ Requires-Dist: PyYAML>=5.1
+ Requires-Dist: cryptography
+ Requires-Dist: packaging
+-Requires-Dist: resolvelib<1.1.0,>=0.5.3
++Requires-Dist: resolvelib<0.9.0,>=0.5.3
+
+ [![PyPI version](https://img.shields.io/pypi/v/ansible-core.svg)](https://pypi.org/project/ansible-core)
+ [![Docs badge](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://docs.ansible.com/ansible/latest/)
+--- ansible-core-2.16.5.orig/bin/ansible
++++ ansible-core-2.16.5/bin/ansible
+@@ -14,7 +14,7 @@ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.errors import AnsibleError, AnsibleOptionsError, AnsibleParserError
+ from ansible.executor.task_queue_manager import TaskQueueManager
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.parsing.splitter import parse_kv
+ from ansible.parsing.utils.yaml import from_yaml
+ from ansible.playbook import Playbook
+--- ansible-core-2.16.5.orig/bin/ansible-config
++++ ansible-core-2.16.5/bin/ansible-config
+@@ -23,7 +23,7 @@ from ansible import constants as C
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.config.manager import ConfigManager, Setting
+ from ansible.errors import AnsibleError, AnsibleOptionsError
+-from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
++from ansible.module_utils._text import to_native, to_text, to_bytes
+ from ansible.module_utils.common.json import json_dump
+ from ansible.module_utils.six import string_types
+ from ansible.parsing.quoting import is_quoted
+@@ -67,7 +67,7 @@ class ConfigCLI(CLI):
+ desc="View ansible configuration.",
+ )
+
+- common = opt_help.ArgumentParser(add_help=False)
++ common = opt_help.argparse.ArgumentParser(add_help=False)
+ opt_help.add_verbosity_options(common)
+ common.add_argument('-c', '--config', dest='config_file',
+ help="path to configuration file, defaults to first file found in precedence.")
+@@ -187,7 +187,7 @@ class ConfigCLI(CLI):
+
+ # pylint: disable=unreachable
+ try:
+- editor = shlex.split(C.config.get_config_value('EDITOR'))
++ editor = shlex.split(os.environ.get('EDITOR', 'vi'))
+ editor.append(self.config_file)
+ subprocess.call(editor)
+ except Exception as e:
+@@ -314,7 +314,7 @@ class ConfigCLI(CLI):
+
+ return data
+
+- def _get_settings_ini(self, settings, seen):
++ def _get_settings_ini(self, settings):
+
+ sections = {}
+ for o in sorted(settings.keys()):
+@@ -327,7 +327,7 @@ class ConfigCLI(CLI):
+
+ if not opt.get('description'):
+ # its a plugin
+- new_sections = self._get_settings_ini(opt, seen)
++ new_sections = self._get_settings_ini(opt)
+ for s in new_sections:
+ if s in sections:
+ sections[s].extend(new_sections[s])
+@@ -343,45 +343,37 @@ class ConfigCLI(CLI):
+
+ if 'ini' in opt and opt['ini']:
+ entry = opt['ini'][-1]
+- if entry['section'] not in seen:
+- seen[entry['section']] = []
+ if entry['section'] not in sections:
+ sections[entry['section']] = []
+
+- # avoid dupes
+- if entry['key'] not in seen[entry['section']]:
+- seen[entry['section']].append(entry['key'])
+-
+- default = opt.get('default', '')
+- if opt.get('type', '') == 'list' and not isinstance(default, string_types):
+- # python lists are not valid ini ones
+- default = ', '.join(default)
+- elif default is None:
+- default = ''
+-
+- if context.CLIARGS['commented']:
+- entry['key'] = ';%s' % entry['key']
++ default = opt.get('default', '')
++ if opt.get('type', '') == 'list' and not isinstance(default, string_types):
++ # python lists are not valid ini ones
++ default = ', '.join(default)
++ elif default is None:
++ default = ''
+
+- key = desc + '\n%s=%s' % (entry['key'], default)
++ if context.CLIARGS['commented']:
++ entry['key'] = ';%s' % entry['key']
+
+- sections[entry['section']].append(key)
++ key = desc + '\n%s=%s' % (entry['key'], default)
++ sections[entry['section']].append(key)
+
+ return sections
+
+ def execute_init(self):
+ """Create initial configuration"""
+
+- seen = {}
+ data = []
+ config_entries = self._list_entries_from_args()
+ plugin_types = config_entries.pop('PLUGINS', None)
+
+ if context.CLIARGS['format'] == 'ini':
+- sections = self._get_settings_ini(config_entries, seen)
++ sections = self._get_settings_ini(config_entries)
+
+ if plugin_types:
+ for ptype in plugin_types:
+- plugin_sections = self._get_settings_ini(plugin_types[ptype], seen)
++ plugin_sections = self._get_settings_ini(plugin_types[ptype])
+ for s in plugin_sections:
+ if s in sections:
+ sections[s].extend(plugin_sections[s])
+--- ansible-core-2.16.5.orig/bin/ansible-connection
++++ ansible-core-2.16.5/bin/ansible-connection
+@@ -6,6 +6,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+
++import argparse
+ import fcntl
+ import hashlib
+ import io
+@@ -23,12 +24,12 @@ from contextlib import contextmanager
+
+ from ansible import constants as C
+ from ansible.cli.arguments import option_helpers as opt_help
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.module_utils.connection import Connection, ConnectionError, send_data, recv_data
+ from ansible.module_utils.service import fork_process
+ from ansible.parsing.ajson import AnsibleJSONEncoder, AnsibleJSONDecoder
+ from ansible.playbook.play_context import PlayContext
+-from ansible.plugins.loader import connection_loader, init_plugin_loader
++from ansible.plugins.loader import connection_loader
+ from ansible.utils.path import unfrackpath, makedirs_safe
+ from ansible.utils.display import Display
+ from ansible.utils.jsonrpc import JsonRpcServer
+@@ -229,7 +230,6 @@ def main(args=None):
+ parser.add_argument('playbook_pid')
+ parser.add_argument('task_uuid')
+ args = parser.parse_args(args[1:] if args is not None else args)
+- init_plugin_loader()
+
+ # initialize verbosity
+ display.verbosity = args.verbosity
+--- ansible-core-2.16.5.orig/bin/ansible-console
++++ ansible-core-2.16.5/bin/ansible-console
+@@ -22,7 +22,7 @@ from ansible import constants as C
+ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.executor.task_queue_manager import TaskQueueManager
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.parsing.splitter import parse_kv
+ from ansible.playbook.play import Play
+@@ -39,30 +39,26 @@ class ConsoleCLI(CLI, cmd.Cmd):
+ '''
+ A REPL that allows for running ad-hoc tasks against a chosen inventory
+ from a nice shell with built-in tab completion (based on dominis'
+- ``ansible-shell``).
++ ansible-shell).
+
+ It supports several commands, and you can modify its configuration at
+ runtime:
+
+- - ``cd [pattern]``: change host/group
+- (you can use host patterns eg.: ``app*.dc*:!app01*``)
+- - ``list``: list available hosts in the current path
+- - ``list groups``: list groups included in the current path
+- - ``become``: toggle the become flag
+- - ``!``: forces shell module instead of the ansible module
+- (``!yum update -y``)
+- - ``verbosity [num]``: set the verbosity level
+- - ``forks [num]``: set the number of forks
+- - ``become_user [user]``: set the become_user
+- - ``remote_user [user]``: set the remote_user
+- - ``become_method [method]``: set the privilege escalation method
+- - ``check [bool]``: toggle check mode
+- - ``diff [bool]``: toggle diff mode
+- - ``timeout [integer]``: set the timeout of tasks in seconds
+- (0 to disable)
+- - ``help [command/module]``: display documentation for
+- the command or module
+- - ``exit``: exit ``ansible-console``
++ - `cd [pattern]`: change host/group (you can use host patterns eg.: app*.dc*:!app01*)
++ - `list`: list available hosts in the current path
++ - `list groups`: list groups included in the current path
++ - `become`: toggle the become flag
++ - `!`: forces shell module instead of the ansible module (!yum update -y)
++ - `verbosity [num]`: set the verbosity level
++ - `forks [num]`: set the number of forks
++ - `become_user [user]`: set the become_user
++ - `remote_user [user]`: set the remote_user
++ - `become_method [method]`: set the privilege escalation method
++ - `check [bool]`: toggle check mode
++ - `diff [bool]`: toggle diff mode
++ - `timeout [integer]`: set the timeout of tasks in seconds (0 to disable)
++ - `help [command/module]`: display documentation for the command or module
++ - `exit`: exit ansible-console
+ '''
+
+ name = 'ansible-console'
+--- ansible-core-2.16.5.orig/bin/ansible-doc
++++ ansible-core-2.16.5/bin/ansible-doc
+@@ -26,7 +26,7 @@ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.collections.list import list_collection_dirs
+ from ansible.errors import AnsibleError, AnsibleOptionsError, AnsibleParserError, AnsiblePluginNotFound
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.common.collections import is_sequence
+ from ansible.module_utils.common.json import json_dump
+ from ansible.module_utils.common.yaml import yaml_dump
+@@ -163,8 +163,8 @@ class RoleMixin(object):
+ might be fully qualified with the collection name (e.g., community.general.roleA)
+ or not (e.g., roleA).
+
+- :param collection_filter: A list of strings containing the FQCN of a collection which will
+- be used to limit results. This filter will take precedence over the name_filters.
++ :param collection_filter: A string containing the FQCN of a collection which will be
++ used to limit results. This filter will take precedence over the name_filters.
+
+ :returns: A set of tuples consisting of: role name, collection name, collection path
+ """
+@@ -362,23 +362,12 @@ class DocCLI(CLI, RoleMixin):
+ _ITALIC = re.compile(r"\bI\(([^)]+)\)")
+ _BOLD = re.compile(r"\bB\(([^)]+)\)")
+ _MODULE = re.compile(r"\bM\(([^)]+)\)")
+- _PLUGIN = re.compile(r"\bP\(([^#)]+)#([a-z]+)\)")
+ _LINK = re.compile(r"\bL\(([^)]+), *([^)]+)\)")
+ _URL = re.compile(r"\bU\(([^)]+)\)")
+ _REF = re.compile(r"\bR\(([^)]+), *([^)]+)\)")
+ _CONST = re.compile(r"\bC\(([^)]+)\)")
+- _SEM_PARAMETER_STRING = r"\(((?:[^\\)]+|\\.)+)\)"
+- _SEM_OPTION_NAME = re.compile(r"\bO" + _SEM_PARAMETER_STRING)
+- _SEM_OPTION_VALUE = re.compile(r"\bV" + _SEM_PARAMETER_STRING)
+- _SEM_ENV_VARIABLE = re.compile(r"\bE" + _SEM_PARAMETER_STRING)
+- _SEM_RET_VALUE = re.compile(r"\bRV" + _SEM_PARAMETER_STRING)
+ _RULER = re.compile(r"\bHORIZONTALLINE\b")
+
+- # helper for unescaping
+- _UNESCAPE = re.compile(r"\\(.)")
+- _FQCN_TYPE_PREFIX_RE = re.compile(r'^([^.]+\.[^.]+\.[^#]+)#([a-z]+):(.*)$')
+- _IGNORE_MARKER = 'ignore:'
+-
+ # rst specific
+ _RST_NOTE = re.compile(r".. note::")
+ _RST_SEEALSO = re.compile(r".. seealso::")
+@@ -390,40 +379,6 @@ class DocCLI(CLI, RoleMixin):
+ super(DocCLI, self).__init__(args)
+ self.plugin_list = set()
+
+- @staticmethod
+- def _tty_ify_sem_simle(matcher):
+- text = DocCLI._UNESCAPE.sub(r'\1', matcher.group(1))
+- return f"`{text}'"
+-
+- @staticmethod
+- def _tty_ify_sem_complex(matcher):
+- text = DocCLI._UNESCAPE.sub(r'\1', matcher.group(1))
+- value = None
+- if '=' in text:
+- text, value = text.split('=', 1)
+- m = DocCLI._FQCN_TYPE_PREFIX_RE.match(text)
+- if m:
+- plugin_fqcn = m.group(1)
+- plugin_type = m.group(2)
+- text = m.group(3)
+- elif text.startswith(DocCLI._IGNORE_MARKER):
+- text = text[len(DocCLI._IGNORE_MARKER):]
+- plugin_fqcn = plugin_type = ''
+- else:
+- plugin_fqcn = plugin_type = ''
+- entrypoint = None
+- if ':' in text:
+- entrypoint, text = text.split(':', 1)
+- if value is not None:
+- text = f"{text}={value}"
+- if plugin_fqcn and plugin_type:
+- plugin_suffix = '' if plugin_type in ('role', 'module', 'playbook') else ' plugin'
+- plugin = f"{plugin_type}{plugin_suffix} {plugin_fqcn}"
+- if plugin_type == 'role' and entrypoint is not None:
+- plugin = f"{plugin}, {entrypoint} entrypoint"
+- return f"`{text}' (of {plugin})"
+- return f"`{text}'"
+-
+ @classmethod
+ def find_plugins(cls, path, internal, plugin_type, coll_filter=None):
+ display.deprecated("find_plugins method as it is incomplete/incorrect. use ansible.plugins.list functions instead.", version='2.17')
+@@ -438,13 +393,8 @@ class DocCLI(CLI, RoleMixin):
+ t = cls._MODULE.sub("[" + r"\1" + "]", t) # M(word) => [word]
+ t = cls._URL.sub(r"\1", t) # U(word) => word
+ t = cls._LINK.sub(r"\1 <\2>", t) # L(word, url) => word <url>
+- t = cls._PLUGIN.sub("[" + r"\1" + "]", t) # P(word#type) => [word]
+ t = cls._REF.sub(r"\1", t) # R(word, sphinx-ref) => word
+ t = cls._CONST.sub(r"`\1'", t) # C(word) => `word'
+- t = cls._SEM_OPTION_NAME.sub(cls._tty_ify_sem_complex, t) # O(expr)
+- t = cls._SEM_OPTION_VALUE.sub(cls._tty_ify_sem_simle, t) # V(expr)
+- t = cls._SEM_ENV_VARIABLE.sub(cls._tty_ify_sem_simle, t) # E(expr)
+- t = cls._SEM_RET_VALUE.sub(cls._tty_ify_sem_complex, t) # RV(expr)
+ t = cls._RULER.sub("\n{0}\n".format("-" * 13), t) # HORIZONTALLINE => -------
+
+ # remove rst
+@@ -545,9 +495,7 @@ class DocCLI(CLI, RoleMixin):
+ desc = desc[:linelimit] + '...'
+
+ pbreak = plugin.split('.')
+- # TODO: add mark for deprecated collection plugins
+- if pbreak[-1].startswith('_') and plugin.startswith(('ansible.builtin.', 'ansible.legacy.')):
+- # Handle deprecated ansible.builtin plugins
++ if pbreak[-1].startswith('_'): # Handle deprecated # TODO: add mark for deprecated collection plugins
+ pbreak[-1] = pbreak[-1][1:]
+ plugin = '.'.join(pbreak)
+ deprecated.append("%-*s %-*.*s" % (displace, plugin, linelimit, len(desc), desc))
+@@ -678,11 +626,12 @@ class DocCLI(CLI, RoleMixin):
+ def _get_collection_filter(self):
+
+ coll_filter = None
+- if len(context.CLIARGS['args']) >= 1:
+- coll_filter = context.CLIARGS['args']
+- for coll_name in coll_filter:
+- if not AnsibleCollectionRef.is_valid_collection_name(coll_name):
+- raise AnsibleError('Invalid collection name (must be of the form namespace.collection): {0}'.format(coll_name))
++ if len(context.CLIARGS['args']) == 1:
++ coll_filter = context.CLIARGS['args'][0]
++ if not AnsibleCollectionRef.is_valid_collection_name(coll_filter):
++ raise AnsibleError('Invalid collection name (must be of the form namespace.collection): {0}'.format(coll_filter))
++ elif len(context.CLIARGS['args']) > 1:
++ raise AnsibleOptionsError("Only a single collection filter is supported.")
+
+ return coll_filter
+
+@@ -1302,20 +1251,6 @@ class DocCLI(CLI, RoleMixin):
+ relative_url = 'collections/%s_module.html' % item['module'].replace('.', '/', 2)
+ text.append(textwrap.fill(DocCLI.tty_ify(get_versioned_doclink(relative_url)),
+ limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent))
+- elif 'plugin' in item and 'plugin_type' in item:
+- plugin_suffix = ' plugin' if item['plugin_type'] not in ('module', 'role') else ''
+- text.append(textwrap.fill(DocCLI.tty_ify('%s%s %s' % (item['plugin_type'].title(), plugin_suffix, item['plugin'])),
+- limit - 6, initial_indent=opt_indent[:-2] + "* ", subsequent_indent=opt_indent))
+- description = item.get('description')
+- if description is None and item['plugin'].startswith('ansible.builtin.'):
+- description = 'The official documentation on the %s %s%s.' % (item['plugin'], item['plugin_type'], plugin_suffix)
+- if description is not None:
+- text.append(textwrap.fill(DocCLI.tty_ify(description),
+- limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent + ' '))
+- if item['plugin'].startswith('ansible.builtin.'):
+- relative_url = 'collections/%s_%s.html' % (item['plugin'].replace('.', '/', 2), item['plugin_type'])
+- text.append(textwrap.fill(DocCLI.tty_ify(get_versioned_doclink(relative_url)),
+- limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent))
+ elif 'name' in item and 'link' in item and 'description' in item:
+ text.append(textwrap.fill(DocCLI.tty_ify(item['name']),
+ limit - 6, initial_indent=opt_indent[:-2] + "* ", subsequent_indent=opt_indent))
+--- ansible-core-2.16.5.orig/bin/ansible-galaxy
++++ ansible-core-2.16.5/bin/ansible-galaxy
+@@ -10,11 +10,9 @@ __metaclass__ = type
+ # ansible.cli needs to be imported first, to ensure the source bin/* scripts run that code first
+ from ansible.cli import CLI
+
+-import argparse
+ import functools
+ import json
+ import os.path
+-import pathlib
+ import re
+ import shutil
+ import sys
+@@ -53,7 +51,7 @@ from ansible.galaxy.token import BasicAu
+ from ansible.module_utils.ansible_release import __version__ as ansible_version
+ from ansible.module_utils.common.collections import is_iterable
+ from ansible.module_utils.common.yaml import yaml_dump, yaml_load
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils import six
+ from ansible.parsing.dataloader import DataLoader
+ from ansible.parsing.yaml.loader import AnsibleLoader
+@@ -73,7 +71,7 @@ SERVER_DEF = [
+ ('password', False, 'str'),
+ ('token', False, 'str'),
+ ('auth_url', False, 'str'),
+- ('api_version', False, 'int'),
++ ('v3', False, 'bool'),
+ ('validate_certs', False, 'bool'),
+ ('client_id', False, 'str'),
+ ('timeout', False, 'int'),
+@@ -81,9 +79,9 @@ SERVER_DEF = [
+
+ # config definition fields
+ SERVER_ADDITIONAL = {
+- 'api_version': {'default': None, 'choices': [2, 3]},
++ 'v3': {'default': 'False'},
+ 'validate_certs': {'cli': [{'name': 'validate_certs'}]},
+- 'timeout': {'default': C.GALAXY_SERVER_TIMEOUT, 'cli': [{'name': 'timeout'}]},
++ 'timeout': {'default': '60', 'cli': [{'name': 'timeout'}]},
+ 'token': {'default': None},
+ }
+
+@@ -101,8 +99,7 @@ def with_collection_artifacts_manager(wr
+ return wrapped_method(*args, **kwargs)
+
+ # FIXME: use validate_certs context from Galaxy servers when downloading collections
+- # .get used here for when this is used in a non-CLI context
+- artifacts_manager_kwargs = {'validate_certs': context.CLIARGS.get('resolved_validate_certs', True)}
++ artifacts_manager_kwargs = {'validate_certs': context.CLIARGS['resolved_validate_certs']}
+
+ keyring = context.CLIARGS.get('keyring', None)
+ if keyring is not None:
+@@ -159,8 +156,8 @@ def _get_collection_widths(collections):
+ fqcn_set = {to_text(c.fqcn) for c in collections}
+ version_set = {to_text(c.ver) for c in collections}
+
+- fqcn_length = len(max(fqcn_set or [''], key=len))
+- version_length = len(max(version_set or [''], key=len))
++ fqcn_length = len(max(fqcn_set, key=len))
++ version_length = len(max(version_set, key=len))
+
+ return fqcn_length, version_length
+
+@@ -241,49 +238,45 @@ class GalaxyCLI(CLI):
+ )
+
+ # Common arguments that apply to more than 1 action
+- common = opt_help.ArgumentParser(add_help=False)
++ common = opt_help.argparse.ArgumentParser(add_help=False)
+ common.add_argument('-s', '--server', dest='api_server', help='The Galaxy API server URL')
+- common.add_argument('--api-version', type=int, choices=[2, 3], help=argparse.SUPPRESS) # Hidden argument that should only be used in our tests
+ common.add_argument('--token', '--api-key', dest='api_key',
+ help='The Ansible Galaxy API key which can be found at '
+ 'https://galaxy.ansible.com/me/preferences.')
+ common.add_argument('-c', '--ignore-certs', action='store_true', dest='ignore_certs', help='Ignore SSL certificate validation errors.', default=None)
+-
+- # --timeout uses the default None to handle two different scenarios.
+- # * --timeout > C.GALAXY_SERVER_TIMEOUT for non-configured servers
+- # * --timeout > server-specific timeout > C.GALAXY_SERVER_TIMEOUT for configured servers.
+- common.add_argument('--timeout', dest='timeout', type=int,
++ common.add_argument('--timeout', dest='timeout', type=int, default=60,
+ help="The time to wait for operations against the galaxy server, defaults to 60s.")
+
+ opt_help.add_verbosity_options(common)
+
+- force = opt_help.ArgumentParser(add_help=False)
++ force = opt_help.argparse.ArgumentParser(add_help=False)
+ force.add_argument('-f', '--force', dest='force', action='store_true', default=False,
+ help='Force overwriting an existing role or collection')
+
+- github = opt_help.ArgumentParser(add_help=False)
++ github = opt_help.argparse.ArgumentParser(add_help=False)
+ github.add_argument('github_user', help='GitHub username')
+ github.add_argument('github_repo', help='GitHub repository')
+
+- offline = opt_help.ArgumentParser(add_help=False)
++ offline = opt_help.argparse.ArgumentParser(add_help=False)
+ offline.add_argument('--offline', dest='offline', default=False, action='store_true',
+ help="Don't query the galaxy API when creating roles")
+
+ default_roles_path = C.config.get_configuration_definition('DEFAULT_ROLES_PATH').get('default', '')
+- roles_path = opt_help.ArgumentParser(add_help=False)
++ roles_path = opt_help.argparse.ArgumentParser(add_help=False)
+ roles_path.add_argument('-p', '--roles-path', dest='roles_path', type=opt_help.unfrack_path(pathsep=True),
+ default=C.DEFAULT_ROLES_PATH, action=opt_help.PrependListAction,
+ help='The path to the directory containing your roles. The default is the first '
+ 'writable one configured via DEFAULT_ROLES_PATH: %s ' % default_roles_path)
+
+- collections_path = opt_help.ArgumentParser(add_help=False)
++ collections_path = opt_help.argparse.ArgumentParser(add_help=False)
+ collections_path.add_argument('-p', '--collections-path', dest='collections_path', type=opt_help.unfrack_path(pathsep=True),
++ default=AnsibleCollectionConfig.collection_paths,
+ action=opt_help.PrependListAction,
+ help="One or more directories to search for collections in addition "
+ "to the default COLLECTIONS_PATHS. Separate multiple paths "
+ "with '{0}'.".format(os.path.pathsep))
+
+- cache_options = opt_help.ArgumentParser(add_help=False)
++ cache_options = opt_help.argparse.ArgumentParser(add_help=False)
+ cache_options.add_argument('--clear-response-cache', dest='clear_response_cache', action='store_true',
+ default=False, help='Clear the existing server response cache.')
+ cache_options.add_argument('--no-cache', dest='no_cache', action='store_true', default=False,
+@@ -467,15 +460,12 @@ class GalaxyCLI(CLI):
+ valid_signature_count_help = 'The number of signatures that must successfully verify the collection. This should be a positive integer ' \
+ 'or all to signify that all signatures must be used to verify the collection. ' \
+ 'Prepend the value with + to fail if no valid signatures are found for the collection (e.g. +all).'
+- ignore_gpg_status_help = 'A space separated list of status codes to ignore during signature verification (for example, NO_PUBKEY FAILURE). ' \
+- 'Descriptions for the choices can be seen at L(https://github.com/gpg/gnupg/blob/master/doc/DETAILS#general-status-codes).' \
+- 'Note: specify these after positional arguments or use -- to separate them.'
++ ignore_gpg_status_help = 'A status code to ignore during signature verification (for example, NO_PUBKEY). ' \
++ 'Provide this option multiple times to ignore a list of status codes. ' \
++ 'Descriptions for the choices can be seen at L(https://github.com/gpg/gnupg/blob/master/doc/DETAILS#general-status-codes).'
+ verify_parser.add_argument('--required-valid-signature-count', dest='required_valid_signature_count', type=validate_signature_count,
+ help=valid_signature_count_help, default=C.GALAXY_REQUIRED_VALID_SIGNATURE_COUNT)
+ verify_parser.add_argument('--ignore-signature-status-code', dest='ignore_gpg_errors', type=str, action='append',
+- help=opt_help.argparse.SUPPRESS, default=C.GALAXY_IGNORE_INVALID_SIGNATURE_STATUS_CODES,
+- choices=list(GPG_ERROR_MAP.keys()))
+- verify_parser.add_argument('--ignore-signature-status-codes', dest='ignore_gpg_errors', type=str, action='extend', nargs='+',
+ help=ignore_gpg_status_help, default=C.GALAXY_IGNORE_INVALID_SIGNATURE_STATUS_CODES,
+ choices=list(GPG_ERROR_MAP.keys()))
+
+@@ -511,9 +501,9 @@ class GalaxyCLI(CLI):
+ valid_signature_count_help = 'The number of signatures that must successfully verify the collection. This should be a positive integer ' \
+ 'or -1 to signify that all signatures must be used to verify the collection. ' \
+ 'Prepend the value with + to fail if no valid signatures are found for the collection (e.g. +all).'
+- ignore_gpg_status_help = 'A space separated list of status codes to ignore during signature verification (for example, NO_PUBKEY FAILURE). ' \
+- 'Descriptions for the choices can be seen at L(https://github.com/gpg/gnupg/blob/master/doc/DETAILS#general-status-codes).' \
+- 'Note: specify these after positional arguments or use -- to separate them.'
++ ignore_gpg_status_help = 'A status code to ignore during signature verification (for example, NO_PUBKEY). ' \
++ 'Provide this option multiple times to ignore a list of status codes. ' \
++ 'Descriptions for the choices can be seen at L(https://github.com/gpg/gnupg/blob/master/doc/DETAILS#general-status-codes).'
+
+ if galaxy_type == 'collection':
+ install_parser.add_argument('-p', '--collections-path', dest='collections_path',
+@@ -537,9 +527,6 @@ class GalaxyCLI(CLI):
+ install_parser.add_argument('--required-valid-signature-count', dest='required_valid_signature_count', type=validate_signature_count,
+ help=valid_signature_count_help, default=C.GALAXY_REQUIRED_VALID_SIGNATURE_COUNT)
+ install_parser.add_argument('--ignore-signature-status-code', dest='ignore_gpg_errors', type=str, action='append',
+- help=opt_help.argparse.SUPPRESS, default=C.GALAXY_IGNORE_INVALID_SIGNATURE_STATUS_CODES,
+- choices=list(GPG_ERROR_MAP.keys()))
+- install_parser.add_argument('--ignore-signature-status-codes', dest='ignore_gpg_errors', type=str, action='extend', nargs='+',
+ help=ignore_gpg_status_help, default=C.GALAXY_IGNORE_INVALID_SIGNATURE_STATUS_CODES,
+ choices=list(GPG_ERROR_MAP.keys()))
+ install_parser.add_argument('--offline', dest='offline', action='store_true', default=False,
+@@ -564,9 +551,6 @@ class GalaxyCLI(CLI):
+ install_parser.add_argument('--required-valid-signature-count', dest='required_valid_signature_count', type=validate_signature_count,
+ help=valid_signature_count_help, default=C.GALAXY_REQUIRED_VALID_SIGNATURE_COUNT)
+ install_parser.add_argument('--ignore-signature-status-code', dest='ignore_gpg_errors', type=str, action='append',
+- help=opt_help.argparse.SUPPRESS, default=C.GALAXY_IGNORE_INVALID_SIGNATURE_STATUS_CODES,
+- choices=list(GPG_ERROR_MAP.keys()))
+- install_parser.add_argument('--ignore-signature-status-codes', dest='ignore_gpg_errors', type=str, action='extend', nargs='+',
+ help=ignore_gpg_status_help, default=C.GALAXY_IGNORE_INVALID_SIGNATURE_STATUS_CODES,
+ choices=list(GPG_ERROR_MAP.keys()))
+
+@@ -638,7 +622,7 @@ class GalaxyCLI(CLI):
+ return config_def
+
+ galaxy_options = {}
+- for optional_key in ['clear_response_cache', 'no_cache']:
++ for optional_key in ['clear_response_cache', 'no_cache', 'timeout']:
+ if optional_key in context.CLIARGS:
+ galaxy_options[optional_key] = context.CLIARGS[optional_key]
+
+@@ -663,22 +647,17 @@ class GalaxyCLI(CLI):
+ client_id = server_options.pop('client_id')
+ token_val = server_options['token'] or NoTokenSentinel
+ username = server_options['username']
+- api_version = server_options.pop('api_version')
++ v3 = server_options.pop('v3')
+ if server_options['validate_certs'] is None:
+ server_options['validate_certs'] = context.CLIARGS['resolved_validate_certs']
+ validate_certs = server_options['validate_certs']
+
+- # This allows a user to explicitly force use of an API version when
+- # multiple versions are supported. This was added for testing
+- # against pulp_ansible and I'm not sure it has a practical purpose
+- # outside of this use case. As such, this option is not documented
+- # as of now
+- if api_version:
+- display.warning(
+- f'The specified "api_version" configuration for the galaxy server "{server_key}" is '
+- 'not a public configuration, and may be removed at any time without warning.'
+- )
+- server_options['available_api_versions'] = {'v%s' % api_version: '/v%s' % api_version}
++ if v3:
++ # This allows a user to explicitly indicate the server uses the /v3 API
++ # This was added for testing against pulp_ansible and I'm not sure it has
++ # a practical purpose outside of this use case. As such, this option is not
++ # documented as of now
++ server_options['available_api_versions'] = {'v3': '/v3'}
+
+ # default case if no auth info is provided.
+ server_options['token'] = None
+@@ -704,17 +683,9 @@ class GalaxyCLI(CLI):
+ ))
+
+ cmd_server = context.CLIARGS['api_server']
+- if context.CLIARGS['api_version']:
+- api_version = context.CLIARGS['api_version']
+- display.warning(
+- 'The --api-version is not a public argument, and may be removed at any time without warning.'
+- )
+- galaxy_options['available_api_versions'] = {'v%s' % api_version: '/v%s' % api_version}
+-
+ cmd_token = GalaxyToken(token=context.CLIARGS['api_key'])
+
+ validate_certs = context.CLIARGS['resolved_validate_certs']
+- default_server_timeout = context.CLIARGS['timeout'] if context.CLIARGS['timeout'] is not None else C.GALAXY_SERVER_TIMEOUT
+ if cmd_server:
+ # Cmd args take precedence over the config entry but fist check if the arg was a name and use that config
+ # entry, otherwise create a new API entry for the server specified.
+@@ -726,7 +697,6 @@ class GalaxyCLI(CLI):
+ self.galaxy, 'cmd_arg', cmd_server, token=cmd_token,
+ priority=len(config_servers) + 1,
+ validate_certs=validate_certs,
+- timeout=default_server_timeout,
+ **galaxy_options
+ ))
+ else:
+@@ -738,7 +708,6 @@ class GalaxyCLI(CLI):
+ self.galaxy, 'default', C.GALAXY_SERVER, token=cmd_token,
+ priority=0,
+ validate_certs=validate_certs,
+- timeout=default_server_timeout,
+ **galaxy_options
+ ))
+
+@@ -835,7 +804,7 @@ class GalaxyCLI(CLI):
+ for role_req in file_requirements:
+ requirements['roles'] += parse_role_req(role_req)
+
+- elif isinstance(file_requirements, dict):
++ else:
+ # Newer format with a collections and/or roles key
+ extra_keys = set(file_requirements.keys()).difference(set(['roles', 'collections']))
+ if extra_keys:
+@@ -854,9 +823,6 @@ class GalaxyCLI(CLI):
+ for collection_req in file_requirements.get('collections') or []
+ ]
+
+- else:
+- raise AnsibleError(f"Expecting requirements yaml to be a list or dictionary but got {type(file_requirements).__name__}")
+-
+ return requirements
+
+ def _init_coll_req_dict(self, coll_req):
+@@ -1220,16 +1186,11 @@ class GalaxyCLI(CLI):
+ df.write(b_rendered)
+ else:
+ f_rel_path = os.path.relpath(os.path.join(root, f), obj_skeleton)
+- shutil.copyfile(os.path.join(root, f), os.path.join(obj_path, f_rel_path), follow_symlinks=False)
++ shutil.copyfile(os.path.join(root, f), os.path.join(obj_path, f_rel_path))
+
+ for d in dirs:
+ b_dir_path = to_bytes(os.path.join(obj_path, rel_root, d), errors='surrogate_or_strict')
+- if os.path.exists(b_dir_path):
+- continue
+- b_src_dir = to_bytes(os.path.join(root, d), errors='surrogate_or_strict')
+- if os.path.islink(b_src_dir):
+- shutil.copyfile(b_src_dir, b_dir_path, follow_symlinks=False)
+- else:
++ if not os.path.exists(b_dir_path):
+ os.makedirs(b_dir_path)
+
+ display.display("- %s %s was created successfully" % (galaxy_type.title(), obj_name))
+@@ -1293,7 +1254,7 @@ class GalaxyCLI(CLI):
+ """Compare checksums with the collection(s) found on the server and the installed copy. This does not verify dependencies."""
+
+ collections = context.CLIARGS['args']
+- search_paths = AnsibleCollectionConfig.collection_paths
++ search_paths = context.CLIARGS['collections_path']
+ ignore_errors = context.CLIARGS['ignore_errors']
+ local_verify_only = context.CLIARGS['offline']
+ requirements_file = context.CLIARGS['requirements']
+@@ -1433,19 +1394,7 @@ class GalaxyCLI(CLI):
+ upgrade = context.CLIARGS.get('upgrade', False)
+
+ collections_path = C.COLLECTIONS_PATHS
+-
+- managed_paths = set(validate_collection_path(p) for p in C.COLLECTIONS_PATHS)
+- read_req_paths = set(validate_collection_path(p) for p in AnsibleCollectionConfig.collection_paths)
+-
+- unexpected_path = C.GALAXY_COLLECTIONS_PATH_WARNING and not any(p.startswith(path) for p in managed_paths)
+- if unexpected_path and any(p.startswith(path) for p in read_req_paths):
+- display.warning(
+- f"The specified collections path '{path}' appears to be part of the pip Ansible package. "
+- "Managing these directly with ansible-galaxy could break the Ansible package. "
+- "Install collections to a configured collections path, which will take precedence over "
+- "collections found in the PYTHONPATH."
+- )
+- elif unexpected_path:
++ if len([p for p in collections_path if p.startswith(path)]) == 0:
+ display.warning("The specified collections path '%s' is not part of the configured Ansible "
+ "collections paths '%s'. The installed collection will not be picked up in an Ansible "
+ "run, unless within a playbook-adjacent collections directory." % (to_text(path), to_text(":".join(collections_path))))
+@@ -1462,7 +1411,6 @@ class GalaxyCLI(CLI):
+ artifacts_manager=artifacts_manager,
+ disable_gpg_verify=disable_gpg_verify,
+ offline=context.CLIARGS.get('offline', False),
+- read_requirement_paths=read_req_paths,
+ )
+
+ return 0
+@@ -1631,9 +1579,7 @@ class GalaxyCLI(CLI):
+ display.warning(w)
+
+ if not path_found:
+- raise AnsibleOptionsError(
+- "- None of the provided paths were usable. Please specify a valid path with --{0}s-path".format(context.CLIARGS['type'])
+- )
++ raise AnsibleOptionsError("- None of the provided paths were usable. Please specify a valid path with --{0}s-path".format(context.CLIARGS['type']))
+
+ return 0
+
+@@ -1648,65 +1594,100 @@ class GalaxyCLI(CLI):
+ artifacts_manager.require_build_metadata = False
+
+ output_format = context.CLIARGS['output_format']
++ collections_search_paths = set(context.CLIARGS['collections_path'])
+ collection_name = context.CLIARGS['collection']
+- default_collections_path = set(C.COLLECTIONS_PATHS)
+- collections_search_paths = (
+- set(context.CLIARGS['collections_path'] or []) | default_collections_path | set(AnsibleCollectionConfig.collection_paths)
+- )
++ default_collections_path = AnsibleCollectionConfig.collection_paths
+ collections_in_paths = {}
+
+ warnings = []
+ path_found = False
+ collection_found = False
+-
+- namespace_filter = None
+- collection_filter = None
+- if collection_name:
+- # list a specific collection
+-
+- validate_collection_name(collection_name)
+- namespace_filter, collection_filter = collection_name.split('.')
+-
+- collections = list(find_existing_collections(
+- list(collections_search_paths),
+- artifacts_manager,
+- namespace_filter=namespace_filter,
+- collection_filter=collection_filter,
+- dedupe=False
+- ))
+-
+- seen = set()
+- fqcn_width, version_width = _get_collection_widths(collections)
+- for collection in sorted(collections, key=lambda c: c.src):
+- collection_found = True
+- collection_path = pathlib.Path(to_text(collection.src)).parent.parent.as_posix()
+-
+- if output_format in {'yaml', 'json'}:
+- collections_in_paths.setdefault(collection_path, {})
+- collections_in_paths[collection_path][collection.fqcn] = {'version': collection.ver}
+- else:
+- if collection_path not in seen:
+- _display_header(
+- collection_path,
+- 'Collection',
+- 'Version',
+- fqcn_width,
+- version_width
+- )
+- seen.add(collection_path)
+- _display_collection(collection, fqcn_width, version_width)
+-
+- path_found = False
+ for path in collections_search_paths:
++ collection_path = GalaxyCLI._resolve_path(path)
+ if not os.path.exists(path):
+ if path in default_collections_path:
+ # don't warn for missing default paths
+ continue
+- warnings.append("- the configured path {0} does not exist.".format(path))
+- elif os.path.exists(path) and not os.path.isdir(path):
+- warnings.append("- the configured path {0}, exists, but it is not a directory.".format(path))
++ warnings.append("- the configured path {0} does not exist.".format(collection_path))
++ continue
++
++ if not os.path.isdir(collection_path):
++ warnings.append("- the configured path {0}, exists, but it is not a directory.".format(collection_path))
++ continue
++
++ path_found = True
++
++ if collection_name:
++ # list a specific collection
++
++ validate_collection_name(collection_name)
++ namespace, collection = collection_name.split('.')
++
++ collection_path = validate_collection_path(collection_path)
++ b_collection_path = to_bytes(os.path.join(collection_path, namespace, collection), errors='surrogate_or_strict')
++
++ if not os.path.exists(b_collection_path):
++ warnings.append("- unable to find {0} in collection paths".format(collection_name))
++ continue
++
++ if not os.path.isdir(collection_path):
++ warnings.append("- the configured path {0}, exists, but it is not a directory.".format(collection_path))
++ continue
++
++ collection_found = True
++
++ try:
++ collection = Requirement.from_dir_path_as_unknown(
++ b_collection_path,
++ artifacts_manager,
++ )
++ except ValueError as val_err:
++ six.raise_from(AnsibleError(val_err), val_err)
++
++ if output_format in {'yaml', 'json'}:
++ collections_in_paths[collection_path] = {
++ collection.fqcn: {'version': collection.ver}
++ }
++
++ continue
++
++ fqcn_width, version_width = _get_collection_widths([collection])
++
++ _display_header(collection_path, 'Collection', 'Version', fqcn_width, version_width)
++ _display_collection(collection, fqcn_width, version_width)
++
+ else:
+- path_found = True
++ # list all collections
++ collection_path = validate_collection_path(path)
++ if os.path.isdir(collection_path):
++ display.vvv("Searching {0} for collections".format(collection_path))
++ collections = list(find_existing_collections(
++ collection_path, artifacts_manager,
++ ))
++ else:
++ # There was no 'ansible_collections/' directory in the path, so there
++ # or no collections here.
++ display.vvv("No 'ansible_collections' directory found at {0}".format(collection_path))
++ continue
++
++ if not collections:
++ display.vvv("No collections found at {0}".format(collection_path))
++ continue
++
++ if output_format in {'yaml', 'json'}:
++ collections_in_paths[collection_path] = {
++ collection.fqcn: {'version': collection.ver} for collection in collections
++ }
++
++ continue
++
++ # Display header
++ fqcn_width, version_width = _get_collection_widths(collections)
++ _display_header(collection_path, 'Collection', 'Version', fqcn_width, version_width)
++
++ # Sort collections by the namespace and name
++ for collection in sorted(collections, key=to_text):
++ _display_collection(collection, fqcn_width, version_width)
+
+ # Do not warn if the specific collection was found in any of the search paths
+ if collection_found and collection_name:
+@@ -1715,10 +1696,8 @@ class GalaxyCLI(CLI):
+ for w in warnings:
+ display.warning(w)
+
+- if not collections and not path_found:
+- raise AnsibleOptionsError(
+- "- None of the provided paths were usable. Please specify a valid path with --{0}s-path".format(context.CLIARGS['type'])
+- )
++ if not path_found:
++ raise AnsibleOptionsError("- None of the provided paths were usable. Please specify a valid path with --{0}s-path".format(context.CLIARGS['type']))
+
+ if output_format == 'json':
+ display.display(json.dumps(collections_in_paths))
+@@ -1752,8 +1731,8 @@ class GalaxyCLI(CLI):
+ tags=context.CLIARGS['galaxy_tags'], author=context.CLIARGS['author'], page_size=page_size)
+
+ if response['count'] == 0:
+- display.warning("No roles match your search.")
+- return 0
++ display.display("No roles match your search.", color=C.COLOR_ERROR)
++ return 1
+
+ data = [u'']
+
+@@ -1792,7 +1771,6 @@ class GalaxyCLI(CLI):
+ github_user = to_text(context.CLIARGS['github_user'], errors='surrogate_or_strict')
+ github_repo = to_text(context.CLIARGS['github_repo'], errors='surrogate_or_strict')
+
+- rc = 0
+ if context.CLIARGS['check_status']:
+ task = self.api.get_import_task(github_user=github_user, github_repo=github_repo)
+ else:
+@@ -1810,7 +1788,7 @@ class GalaxyCLI(CLI):
+ display.display('%s.%s' % (t['summary_fields']['role']['namespace'], t['summary_fields']['role']['name']), color=C.COLOR_CHANGED)
+ display.display(u'\nTo properly namespace this role, remove each of the above and re-import %s/%s from scratch' % (github_user, github_repo),
+ color=C.COLOR_CHANGED)
+- return rc
++ return 0
+ # found a single role as expected
+ display.display("Successfully submitted import request %d" % task[0]['id'])
+ if not context.CLIARGS['wait']:
+@@ -1827,13 +1805,12 @@ class GalaxyCLI(CLI):
+ if msg['id'] not in msg_list:
+ display.display(msg['message_text'], color=colors[msg['message_type']])
+ msg_list.append(msg['id'])
+- if (state := task[0]['state']) in ['SUCCESS', 'FAILED']:
+- rc = ['SUCCESS', 'FAILED'].index(state)
++ if task[0]['state'] in ['SUCCESS', 'FAILED']:
+ finished = True
+ else:
+ time.sleep(10)
+
+- return rc
++ return 0
+
+ def execute_setup(self):
+ """ Setup an integration from Github or Travis for Ansible Galaxy roles"""
+--- ansible-core-2.16.5.orig/bin/ansible-inventory
++++ ansible-core-2.16.5/bin/ansible-inventory
+@@ -18,7 +18,7 @@ from ansible import constants as C
+ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.errors import AnsibleError, AnsibleOptionsError
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.utils.vars import combine_vars
+ from ansible.utils.display import Display
+ from ansible.vars.plugins import get_vars_from_inventory_sources, get_vars_from_path
+@@ -72,6 +72,7 @@ class InventoryCLI(CLI):
+ opt_help.add_runtask_options(self.parser)
+
+ # remove unused default options
++ self.parser.add_argument('-l', '--limit', help=argparse.SUPPRESS, action=opt_help.UnrecognizedArgument, nargs='?')
+ self.parser.add_argument('--list-hosts', help=argparse.SUPPRESS, action=opt_help.UnrecognizedArgument)
+
+ self.parser.add_argument('args', metavar='host|group', nargs='?')
+@@ -79,10 +80,9 @@ class InventoryCLI(CLI):
+ # Actions
+ action_group = self.parser.add_argument_group("Actions", "One of following must be used on invocation, ONLY ONE!")
+ action_group.add_argument("--list", action="store_true", default=False, dest='list', help='Output all hosts info, works as inventory script')
+- action_group.add_argument("--host", action="store", default=None, dest='host',
+- help='Output specific host info, works as inventory script. It will ignore limit')
++ action_group.add_argument("--host", action="store", default=None, dest='host', help='Output specific host info, works as inventory script')
+ action_group.add_argument("--graph", action="store_true", default=False, dest='graph',
+- help='create inventory graph, if supplying pattern it must be a valid group name. It will ignore limit')
++ help='create inventory graph, if supplying pattern it must be a valid group name')
+ self.parser.add_argument_group(action_group)
+
+ # graph
+@@ -144,22 +144,17 @@ class InventoryCLI(CLI):
+ # FIXME: should we template first?
+ results = self.dump(myvars)
+
+- else:
+- if context.CLIARGS['subset']:
+- # not doing single host, set limit in general if given
+- self.inventory.subset(context.CLIARGS['subset'])
+-
+- if context.CLIARGS['graph']:
+- results = self.inventory_graph()
+- elif context.CLIARGS['list']:
+- top = self._get_group('all')
+- if context.CLIARGS['yaml']:
+- results = self.yaml_inventory(top)
+- elif context.CLIARGS['toml']:
+- results = self.toml_inventory(top)
+- else:
+- results = self.json_inventory(top)
+- results = self.dump(results)
++ elif context.CLIARGS['graph']:
++ results = self.inventory_graph()
++ elif context.CLIARGS['list']:
++ top = self._get_group('all')
++ if context.CLIARGS['yaml']:
++ results = self.yaml_inventory(top)
++ elif context.CLIARGS['toml']:
++ results = self.toml_inventory(top)
++ else:
++ results = self.json_inventory(top)
++ results = self.dump(results)
+
+ if results:
+ outfile = context.CLIARGS['output_file']
+@@ -254,7 +249,7 @@ class InventoryCLI(CLI):
+ return dump
+
+ @staticmethod
+- def _remove_empty_keys(dump):
++ def _remove_empty(dump):
+ # remove empty keys
+ for x in ('hosts', 'vars', 'children'):
+ if x in dump and not dump[x]:
+@@ -301,34 +296,33 @@ class InventoryCLI(CLI):
+
+ def json_inventory(self, top):
+
+- seen_groups = set()
++ seen = set()
+
+- def format_group(group, available_hosts):
++ def format_group(group):
+ results = {}
+ results[group.name] = {}
+ if group.name != 'all':
+- results[group.name]['hosts'] = [h.name for h in group.hosts if h.name in available_hosts]
++ results[group.name]['hosts'] = [h.name for h in group.hosts]
+ results[group.name]['children'] = []
+ for subgroup in group.child_groups:
+ results[group.name]['children'].append(subgroup.name)
+- if subgroup.name not in seen_groups:
+- results.update(format_group(subgroup, available_hosts))
+- seen_groups.add(subgroup.name)
++ if subgroup.name not in seen:
++ results.update(format_group(subgroup))
++ seen.add(subgroup.name)
+ if context.CLIARGS['export']:
+ results[group.name]['vars'] = self._get_group_variables(group)
+
+- self._remove_empty_keys(results[group.name])
+- # remove empty groups
++ self._remove_empty(results[group.name])
+ if not results[group.name]:
+ del results[group.name]
+
+ return results
+
+- hosts = self.inventory.get_hosts(top.name)
+- results = format_group(top, frozenset(h.name for h in hosts))
++ results = format_group(top)
+
+ # populate meta
+ results['_meta'] = {'hostvars': {}}
++ hosts = self.inventory.get_hosts()
+ for host in hosts:
+ hvars = self._get_host_variables(host)
+ if hvars:
+@@ -338,10 +332,9 @@ class InventoryCLI(CLI):
+
+ def yaml_inventory(self, top):
+
+- seen_hosts = set()
+- seen_groups = set()
++ seen = []
+
+- def format_group(group, available_hosts):
++ def format_group(group):
+ results = {}
+
+ # initialize group + vars
+@@ -351,21 +344,15 @@ class InventoryCLI(CLI):
+ results[group.name]['children'] = {}
+ for subgroup in group.child_groups:
+ if subgroup.name != 'all':
+- if subgroup.name in seen_groups:
+- results[group.name]['children'].update({subgroup.name: {}})
+- else:
+- results[group.name]['children'].update(format_group(subgroup, available_hosts))
+- seen_groups.add(subgroup.name)
++ results[group.name]['children'].update(format_group(subgroup))
+
+ # hosts for group
+ results[group.name]['hosts'] = {}
+ if group.name != 'all':
+ for h in group.hosts:
+- if h.name not in available_hosts:
+- continue # observe limit
+ myvars = {}
+- if h.name not in seen_hosts: # avoid defining host vars more than once
+- seen_hosts.add(h.name)
++ if h.name not in seen: # avoid defining host vars more than once
++ seen.append(h.name)
+ myvars = self._get_host_variables(host=h)
+ results[group.name]['hosts'][h.name] = myvars
+
+@@ -374,22 +361,17 @@ class InventoryCLI(CLI):
+ if gvars:
+ results[group.name]['vars'] = gvars
+
+- self._remove_empty_keys(results[group.name])
+- # remove empty groups
+- if not results[group.name]:
+- del results[group.name]
++ self._remove_empty(results[group.name])
+
+ return results
+
+- available_hosts = frozenset(h.name for h in self.inventory.get_hosts(top.name))
+- return format_group(top, available_hosts)
++ return format_group(top)
+
+ def toml_inventory(self, top):
+- seen_hosts = set()
+- seen_hosts = set()
++ seen = set()
+ has_ungrouped = bool(next(g.hosts for g in top.child_groups if g.name == 'ungrouped'))
+
+- def format_group(group, available_hosts):
++ def format_group(group):
+ results = {}
+ results[group.name] = {}
+
+@@ -399,14 +381,12 @@ class InventoryCLI(CLI):
+ continue
+ if group.name != 'all':
+ results[group.name]['children'].append(subgroup.name)
+- results.update(format_group(subgroup, available_hosts))
++ results.update(format_group(subgroup))
+
+ if group.name != 'all':
+ for host in group.hosts:
+- if host.name not in available_hosts:
+- continue
+- if host.name not in seen_hosts:
+- seen_hosts.add(host.name)
++ if host.name not in seen:
++ seen.add(host.name)
+ host_vars = self._get_host_variables(host=host)
+ else:
+ host_vars = {}
+@@ -418,15 +398,13 @@ class InventoryCLI(CLI):
+ if context.CLIARGS['export']:
+ results[group.name]['vars'] = self._get_group_variables(group)
+
+- self._remove_empty_keys(results[group.name])
+- # remove empty groups
++ self._remove_empty(results[group.name])
+ if not results[group.name]:
+ del results[group.name]
+
+ return results
+
+- available_hosts = frozenset(h.name for h in self.inventory.get_hosts(top.name))
+- results = format_group(top, available_hosts)
++ results = format_group(top)
+
+ return results
+
+--- ansible-core-2.16.5.orig/bin/ansible-playbook
++++ ansible-core-2.16.5/bin/ansible-playbook
+@@ -18,7 +18,7 @@ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.errors import AnsibleError
+ from ansible.executor.playbook_executor import PlaybookExecutor
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+ from ansible.playbook.block import Block
+ from ansible.plugins.loader import add_all_plugin_dirs
+ from ansible.utils.collection_loader import AnsibleCollectionConfig
+@@ -67,19 +67,8 @@ class PlaybookCLI(CLI):
+ self.parser.add_argument('args', help='Playbook(s)', metavar='playbook', nargs='+')
+
+ def post_process_args(self, options):
+-
+- # for listing, we need to know if user had tag input
+- # capture here as parent function sets defaults for tags
+- havetags = bool(options.tags or options.skip_tags)
+-
+ options = super(PlaybookCLI, self).post_process_args(options)
+
+- if options.listtags:
+- # default to all tags (including never), when listing tags
+- # unless user specified tags
+- if not havetags:
+- options.tags = ['never', 'all']
+-
+ display.verbosity = options.verbosity
+ self.validate_conflicts(options, runas_opts=True, fork_opts=True)
+
+--- ansible-core-2.16.5.orig/bin/ansible-pull
++++ ansible-core-2.16.5/bin/ansible-pull
+@@ -24,7 +24,7 @@ from ansible import constants as C
+ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.errors import AnsibleOptionsError
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.plugins.loader import module_loader
+ from ansible.utils.cmd_functions import run_cmd
+ from ansible.utils.display import Display
+@@ -81,7 +81,7 @@ class PullCLI(CLI):
+
+ super(PullCLI, self).init_parser(
+ usage='%prog -U <repository> [options] [<playbook.yml>]',
+- desc="pulls playbooks from a VCS repo and executes them on target host")
++ desc="pulls playbooks from a VCS repo and executes them for the local host")
+
+ # Do not add check_options as there's a conflict with --checkout/-C
+ opt_help.add_connect_options(self.parser)
+@@ -275,15 +275,8 @@ class PullCLI(CLI):
+ for vault_id in context.CLIARGS['vault_ids']:
+ cmd += " --vault-id=%s" % vault_id
+
+- if context.CLIARGS['become_password_file']:
+- cmd += " --become-password-file=%s" % context.CLIARGS['become_password_file']
+-
+- if context.CLIARGS['connection_password_file']:
+- cmd += " --connection-password-file=%s" % context.CLIARGS['connection_password_file']
+-
+ for ev in context.CLIARGS['extra_vars']:
+ cmd += ' -e %s' % shlex.quote(ev)
+-
+ if context.CLIARGS['become_ask_pass']:
+ cmd += ' --ask-become-pass'
+ if context.CLIARGS['skip_tags']:
+--- ansible-core-2.16.5.orig/bin/ansible-vault
++++ ansible-core-2.16.5/bin/ansible-vault
+@@ -17,7 +17,7 @@ from ansible import constants as C
+ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.errors import AnsibleOptionsError
+-from ansible.module_utils.common.text.converters import to_text, to_bytes
++from ansible.module_utils._text import to_text, to_bytes
+ from ansible.parsing.dataloader import DataLoader
+ from ansible.parsing.vault import VaultEditor, VaultLib, match_encrypt_secret
+ from ansible.utils.display import Display
+@@ -61,20 +61,20 @@ class VaultCLI(CLI):
+ epilog="\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0])
+ )
+
+- common = opt_help.ArgumentParser(add_help=False)
++ common = opt_help.argparse.ArgumentParser(add_help=False)
+ opt_help.add_vault_options(common)
+ opt_help.add_verbosity_options(common)
+
+ subparsers = self.parser.add_subparsers(dest='action')
+ subparsers.required = True
+
+- output = opt_help.ArgumentParser(add_help=False)
++ output = opt_help.argparse.ArgumentParser(add_help=False)
+ output.add_argument('--output', default=None, dest='output_file',
+ help='output file name for encrypt or decrypt; use - for stdout',
+ type=opt_help.unfrack_path())
+
+ # For encrypting actions, we can also specify which of multiple vault ids should be used for encrypting
+- vault_id = opt_help.ArgumentParser(add_help=False)
++ vault_id = opt_help.argparse.ArgumentParser(add_help=False)
+ vault_id.add_argument('--encrypt-vault-id', default=[], dest='encrypt_vault_id',
+ action='store', type=str,
+ help='the vault id used to encrypt (required if more than one vault-id is provided)')
+@@ -82,8 +82,6 @@ class VaultCLI(CLI):
+ create_parser = subparsers.add_parser('create', help='Create new vault encrypted file', parents=[vault_id, common])
+ create_parser.set_defaults(func=self.execute_create)
+ create_parser.add_argument('args', help='Filename', metavar='file_name', nargs='*')
+- create_parser.add_argument('--skip-tty-check', default=False, help='allows editor to be opened when no tty attached',
+- dest='skip_tty_check', action='store_true')
+
+ decrypt_parser = subparsers.add_parser('decrypt', help='Decrypt vault encrypted file', parents=[output, common])
+ decrypt_parser.set_defaults(func=self.execute_decrypt)
+@@ -386,11 +384,6 @@ class VaultCLI(CLI):
+ sys.stderr.write(err)
+ b_outs.append(to_bytes(out))
+
+- # The output must end with a newline to play nice with terminal representation.
+- # Refs:
+- # * https://stackoverflow.com/a/729795/595220
+- # * https://github.com/ansible/ansible/issues/78932
+- b_outs.append(b'')
+ self.editor.write_data(b'\n'.join(b_outs), context.CLIARGS['output_file'] or '-')
+
+ if sys.stdout.isatty():
+@@ -449,11 +442,8 @@ class VaultCLI(CLI):
+ if len(context.CLIARGS['args']) != 1:
+ raise AnsibleOptionsError("ansible-vault create can take only one filename argument")
+
+- if sys.stdout.isatty() or context.CLIARGS['skip_tty_check']:
+- self.editor.create_file(context.CLIARGS['args'][0], self.encrypt_secret,
+- vault_id=self.encrypt_vault_id)
+- else:
+- raise AnsibleOptionsError("not a tty, editor cannot be opened")
++ self.editor.create_file(context.CLIARGS['args'][0], self.encrypt_secret,
++ vault_id=self.encrypt_vault_id)
+
+ def execute_edit(self):
+ ''' open and decrypt an existing vaulted file in an editor, that will be encrypted again when closed'''
+--- ansible-core-2.16.5.orig/changelogs/changelog.yaml
++++ ansible-core-2.16.5/changelogs/changelog.yaml
+@@ -1,977 +1,1600 @@
+-ancestor: 2.15.0
++ancestor: 2.13.0
+ releases:
+- 2.16.0:
++ 2.14.0:
+ changes:
+ bugfixes:
+- - ansible-test - Fix parsing of cgroup entries which contain a ``:`` in the
+- path (https://github.com/ansible/ansible/issues/81977).
+- release_summary: '| Release Date: 2023-11-06
++ - ansible-test - Fix broken documentation link for ``aws`` test plugin error
++ messages.
++ minor_changes:
++ - ansible-test - Improve consistency of version specific documentation links.
++ release_summary: '| Release Date: 2022-11-07
+
+- | `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
++ | `Porting Guide <https://docs.ansible.com/ansible/devel/porting_guides.html>`__
+
+ '
+- codename: All My Love
++ codename: C'mon Everybody
+ fragments:
+- - 2.16.0_summary.yaml
+- - ansible-test-cgroup-split.yml
+- release_date: '2023-11-06'
+- 2.16.0b1:
++ - ansible-test-docs-links.yml
++ - v2.14.0_summary.yaml
++ release_date: '2022-11-07'
++ 2.14.0b1:
+ changes:
+ breaking_changes:
+- - Any plugin using the config system and the `cli` entry to use the `timeout`
+- from the command line, will see the value change if the use had configured
+- it in any of the lower precedence methods. If relying on this behaviour to
+- consume the global/generic timeout from the DEFAULT_TIMEOUT constant, please
+- consult the documentation on plugin configuration to add the overlaping entries.
+- - ansible-test - Test plugins that rely on containers no longer support reusing
+- running containers. The previous behavior was an undocumented, untested feature.
+- - service module will not permanently configure variables/flags for openbsd
+- when doing enable/disable operation anymore, this module was never meant to
+- do this type of work, just to manage the service state itself. A rcctl_config
+- or similar module should be created and used instead.
+- bugfixes:
+- - Allow for searching handler subdir for included task via include_role (https://github.com/ansible/ansible/issues/81722)
+- - AnsibleModule.run_command - Only use selectors when needed, and rely on Python
+- stdlib subprocess for the simple task of collecting stdout/stderr when prompt
+- matching is not required.
+- - Display - Defensively configure writing to stdout and stderr with a custom
+- encoding error handler that will replace invalid characters while providing
+- a deprecation warning that non-utf8 text will result in an error in a future
+- version.
+- - Exclude internal options from man pages and docs.
+- - Fix ``ansible-config init`` man page option indentation.
+- - Fix ``ast`` deprecation warnings for ``Str`` and ``value.s`` when using Python
+- 3.12.
+- - Fix exceptions caused by various inputs when performing arg splitting or parsing
+- key/value pairs. Resolves issue https://github.com/ansible/ansible/issues/46379
+- and issue https://github.com/ansible/ansible/issues/61497
+- - Fix incorrect parsing of multi-line Jinja2 blocks when performing arg splitting
+- or parsing key/value pairs.
+- - Fix post-validating looped task fields so the strategy uses the correct values
+- after task execution.
+- - Fixed `pip` module failure in case of usage quotes for `virtualenv_command`
+- option for the venv command. (https://github.com/ansible/ansible/issues/76372)
+- - From issue https://github.com/ansible/ansible/issues/80880, when notifying
+- a handler from another handler, handler notifications must be registered immediately
+- as the flush_handler call is not recursive.
+- - Import ``FILE_ATTRIBUTES`` from ``ansible.module_utils.common.file`` in ``ansible.module_utils.basic``
+- instead of defining it twice.
+- - Inventory scripts parser not treat exception when getting hostsvar (https://github.com/ansible/ansible/issues/81103)
+- - On Python 3 use datetime methods ``fromtimestamp`` and ``now`` with UTC timezone
+- instead of ``utcfromtimestamp`` and ``utcnow``, which are deprecated in Python
+- 3.12.
+- - PluginLoader - fix Jinja plugin performance issues (https://github.com/ansible/ansible/issues/79652)
++ - Allow for lazy evaluation of Jinja2 expressions (https://github.com/ansible/ansible/issues/56017)
++ - The default ansible-galaxy role skeletons no longer contain .travis.yml files.
++ You can configure ansible-galaxy to use a custom role skeleton that contains
++ a .travis.yml file to continue using Galaxy's integration with Travis CI.
++ - ansible - At startup the filesystem encoding and locale are checked to verify
++ they are UTF-8. If not, the process exits with an error reporting the errant
++ encoding.
++ - ansible - Increase minimum Python requirement to Python 3.9 for CLI utilities
++ and controller code
++ - ansible-test - At startup the filesystem encoding is checked to verify it
++ is UTF-8. If not, the process exits with an error reporting the errant encoding.
++ - ansible-test - At startup the locale is configured as ``en_US.UTF-8``, with
++ a fallback to ``C.UTF-8``. If neither encoding is available the process exits
++ with an error. If the fallback is used, a warning is displayed. In previous
++ versions the ``en_US.UTF-8`` locale was always requested. However, no startup
++ checking was performed to verify the locale was successfully configured.
++ - strategy plugins - Make ``ignore_unreachable`` to increase ``ignored`` and
++ ``ok`` and counter, not ``skipped`` and ``unreachable``. (https://github.com/ansible/ansible/issues/77690)
++ bugfixes:
++ - '"meta: refresh_inventory" does not clobber entries added by add_host/group_by
++ anymore.'
++ - Add PyYAML >= 5.1 as a dependency of ansible-core to be compatible with Python
++ 3.8+.
++ - Avoid 'unreachable' error when chmod on AIX has 255 as return code.
++ - Bug fix for when handlers were ran on failed hosts after an ``always`` section
++ was executed (https://github.com/ansible/ansible/issues/52561)
++ - Do not allow handlers from dynamic includes to be notified (https://github.com/ansible/ansible/pull/78399)
++ - Ensure handlers observe ``any_errors_fatal`` (https://github.com/ansible/ansible/issues/46447)
++ - Ensure syntax check errors include playbook filenames
++ - Ensure the correct ``environment_class`` is set on ``AnsibleJ2Template``
++ - Error for collection redirects that do not use fully qualified collection
++ names, as the redirect would be determined by the ``collections`` keyword.
++ - Fix PluginLoader to mimic Python import machinery by adding module to sys.modules
++ before exec
++ - Fix ``-vv`` output for meta tasks to not have an empty message when skipped,
++ print the skip reason instead. (https://github.com/ansible/ansible/issues/77315)
++ - Fix an issue where ``ansible_play_hosts`` and ``ansible_play_batch`` were
++ not properly updated when a failure occured in an explicit block inside the
++ rescue section (https://github.com/ansible/ansible/issues/78612)
++ - Fix dnf module documentation to indicate that comparison operators for package
++ version require spaces around them (https://github.com/ansible/ansible/issues/78295)
++ - Fix for linear strategy when tasks were executed in incorrect order or even
++ removed from execution. (https://github.com/ansible/ansible/issues/64611,
++ https://github.com/ansible/ansible/issues/64999, https://github.com/ansible/ansible/issues/72725,
++ https://github.com/ansible/ansible/issues/72781)
++ - Fix for network_cli not getting all relevant connection options
++ - Fix handlers execution with ``serial`` in the ``linear`` strategy (https://github.com/ansible/ansible/issues/54991)
++ - Fix potential, but unlikely, cases of variable use before definition.
++ - Fix traceback when installing a collection from a git repository and git is
++ not installed (https://github.com/ansible/ansible/issues/77479).
++ - GALAXY_IGNORE_CERTS reworked to allow each server entry to override
++ - More gracefully handle separator errors in jinja2 template overrides (https://github.com/ansible/ansible/pull/77495).
++ - Move undefined check from concat to finalize (https://github.com/ansible/ansible/issues/78156)
++ - Prevent losing unsafe on results returned from lookups (https://github.com/ansible/ansible/issues/77535)
++ - Propagate ``ansible_failed_task`` and ``ansible_failed_result`` to an outer
++ rescue (https://github.com/ansible/ansible/issues/43191)
++ - Properly execute rescue section when an include task fails in all loop iterations
++ (https://github.com/ansible/ansible/issues/23161)
++ - Properly send a skipped message when a list in a ``loop`` is empty and comes
++ from a template (https://github.com/ansible/ansible/issues/77934)
++ - Support colons in jinja2 template override values (https://github.com/ansible/ansible/pull/77495).
++ - '``ansible-galaxy`` - remove extra server api call during dependency resolution
++ for requirements and dependencies that are already satisfied (https://github.com/ansible/ansible/issues/77443).'
++ - '`ansible-config init -f vars` will now use shorthand format'
++ - action plugins now pass cannonical info to modules instead of 'temporary'
++ info from play_context
++ - ansible - Exclude Python 2.6 from Python interpreter discovery.
++ - ansible-config dump - Only display plugin type headers when plugin options
++ are changed if --only-changed is specified.
++ - ansible-configi init should now skip internal reserved config entries
++ - ansible-connection - decrypt vaulted parameters before sending over the socket,
++ as vault secrets are not available on the other side.
++ - ansible-console - Renamed the first argument of ``ConsoleCLI.default`` from
++ ``arg`` to ``line`` to match the first argument of the same method on the
++ base class ``Cmd``.
++ - ansible-console commands now all have a help entry.
++ - ansible-console fixed to load modules via fqcn, short names and handle redirects.
++ - ansible-console now shows installed collection modules.
++ - ansible-doc - fix listing plugins.
++ - ansible-doc will not add 'website for' in ":ref:" substitutions as it made
++ them confusing.
++ - ansible-doc will not again warn and skip when missing docs, always show the
++ doc file (for edit on github) and match legacy plugins.
++ - ansible-doc will not traceback when legacy plugins don't have docs nor adjacent
++ file with docs
++ - ansible-doc will now also display until as an 'implicit' templating keyword.
++ - ansible-doc will now not display version_added_collection under same conditions
++ it does not display version_added.
++ - ansible-galaxy - Fix detection of ``--role-file`` in arguments for implicit
++ role invocation (https://github.com/ansible/ansible/issues/78204)
++ - ansible-galaxy - Fix exit codes for role search and delete (https://github.com/ansible/ansible/issues/78516)
++ - ansible-galaxy - Fix loading boolean server options so False doesn't become
++ a truthy string (https://github.com/ansible/ansible/issues/77416).
++ - ansible-galaxy - Fix reinitializing the whole collection directory with ``ansible-galaxy
++ collection init ns.coll --force``. Now directories and files that are not
++ included in the collection skeleton will be removed.
++ - ansible-galaxy - Fix unhandled traceback if a role's dependencies in meta/main.yml
++ or meta/requirements.yml are not lists.
++ - ansible-galaxy - do not require mandatory keys in the ``galaxy.yml`` of source
++ collections when listing them (https://github.com/ansible/ansible/issues/70180).
++ - ansible-galaxy - fix installing collections that have dependencies in the
++ metadata set to null instead of an empty dictionary (https://github.com/ansible/ansible/issues/77560).
++ - ansible-galaxy - fix listing collections that contains metadata but the namespace
++ or name are not strings.
++ - ansible-galaxy - fix missing meta/runtime.yml in default galaxy skeleton used
++ for ansible-galaxy collection init
++ - ansible-galaxy - fix setting the cache for paginated responses from Galaxy
++ NG/AH (https://github.com/ansible/ansible/issues/77911).
++ - ansible-galaxy - handle unsupported versions of resolvelib gracefully.
++ - ansible-galaxy --ignore-certs now has proper precedence over configuration
++ - ansible-test - Allow disabled, unsupported, unstable and destructive integration
++ test targets to be selected using their respective prefixes.
++ - ansible-test - Allow unstable tests to run when targeted changes are made
++ and the ``--allow-unstable-changed`` option is specified (resolves https://github.com/ansible/ansible/issues/74213).
++ - ansible-test - Always remove containers after failing to create/run them.
++ This avoids leaving behind created containers when using podman.
++ - ansible-test - Correctly detect when running as the ``root`` user (UID 0)
++ on the origin host. The result of the detection was incorrectly being inverted.
++ - ansible-test - Delegation for commands which generate output for programmatic
++ consumption no longer redirect all output to stdout. The affected commands
++ and options are ``shell``, ``sanity --lint``, ``sanity --list-tests``, ``integration
++ --list-targets``, ``coverage analyze``
++ - ansible-test - Delegation now properly handles arguments given after ``--``
++ on the command line.
++ - ansible-test - Don't fail if network cannot be disconnected (https://github.com/ansible/ansible/pull/77472)
++ - ansible-test - Fix bootstrapping of Python 3.9 on Ubuntu 20.04 remotes.
++ - ansible-test - Fix change detection for ansible-test's own integration tests.
++ - ansible-test - Fix internal validation of remote completion configuration.
++ - ansible-test - Fix skipping of tests marked ``needs/python`` on the origin
++ host.
++ - ansible-test - Fix skipping of tests marked ``needs/root`` on the origin host.
++ - ansible-test - Prevent ``--target-`` prefixed options for the ``shell`` command
++ from being combined with legacy environment options.
++ - ansible-test - Sanity test output with the ``--lint`` option is no longer
++ mixed in with bootstrapping output.
++ - ansible-test - Subprocesses are now isolated from the stdin, stdout and stderr
++ of ansible-test. This avoids issues with subprocesses tampering with the file
++ descriptors, such as SSH making them non-blocking. As a result of this change,
++ subprocess output from unit and integration tests on stderr now go to stdout.
++ - ansible-test - Subprocesses no longer have access to the TTY ansible-test
++ is connected to, if any. This maintains consistent behavior between local
++ testing and CI systems, which typically do not provide a TTY. Tests which
++ require a TTY should use pexpect or another mechanism to create a PTY.
++ - ansible-test - Temporary executables are now verified as executable after
++ creation. Without this check, path injected scripts may not be found, typically
++ on systems with ``/tmp`` mounted using the "noexec" option. This can manifest
++ as a missing Python interpreter, or use of the wrong Python interpreter, as
++ well as other error conditions.
++ - 'ansible-test - Test configuration for collections is now parsed only once,
++ prior to delegation. Fixes issue: https://github.com/ansible/ansible/issues/78334'
++ - ansible-test - Test containers are now run with the ``--tmpfs`` option for
++ ``/tmp``, ``/run`` and ``/run/lock``. This allows use of containers built
++ without the ``VOLUME`` instruction. Additionally, containers with those volumes
++ defined no longer create anonymous volumes for them. This avoids leaving behind
++ volumes on the container host after the container is stopped and deleted.
++ - ansible-test - The ``shell`` command no longer redirects all output to stdout
++ when running a provided command. Any command output written to stderr will
++ be mixed with the stderr output from ansible-test.
++ - ansible-test - The ``shell`` command no longer requests a TTY when using delegation
++ unless an interactive shell is being used. An interactive shell is the default
++ behavior when no command is given to pass to the shell.
++ - ansible-test - ansible-doc sanity test - Correctly determine the fully-qualified
++ collection name for plugins in subdirectories, resolving https://github.com/ansible/ansible/issues/78490.
++ - ansible-test - validate-modules - Documentation-only modules, used for documenting
++ actions, are now allowed to have docstrings (https://github.com/ansible/ansible/issues/77972).
++ - ansible-test compile sanity test - do not crash if a column could not be determined
++ for an error (https://github.com/ansible/ansible/pull/77465).
++ - apt - Fix module failure when a package is not installed and only_upgrade=True.
++ Skip that package and check the remaining requested packages for upgrades.
++ (https://github.com/ansible/ansible/issues/78762)
++ - apt - don't actually update the cache in check mode with update_cache=true.
++ - apt - don't mark existing packages as manually installed in check mode (https://github.com/ansible/ansible/issues/66413).
++ - apt - fix package selection to include /etc/apt/preferences(.d) (https://github.com/ansible/ansible/issues/77969)
++ - apt module now correctly handles virtual packages.
++ - arg_spec - Fix incorrect ``no_log`` warning when a parameter alias is used
++ (https://github.com/ansible/ansible/pull/77576)
++ - callback plugins - do not crash when ``exception`` passed from a module is
++ not a string (https://github.com/ansible/ansible/issues/75726, https://github.com/ansible/ansible/pull/77781).
++ - cli now emits clearer error on no hosts selected
++ - config, ensure that pulling values from configmanager are templated if possible.
++ - display itself should be single source of 'verbosity' level to the engine.
++ - dnf - Condense a few internal boolean returns.
++ - dnf - The ``nobest`` option now also works for ``state=latest``.
++ - dnf - The ``skip_broken`` option is now used in installs (https://github.com/ansible/ansible/issues/73072).
++ - dnf - fix output parsing on systems with ``LANGUAGE`` set to a language other
++ than English (https://github.com/ansible/ansible/issues/78193)
++ - facts - fix IP address discovery for specific interface names (https://github.com/ansible/ansible/issues/77792).
++ - 'facts - fix processor facts on AIX: correctly detect number of cores and
++ threads, turn ``processor`` into a list (https://github.com/ansible/ansible/pull/78223).'
++ - fetch_file - Ensure we only use the filename when calculating a tempfile,
++ and do not incude the query string (https://github.com/ansible/ansible/issues/29680)
++ - fetch_file - properly split files with multiple file extensions (https://github.com/ansible/ansible/pull/75257)
++ - file - setting attributes of symbolic links or files that are hard linked
++ no longer fails when the link target is unspecified (https://github.com/ansible/ansible/issues/76142).
++ - file backed cache plugins now handle concurrent access by making atomic updates
++ to the files.
++ - git module fix docs and proper use of ssh wrapper script and GIT_SSH_COMMAND
++ depending on version.
++ - if a config setting prevents running ansible it should at least show it's
++ "origin".
++ - include module - add docs url to include deprecation message (https://github.com/ansible/ansible/issues/76684).
++ - items2dict - Handle error if an item is not a dictionary or is missing the
++ required keys (https://github.com/ansible/ansible/issues/70337).
++ - local facts - if a local fact in the facts directory cannot be stated, store
++ an error message as the fact value and emit a warning just as if just as if
++ the facts execution has failed. The stat can fail e.g. on dangling symlinks.
++ - lookup plugin - catch KeyError when lookup returns dictionary (https://github.com/ansible/ansible/pull/77789).
++ - module_utils - Make distro.id() report newer versions of OpenSuSE (at least
++ >=15) also report as ``opensuse``. They report themselves as ``opensuse-leap``.
++ - module_utils.service - daemonize - Avoid modifying the list of file descriptors
++ while iterating over it.
++ - null_representation config entry changed to 'raw' as it must allow 'none/null'
++ and empty string.
++ - paramiko - Add a new option to allow paramiko >= 2.9 to easily work with all
++ devices now that rsa-sha2 support was added to paramiko, which prevented communication
++ with numerous platforms. (https://github.com/ansible/ansible/issues/76737)
++ - paramiko - Add back support for ``ssh_args``, ``ssh_common_args``, and ``ssh_extra_args``
++ for parsing the ``ProxyCommand`` (https://github.com/ansible/ansible/issues/78750)
++ - password lookup does not ignore k=v arguments anymore.
++ - pause module will now report proper 'echo' vs always being true.
++ - pip - fix cases where resolution of pip Python module fails when importlib.util
++ has not already been imported
++ - plugin loader - Sort results when fuzzy matching plugin names (https://github.com/ansible/ansible/issues/77966).
++ - plugin loader will now load config data for plugin by name instead of by file
++ to avoid issues with the same file being loaded under different names (fqcn
++ + short name).
++ - plugin loader, now when skipping a plugin due to an abstract method error
++ we provide that in 'verbose' mode instead of totally obscuring the error.
++ The current implementation assumed only the base classes would trigger this
++ and failed to consider 'in development' plugins.
++ - prevent lusermod from using group name instead of group id (https://github.com/ansible/ansible/pull/77914)
++ - prevent type annotation shim failures from causing runtime failures (https://github.com/ansible/ansible/pull/77860)
++ - psrp connection now handles default to inventory_hostname correctly.
++ - roles, fixed issue with roles loading paths not contained in the role itself
++ when using the `_from` options.
++ - setup - Adds a default value to ``lvm_facts`` when lvm or lvm2 is not installed
++ on linux (https://github.com/ansible/ansible/issues/17393)
++ - shell plugins now give a more user friendly error when fed the wrong type
++ of data.
++ - template module/lookup - fix ``convert_data`` option that was effectively
++ always set to True for Jinja macros (https://github.com/ansible/ansible/issues/78141)
++ - unarchive - if unzip is available but zipinfo is not, use unzip -Z instead
++ of zipinfo (https://github.com/ansible/ansible/issues/76959).
++ - uri - properly use uri parameter use_proxy (https://github.com/ansible/ansible/issues/58632)
++ - uri module - failed status when Authentication Bearer used with netrc, because
++ Basic authentication was by default. Fix now allows to ignore netrc by changing
++ use_netrc=False (https://github.com/ansible/ansible/issues/74397).
++ - urls - Guard imports of ``urllib3`` by catching ``Exception`` instead of ``ImportError``
++ to prevent exceptions in the import process of optional dependencies from
++ preventing use of ``urls.py`` (https://github.com/ansible/ansible/issues/78648)
++ - user - Fix error "Permission denied" in user module while generating SSH keys
++ (https://github.com/ansible/ansible/issues/78017).
++ - user - fix creating a local user if the user group already exists (https://github.com/ansible/ansible/pull/75042)
++ - user module - Replace uses of the deprecated ``spwd`` python module with ctypes
++ (https://github.com/ansible/ansible/pull/78050)
++ - validate-modules - fix validating version_added for new options.
++ - variablemanager, more efficient read of vars files
++ - vault secrets file now executes in the correct context when it is a symlink
++ (not resolved to canonical file).
++ - wait_for - Read file and perform comparisons using bytes to avoid decode errors
++ (https://github.com/ansible/ansible/issues/78214)
++ - winrm - Ensure ``kinit`` is run with the same ``PATH`` env var as the Ansible
++ process
++ - winrm connection now handles default to inventory_hostname correctly.
++ - yaml inventory plugin - fix the error message for non-string hostnames (https://github.com/ansible/ansible/issues/77519).
++ - yum - fix traceback when ``releasever`` is specified with ``latest`` (https://github.com/ansible/ansible/issues/78058)
++ deprecated_features:
++ - Deprecate ability of lookup plugins to return arbitrary data. Lookup plugins
++ must return lists, failing to do so will be an error in 2.18. (https://github.com/ansible/ansible/issues/77788)
++ - Encryption - Deprecate use of the Python crypt module due to it's impending
++ removal from Python 3.13
++ - PlayContext.verbosity is deprecated and will be removed in 2.18. Use ansible.utils.display.Display().verbosity
++ as the single source of truth.
++ - '``DEFAULT_FACT_PATH``, ``DEFAULT_GATHER_SUBSET`` and ``DEFAULT_GATHER_TIMEOUT``
++ are deprecated and will be removed in 2.18. Use ``module_defaults`` keyword
++ instead.'
++ - '``PlayIterator`` - deprecate ``cache_block_tasks`` and ``get_original_task``
++ which are noop and unused.'
++ - '``Templar`` - deprecate ``shared_loader_obj`` option which is unused. ``ansible.plugins.loader``
++ is used directly instead.'
++ - listify_lookup_plugin_terms, deprecate 'loader/dataloader' parameter as it
++ not used.
++ - vars plugins - determining whether or not to run ansible.legacy vars plugins
++ with the class attribute REQUIRES_WHITELIST is deprecated, set REQUIRES_ENABLED
++ instead.
++ major_changes:
++ - Move handler processing into new ``PlayIterator`` phase to use the configured
++ strategy (https://github.com/ansible/ansible/issues/65067)
++ - ansible - At startup the filesystem encoding and locale are checked to verify
++ they are UTF-8. If not, the process exits with an error reporting the errant
++ encoding.
++ - ansible - Increase minimum Python requirement to Python 3.9 for CLI utilities
++ and controller code
++ - ansible-test - At startup the filesystem encoding is checked to verify it
++ is UTF-8. If not, the process exits with an error reporting the errant encoding.
++ - ansible-test - At startup the locale is configured as ``en_US.UTF-8``, with
++ a fallback to ``C.UTF-8``. If neither encoding is available the process exits
++ with an error. If the fallback is used, a warning is displayed. In previous
++ versions the ``en_US.UTF-8`` locale was always requested. However, no startup
++ checking was performed to verify the locale was successfully configured.
++ minor_changes:
++ - Add a new "INVENTORY_UNPARSED_WARNING" flag add to hide the "No inventory
++ was parsed, only implicit localhost is available" warning
++ - "Add an 'action_plugin' field for modules in runtime.yml plugin_routing.\n\nThis
++ fixes module_defaults by supporting modules-as-redirected-actions\nwithout
++ redirecting module_defaults entries to the common action.\n\n.. code: yaml\n\n
++ \ plugin_routing:\n action:\n facts:\n redirect: ns.coll.eos\n
++ \ command:\n redirect: ns.coll.eos\n modules:\n facts:\n
++ \ redirect: ns.coll.eos_facts\n command:\n redirect:
++ ns.coll.eos_command\n\nWith the runtime.yml above for ns.coll, a task such
++ as\n\n.. code: yaml\n\n - hosts: all\n module_defaults:\n ns.coll.eos_facts:
++ {'valid_for_eos_facts': 'value'}\n ns.coll.eos_command: {'not_valid_for_eos_facts':
++ 'value'}\n tasks:\n - ns.coll.facts:\n\nwill end up with defaults
++ for eos_facts and eos_command\nsince both modules redirect to the same action.\n\nTo
++ select an action plugin for a module without merging\nmodule_defaults, define
++ an action_plugin field for the resolved\nmodule in the runtime.yml.\n\n..
++ code: yaml\n\n plugin_routing:\n modules:\n facts:\n redirect:
++ ns.coll.eos_facts\n action_plugin: ns.coll.eos\n command:\n
++ \ redirect: ns.coll.eos_command\n action_plugin: ns.coll.eos\n\nThe
++ action_plugin field can be a redirected action plugin, as\nit is resolved
++ normally.\n\nUsing the modified runtime.yml, the example task will only use\nthe
++ ns.coll.eos_facts defaults.\n"
++ - Add support for parsing ``-a`` module options as JSON and not just key=value
++ arguments - https://github.com/ansible/ansible/issues/78112
++ - Added Kylin Linux Advanced Server OS in RedHat OS Family.
++ - Allow ``when`` conditionals to be used on ``flush_handlers`` (https://github.com/ansible/ansible/issues/77616)
++ - Allow meta tasks to be used as handlers.
++ - Display - The display class will now proxy calls to Display.display via the
++ queue from forks/workers to be handled by the parent process for actual display.
++ This reduces some reliance on the fork start method and improves reliability
++ of displaying messages.
++ - Jinja version test - Add pep440 version_type for version test. (https://github.com/ansible/ansible/issues/78288)
++ - Loops - Add new ``loop_control.extended_allitems`` to allow users to disable
++ tracking all loop items for each loop (https://github.com/ansible/ansible/issues/75216)
++ - NetBSD - Add uptime_seconds fact
++ - Provide a `utc` option for strftime to show time in UTC rather than local
++ time
++ - Raise a proper error when ``include_role`` or ``import_role`` is used as a
++ handler.
++ - Remove the ``AnsibleContext.resolve`` method as its override is not necessary.
++ Furthermore the ability to override the ``resolve`` method was deprecated
++ in Jinja 3.0.0 and removed in Jinja 3.1.0.
++ - Utilize @classmethod and @property together to form classproperty (Python
++ 3.9) to access field attributes of a class
++ - '``LoopControl`` is now templated through standard ``post_validate`` method
++ (https://github.com/ansible/ansible/pull/75715)'
++ - '``ansible-galaxy collection install`` - add an ``--offline`` option to prevent
++ querying distribution servers (https://github.com/ansible/ansible/issues/77443).'
++ - ansible - Add support for Python 3.11 to Python interpreter discovery.
++ - ansible - At startup the stdin/stdout/stderr file handles are checked to verify
++ they are using blocking IO. If not, the process exits with an error reporting
++ which file handle(s) are using non-blocking IO.
++ - ansible-config adds JSON and YAML output formats for list and dump actions.
++ - ansible-connection now supports verbosity directly on cli
++ - ansible-console added 'collections' command to match playbook keyword.
++ - ansible-doc - remove some of the manual formatting, and use YAML more uniformly.
++ This in particular means that ``true`` and ``false`` are used for boolean
++ values, instead of ``True`` and ``False`` (https://github.com/ansible/ansible/pull/78668).
++ - ansible-galaxy - Support resolvelib versions 0.6.x, 0.7.x, and 0.8.x. The
++ full range of supported versions is now >= 0.5.3, < 0.9.0.
++ - ansible-galaxy now supports a user defined timeout, instead of existing hardcoded
++ 60s (now the default).
++ - ansible-test - Add FreeBSD 13.1 remote support.
++ - ansible-test - Add RHEL 9.0 remote support.
++ - ansible-test - Add support for Python 3.11.
++ - ansible-test - Add support for RHEL 8.6 remotes.
++ - ansible-test - Add support for Ubuntu VMs using the ``--remote`` option.
++ - ansible-test - Add support for exporting inventory with ``ansible-test shell
++ --export {path}``.
++ - ansible-test - Add support for multi-arch remotes.
++ - ansible-test - Add support for provisioning Alpine 3.16 remote instances.
++ - ansible-test - Add support for provisioning Fedora 36 remote instances.
++ - ansible-test - Add support for provisioning Ubuntu 20.04 remote instances.
++ - ansible-test - Add support for provisioning remotes which require ``doas``
++ for become.
++ - ansible-test - Add support for running non-interactive commands with ``ansible-test
++ shell``.
++ - ansible-test - Alpine remotes now use ``sudo`` for tests, using ``doas`` only
++ for bootstrapping.
++ - ansible-test - An improved error message is shown when the download of a pip
++ bootstrap script fails. The download now uses ``urllib2`` instead of ``urllib``
++ on Python 2.
++ - ansible-test - Avoid using the ``mock_use_standalone_module`` setting for
++ unit tests running on Python 3.8 or later.
++ - ansible-test - Become support for remote instance provisioning is no longer
++ tied to a fixed list of platforms.
++ - ansible-test - Blocking mode is now enforced for stdin, stdout and stderr.
++ If any of these are non-blocking then ansible-test will exit during startup
++ with an error.
++ - ansible-test - Distribution specific test containers are now multi-arch, supporting
++ both x86_64 and aarch64.
++ - ansible-test - Distribution specific test containers no longer contain a ``/etc/ansible/hosts``
++ file.
++ - ansible-test - Enable loading of ``coverage`` data files created by older
++ supported ansible-test releases.
++ - ansible-test - Fedora 36 has been added as a test container.
++ - ansible-test - FreeBSD remotes now use ``sudo`` for tests, using ``su`` only
++ for bootstrapping.
++ - ansible-test - Improve consistency of output messages by using stdout or stderr
++ for most output, but not both.
++ - ansible-test - Remote Alpine instances now have the ``acl`` package installed.
++ - ansible-test - Remote Fedora instances now have the ``acl`` package installed.
++ - ansible-test - Remote FreeBSD instances now have ACLs enabled on the root
++ filesystem.
++ - ansible-test - Remote Ubuntu instances now have the ``acl`` package installed.
++ - ansible-test - Remove Fedora 34 test container.
++ - ansible-test - Remove Fedora 35 test container.
++ - ansible-test - Remove FreeBSD 13.0 remote support.
++ - ansible-test - Remove RHEL 8.5 remote support.
++ - ansible-test - Remove Ubuntu 18.04 test container.
++ - ansible-test - Remove support for Python 2.7 on provisioned FreeBSD instances.
++ - ansible-test - Remove support for Python 3.8 on the controller.
++ - ansible-test - Remove the ``opensuse15py2`` container.
++ - ansible-test - Support multiple pinned versions of the ``coverage`` module.
++ The version used now depends on the Python version in use.
++ - ansible-test - Test containers have been updated to remove the ``VOLUME``
++ instruction.
++ - ansible-test - The Alpine 3 test container has been updated to Alpine 3.16.0.
++ - ansible-test - The ``http-test-container`` container is now multi-arch, supporting
++ both x86_64 and aarch64.
++ - ansible-test - The ``pypi-test-container`` container is now multi-arch, supporting
++ both x86_64 and aarch64.
++ - ansible-test - The ``shell`` command can be used outside a collection if no
++ controller delegation is required.
++ - ansible-test - The openSUSE test container has been updated to openSUSE Leap
++ 15.4.
++ - ansible-test - Ubuntu 22.04 has been added as a test container.
++ - ansible-test - Update pinned sanity test requirements for all tests.
++ - ansible-test - Update the ``base`` container to 3.4.0.
++ - ansible-test - Update the ``default`` containers to 6.6.0.
++ - apt_repository remove dependency on apt-key and use gpg + /usr/share/keyrings
++ directly instead
++ - blockinfile - The presence of the multiline flag (?m) in the regular expression
++ for insertafter opr insertbefore controls whether the match is done line by
++ line or with multiple lines (https://github.com/ansible/ansible/pull/75090).
++ - calls to listify_lookup_plugin_terms in core do not pass in loader/dataloader
++ anymore.
++ - collections - ``ansible-galaxy collection build`` can now utilize ``MANIFEST.in``
++ style directives from ``galaxy.yml`` instead of ``build_ignore`` effectively
++ inverting the logic from include by default, to exclude by default. (https://github.com/ansible/ansible/pull/78422)
++ - config manager, move templating into main query function in config instead
++ of constants
++ - config manager, remove updates to configdata as it is mostly unused
++ - configuration entry INTERPRETER_PYTHON_DISTRO_MAP is now 'private' and won't
++ show up in normal configuration queries and docs, since it is not 'settable'
++ this avoids user confusion.
++ - distribution - add distribution_minor_version for Debian Distro (https://github.com/ansible/ansible/issues/74481).
++ - documentation construction now gives more information on error.
++ - facts - add OSMC to Debian os_family mapping
++ - get_url - permit to pass to parameter ``checksum`` an URL pointing to a file
++ containing only a checksum (https://github.com/ansible/ansible/issues/54390).
++ - new tests url, uri and urn will verify string as such, but they don't check
++ existance of the resource
++ - plugin loader - add ansible_name and ansible_aliases attributes to plugin
++ objects/classes.
++ - systemd is now systemd_service to better reflect the scope of the module,
++ systemd is kept as an alias for backwards compatibility.
++ - templating - removed internal template cache
++ - uri - cleanup write_file method, remove overkill safety checks and report
++ any exception, change shutilcopyfile to use module.atomic_move
++ - urls - Add support to specify SSL/TLS ciphers to use during a request (https://github.com/ansible/ansible/issues/78633)
++ - 'validate-modules - Allow ``type: raw`` on a module return type definition
++ for values that have a dynamic type'
++ - version output now includes the path to the python executable that Ansible
++ is running under
++ - yum_repository - do not give the ``async`` parameter a default value anymore,
++ since this option is deprecated in RHEL 8. This means that ``async = 1`` won't
++ be added to repository files if omitted, but it can still be set explicitly
++ if needed.
++ release_summary: '| Release Date: 2022-09-26
++
++ | `Porting Guide <https://docs.ansible.com/ansible/devel/porting_guides.html>`__
++
++ '
++ removed_features:
++ - PlayIterator - remove deprecated ``PlayIterator.ITERATING_*`` and ``PlayIterator.FAILED_*``
++ - Remove deprecated ``ALLOW_WORLD_READABLE_TMPFILES`` configuration option (https://github.com/ansible/ansible/issues/77393)
++ - Remove deprecated ``COMMAND_WARNINGS`` configuration option (https://github.com/ansible/ansible/issues/77394)
++ - Remove deprecated ``DISPLAY_SKIPPED_HOSTS`` environment variable (https://github.com/ansible/ansible/issues/77396)
++ - Remove deprecated ``LIBVIRT_LXC_NOSECLABEL`` environment variable (https://github.com/ansible/ansible/issues/77395)
++ - Remove deprecated ``NETWORK_GROUP_MODULES`` environment variable (https://github.com/ansible/ansible/issues/77397)
++ - Remove deprecated ``UnsafeProxy``
++ - Remove deprecated ``plugin_filters_cfg`` config option from ``default`` section
++ (https://github.com/ansible/ansible/issues/77398)
++ - Remove deprecated functionality that allows loading cache plugins directly
++ without using ``cache_loader``.
++ - Remove deprecated functionality that allows subclassing ``DefaultCallback``
++ without the corresponding ``doc_fragment``.
++ - Remove deprecated powershell functions ``Load-CommandUtils`` and ``Import-PrivilegeUtil``
++ - apt_key - remove deprecated ``key`` module param
++ - command/shell - remove deprecated ``warn`` module param
++ - get_url - remove deprecated ``sha256sum`` module param
++ - import_playbook - remove deprecated functionality that allows providing additional
++ parameters in free form
++ codename: C'mon Everybody
++ fragments:
++ - 17393-fix_silently_failing_lvm_facts.yaml
++ - 23161-includes-loops-rescue.yml
++ - 29680-fetch-file-file-name-too-long.yml
++ - 43191-72638-ansible_failed_task-fixes.yml
++ - 56017-allow-lazy-eval-on-jinja2-expr.yml
++ - 58632-uri-include_use_proxy.yaml
++ - 61965-user-module-fails-to-change-primary-group.yml
++ - 64612-fetch_file-multi-part-extension.yml
++ - 65499-no_inventory_parsed.yml
++ - 70180-collection-list-more-robust.yml
++ - 73072-dnf-skip-broken.yml
++ - 74446-network-conn-options.yaml
++ - 74481_debian_minor_version.yml
++ - 75042-lowercase-dash-n-with-luseradd-on-all-distros.yml
++ - 75090-multiline-flag-support-for-blockinfile.yml
++ - 75216-loop-control-extended-allitems.yml
++ - 75364-yum-repository-async.yml
++ - 75431-Add-uptime-fact-for-NetBSD.yml
++ - 75715-post_validate-LoopControl.yml
++ - 75740-remove-travis-file-from-role-skeletons.yml
++ - 76167-update-attributes-of-files-that-are-links.yml
++ - 76737-paramiko-rsa-sha2.yml
++ - 76971-unarchive-remove-unnecessary-zipinfo-dependency.yml
++ - 77014-ansible-galaxy-list-fix-null-metadata-namespace-name.yml
++ - 77265-module_defaults-with-modules-as-redirected-actions.yaml
++ - 77315-fix-meta-vv-header.yml
++ - 77393-remove-allow_world_readable_tmpfiles.yml
++ - 77394-remove-command_warnings.yml
++ - 77395-remove-libvirt_lxc_noseclabel.yml
++ - 77396-remove-display_skipped_hosts.yml
++ - 77397-remove-network_group_modules.yml
++ - 77398-remove-plugin_filters_cfg-default.yml
++ - 77418-ansible-galaxy-init-include-meta-runtime.yml
++ - 77424-fix-False-ansible-galaxy-server-config-options.yaml
++ - 77465-ansible-test-compile-crash.yml
++ - 77468-ansible-galaxy-remove-unnecessary-api-call.yml
++ - 77472-ansible-test-network-disconnect-warning.yml
++ - 77493-ansible-galaxy-find-git-executable-before-using.yaml
++ - 77507-deprecate-pc-verbosity.yml
++ - 77535-prevent-losing-unsafe-lookups.yml
++ - 77544-fix-error-yaml-inventory-int-hostnames.yml
++ - 77561-ansible-galaxy-coll-install-null-dependencies.yml
++ - 77576-arg_spec-no_log-aliases.yml
++ - 77599-add-url-include-deprecation.yml
++ - 77630-ansible-galaxy-fix-unsupported-resolvelib-version.yml
++ - 77649-support-recent-resolvelib-versions.yml
++ - 77679-syntax-error-mention-filename.yml
++ - 77693-actually-ignore-unreachable.yml
++ - 77781-callback-crash.yml
++ - 77788-deprecate-non-lists-lookups.yml
++ - 77789-catch-keyerror-lookup-dict.yml
++ - 77792-fix-facts-discovery-specific-interface-names.yml
++ - 77898-ansible-config-dump-only-changed-all-types.yml
++ - 77934-empty-loop-template-callback.yml
++ - 77936-add-pyyaml-version.yml
++ - 77969-apt-preferences.yml
++ - 78050-replace-spwd.yml
++ - 78058-yum-releasever-latest.yml
++ - 78112-adhoc-args-as-json.yml
++ - 78141-template-fix-convert_data.yml
++ - 78156-undefined-check-in-finalize.yml
++ - 78204-galaxy-role-file-detection.yml
++ - 78214-wait-for-compare-bytes.yml
++ - 78223_aix_fix_processor_facts.yml
++ - 78295-dnf-fix-comparison-operators-docs.yml
++ - 78325-ansible-galaxy-fix-caching-paginated-responses-from-v3-servers.yml
++ - 78496-fix-apt-check-mode.yml
++ - 78512-uri-use-netrc-true-false-argument.yml
++ - 78516-galaxy-cli-exit-codes.yml
++ - 78562-deprecate-vars-plugin-attr.yml
++ - 78612-rescue-block-ansible_play_hosts.yml
++ - 78633-urls-ciphers.yml
++ - 78648-urllib3-import-exceptions.yml
++ - 78668-ansible-doc-formatting.yml
++ - 78678-add-a-g-install-offline.yml
++ - 78700-add-plugin-name-and-aliases.yml
++ - 78750-paramiko-ssh-args-compat.yml
++ - 78781-fix-apt-only_upgrade-behavior.yml
++ - abstract_errors_info.yml
++ - add-omsc-os-family.yml
++ - added_uri_tests.yml
++ - adoc_moarf.yml
++ - aix_chmod_255.yml
++ - ansible-connection_decode.yml
++ - ansible-console-renamed-arg.yml
++ - ansible-galaxy-collection-init-force.yml
++ - ansible-require-blocking-io.yml
++ - ansible-require-utf8.yml
++ - ansible-test-ansible-core-mock.yml
++ - ansible-test-ansible-doc-sanity-fqcn.yml
++ - ansible-test-container-tmpfs.yml
++ - ansible-test-containers-no-volume.yml
++ - ansible-test-content-config.yml
++ - ansible-test-coverage.yml
++ - ansible-test-default-containers.yml
++ - ansible-test-distro-containers-hosts.yml
++ - ansible-test-distro-containers.yml
++ - ansible-test-drop-python-3.8-controller.yml
++ - ansible-test-fedora-35.yml
++ - ansible-test-filter-options.yml
++ - ansible-test-generalize-become.yml
++ - ansible-test-integration-targets-filter.yml
++ - ansible-test-less-python-2.7.yml
++ - ansible-test-locale.yml
++ - ansible-test-more-remotes.yml
++ - ansible-test-multi-arch-cloud-containers.yml
++ - ansible-test-multi-arch-distro-containers.yml
++ - ansible-test-multi-arch-remotes.yml
++ - ansible-test-pip-bootstrap.yml
++ - ansible-test-podman-create-retry.yml
++ - ansible-test-remote-acl.yml
++ - ansible-test-remote-become.yml
++ - ansible-test-remote-completion-validation.yml
++ - ansible-test-remotes.yml
++ - ansible-test-rhel-8.6.yml
++ - ansible-test-sanity-requirements.yml
++ - ansible-test-self-change-classification.yml
++ - ansible-test-shell-features.yml
++ - ansible-test-subprocess-isolation.yml
++ - ansible-test-target-filter.yml
++ - ansible-test-target-options.yml
++ - ansible-test-tty-output-handling.yml
++ - ansible-test-ubuntu-bootstrap-fix.yml
++ - ansible-test-ubuntu-remote.yml
++ - ansible-test-validate-modules-docs-only-docstring.yml
++ - ansible-test-verify-executables.yml
++ - ansible_connection_verbosity.yml
++ - apt_key-remove-deprecated-key.yml
++ - apt_repository_sans_apt_key.yml
++ - apt_virtual_fix.yml
++ - atomic_cache_files.yml
++ - better-msg-role-in-handler.yml
++ - better_info_sources.yml
++ - better_nohosts_error.yml
++ - collection-build-manifest.yml
++ - config_error_origin.yml
++ - config_formats.yml
++ - config_load_by_name.yml
++ - config_manager_changes.yml
++ - console_list_all.yml
++ - deprecate-crypt-support.yml
++ - deprecate-fact_path-gather_subset-gather_timeout-defaults.yml
++ - display_verbosity.yml
++ - dnf-fix-locale-language.yml
++ - doc_errors.yml
++ - doc_vac_ignore.yml
++ - dont-expose-included-handlers.yml
++ - ensure_config_always_templated.yml
++ - fieldattributes-classproperty.yml
++ - fix-change-while-iterating-module-utils-service.yml
++ - fix_adoc_text.yml
++ - fix_init_commented.yml
++ - fix_inv_refresh.yml
++ - forked-display-via-queue.yml
++ - galaxy_server_timeout.yml
++ - get_url-accept-file-for-checksum.yml
++ - get_url-remove-deprecated-sha256sum.yml
++ - git_fixes.yml
++ - handle-role-dependency-type-error.yml
++ - hide_distro_map.yml
++ - import_playbook-remove-params.yml
++ - items2dict-error-handling.yml
++ - kylin_linux_advanced_server_distribution_support.yml
++ - legacy_no_file_skip.yml
++ - loader_in_listify.yml
++ - local_fact_unreadable.yml
++ - null_means_none.yml
++ - opensuse_disto_id.yml
++ - password_lookup_fix.yml
++ - pause_echo_fix.yml
++ - pep440-version-type.yml
++ - permission-denied-spwd-module.yml
++ - pip-lazy-import.yml
++ - play_iterator-remove_deprecations.yml
++ - play_iterator_iterating_handlers.yml
++ - playiterator-deprecate-methods.yml
++ - plugin-loader-deterministic-fuzzy-match.yml
++ - powershell-deprecated-functions.yml
++ - python-2.6-discovery.yml
++ - python-3.11.yml
++ - python39-min-controller.yml
++ - python_version_path.yml
++ - remove-ansiblecontext-resolve.yml
++ - remove-deprecated-default-callback-without-doc.yml
++ - remove-import-cache-plugin-directly.yml
++ - require-fqcn-redirects.yml
++ - restrict_role_files_to_role.yml
++ - self_referential.yml
++ - shell_env_typeerror.yml
++ - strftime-in-utc.yml
++ - systemd_services.yml
++ - templar-correct-environment_class-template.yml
++ - templar-deprecate-shared_loader_obj.yml
++ - template_override.yml
++ - type_shim_exception_swallow.yml
++ - unsafeproxy-deprecated.yml
++ - until_also_implicit.yml
++ - use-before-definition.yml
++ - v2.14.0-initial-commit.yaml
++ - v2.14.0b1_summary.yaml
++ - validate-modules-module-raw-return-type.yml
++ - validate-modules-version_added.yaml
++ - vault_syml_allow.yml
++ - vm_more_efficient.yml
++ - windows_conn_option_fix.yml
++ - winrm-kinit-path.yml
++ - write_file_uri_cleanup.yml
++ - zap_template_cache.yml
++ release_date: '2022-09-26'
++ 2.14.0b2:
++ changes:
++ breaking_changes:
++ - ansible-test validate-modules - Removed the ``missing-python-doc`` error code
++ in validate modules, ``missing-documentation`` is used instead for missing
++ PowerShell module documentation.
++ bugfixes:
++ - Fix reusing a connection in a task loop that uses a redirected or aliased
++ name - https://github.com/ansible/ansible/issues/78425
++ - Fix setting become activation in a task loop - https://github.com/ansible/ansible/issues/78425
++ - apt module should not traceback on invalid type given as package. issue 78663.
++ - known_hosts - do not return changed status when a non-existing key is removed
++ (https://github.com/ansible/ansible/issues/78598)
++ - plugin loader, fix detection for existing configuration before initializing
++ for a plugin
++ minor_changes:
++ - ansible-test validate-modules - Added support for validating module documentation
++ stored in a sidecar file alongside the module (``{module}.yml`` or ``{module}.yaml``).
++ Previously these files were ignored and documentation had to be placed in
++ ``{module}.py``.
++ - apt_repository will use the trust repo directories in order of preference
++ (more appropriate to less) as they exist on the target.
++ release_summary: '| Release Date: 2022-10-03
++
++ | `Porting Guide <https://docs.ansible.com/ansible/devel/porting_guides.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 78881-fix-known-hosts-wrong-changed-status.yaml
++ - apt_notb.yml
++ - apt_repo_trust_prefs.yml
++ - become-loop-setting.yml
++ - plugin_loader_fix.yml
++ - v2.14.0b2_summary.yaml
++ - validate-modules-sidecar.yml
++ release_date: '2022-10-03'
++ 2.14.0b3:
++ changes:
++ bugfixes:
++ - Do not crash when templating an expression with a test or filter that is not
++ a valid Ansible filter name (https://github.com/ansible/ansible/issues/78912,
++ https://github.com/ansible/ansible/pull/78913).
++ - 'handlers - fix an issue where the ``flush_handlers`` meta task could not
++ be used with FQCN: ``ansible.builtin.meta`` (https://github.com/ansible/ansible/issues/79023)'
++ - keyword inheritance - Ensure that we do not squash keywords in validate (https://github.com/ansible/ansible/issues/79021)
++ - omit on keywords was resetting to default value, ignoring inheritance.
++ - service_facts - Use python re to parse service output instead of grep (https://github.com/ansible/ansible/issues/78541)
++ release_summary: '| Release Date: 2022-10-10
++
++ | `Porting Guide <https://docs.ansible.com/ansible/devel/porting_guides.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 78541-service-facts-re.yml
++ - 78913-template-missing-filter-test.yml
++ - 79021-dont-squash-in-validate.yml
++ - 79023-fix-flush_handlers-fqcn.yml
++ - fix_omit_key.yml
++ - v2.14.0b3_summary.yaml
++ plugins:
++ test:
++ - description: is the string a valid URI
++ name: uri
++ namespace: null
++ - description: is the string a valid URL
++ name: url
++ namespace: null
++ - description: is the string a valid URN
++ name: urn
++ namespace: null
++ release_date: '2022-10-10'
++ 2.14.0rc1:
++ changes:
++ bugfixes:
++ - BSD network facts - Do not assume column indexes, look for ``netmask`` and
++ ``broadcast`` for determining the correct columns when parsing ``inet`` line
++ (https://github.com/ansible/ansible/issues/79117)
++ - ansible-config limit shorthand format to assigned values
++ - ansible-test - Update the ``pylint`` sanity test to use version 2.15.4.
++ release_summary: '| Release Date: 2022-10-17
++
++ | `Porting Guide <https://docs.ansible.com/ansible/devel/porting_guides.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 79117-bsd-ifconfig-inet-fix.yml
++ - adjust_config_list.yml
++ - ansible-test-pylint-2.15.4.yml
++ - v2.14.0rc1_summary.yaml
++ release_date: '2022-10-13'
++ 2.14.0rc2:
++ changes:
++ bugfixes:
++ - ansible-test - Add ``wheel < 0.38.0`` constraint for Python 3.6 and earlier.
++ - ansible-test - Update the ``pylint`` sanity test requirements to resolve crashes
++ on Python 3.11. (https://github.com/ansible/ansible/issues/78882)
++ - ansible-test - Update the ``pylint`` sanity test to use version 2.15.5.
++ minor_changes:
++ - ansible-test - Update ``base`` and ``default`` containers to include Python
++ 3.11.0.
++ - ansible-test - Update ``default`` containers to include new ``docs-build``
++ sanity test requirements.
++ release_summary: '| Release Date: 2022-10-31
++
++ | `Porting Guide <https://docs.ansible.com/ansible/devel/porting_guides.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 79187--wheel-0.38.0.yml
++ - ansible-test-containers-docs-build.yml
++ - ansible-test-containers-python-3.11.0.yml
++ - ansible-test-pylint-2.15.5.yml
++ - v2.14.0rc2_summary.yaml
++ release_date: '2022-10-31'
++ 2.14.1:
++ changes:
++ bugfixes:
++ - display - reduce risk of post-fork output deadlocks (https://github.com/ansible/ansible/pull/79522)
++ release_summary: '| Release Date: 2022-12-06
++
++ | `Porting Guide <https://docs.ansible.com/ansible/devel/porting_guides.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - fork_safe_stdio.yml
++ - v2.14.1_summary.yaml
++ release_date: '2022-12-06'
++ 2.14.10:
++ changes:
++ release_summary: '| Release Date: 2023-09-11
++
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 2.14.10_summary.yaml
++ release_date: '2023-09-11'
++ 2.14.10rc1:
++ changes:
++ bugfixes:
+ - PowerShell - Remove some code which is no longer valid for dotnet 5+
+- - Prevent running same handler multiple times when included via ``include_role``
+- (https://github.com/ansible/ansible/issues/73643)
+- - Prompting - add a short sleep between polling for user input to reduce CPU
+- consumption (https://github.com/ansible/ansible/issues/81516).
+- - Properly disable ``jinja2_native`` in the template module when jinja2 override
+- is used in the template (https://github.com/ansible/ansible/issues/80605)
+- - Remove unreachable parser error for removed ``static`` parameter of ``include_role``
+- - Replace uses of ``configparser.ConfigParser.readfp()`` which was removed in
+- Python 3.12 with ``configparser.ConfigParser.read_file()`` (https://github.com/ansible/ansible/issues/81656)
+- - Set filters ``intersect``, ``difference``, ``symmetric_difference`` and ``union``
+- now always return a ``list``, never a ``set``. Previously, a ``set`` would
+- be returned if the inputs were a hashable type such as ``str``, instead of
+- a collection, such as a ``list`` or ``tuple``.
+- - Set filters ``intersect``, ``difference``, ``symmetric_difference`` and ``union``
+- now use set operations when the given items are hashable. Previously, list
+- operations were performed unless the inputs were a hashable type such as ``str``,
+- instead of a collection, such as a ``list`` or ``tuple``.
+- - Switch result queue from a ``multiprocessing.queues.Queue` to ``multiprocessing.queues.SimpleQueue``,
+- primarily to allow properly handling pickling errors, to prevent an infinite
+- hang waiting for task results
+- - The ``ansible-config init`` command now has a documentation description.
+- - The ``ansible-galaxy collection download`` command now has a documentation
+- description.
+- - The ``ansible-galaxy collection install`` command documentation is now visible
+- (previously hidden by a decorator).
+- - The ``ansible-galaxy collection verify`` command now has a documentation description.
+- - The ``ansible-galaxy role install`` command documentation is now visible (previously
+- hidden by a decorator).
+- - The ``ansible-inventory`` command command now has a documentation description
+- (previously used as the epilog).
+- - The ``hostname`` module now also updates both current and permanent hostname
+- on OpenBSD. Before it only updated the permanent hostname (https://github.com/ansible/ansible/issues/80520).
+- - Update module_utils.urls unit test to work with cryptography >= 41.0.0.
+- - When generating man pages, use ``func`` to find the command function instead
+- of looking it up by the command name.
+- - '``StrategyBase._process_pending_results`` - create a ``Templar`` on demand
+- for templating ``changed_when``/``failed_when``.'
+- - '``ansible-galaxy`` now considers all collection paths when identifying which
+- collection requirements are already installed. Use the ``COLLECTIONS_PATHS``
+- and ``COLLECTIONS_SCAN_SYS_PATHS`` config options to modify these. Previously
+- only the install path was considered when resolving the candidates. The install
+- path will remain the only one potentially modified. (https://github.com/ansible/ansible/issues/79767,
+- https://github.com/ansible/ansible/issues/81163)'
+- - '``ansible.module_utils.service`` - ensure binary data transmission in ``daemonize()``'
+- - '``ansible.module_utils.service`` - fix inter-process communication in ``daemonize()``'
+- - '``pkg_mgr`` - fix the default dnf version detection'
+- - ansiballz - Prevent issue where the time on the control host could change
+- part way through building the ansiballz file, potentially causing a pre-1980
+- date to be used during ansiballz unpacking leading to a zip file error (https://github.com/ansible/ansible/issues/80089)
+- - ansible terminal color settings were incorrectly limited to 16 options via
+- 'choices', removing so all 256 can be accessed.
+- - ansible-console - fix filtering by collection names when a collection search
+- path was set (https://github.com/ansible/ansible/pull/81450).
+ - ansible-galaxy - Enabled the ``data`` tarfile filter during role installation
+ for Python versions that support it. A probing mechanism is used to avoid
+ Python versions with a broken implementation.
+- - ansible-galaxy - Fix issue installing collections containing directories with
+- more than 100 characters on python versions before 3.10.6
+- - ansible-galaxy - Fix variable type error when installing subdir collections
+- (https://github.com/ansible/ansible/issues/80943)
+- - ansible-galaxy - fix installing collections from directories that have a trailing
+- path separator (https://github.com/ansible/ansible/issues/77803).
+- - ansible-galaxy - fix installing signed collections (https://github.com/ansible/ansible/issues/80648).
+- - ansible-galaxy - reduce API calls to servers by fetching signatures only for
+- final candidates.
+- - ansible-galaxy - started allowing the use of pre-releases for collections
+- that do not have any stable versions published. (https://github.com/ansible/ansible/pull/81606)
+- - ansible-galaxy - started allowing the use of pre-releases for dependencies
+- on any level of the dependency tree that specifically demand exact pre-release
+- versions of collections and not version ranges. (https://github.com/ansible/ansible/pull/81606)
+- - ansible-galaxy collection verify - fix verifying signed collections when the
+- keyring is not configured.
+- - ansible-test - Add support for ``argcomplete`` version 3.
+- - ansible-test - All containers created by ansible-test now include the current
+- test session ID in their name. This avoids conflicts between concurrent ansible-test
+- invocations using the same container host.
+ - ansible-test - Always use ansible-test managed entry points for ansible-core
+ CLI tools when not running from source. This fixes issues where CLI entry
+ points created during install are not compatible with ansible-test.
+- - ansible-test - Fix a traceback that occurs when attempting to test Ansible
+- source using a different ansible-test. A clear error message is now given
+- when this scenario occurs.
+- - ansible-test - Fix handling of timeouts exceeding one day.
+- - ansible-test - Fix several possible tracebacks when using the ``-e`` option
+- with sanity tests.
+- - ansible-test - Fix various cases where the test timeout could expire without
+- terminating the tests.
+- - ansible-test - Pre-build a PyYAML wheel before installing requirements to
+- avoid a potential Cython build failure.
+- - ansible-test - Remove redundant warning about missing programs before attempting
+- to execute them.
+- - ansible-test - The ``import`` sanity test now checks the collection loader
+- for remote-only Python support when testing ansible-core.
+- - ansible-test - Unit tests now report warnings generated during test runs.
+- Previously only warnings generated during test collection were reported.
+- - ansible-test - Update ``pylint`` to 2.17.2 to resolve several possible false
+- positives.
+- - ansible-test - Update ``pylint`` to 2.17.3 to resolve several possible false
+- positives.
+- - ansible-test - Use ``raise ... from ...`` when raising exceptions from within
+- an exception handler.
+- - ansible-test - When bootstrapping remote FreeBSD instances, use the OS packaged
+- ``setuptools`` instead of installing the latest version from PyPI.
+- - ansible-test local change detection - use ``git merge-base <branch> HEAD``
+- instead of ``git merge-base --fork-point <branch>`` (https://github.com/ansible/ansible/pull/79734).
+- - ansible-vault - fail when the destination file location is not writable before
+- performing encryption (https://github.com/ansible/ansible/issues/81455).
+- - apt - ignore fail_on_autoremove and allow_downgrade parameters when using
+- aptitude (https://github.com/ansible/ansible/issues/77868).
+- - blockinfile - avoid crash with Python 3 if creating the directory fails when
+- ``create=true`` (https://github.com/ansible/ansible/pull/81662).
+- - connection timeouts defined in ansible.cfg will now be properly used, the
+- --timeout cli option was obscuring them by always being set.
+- - copy - print correct destination filename when using `content` and `--diff`
+- (https://github.com/ansible/ansible/issues/79749).
+- - copy unit tests - Fixing "dir all perms" documentation and formatting for
+- easier reading.
+- - core will now also look at the connection plugin to force 'local' interpreter
+- for networking path compatibility as just ansible_network_os could be misleading.
+- - deb822_repository - use http-agent for receiving content (https://github.com/ansible/ansible/issues/80809).
+- - debconf - idempotency in questions with type 'password' (https://github.com/ansible/ansible/issues/47676).
+- - distribution facts - fix Source Mage family mapping
+- - dnf - fix a failure when a package from URI was specified and ``update_only``
+- was set (https://github.com/ansible/ansible/issues/81376).
+- - dnf5 - Update dnf5 module to handle API change for setting the download directory
+- (https://github.com/ansible/ansible/issues/80887)
+- - dnf5 - Use ``transaction.check_gpg_signatures`` API call to check package
+- signatures AND possibly to recover from when keys are missing.
+- - dnf5 - fix module and package names in the message following failed module
+- respawn attempt
+- - dnf5 - use the logs API to determine transaction problems
+- - dpkg_selections - check if the package exists before performing the selection
+- operation (https://github.com/ansible/ansible/issues/81404).
+- - encrypt - deprecate passlib_or_crypt API (https://github.com/ansible/ansible/issues/55839).
+- - fetch - Handle unreachable errors properly (https://github.com/ansible/ansible/issues/27816)
+- - file modules - Make symbolic modes with X use the computed permission, not
+- original file (https://github.com/ansible/ansible/issues/80128)
+- - file modules - fix validating invalid symbolic modes.
+- - first found lookup has been updated to use the normalized argument parsing
+- (pythonic) matching the documented examples.
+- - first found lookup, fixed an issue with subsequent items clobbering information
+- from previous ones.
+- - first_found lookup now gets 'untemplated' loop entries and handles templating
+- itself as task_executor was removing even 'templatable' entries and breaking
+- functionality. https://github.com/ansible/ansible/issues/70772
+- - galaxy - check if the target for symlink exists (https://github.com/ansible/ansible/pull/81586).
+- - galaxy - cross check the collection type and collection source (https://github.com/ansible/ansible/issues/79463).
+- - gather_facts parallel option was doing the reverse of what was stated, now
+- it does run modules in parallel when True and serially when False.
+- - handlers - fix ``v2_playbook_on_notify`` callback not being called when notifying
+- handlers
+- - handlers - the ``listen`` keyword can affect only one handler with the same
+- name, the last one defined as it is a case with the ``notify`` keyword (https://github.com/ansible/ansible/issues/81013)
+- - include_role - expose variables from parent roles to role's handlers (https://github.com/ansible/ansible/issues/80459)
+- - inventory_ini - handle SyntaxWarning while parsing ini file in inventory (https://github.com/ansible/ansible/issues/81457).
+- - iptables - remove default rule creation when creating iptables chain to be
+- more similar to the command line utility (https://github.com/ansible/ansible/issues/80256).
+- - lib/ansible/utils/encrypt.py - remove unused private ``_LOCK`` (https://github.com/ansible/ansible/issues/81613)
+- - lookup/url.py - Fix incorrect var/env/ini entry for `force_basic_auth`
+- - man page build - Remove the dependency on the ``docs`` directory for building
+- man pages.
+- - man page build - Sub commands of ``ansible-galaxy role`` and ``ansible-galaxy
+- collection`` are now documented.
+- - module responses - Ensure that module responses are utf-8 adhereing to JSON
+- RFC and expectations of the core code.
+- - module/role argument spec - validate the type for options that are None when
+- the option is required or has a non-None default (https://github.com/ansible/ansible/issues/79656).
+- - modules/user.py - Add check for valid directory when creating new user homedir
+- (allows /dev/null as skeleton) (https://github.com/ansible/ansible/issues/75063)
+- - paramiko_ssh, psrp, and ssh connection plugins - ensure that all values for
+- options that should be strings are actually converted to strings (https://github.com/ansible/ansible/pull/81029).
+- - password_hash - fix salt format for ``crypt`` (only used if ``passlib`` is
+- not installed) for the ``bcrypt`` algorithm.
+- - pep517 build backend - Copy symlinks when copying the source tree. This avoids
+- tracebacks in various scenarios, such as when a venv is present in the source
+- tree.
+- - pep517 build backend - Use the documented ``import_module`` import from ``importlib``.
+- - pip module - Update module to prefer use of the python ``packaging`` and ``importlib.metadata``
+- modules due to ``pkg_resources`` being deprecated (https://github.com/ansible/ansible/issues/80488)
+- - pkg_mgr.py - Fix `ansible_pkg_mgr` incorrect in TencentOS Server Linux
+- - pkg_mgr.py - Fix `ansible_pkg_mgr` is unknown in Kylin Linux (https://github.com/ansible/ansible/issues/81332)
+- - powershell modules - Only set an rc of 1 if the PowerShell pipeline signaled
+- an error occurred AND there are error records present. Previously it would
+- do so only if the error signal was present without checking the error count.
+- - replace - handle exception when bad escape character is provided in replace
+- (https://github.com/ansible/ansible/issues/79364).
+- - role deduplication - don't deduplicate before a role has had a task run for
+- that particular host (https://github.com/ansible/ansible/issues/81486).
+- - service module, does not permanently configure flags flags on Openbsd when
+- enabling/disabling a service.
+- - service module, enable/disable is not a exclusive action in checkmode anymore.
+- - setup gather_timeout - Fix timeout in get_mounts_facts for linux.
+- - setup module (fact gathering) will now try to be smarter about different versions
+- of facter emitting error when --puppet flag is used w/o puppet.
+- - syntax check - Limit ``--syntax-check`` to ``ansible-playbook`` only, as that
+- is the only CLI affected by this argument (https://github.com/ansible/ansible/issues/80506)
+ - tarfile - handle data filter deprecation warning message for extract and extractall
+ (https://github.com/ansible/ansible/issues/80832).
+- - template - Fix for formatting issues when a template path contains valid jinja/strftime
+- pattern (especially line break one) and using the template path in ansible_managed
+- (https://github.com/ansible/ansible/pull/79129)
+- - templating - In the template action and lookup, use local jinja2 environment
+- overlay overrides instead of mutating the templars environment
+- - templating - prevent setting arbitrary attributes on Jinja2 environments via
+- Jinja2 overrides in templates
+- - templating escape and single var optimization now use correct delimiters when
+- custom ones are provided either via task or template header.
+- - unarchive - fix unarchiving sources that are copied to the remote node using
+- a relative temporory directory path (https://github.com/ansible/ansible/issues/80710).
+- - uri - fix search for JSON type to include complex strings containing '+'
+- - urls.py - fixed cert_file and key_file parameters when running on Python 3.12
+- - https://github.com/ansible/ansible/issues/80490
+- - user - set expiration value correctly when unable to retrieve the current
+- value from the system (https://github.com/ansible/ansible/issues/71916)
+- - validate-modules sanity test - replace semantic markup parsing and validating
+- code with the code from `antsibull-docs-parser 0.2.0 <https://github.com/ansible-community/antsibull-docs-parser/releases/tag/0.2.0>`__
+- (https://github.com/ansible/ansible/pull/80406).
+- - vars_prompt - internally convert the ``unsafe`` value to ``bool``
+- - vault and unvault filters now properly take ``vault_id`` parameter.
+- - win_fetch - Add support for using file with wildcards in file name. (https://github.com/ansible/ansible/issues/73128)
+- deprecated_features:
+- - Deprecated ini config option ``collections_paths``, use the singular form
+- ``collections_path`` instead
+- - Deprecated the env var ``ANSIBLE_COLLECTIONS_PATHS``, use the singular form
+- ``ANSIBLE_COLLECTIONS_PATH`` instead
+- - Support for Windows Server 2012 and 2012 R2 has been removed as the support
+- end of life from Microsoft is October 10th 2023. These versions of Windows
+- will no longer be tested in this Ansible release and it cannot be guaranteed
+- that they will continue to work going forward.
+- - '``STRING_CONVERSION_ACTION`` config option is deprecated as it is no longer
+- used in the Ansible Core code base.'
+- - the 'smart' option for setting a connection plugin is being removed as its
+- main purpose (choosing between ssh and paramiko) is now irrelevant.
+- - vault and unfault filters - the undocumented ``vaultid`` parameter is deprecated
+- and will be removed in ansible-core 2.20. Use ``vault_id`` instead.
+- - yum_repository - deprecated parameter 'keepcache' (https://github.com/ansible/ansible/issues/78693).
+- known_issues:
+- - ansible-galaxy - dies in the middle of installing a role when that role contains
+- Java inner classes (files with $ in the file name). This is by design, to
+- exclude temporary or backup files. (https://github.com/ansible/ansible/pull/81553).
+- - ansible-test - The ``pep8`` sanity test is unable to detect f-string spacing
+- issues (E201, E202) on Python 3.10 and 3.11. They are correctly detected under
+- Python 3.12. See (https://github.com/PyCQA/pycodestyle/issues/1190).
+- minor_changes:
+- - Add Python type hints to the Display class (https://github.com/ansible/ansible/issues/80841)
+- - Add ``GALAXY_COLLECTIONS_PATH_WARNING`` option to disable the warning given
+- by ``ansible-galaxy collection install`` when installing a collection to a
+- path that isn't in the configured collection paths.
+- - Add ``python3.12`` to the default ``INTERPRETER_PYTHON_FALLBACK`` list.
+- - Add ``utcfromtimestamp`` and ``utcnow`` to ``ansible.module_utils.compat.datetime``
+- to return fixed offset datetime objects.
+- - Add a general ``GALAXY_SERVER_TIMEOUT`` config option for distribution servers
+- (https://github.com/ansible/ansible/issues/79833).
+- - Added Python type annotation to connection plugins
+- - CLI argument parsing - Automatically prepend to the help of CLI arguments
+- that support being specified multiple times. (https://github.com/ansible/ansible/issues/22396)
+- - DEFAULT_TRANSPORT now defaults to 'ssh', the old 'smart' option is being deprecated
+- as versions of OpenSSH without control persist are basically not present anymore.
+- - Documentation for set filters ``intersect``, ``difference``, ``symmetric_difference``
+- and ``union`` now states that the returned list items are in arbitrary order.
+- - Record ``removal_date`` in runtime metadata as a string instead of a date.
+- - Remove the ``CleansingNodeVisitor`` class and its usage due to the templating
+- changes that made it superfluous. Also simplify the ``Conditional`` class.
+- - Removed ``exclude`` and ``recursive-exclude`` commands for generated files
+- from the ``MANIFEST.in`` file. These excludes were unnecessary since releases
+- are expected to be built with a clean worktree.
+- - Removed ``exclude`` commands for sanity test files from the ``MANIFEST.in``
+- file. These tests were previously excluded because they did not pass when
+- run from an sdist. However, sanity tests are not expected to pass from an
+- sdist, so excluding some (but not all) of the failing tests makes little sense.
+- - Removed redundant ``include`` commands from the ``MANIFEST.in`` file. These
+- includes either duplicated default behavior or another command.
+- - The ``ansible-core`` sdist no longer contains pre-generated man pages. Instead,
+- a ``packaging/cli-doc/build.py`` script is included in the sdist. This script
+- can generate man pages and standalone RST documentation for ``ansible-core``
+- CLI programs.
+- - The ``docs`` and ``examples`` directories are no longer included in the ``ansible-core``
+- sdist. These directories have been moved to the https://github.com/ansible/ansible-documentation
+- repository.
+- - The minimum required ``setuptools`` version is now 66.1.0, as it is the oldest
+- version to support Python 3.12.
+- - Update ``ansible_service_mgr`` fact to include init system for SMGL OS family
+- - Use ``ansible.module_utils.common.text.converters`` instead of ``ansible.module_utils._text``.
+- - Use ``importlib.resources.abc.TraversableResources`` instead of deprecated
+- ``importlib.abc.TraversableResources`` where available (https:/github.com/ansible/ansible/pull/81082).
+- - Use ``include`` where ``recursive-include`` is unnecessary in the ``MANIFEST.in``
+- file.
+- - Use ``package_data`` instead of ``include_package_data`` for ``setup.cfg``
+- to avoid ``setuptools`` warnings.
+- - Utilize gpg check provided internally by the ``transaction.run`` method as
+- oppose to calling it manually.
+- - '``Templar`` - do not add the ``dict`` constructor to ``globals`` as all required
+- Jinja2 versions already do so'
+- - ansible-doc - allow to filter listing of collections and metadata dump by
+- more than one collection (https://github.com/ansible/ansible/pull/81450).
+- - ansible-galaxy - Add a plural option to improve ignoring multiple signature
+- error status codes when installing or verifying collections. A space-separated
+- list of error codes can follow --ignore-signature-status-codes in addition
+- to specifying --ignore-signature-status-code multiple times (for example,
+- ``--ignore-signature-status-codes NO_PUBKEY UNEXPECTED``).
+- - ansible-galaxy - Remove internal configuration argument ``v3`` (https://github.com/ansible/ansible/pull/80721)
+- - ansible-galaxy - add note to the collection dependency resolver error message
+- about pre-releases if ``--pre`` was not provided (https://github.com/ansible/ansible/issues/80048).
+- - ansible-galaxy - used to crash out with a "Errno 20 Not a directory" error
+- when extracting files from a role when hitting a file with an illegal name
+- (https://github.com/ansible/ansible/pull/81553). Now it gives a warning identifying
+- the culprit file and the rule violation (e.g., ``my$class.jar`` has a ``$``
+- in the name) before crashing out, giving the user a chance to remove the invalid
+- file and try again. (https://github.com/ansible/ansible/pull/81555).
+- - ansible-test - Add Alpine 3.18 to remotes
+- - ansible-test - Add Fedora 38 container.
+- - ansible-test - Add Fedora 38 remote.
+- - ansible-test - Add FreeBSD 13.2 remote.
+- - ansible-test - Add new pylint checker for new ``# deprecated:`` comments within
+- code to trigger errors when time to remove code that has no user facing deprecation
+- message. Only supported in ansible-core, not collections.
+- - ansible-test - Add support for RHEL 8.8 remotes.
+- - ansible-test - Add support for RHEL 9.2 remotes.
+- - ansible-test - Add support for testing with Python 3.12.
+- - ansible-test - Allow float values for the ``--timeout`` option to the ``env``
+- command. This simplifies testing.
+- - ansible-test - Enable ``thread`` code coverage in addition to the existing
+- ``multiprocessing`` coverage.
+- - ansible-test - RHEL 8.8 provisioning can now be used with the ``--python 3.11``
+- option.
+- - ansible-test - RHEL 9.2 provisioning can now be used with the ``--python 3.11``
+- option.
+- - ansible-test - Refactored ``env`` command logic and timeout handling.
+- - ansible-test - Remove Fedora 37 remote support.
+- - ansible-test - Remove Fedora 37 test container.
+- - ansible-test - Remove Python 3.8 and 3.9 from RHEL 8.8.
+- - ansible-test - Remove obsolete embedded script for configuring WinRM on Windows
+- remotes.
+- - ansible-test - Removed Ubuntu 20.04 LTS image from the `--remote` option.
+- - ansible-test - Removed `freebsd/12.4` remote.
+- - ansible-test - Removed `freebsd/13.1` remote.
+- - 'ansible-test - Removed test remotes: rhel/8.7, rhel/9.1'
+- - ansible-test - Removed the deprecated ``--docker-no-pull`` option.
+- - ansible-test - Removed the deprecated ``--no-pip-check`` option.
+- - ansible-test - Removed the deprecated ``foreman`` test plugin.
+- - ansible-test - Removed the deprecated ``govcsim`` support from the ``vcenter``
+- test plugin.
+- - ansible-test - Replace the ``pytest-forked`` pytest plugin with a custom plugin.
+- - ansible-test - The ``no-get-exception`` sanity test is now limited to plugins
+- in collections. Previously any Python file in a collection was checked for
+- ``get_exception`` usage.
+- - ansible-test - The ``replace-urlopen`` sanity test is now limited to plugins
+- in collections. Previously any Python file in a collection was checked for
+- ``urlopen`` usage.
+- - ansible-test - The ``use-compat-six`` sanity test is now limited to plugins
+- in collections. Previously any Python file in a collection was checked for
+- ``six`` usage.
+- - ansible-test - The openSUSE test container has been updated to openSUSE Leap
+- 15.5.
+- - ansible-test - Update pip to ``23.1.2`` and setuptools to ``67.7.2``.
+- - ansible-test - Update the ``default`` containers.
+- - ansible-test - Update the ``nios-test-container`` to version 2.0.0, which
+- supports API version 2.9.
+- - ansible-test - Update the logic used to detect when ``ansible-test`` is running
+- from source.
+- - ansible-test - Updated the CloudStack test container to version 1.6.1.
+- - ansible-test - Updated the distro test containers to version 6.3.0 to include
+- coverage 7.3.2 for Python 3.8+. The alpine3 container is now based on 3.18
+- instead of 3.17 and includes Python 3.11 instead of Python 3.10.
+- - ansible-test - Use ``datetime.datetime.now`` with ``tz`` specified instead
+- of ``datetime.datetime.utcnow``.
+- - ansible-test - Use a context manager to perform cleanup at exit instead of
+- using the built-in ``atexit`` module.
+- - ansible-test - remove Alpine 3.17 from remotes
+- - "ansible-test \u2014 Python 3.8\u20133.12 will use ``coverage`` v7.3.2."
+- - "ansible-test \u2014 ``coverage`` v6.5.0 is to be used only under Python 3.7."
+- - 'ansible-vault create: Now raises an error when opening the editor without
+- tty. The flag --skip-tty-check restores previous behaviour.'
+- - ansible_user_module - tweaked macos user defaults to reflect expected defaults
+- (https://github.com/ansible/ansible/issues/44316)
+- - apt - return calculated diff while running apt clean operation.
+- - blockinfile - add append_newline and prepend_newline options (https://github.com/ansible/ansible/issues/80835).
+- - cli - Added short option '-J' for asking for vault password (https://github.com/ansible/ansible/issues/80523).
+- - command - Add option ``expand_argument_vars`` to disable argument expansion
+- and use literal values - https://github.com/ansible/ansible/issues/54162
+- - config lookup new option show_origin to also return the origin of a configuration
+- value.
+- - display methods for warning and deprecation are now proxied to main process
+- when issued from a fork. This allows for the deduplication of warnings and
+- deprecations to work globally.
+- - dnf5 - enable environment groups installation testing in CI as its support
+- was added.
+- - dnf5 - enable now implemented ``cacheonly`` functionality
+- - executor now skips persistent connection when it detects an action that does
+- not require a connection.
+- - find module - Add ability to filter based on modes
+- - gather_facts now will use gather_timeout setting to limit parallel execution
+- of modules that do not themselves use gather_timeout.
+- - group - remove extraneous warning shown when user does not exist (https://github.com/ansible/ansible/issues/77049).
+- - include_vars - os.walk now follows symbolic links when traversing directories
+- (https://github.com/ansible/ansible/pull/80460)
+- - module compression is now sourced directly via config, bypassing play_context
+- possibly stale values.
+- - reboot - show last error message in verbose logs (https://github.com/ansible/ansible/issues/81574).
+- - service_facts now returns more info for rcctl managed systesm (OpenBSD).
+- - tasks - the ``retries`` keyword can be specified without ``until`` in which
+- case the task is retried until it succeeds but at most ``retries`` times (https://github.com/ansible/ansible/issues/20802)
+- - user - add new option ``password_expire_warn`` (supported on Linux only) to
+- set the number of days of warning before a password change is required (https://github.com/ansible/ansible/issues/79882).
+- - yum_repository - Align module documentation with parameters
+- release_summary: '| Release Date: 2023-09-26
++ minor_changes:
++ - "ansible-test \u2014 Replaced `freebsd/12.3` remote with `freebsd/12.4`. The
++ former is no longer functional."
++ release_summary: '| Release Date: 2023-09-05
+
+- | `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
+
+ '
+- removed_features:
+- - ActionBase - remove deprecated ``_remote_checksum`` method
+- - PlayIterator - remove deprecated ``cache_block_tasks`` and ``get_original_task``
+- methods
+- - Remove deprecated ``FileLock`` class
+- - Removed Python 3.9 as a supported version on the controller. Python 3.10 or
+- newer is required.
+- - Removed ``include`` which has been deprecated in Ansible 2.12. Use ``include_tasks``
+- or ``import_tasks`` instead.
+- - '``Templar`` - remove deprecated ``shared_loader_obj`` parameter of ``__init__``'
+- - '``fetch_url`` - remove auto disabling ``decompress`` when gzip is not available'
+- - '``get_action_args_with_defaults`` - remove deprecated ``redirected_names``
+- method parameter'
+- - ansible-test - Removed support for the remote Windows targets 2012 and 2012-R2
+- - inventory_cache - remove deprecated ``default.fact_caching_prefix`` ini configuration
+- option, use ``defaults.fact_caching_prefix`` instead.
+- - module_utils/basic.py - Removed Python 3.5 as a supported remote version.
+- Python 2.7 or Python 3.6+ is now required.
+- - stat - removed unused `get_md5` parameter.
+- codename: All My Love
+- fragments:
+- - 2.16.0b1_summary.yaml
+- - 20802-until-default.yml
+- - 22396-indicate-which-args-are-multi.yml
+- - 27816-fetch-unreachable.yml
+- - 50603-tty-check.yaml
+- - 71916-user-expires-int.yml
+- - 73643-handlers-prevent-multiple-runs.yml
+- - 74723-support-wildcard-win_fetch.yml
+- - 75063-allow-dev-nul-as-skeleton-for-new-homedir.yml
+- - 76372-fix-pip-virtualenv-command-parsing.yml
+- - 78487-galaxy-collections-path-warnings.yml
+- - 79129-ansible-managed-filename-format.yaml
+- - 79364_replace.yml
+- - 79677-fix-argspec-type-check.yml
+- - 79734-ansible-test-change-detection.yml
+- - 79844-fix-timeout-mounts-linux.yml
+- - 79999-ansible-user-tweak-macos-defaults.yaml
+- - 80089-prevent-module-build-date-issue.yml
+- - 80128-symbolic-modes-X-use-computed.yml
+- - 80257-iptables-chain-creation-does-not-populate-a-rule.yml
+- - 80258-defensive-display-non-utf8.yml
+- - 80334-reduce-ansible-galaxy-api-calls.yml
+- - 80406-validate-modules-semantic-markup.yml
+- - 80449-fix-symbolic-mode-error-msg.yml
+- - 80459-handlers-nested-includes-vars.yml
+- - 80460-add-symbolic-links-with-dir.yml
+- - 80476-fix-loop-task-post-validation.yml
+- - 80488-pip-pkg-resources.yml
+- - 80506-syntax-check-playbook-only.yml
+- - 80520-fix-current-hostname-openbsd.yml
+- - 80523_-_adding_short_option_for_--ask-vault-pass.yml
+- - 80605-template-overlay-native-jinja.yml
+- - 80648-fix-ansible-galaxy-cache-signatures-bug.yml
+- - 80721-ansible-galaxy.yml
+- - 80738-abs-unarachive-src.yml
+- - 80841-display-type-annotation.yml
+- - 80880-register-handlers-immediately-if-iterating-handlers.yml
+- - 80887-dnf5-api-change.yml
+- - 80943-ansible-galaxy-collection-subdir-install.yml
+- - 80968-replace-deprecated-ast-attr.yml
+- - 80985-fix-smgl-family-mapping.yml
+- - 81005-use-overlay-overrides.yml
+- - 81013-handlers-listen-last-defined-only.yml
+- - 81029-connection-types.yml
+- - 81064-daemonize-fixes.yml
+- - 81082-deprecated-importlib-abc.yml
+- - 81083-add-blockinfile-append-and-prepend-new-line-options.yml
+- - 81104-inventory-script-plugin-raise-execution-error.yml
+- - 81319-cloudstack-test-container-bump-version.yml
+- - 81332-fix-pkg-mgr-in-kylin.yml
+- - 81450-list-filters.yml
+- - 81494-remove-duplicated-file-attribute-constant.yml
+- - 81555-add-warning-for-illegal-filenames-in-roles.yaml
+- - 81584-daemonize-follow-up-fixes.yml
+- - 81606-ansible-galaxy-collection-pre-releases.yml
+- - 81613-remove-unusued-private-lock.yml
+- - 81656-cf_readfp-deprecated.yml
+- - 81662-blockinfile-exc.yml
+- - 81722-handler-subdir-include_tasks.yml
+- - CleansingNodeVisitor-removal.yml
+- - a-g-col-install-directory-with-trailing-sep.yml
+- - a-g-col-prevent-reinstalling-satisfied-req.yml
+- - a_test_rmv_alpine_317.yml
+- - add-missing-cli-docs.yml
+- - ag-ignore-multiple-signature-statuses.yml
+- - ansible-galaxy-server-timeout.yml
+- - ansible-runtime-metadata-removal-date.yml
+- - ansible-test-added-fedora-38.yml
+- - ansible-test-argcomplete-3.yml
+- - ansible-test-atexit.yml
+- - ansible-test-coverage-update.yml
+- - ansible-test-default-containers.yml
+- - ansible-test-deprecated-cleanup.yml
+- - ansible-test-distro-containers.yml
++ codename: C'mon Everybody
++ fragments:
++ - 2.14.10rc1_summary.yaml
+ - ansible-test-entry-points.yml
+- - ansible-test-explain-traceback.yml
+- - ansible-test-fedora-37.yml
+- - ansible-test-freebsd-bootstrap-setuptools.yml
+- - ansible-test-import-sanity-fix.yml
+- - ansible-test-layout-detection.yml
+- - ansible-test-long-timeout-fix.yml
+- - ansible-test-minimum-setuptools.yml
+- - ansible-test-nios-container.yml
+- - ansible-test-pylint-update.yml
+- - ansible-test-pytest-forked.yml
+- - ansible-test-python-3.12.yml
+- - ansible-test-pyyaml-build.yml
+- - ansible-test-remove-old-rhel-remotes.yml
+- - ansible-test-remove-ubuntu-2004.yml
+- - ansible-test-rhel-9.2-python-3.11.yml
+- - ansible-test-rhel-9.2.yml
+- - ansible-test-sanity-scope.yml
+- - ansible-test-source-detection.yml
+- - ansible-test-thread-coverage.yml
+- - ansible-test-timeout-fix.yml
+- - ansible-test-unique-container-names.yml
+- - ansible-test-use-raise-from.yml
+- - ansible-test-utcnow.yml
+- - ansible-test-winrm-config.yml
+- - ansible-vault.yml
+- - ansible_test_alpine_3.18.yml
+- - apt_fail_on_autoremove.yml
+- - aptclean_diff.yml
+- - basestrategy-lazy-templar.yml
+- - ci_freebsd_new.yml
+- - collections_paths-deprecation.yml
+- - colors.yml
+- - command-expand-args.yml
+- - config_origins_option.yml
+- - connection-type-annotation.yml
+- - copy_diff.yml
+- - deb822_open_url.yml
+- - debconf.yml
+- - deprecated_string_conversion_action.yml
+- - display_proxy.yml
+- - dnf-update-only-latest.yml
+- - dnf5-cacheonly.yml
+- - dnf5-fix-interpreter-fail-msg.yml
+- - dnf5-gpg-check-api.yml
+- - dnf5-gpg-check-builtin.yml
+- - dnf5-logs-api.yml
+- - dnf5-test-env-groups.yml
+ - dotnet-preparation.yml
+- - dpkg_selections.yml
+- - fbsd13_1_remove.yml
+- - fetch_url-remove-auto-disable-decompress.yml
+- - find-mode.yml
+- - first_found_fixes.yml
+- - first_found_template_fix.yml
+- - fix-display-prompt-cpu-consumption.yml
+- - fix-handlers-callback.yml
+- - fix-pkg-mgr-in-TencentOS.yml
+- - fix-setuptools-warnings.yml
+- - fix-url-lookup-plugin-docs.yml
+- - forced_local+fix+.yml
+- - freebsd_12_4_removal.yml
+- - galaxy_check_type.yml
+- - galaxy_symlink.yml
+- - gather_facts_fix_parallel.yml
+- - get_action_args_with_defaults-remove-deprecated-arg.yml
+- - group_warning.yml
+- - inventory_cache-remove-deprecated-default-section.yml
+- - inventory_ini.yml
+- - jinja_plugin_cache_cleanup.yml
+- - long-collection-paths-fix.yml
+- - man-page-build-docs-dependency.yml
+- - man-page-subcommands.yml
+- - manifest-in-cleanup.yml
+- - mc_from_config.yml
+- - missing-doc-func.yml
+- - no-arbitrary-j2-override.yml
+- - omit-man-pages-from-sdist.yml
+- - parsing-splitter-fixes.yml
+- - passlib_or_crypt.yml
+- - password_hash-fix-crypt-salt-bcrypt.yml
+- - pep517-backend-import-fix.yml
+- - pep517-backend-traceback-fix.yml
+- - pep8-known-issue.yml
+- - persist_skip.yml
+- - pkg_mgr-default-dnf.yml
+- - powershell-module-error-handling.yml
+- - pre-release-hint-for-dep-resolution-error.yml
+- - pylint-deprecated-comment-checker.yml
+- - reboot.yml
+- - remove-deprecated-actionbase-_remote_checksum.yml
+- - remove-deprecated-datetime-methods.yml
+- - remove-deprecated-filelock-class.yml
+- - remove-docs-examples.yml
+- - remove-include.yml
+- - remove-play_iterator-deprecated-methods.yml
+- - remove-python3.5.yml
+- - remove-python3.9-controller-support.yml
+- - remove-templar-shared_loader_obj-arg.yml
+- - remove-unreachable-include_role-static-err.yml
+- - remove_md5.yml
+- - role-deduplication-condition.yml
+- - run-command-selectors-prompt-only.yml
+- - server2012-deprecation.yml
+- - service_facts_rcctl.yml
+- - service_facts_simpleinit_msb.yml
+- - service_fix_obsd.yml
+- - set-filters.yml
+- - setup_facter_fix.yml
+- - simple-result-queue.yml
+- - smart_connection_bye.yml
+- - suppressed-options.yml
++ - freebsd-12.3-replacement.yml
+ - tarfile_extract_warn.yml
+- - templar-globals-dict.yml
+- - templating_fixes.yml
+- - text-converters.yml
+- - timeout_config_fix.yml
+- - update-maybe-json-uri.yml
+- - urls-client-cert-py12.yml
+- - urls-unit-test-latest-cryptography.yml
+- - user-add-password-exp-warning.yml
+- - v2.16.0-initial-commit.yaml
+- - vault_unvault_id_fix.yml
+- - yum-repository-docs-fixes.yml
+- - yum_repository_keepcache.yml
+- release_date: '2023-09-26'
+- 2.16.0b2:
++ release_date: '2023-09-05'
++ 2.14.11:
++ changes:
++ release_summary: '| Release Date: 2023-10-09
++
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 2.14.11_summary.yaml
++ release_date: '2023-10-09'
++ 2.14.11rc1:
+ changes:
+ bugfixes:
+- - '``import_role`` reverts to previous behavior of exporting vars at compile
+- time.'
++ - PluginLoader - fix Jinja plugin performance issues (https://github.com/ansible/ansible/issues/79652)
++ - ansible-galaxy error on dependency resolution will not error itself due to
++ 'virtual' collections not having a name/namespace.
+ - ansible-galaxy info - fix reporting no role found when lookup_role_by_name
+ returns None.
+- - uri/urls - Add compat function to handle the ability to parse the filename
+- from a Content-Disposition header (https://github.com/ansible/ansible/issues/81806)
+ - winrm - Better handle send input failures when communicating with hosts under
+ load
+ minor_changes:
+- - ansible-test - When invoking ``sleep`` in containers during container setup,
+- the ``env`` command is used to avoid invoking the shell builtin, if present.
++ - ansible-galaxy dependency resolution messages have changed the unexplained
++ 'virtual' collection for the specific type ('scm', 'dir', etc) that is more
++ user friendly
+ release_summary: '| Release Date: 2023-10-03
+
+- | `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
+
+ '
+ security_fixes:
+ - ansible-galaxy - Prevent roles from using symlinks to overwrite files outside
+ of the installation directory (CVE-2023-5115)
+- codename: All My Love
++ codename: C'mon Everybody
+ fragments:
+- - 2.16.0b2_summary.yaml
+- - 81806-py2-content-disposition.yml
+- - ansible-test-container-sleep.yml
++ - 2.14.11rc1_summary.yaml
+ - cve-2023-5115.yml
+ - fix-ansible-galaxy-info-no-role-found.yml
+- - import_role_goes_public.yml
++ - galaxy_dep_res_msgs.yml
++ - jinja_plugin_cache_cleanup.yml
+ - winrm-send-input.yml
+ release_date: '2023-10-03'
+- 2.16.0rc1:
+- changes:
+- bugfixes:
+- - Cache host_group_vars after instantiating it once and limit the amount of
+- repetitive work it needs to do every time it runs.
+- - Call PluginLoader.all() once for vars plugins, and load vars plugins that
+- run automatically or are enabled specifically by name subsequently.
+- - Fix ``run_once`` being incorrectly interpreted on handlers (https://github.com/ansible/ansible/issues/81666)
+- - Properly template tags in parent blocks (https://github.com/ansible/ansible/issues/81053)
+- - ansible-galaxy - Provide a better error message when using a requirements
+- file with an invalid format - https://github.com/ansible/ansible/issues/81901
+- - ansible-inventory - index available_hosts for major performance boost when
+- dumping large inventories
+- - ansible-test - Add a ``pylint`` plugin to work around a known issue on Python
+- 3.12.
+- - ansible-test - Include missing ``pylint`` requirements for Python 3.10.
+- - ansible-test - Update ``pylint`` to version 3.0.1.
+- deprecated_features:
+- - Old style vars plugins which use the entrypoints `get_host_vars` or `get_group_vars`
+- are deprecated. The plugin should be updated to inherit from `BaseVarsPlugin`
+- and define a `get_vars` method as the entrypoint.
+- minor_changes:
+- - ansible-test - Make Python 3.12 the default version used in the ``base`` and
+- ``default`` containers.
+- release_summary: '| Release Date: 2023-10-16
+-
+- | `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
+-
+- '
+- codename: All My Love
+- fragments:
+- - 2.16.0rc1_summary.yaml
+- - 79945-host_group_vars-improvements.yml
+- - 81053-templated-tags-inheritance.yml
+- - 81666-handlers-run_once.yml
+- - 81901-galaxy-requirements-format.yml
+- - ansible-test-pylint3-update.yml
+- - ansible-test-python-3.12-compat.yml
+- - ansible-test-python-default.yml
+- - inv_available_hosts_to_frozenset.yml
+- release_date: '2023-10-16'
+- 2.16.1:
++ 2.14.12:
+ changes:
+ release_summary: '| Release Date: 2023-12-04
+
+- | `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
+
+ '
+- codename: All My Love
++ codename: C'mon Everybody
+ fragments:
+- - 2.16.1_summary.yaml
++ - 2.14.12_summary.yaml
+ release_date: '2023-12-04'
+- 2.16.1rc1:
++ 2.14.12rc1:
+ changes:
+ breaking_changes:
+ - assert - Nested templating may result in an inability for the conditional
+ to be evaluated. See the porting guide for more information.
+ bugfixes:
+- - Fix issue where an ``include_tasks`` handler in a role was not able to locate
+- a file in ``tasks/`` when ``tasks_from`` was used as a role entry point and
+- ``main.yml`` was not present (https://github.com/ansible/ansible/issues/82241)
+- - Plugin loader does not dedupe nor cache filter/test plugins by file basename,
+- but full path name.
+- - Restoring the ability of filters/tests can have same file base name but different
+- tests/filters defined inside.
+ - ansible-pull now will expand relative paths for the ``-d|--directory`` option
+ is now expanded before use.
+- - ansible-pull will now correctly handle become and connection password file
+- options for ansible-playbook.
+- - flush_handlers - properly handle a handler failure in a nested block when
+- ``force_handlers`` is set (http://github.com/ansible/ansible/issues/81532)
+- - module no_log will no longer affect top level booleans, for example ``no_log_module_parameter='a'``
+- will no longer hide ``changed=False`` as a 'no log value' (matches 'a').
+- - role params now have higher precedence than host facts again, matching documentation,
+- this had unintentionally changed in 2.15.
+- - wait_for should not handle 'non mmapable files' again.
++ - ansible-test - Fix parsing of cgroup entries which contain a ``:`` in the
++ path (https://github.com/ansible/ansible/issues/81977).
++ minor_changes:
++ - ansible-test - Windows 2012 and 2012-R2 instances are now requested from Azure
++ instead of AWS.
+ release_summary: '| Release Date: 2023-11-27
+
+- | `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
+
+ '
+ security_fixes:
+ - templating - Address issues where internal templating can cause unsafe variables
+ to lose their unsafe designation (CVE-2023-5764)
+- codename: All My Love
++ codename: C'mon Everybody
+ fragments:
+- - 2.16.1rc1_summary.yaml
+- - 81532-fix-nested-flush_handlers.yml
+- - 82241-handler-include-tasks-from.yml
++ - 2.14.12rc1_summary.yaml
++ - ansible-test-cgroup-split.yml
++ - ansible-test-windows-2012-and-2012-R2.yml
+ - cve-2023-5764.yml
+- - j2_load_fix.yml
+- - no_log_booly.yml
+- - pull_file_secrets.yml
+ - pull_unfrack_dest.yml
+- - restore_role_param_precedence.yml
+- - wait_for_mmap.yml
+ release_date: '2023-11-27'
+- 2.16.2:
++ 2.14.13:
+ changes:
+ bugfixes:
+ - unsafe data - Address an incompatibility when iterating or getting a single
+ index from ``AnsibleUnsafeBytes``
+ - unsafe data - Address an incompatibility with ``AnsibleUnsafeText`` and ``AnsibleUnsafeBytes``
+ when pickling with ``protocol=0``
++ minor_changes:
++ - ansible-test - Add FreeBSD 13.2 remote.
++ - ansible-test - Removed `freebsd/13.1` remote.
+ release_summary: '| Release Date: 2023-12-11
+
+- | `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
+
+ '
+- codename: All My Love
++ codename: C'mon Everybody
+ fragments:
+- - 2.16.2_summary.yaml
++ - 2.14.13_summary.yaml
++ - ci_freebsd_new.yml
++ - fbsd13_1_remove.yml
+ - unsafe-fixes-2.yml
+ release_date: '2023-12-11'
+- 2.16.3:
++ 2.14.1rc1:
++ changes:
++ bugfixes:
++ - Fixes leftover _valid_attrs usage.
++ - ansible-galaxy - make initial call to Galaxy server on-demand only when installing,
++ getting info about, and listing roles.
++ - copy module will no longer move 'non files' set as src when remote_src=true.
++ - 'jinja2_native: preserve quotes in strings (https://github.com/ansible/ansible/issues/79083)'
++ - updated error messages to include 'acl' and not just mode changes when failing
++ to set required permissions on remote.
++ minor_changes:
++ - ansible-test - Improve consistency of executed ``pylint`` commands by making
++ the plugins ordered.
++ release_summary: '| Release Date: 2022-11-28
++
++ | `Porting Guide <https://docs.ansible.com/ansible/devel/porting_guides.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 79083-jinja2_native-preserve-quotes-in-strings.yml
++ - 79376-replace-valid-attrs-with-fattributes.yaml
++ - ansible-galaxy-install-delay-initial-api-call.yml
++ - ansible-test-pylint-command.yml
++ - dont_move_non_files.yml
++ - mention_acl.yml
++ - v2.14.1rc1_summary.yaml
++ release_date: '2022-11-28'
++ 2.14.2:
+ changes:
+- release_summary: '| Release Date: 2024-01-29
++ bugfixes:
++ - Fix traceback when using the ``template`` module and running with ``ANSIBLE_DEBUG=1``
++ (https://github.com/ansible/ansible/issues/79763)
++ release_summary: '| Release Date: 2023-01-30
+
+- | `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
++ | `Porting Guide <https://docs.ansible.com/ansible/devel/porting_guides.html>`__
+
+ '
+- codename: All My Love
++ codename: C'mon Everybody
+ fragments:
+- - 2.16.3_summary.yaml
+- release_date: '2024-01-29'
+- 2.16.3rc1:
+- changes:
+- bugfixes:
+- - Run all handlers with the same ``listen`` topic, even when notified from another
+- handler (https://github.com/ansible/ansible/issues/82363).
+- - '``ansible-galaxy role import`` - fix using the ``role_name`` in a standalone
+- role''s ``galaxy_info`` metadata by disabling automatic removal of the ``ansible-role-``
+- prefix. This matches the behavior of the Galaxy UI which also no longer implicitly
+- removes the ``ansible-role-`` prefix. Use the ``--role-name`` option or add
+- a ``role_name`` to the ``galaxy_info`` dictionary in the role''s ``meta/main.yml``
+- to use an alternate role name.'
+- - '``ansible-test sanity --test runtime-metadata`` - add ``action_plugin`` as
+- a valid field for modules in the schema (https://github.com/ansible/ansible/pull/82562).'
+- - ansible-config init will now dedupe ini entries from plugins.
+- - ansible-galaxy role import - exit with 1 when the import fails (https://github.com/ansible/ansible/issues/82175).
+- - ansible-galaxy role install - normalize tarfile paths and symlinks using ``ansible.utils.path.unfrackpath``
+- and consider them valid as long as the realpath is in the tarfile's role directory
+- (https://github.com/ansible/ansible/issues/81965).
+- - delegate_to when set to an empty or undefined variable will now give a proper
+- error.
+- - dwim functions for lookups should be better at detectging role context even
+- in abscense of tasks/main.
+- - roles, code cleanup and performance optimization of dependencies, now cached, and
+- ``public`` setting is now determined once, at role instantiation.
+- - roles, the ``static`` property is now correctly set, this will fix issues
+- with ``public`` and ``DEFAULT_PRIVATE_ROLE_VARS`` controls on exporting vars.
+- - unsafe data - Enable directly using ``AnsibleUnsafeText`` with Python ``pathlib``
+- (https://github.com/ansible/ansible/issues/82414)
+- release_summary: '| Release Date: 2024-01-22
++ - 79763-ansible_debug_template_tb_fix.yml
++ - v2.14.2_summary.yaml
++ release_date: '2023-01-30'
++ 2.14.2rc1:
++ changes:
++ bugfixes:
++ - Correctly count rescued tasks in play recap (https://github.com/ansible/ansible/issues/79711)
++ - Fix using ``GALAXY_IGNORE_CERTS`` in conjunction with collections in requirements
++ files which specify a specific ``source`` that isn't in the configured servers.
++ - Fix using ``GALAXY_IGNORE_CERTS`` when downloading tarballs from Galaxy servers
++ (https://github.com/ansible/ansible/issues/79557).
++ - Module and role argument validation - include the valid suboption choices
++ in the error when an invalid suboption is provided.
++ - ansible-doc now will correctly display short descriptions on listing filters/tests
++ no matter the directory sorting.
++ - ansible-inventory will not explicitly sort groups/hosts anymore, giving a
++ chance (depending on output format) to match the order in the input sources.
++ - ansible-test - Added a work-around for a traceback under Python 3.11 when
++ completing certain command line options.
++ - ansible-test - Avoid using ``exec`` after container startup when possible.
++ This improves container startup performance and avoids intermittent startup
++ issues with some old containers.
++ - ansible-test - Connection attempts to managed remote instances no longer abort
++ on ``Permission denied`` errors.
++ - ansible-test - Detection for running in a Podman or Docker container has been
++ fixed to detect more scenarios. The new detection relies on ``/proc/self/mountinfo``
++ instead of ``/proc/self/cpuset``. Detection now works with custom cgroups
++ and private cgroup namespaces.
++ - ansible-test - Fix validate-modules error when retrieving PowerShell argspec
++ when retrieved inside a Cmdlet
++ - ansible-test - Handle server errors when executing the ``docker info`` command.
++ - ansible-test - Multiple containers now work under Podman without specifying
++ the ``--docker-network`` option.
++ - ansible-test - Pass the ``XDG_RUNTIME_DIR`` environment variable through to
++ container commands.
++ - ansible-test - Perform PyPI proxy configuration after instances are ready
++ and bootstrapping has been completed. Only target instances are affected,
++ as controller instances were already handled this way. This avoids proxy configuration
++ errors when target instances are not yet ready for use.
++ - ansible-test - Prevent concurrent / repeat inspections of the same container
++ image.
++ - ansible-test - Prevent concurrent / repeat pulls of the same container image.
++ - ansible-test - Prevent concurrent execution of cached methods.
++ - ansible-test - Show the exception type when reporting errors during instance
++ provisioning.
++ - ansible-test sanity - correctly report invalid YAML in validate-modules (https://github.com/ansible/ansible/issues/75837).
++ - argument spec validation - again report deprecated parameters for Python-based
++ modules. This was accidentally removed in ansible-core 2.11 when argument
++ spec validation was refactored (https://github.com/ansible/ansible/issues/79680,
++ https://github.com/ansible/ansible/pull/79681).
++ - argument spec validation - ensure that deprecated aliases in suboptions are
++ also reported (https://github.com/ansible/ansible/pull/79740).
++ - argument spec validation - fix warning message when two aliases of the same
++ option are used for suboptions to also mention the option's name they are
++ in (https://github.com/ansible/ansible/pull/79740).
++ - connection local now avoids traceback on invalid user being used to execuet
++ ansible (valid in host, but not in container).
++ - file - touch action in check mode was always returning ok. Fix now evaluates
++ the different conditions and returns the appropriate changed status. (https://github.com/ansible/ansible/issues/79360)
++ - get_url - Ensure we are passing ciphers to all url_get calls (https://github.com/ansible/ansible/issues/79717)
++ - plugin filter now works with rejectlist as documented (still falls back to
++ blacklist if used).
++ - uri - improve JSON content type detection
++ known_issues:
++ - ansible-test - Additional configuration may be required for certain container
++ host and container combinations. Further details are available in the testing
++ documentation.
++ - ansible-test - Custom containers with ``VOLUME`` instructions may be unable
++ to start, when previously the containers started correctly. Remove the ``VOLUME``
++ instructions to resolve the issue. Containers with this condition will cause
++ ``ansible-test`` to emit a warning.
++ - ansible-test - Systems with Podman networking issues may be unable to run
++ containers, when previously the issue went unreported. Correct the networking
++ issues to continue using ``ansible-test`` with Podman.
++ - ansible-test - Using Docker on systems with SELinux may require setting SELinux
++ to permissive mode. Podman should work with SELinux in enforcing mode.
++ major_changes:
++ - ansible-test - Docker Desktop on WSL2 is now supported (additional configuration
++ required).
++ - ansible-test - Docker and Podman are now supported on hosts with cgroup v2
++ unified. Previously only cgroup v1 and cgroup v2 hybrid were supported.
++ - ansible-test - Podman now works on container hosts without systemd. Previously
++ only some containers worked, while others required rootfull or rootless Podman,
++ but would not work with both. Some containers did not work at all.
++ - ansible-test - Podman on WSL2 is now supported.
++ - ansible-test - When additional cgroup setup is required on the container host,
++ this will be automatically detected. Instructions on how to configure the
++ host will be provided in the error message shown.
++ minor_changes:
++ - ansible-test - A new ``audit`` option is available when running custom containers.
++ This option can be used to indicate whether a container requires the AUDIT_WRITE
++ capability. The default is ``required``, which most containers will need when
++ using Podman. If necessary, the ``none`` option can be used to opt-out of
++ the capability. This has no effect on Docker, which always provides the capability.
++ - ansible-test - A new ``cgroup`` option is available when running custom containers.
++ This option can be used to indicate a container requires cgroup v1 or that
++ it does not use cgroup. The default behavior assumes the container works with
++ cgroup v2 (as well as v1).
++ - ansible-test - Additional log details are shown when containers fail to start
++ or SSH connections to containers fail.
++ - ansible-test - Connection failures to remote provisioned hosts now show failure
++ details as a warning.
++ - ansible-test - Containers included with ansible-test no longer disable seccomp
++ by default.
++ - ansible-test - Failure to connect to a container over SSH now results in a
++ clear error. Previously tests would be attempted even after initial connection
++ attempts failed.
++ - ansible-test - Integration tests can be excluded from retries triggered by
++ the ``--retry-on-error`` option by adding the ``retry/never`` alias. This
++ is useful for tests that cannot pass on a retry or are too slow to make retries
++ useful.
++ - ansible-test - More details are provided about an instance when provisioning
++ fails.
++ - ansible-test - Reduce the polling limit for SSHD startup in containers from
++ 60 retries to 10. The one second delay between retries remains in place.
++ - ansible-test - SSH connections from OpenSSH 8.8+ to CentOS 6 containers now
++ work without additional configuration. However, clients older than OpenSSH
++ 7.0 can no longer connect to CentOS 6 containers as a result. The container
++ must have ``centos6`` in the image name for this work-around to be applied.
++ - ansible-test - SSH shell connections from OpenSSH 8.8+ to ansible-test provisioned
++ network instances now work without additional configuration. However, clients
++ older than OpenSSH 7.0 can no longer open shell sessions for ansible-test
++ provisioned network instances as a result.
++ - ansible-test - The ``ansible-test env`` command now detects and reports the
++ container ID if running in a container.
++ - ansible-test - Unit tests now support network disconnect by default when running
++ under Podman. Previously this feature only worked by default under Docker.
++ - ansible-test - Use ``stop --time 0`` followed by ``rm`` to remove ephemeral
++ containers instead of ``rm -f``. This speeds up teardown of ephemeral containers.
++ - ansible-test - Warnings are now shown when using containers that were built
++ with VOLUME instructions.
++ - ansible-test - When setting the max open files for containers, the container
++ host's limit will be checked. If the host limit is lower than the preferred
++ value, it will be used and a warning will be shown.
++ - ansible-test - When using Podman, ansible-test will detect if the loginuid
++ used in containers is incorrect. When this occurs a warning is displayed and
++ the container is run with the AUDIT_CONTROL capability. Previously containers
++ would fail under this situation, with no useful warnings or errors given.
++ release_summary: '| Release Date: 2023-01-23
+
+- | `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
++ | `Porting Guide <https://docs.ansible.com/ansible/devel/porting_guides.html>`__
+
+ '
+- security_fixes:
+- - ANSIBLE_NO_LOG - Address issue where ANSIBLE_NO_LOG was ignored (CVE-2024-0690)
+- codename: All My Love
++ codename: C'mon Everybody
++ fragments:
++ - 75837-validate-modules-invalid-yaml.yml
++ - 76578-fix-role-argspec-suboptions-error.yml
++ - 79525-fix-file-touch-check-mode-status.yaml
++ - 79561-fix-a-g-global-ignore-certs-cfg.yml
++ - 79681-argspec-param-deprecation.yml
++ - 79711-fix-play-stats-rescued.yml
++ - 79717-get-url-ciphers.yml
++ - 79740-aliases-warnings-deprecations-in-suboptions.yml
++ - adoc_fix_list.yml
++ - ansible-test-container-management.yml
++ - ansible-test-fix-python-3.11-traceback.yml
++ - ansible-test-pypi-proxy-fix.yml
++ - better-maybe-json-uri.yml
++ - local_bad_user.yml
++ - rejectlist_fix.yml
++ - unsorted.yml
++ - v2.14.2rc1_summary.yaml
++ - validate-module-ps-cmdlet.yml
++ release_date: '2023-01-23'
++ 2.14.3:
++ changes:
++ release_summary: '| Release Date: 2023-02-27
++
++ | `Porting Guide <https://docs.ansible.com/ansible/devel/porting_guides.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - v2.14.3_summary.yaml
++ release_date: '2023-02-27'
++ 2.14.3rc1:
++ changes:
++ bugfixes:
++ - Ansible.Basic.cs - Ignore compiler warning (reported as an error) when running
++ under PowerShell 7.3.x.
++ - Fix conditionally notifying ``include_tasks` handlers when ``force_handlers``
++ is used (https://github.com/ansible/ansible/issues/79776)
++ - TaskExecutor - don't ignore templated _raw_params that k=v parser failed to
++ parse (https://github.com/ansible/ansible/issues/79862)
++ - ansible-galaxy - fix installing collections in git repositories/directories
++ which contain a MANIFEST.json file (https://github.com/ansible/ansible/issues/79796).
++ - ansible-test - Support Podman 4.4.0+ by adding the ``SYS_CHROOT`` capability
++ when running containers.
++ - ansible-test - fix warning message about failing to run an image to include
++ the image name
++ - strategy plugins now correctly identify bad registered variables, even on
++ skip.
++ minor_changes:
++ - Make using blocks as handlers a parser error (https://github.com/ansible/ansible/issues/79968)
++ - 'ansible-test - Specify the configuration file location required by test plugins
++ when the config file is not found. This resolves issue: https://github.com/ansible/ansible/issues/79411'
++ - ansible-test - Update error handling code to use Python 3.x constructs, avoiding
++ direct use of ``errno``.
++ - ansible-test acme test container - update version to update used Pebble version,
++ underlying Python and Go base containers, and Python requirements (https://github.com/ansible/ansible/pull/79783).
++ release_summary: '| Release Date: 2023-02-20
++
++ | `Porting Guide <https://docs.ansible.com/ansible/devel/porting_guides.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 79776-fix-force_handlers-cond-include.yml
++ - 79783-acme-test-container.yml
++ - 79862-fix-varargs.yml
++ - 79968-blocks-handlers-error.yml
++ - ansible-galaxy-install-git-src-manifest.yml
++ - ansible-test-errno.yml
++ - ansible-test-fix-warning-msg.yml
++ - ansible-test-podman-chroot.yml
++ - ansible-test-test-plugin-error-message.yml
++ - powershell-7.3-fix.yml
++ - strategy_badid_fix.yml
++ - v2.14.3rc1_summary.yaml
++ release_date: '2023-02-20'
++ 2.14.4:
++ changes:
++ release_summary: '| Release Date: 2023-03-27
++
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 2.14.4_summary.yaml
++ release_date: '2023-03-27'
++ 2.14.4rc1:
++ changes:
++ breaking_changes:
++ - ansible-test - Integration tests which depend on specific file permissions
++ when running in an ansible-test managed host environment may require changes.
++ Tests that require permissions other than ``755`` or ``644`` may need to be
++ updated to set the necessary permissions as part of the test run.
++ bugfixes:
++ - Fix ``MANIFEST.in`` to exclude unwanted files in the ``packaging/`` directory.
++ - Fix ``MANIFEST.in`` to include ``*.md`` files in the ``test/support/`` directory.
++ - Fix an issue where the value of ``become`` was ignored when used on a role
++ used as a dependency in ``main/meta.yml`` (https://github.com/ansible/ansible/issues/79777)
++ - '``ansible_eval_concat`` - avoid redundant unsafe wrapping of templated strings
++ converted to Python types'
++ - ansible-galaxy role info - fix unhandled AttributeError by catching the correct
++ exception.
++ - ansible-test - Always indicate the Python version being used before installing
++ requirements. Resolves issue https://github.com/ansible/ansible/issues/72855
++ - ansible-test - Exclude ansible-core vendored Python packages from ansible-test
++ payloads.
++ - ansible-test - Integration test target prefixes defined in a ``tests/integration/target-prefixes.{group}``
++ file can now contain an underscore (``_``) character. Resolves issue https://github.com/ansible/ansible/issues/79225
++ - ansible-test - Removed pointless comparison in diff evaluation logic.
++ - ansible-test - Set ``PYLINTHOME`` for the ``pylint`` sanity test to prevent
++ failures due to ``pylint`` checking for the existence of an obsolete home
++ directory.
++ - ansible-test - Support loading of vendored Python packages from ansible-core.
++ - ansible-test - Use consistent file permissions when delegating tests to a
++ container or remote host. Files with any execute bit set will use permissions
++ ``755``. All other files will use permissions ``644``. (Resolves issue https://github.com/ansible/ansible/issues/75079)
++ - copy - fix creating the dest directory in check mode with remote_src=True
++ (https://github.com/ansible/ansible/issues/78611).
++ - copy - fix reporting changes to file attributes in check mode with remote_src=True
++ (https://github.com/ansible/ansible/issues/77957).
++ minor_changes:
++ - ansible-test - Moved git handling out of the validate-modules sanity test
++ and into ansible-test.
++ - ansible-test - Removed the ``--keep-git`` sanity test option, which was limited
++ to testing ansible-core itself.
++ - ansible-test - Updated the Azure Pipelines CI plugin to work with newer versions
++ of git.
++ release_summary: '| Release Date: 2023-03-21
++
++ | `Porting Guide <https://docs.ansible.com/ansible/devel/porting_guides.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 2.14.4rc1_summary.yaml
++ - 78624-copy-remote-src-check-mode.yml
++ - 79777-fix-inheritance-roles-meta.yml
++ - a-g-role-fix-catching-exception.yml
++ - ansible-test-fix-pointless-comparison.yml
++ - ansible-test-git-handling.yml
++ - ansible-test-integration-target-prefixes.yml
++ - ansible-test-payload-file-permissions.yml
++ - ansible-test-pylint-home.yml
++ - ansible-test-requirements-message.yml
++ - ansible-test-vendoring-support.yml
++ - ansible_eval_concat-remove-redundant-unsafe-wrap.yml
++ - fix-manifest.yml
++ release_date: '2023-03-21'
++ 2.14.5:
++ changes:
++ release_summary: '| Release Date: 2023-04-24
++
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 2.14.5_summary.yaml
++ release_date: '2023-04-24'
++ 2.14.5rc1:
++ changes:
++ bugfixes:
++ - Windows - Display a warning if the module failed to cleanup any temporary
++ files rather than failing the task. The warning contains a brief description
++ of what failed to be deleted.
++ - Windows - Ensure the module temp directory contains more unique values to
++ avoid conflicts with concurrent runs - https://github.com/ansible/ansible/issues/80294
++ - Windows - Improve temporary file cleanup used by modules. Will use a more
++ reliable delete operation on Windows Server 2016 and newer to delete files
++ that might still be open by other software like Anti Virus scanners. There
++ are still scenarios where a file or directory cannot be deleted but the new
++ method should work in more scenarios.
++ - ansible-doc - stop generating wrong module URLs for module see-alsos. The
++ URLs for modules in ansible.builtin do now work, and URLs for modules outside
++ ansible.builtin are no longer added (https://github.com/ansible/ansible/pull/80280).
++ - ansible-galaxy - Improve retries for collection installs, to properly retry,
++ and extend retry logic to common URL related connection errors (https://github.com/ansible/ansible/issues/80170
++ https://github.com/ansible/ansible/issues/80174)
++ - ansible-galaxy - reduce API calls to servers by fetching signatures only for
++ final candidates.
++ - ansible-test - Add support for ``argcomplete`` version 3.
++ - jinja2_native - fix intermittent 'could not find job' failures when a value
++ of ``ansible_job_id`` from a result of an async task was inadvertently changed
++ during execution; to prevent this a format of ``ansible_job_id`` was changed.
++ - password lookup now correctly reads stored ident fields.
++ - pep517 build backend - Use the documented ``import_module`` import from ``importlib``.
++ - roles - Fix templating ``public``, ``allow_duplicates`` and ``rolespec_validate``
++ (https://github.com/ansible/ansible/issues/80304).
++ - syntax check - Limit ``--syntax-check`` to ``ansible-playbook`` only, as that
++ is the only CLI affected by this argument (https://github.com/ansible/ansible/issues/80506)
++ release_summary: '| Release Date: 2023-04-17
++
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 2.14.5rc1_summary.yaml
++ - 80280-ansible-doc-seealso-urls.yml
++ - 80334-reduce-ansible-galaxy-api-calls.yml
++ - 80506-syntax-check-playbook-only.yml
++ - ansible-basic-tmpdir-uniqueness.yml
++ - ansible-test-argcomplete-3.yml
++ - fix-templating-private-role-FA.yml
++ - fix_jinja_native_async.yml
++ - galaxy-improve-retries.yml
++ - password_lookup_file_fix.yml
++ - pep517-backend-import-fix.yml
++ - win-temp-cleanup.yml
++ release_date: '2023-04-17'
++ 2.14.6:
++ changes:
++ release_summary: '| Release Date: 2023-05-22
++
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 2.14.6_summary.yaml
++ release_date: '2023-05-22'
++ 2.14.6rc1:
++ changes:
++ bugfixes:
++ - Display - Defensively configure writing to stdout and stderr with the replace
++ encoding error handler that will replace invalid characters (https://github.com/ansible/ansible/issues/80258)
++ - Properly disable ``jinja2_native`` in the template module when jinja2 override
++ is used in the template (https://github.com/ansible/ansible/issues/80605)
++ - ansible-galaxy - fix installing signed collections (https://github.com/ansible/ansible/issues/80648).
++ - ansible-galaxy collection verify - fix verifying signed collections when the
++ keyring is not configured.
++ - ansible-test - Fix handling of timeouts exceeding one day.
++ - ansible-test - Fix various cases where the test timeout could expire without
++ terminating the tests.
++ - ansible-test - When bootstrapping remote FreeBSD instances, use the OS packaged
++ ``setuptools`` instead of installing the latest version from PyPI.
++ - pep517 build backend - Copy symlinks when copying the source tree. This avoids
++ tracebacks in various scenarios, such as when a venv is present in the source
++ tree.
++ minor_changes:
++ - ansible-test - Allow float values for the ``--timeout`` option to the ``env``
++ command. This simplifies testing.
++ - ansible-test - Refactored ``env`` command logic and timeout handling.
++ - ansible-test - Use ``datetime.datetime.now`` with ``tz`` specified instead
++ of ``datetime.datetime.utcnow``.
++ release_summary: '| Release Date: 2023-05-15
++
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
++
++ '
++ codename: C'mon Everybody
+ fragments:
+- - 2.16.3rc1_summary.yaml
+- - 82175-fix-ansible-galaxy-role-import-rc.yml
+- - 82363-multiple-handlers-with-recursive-notification.yml
+- - ansible-galaxy-role-install-symlink.yml
+- - cve-2024-0690.yml
+- - dedupe_config_init.yml
+- - delegate_to_invalid.yml
+- - dwim_is_role_fix.yml
+- - fix-default-ansible-galaxy-role-import-name.yml
+- - fix-runtime-metadata-modules-action_plugin.yml
+- - role_fixes.yml
+- - unsafe-intern.yml
+- release_date: '2024-01-22'
+- 2.16.4:
++ - 2.14.6rc1_summary.yaml
++ - 80258-defensive-display-non-utf8.yml
++ - 80605-template-overlay-native-jinja.yml
++ - 80648-fix-ansible-galaxy-cache-signatures-bug.yml
++ - ansible-test-freebsd-bootstrap-setuptools.yml
++ - ansible-test-long-timeout-fix.yml
++ - ansible-test-timeout-fix.yml
++ - ansible-test-utcnow.yml
++ - pep517-backend-traceback-fix.yml
++ release_date: '2023-05-15'
++ 2.14.7:
+ changes:
+- release_summary: '| Release Date: 2024-02-26
++ release_summary: '| Release Date: 2023-06-20
+
+- | `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
+
+ '
+- codename: All My Love
++ codename: C'mon Everybody
+ fragments:
+- - 2.16.4_summary.yaml
+- release_date: '2024-02-26'
+- 2.16.4rc1:
++ - 2.14.7_summary.yaml
++ release_date: '2023-06-20'
++ 2.14.7rc1:
+ changes:
+ bugfixes:
+- - Fix loading vars_plugins in roles (https://github.com/ansible/ansible/issues/82239).
+- - expect - fix argument spec error using timeout=null (https://github.com/ansible/ansible/issues/80982).
+- - include_vars - fix calculating ``depth`` relative to the root and ensure all
+- files are included (https://github.com/ansible/ansible/issues/80987).
+- - templating - ensure syntax errors originating from a template being compiled
+- into Python code object result in a failure (https://github.com/ansible/ansible/issues/82606)
+- release_summary: '| Release Date: 2024-02-19
++ - ansible-test - Fix a traceback that occurs when attempting to test Ansible
++ source using a different ansible-test. A clear error message is now given
++ when this scenario occurs.
++ - ansible-test local change detection - use ``git merge-base <branch> HEAD``
++ instead of ``git merge-base --fork-point <branch>`` (https://github.com/ansible/ansible/pull/79734).
++ - man page build - Remove the dependency on the ``docs`` directory for building
++ man pages.
++ - uri - fix search for JSON type to include complex strings containing '+'
++ minor_changes:
++ - Removed ``straight.plugin`` from the build and packaging requirements.
++ release_summary: '| Release Date: 2023-06-12
+
+- | `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
+
+ '
+- codename: All My Love
++ codename: C'mon Everybody
+ fragments:
+- - 2.16.4rc1_summary.yaml
+- - 80995-include-all-var-files.yml
+- - 82606-template-python-syntax-error.yml
+- - fix-expect-indefinite-timeout.yml
+- - fix-vars-plugins-in-roles.yml
+- release_date: '2024-02-19'
+- 2.16.5:
++ - 2.14.7rc1_summary.yaml
++ - 79734-ansible-test-change-detection.yml
++ - ansible-test-source-detection.yml
++ - build-no-straight.yaml
++ - man-page-build-docs-dependency.yml
++ - update-maybe-json-uri.yml
++ release_date: '2023-06-12'
++ 2.14.8:
++ changes:
++ release_summary: '| Release Date: 2023-07-18
++
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 2.14.8_summary.yaml
++ release_date: '2023-07-17'
++ 2.14.8rc1:
+ changes:
+ bugfixes:
+- - ansible-test - The ``libexpat`` package is automatically upgraded during remote
+- bootstrapping to maintain compatibility with newer Python packages.
+- release_summary: '| Release Date: 2024-03-25
++ - ansible-galaxy - Fix issue installing collections containing directories with
++ more than 100 characters on python versions before 3.10.6
++ minor_changes:
++ - Cache field attributes list on the playbook classes
++ - Playbook objects - Replace deprecated stacked ``@classmethod`` and ``@property``
++ - ansible-test - Use a context manager to perform cleanup at exit instead of
++ using the built-in ``atexit`` module.
++ release_summary: '| Release Date: 2023-07-10
+
+- | `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
+
+ '
+- codename: All My Love
++ codename: C'mon Everybody
+ fragments:
+- - 2.16.5_summary.yaml
+- - ansible-test-alpine-libexpat.yml
+- release_date: '2024-03-25'
+- 2.16.5rc1:
++ - 2.14.8rc1_summary.yaml
++ - ansible-test-atexit.yml
++ - cache-fa-on-pb-cls.yml
++ - long-collection-paths-fix.yml
++ - no-stacked-descriptors.yaml
++ release_date: '2023-07-10'
++ 2.14.9:
++ changes:
++ release_summary: '| Release Date: 2023-08-14
++
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
++
++ '
++ codename: C'mon Everybody
++ fragments:
++ - 2.14.9_summary.yaml
++ release_date: '2023-08-14'
++ 2.14.9rc1:
+ changes:
+ bugfixes:
+- - 'Fix an issue when setting a plugin name from an unsafe source resulted in
+- ``ValueError: unmarshallable object`` (https://github.com/ansible/ansible/issues/82708)'
+- - Harden python templates for respawn and ansiballz around str literal quoting
+- - template - Fix error when templating an unsafe string which corresponds to
+- an invalid type in Python (https://github.com/ansible/ansible/issues/82600).
+- - winrm - does not hang when attempting to get process output when stdin write
+- failed
++ - Exclude internal options from man pages and docs.
++ - Fix ``ansible-config init`` man page option indentation.
++ - The ``ansible-config init`` command now has a documentation description.
++ - The ``ansible-galaxy collection download`` command now has a documentation
++ description.
++ - The ``ansible-galaxy collection install`` command documentation is now visible
++ (previously hidden by a decorator).
++ - The ``ansible-galaxy collection verify`` command now has a documentation description.
++ - The ``ansible-galaxy role install`` command documentation is now visible (previously
++ hidden by a decorator).
++ - The ``ansible-inventory`` command command now has a documentation description
++ (previously used as the epilog).
++ - Update module_utils.urls unit test to work with cryptography >= 41.0.0.
++ - When generating man pages, use ``func`` to find the command function instead
++ of looking it up by the command name.
++ - ansible-test - Pre-build a PyYAML wheel before installing requirements to
++ avoid a potential Cython build failure.
++ - man page build - Sub commands of ``ansible-galaxy role`` and ``ansible-galaxy
++ collection`` are now documented.
+ minor_changes:
+- - ansible-test - Add a work-around for permission denied errors when using ``pytest
+- >= 8`` on multi-user systems with an installed version of ``ansible-test``.
+- release_summary: '| Release Date: 2024-03-18
++ - Removed ``exclude`` and ``recursive-exclude`` commands for generated files
++ from the ``MANIFEST.in`` file. These excludes were unnecessary since releases
++ are expected to be built with a clean worktree.
++ - Removed ``exclude`` commands for sanity test files from the ``MANIFEST.in``
++ file. These tests were previously excluded because they did not pass when
++ run from an sdist. However, sanity tests are not expected to pass from an
++ sdist, so excluding some (but not all) of the failing tests makes little sense.
++ - Removed redundant ``include`` commands from the ``MANIFEST.in`` file. These
++ includes either duplicated default behavior or another command.
++ - The ``ansible-core`` sdist no longer contains pre-generated man pages. Instead,
++ a ``packaging/cli-doc/build.py`` script is included in the sdist. This script
++ can generate man pages and standalone RST documentation for ``ansible-core``
++ CLI programs.
++ - The ``docs`` and ``examples`` directories are no longer included in the ``ansible-core``
++ sdist. These directories have been moved to the https://github.com/ansible/ansible-documentation
++ repository.
++ - The minimum required ``setuptools`` version is now 45.2.0, as it is the oldest
++ version to support Python 3.10.
++ - Use ``include`` where ``recursive-include`` is unnecessary in the ``MANIFEST.in``
++ file.
++ - Use ``package_data`` instead of ``include_package_data`` for ``setup.cfg``
++ to avoid ``setuptools`` warnings.
++ - ansible-test - Update the logic used to detect when ``ansible-test`` is running
++ from source.
++ release_summary: '| Release Date: 2023-08-07
+
+- | `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
++ | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__
+
+ '
+- codename: All My Love
++ codename: C'mon Everybody
+ fragments:
+- - 2.16.5rc1_summary.yaml
+- - 82675-fix-unsafe-templating-leading-to-type-error.yml
+- - 82708-unsafe-plugin-name-error.yml
+- - ansible-test-pytest-8.yml
+- - py-tmpl-hardening.yml
+- - winrm-timeout.yml
+- release_date: '2024-03-18'
++ - 2.14.9rc1_summary.yaml
++ - add-missing-cli-docs.yml
++ - ansible-test-layout-detection.yml
++ - ansible-test-minimum-setuptools.yml
++ - ansible-test-pyyaml-build.yml
++ - fix-setuptools-warnings.yml
++ - man-page-subcommands.yml
++ - manifest-in-cleanup.yml
++ - missing-doc-func.yml
++ - omit-man-pages-from-sdist.yml
++ - remove-docs-examples.yml
++ - suppressed-options.yml
++ - urls-unit-test-latest-cryptography.yml
++ release_date: '2023-08-07'
+--- ansible-core-2.16.5.orig/lib/ansible/cli/__init__.py
++++ ansible-core-2.16.5/lib/ansible/cli/__init__.py
+@@ -13,9 +13,9 @@ import sys
+
+ # Used for determining if the system is running a new enough python version
+ # and should only restrict on our documented minimum versions
+-if sys.version_info < (3, 10):
++if sys.version_info < (3, 9):
+ raise SystemExit(
+- 'ERROR: Ansible requires Python 3.10 or newer on the controller. '
++ 'ERROR: Ansible requires Python 3.9 or newer on the controller. '
+ 'Current version: %s' % ''.join(sys.version.splitlines())
+ )
+
+@@ -97,12 +97,11 @@ from ansible.cli.arguments import option
+ from ansible.errors import AnsibleError, AnsibleOptionsError, AnsibleParserError
+ from ansible.inventory.manager import InventoryManager
+ from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
+-from ansible.module_utils.common.collections import is_sequence
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.module_utils.common.file import is_executable
+ from ansible.parsing.dataloader import DataLoader
+ from ansible.parsing.vault import PromptVaultSecret, get_file_vault_secret
+-from ansible.plugins.loader import add_all_plugin_dirs, init_plugin_loader
++from ansible.plugins.loader import add_all_plugin_dirs
+ from ansible.release import __version__
+ from ansible.utils.collection_loader import AnsibleCollectionConfig
+ from ansible.utils.collection_loader._collection_finder import _get_collection_name_from_path
+@@ -120,7 +119,7 @@ except ImportError:
+ class CLI(ABC):
+ ''' code behind bin/ansible* programs '''
+
+- PAGER = C.config.get_config_value('PAGER')
++ PAGER = 'less'
+
+ # -F (quit-if-one-screen) -R (allow raw ansi control chars)
+ # -S (chop long lines) -X (disable termcap init and de-init)
+@@ -155,13 +154,6 @@ class CLI(ABC):
+ """
+ self.parse()
+
+- # Initialize plugin loader after parse, so that the init code can utilize parsed arguments
+- cli_collections_path = context.CLIARGS.get('collections_path') or []
+- if not is_sequence(cli_collections_path):
+- # In some contexts ``collections_path`` is singular
+- cli_collections_path = [cli_collections_path]
+- init_plugin_loader(cli_collections_path)
+-
+ display.vv(to_text(opt_help.version(self.parser.prog)))
+
+ if C.CONFIG_FILE:
+@@ -502,11 +494,11 @@ class CLI(ABC):
+ # this is a much simpler form of what is in pydoc.py
+ if not sys.stdout.isatty():
+ display.display(text, screen_only=True)
+- elif CLI.PAGER:
++ elif 'PAGER' in os.environ:
+ if sys.platform == 'win32':
+ display.display(text, screen_only=True)
+ else:
+- CLI.pager_pipe(text)
++ CLI.pager_pipe(text, os.environ['PAGER'])
+ else:
+ p = subprocess.Popen('less --version', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ p.communicate()
+@@ -516,12 +508,12 @@ class CLI(ABC):
+ display.display(text, screen_only=True)
+
+ @staticmethod
+- def pager_pipe(text):
++ def pager_pipe(text, cmd):
+ ''' pipe text through a pager '''
+- if 'less' in CLI.PAGER:
++ if 'LESS' not in os.environ:
+ os.environ['LESS'] = CLI.LESS_OPTS
+ try:
+- cmd = subprocess.Popen(CLI.PAGER, shell=True, stdin=subprocess.PIPE, stdout=sys.stdout)
++ cmd = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=sys.stdout)
+ cmd.communicate(input=to_bytes(text))
+ except IOError:
+ pass
+@@ -530,10 +522,6 @@ class CLI(ABC):
+
+ @staticmethod
+ def _play_prereqs():
+- # TODO: evaluate moving all of the code that touches ``AnsibleCollectionConfig``
+- # into ``init_plugin_loader`` so that we can specifically remove
+- # ``AnsibleCollectionConfig.playbook_paths`` to make it immutable after instantiation
+-
+ options = context.CLIARGS
+
+ # all needs loader
+--- ansible-core-2.16.5.orig/lib/ansible/cli/adhoc.py
++++ ansible-core-2.16.5/lib/ansible/cli/adhoc.py
+@@ -14,7 +14,7 @@ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.errors import AnsibleError, AnsibleOptionsError, AnsibleParserError
+ from ansible.executor.task_queue_manager import TaskQueueManager
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.parsing.splitter import parse_kv
+ from ansible.parsing.utils.yaml import from_yaml
+ from ansible.playbook import Playbook
+--- ansible-core-2.16.5.orig/lib/ansible/cli/arguments/option_helpers.py
++++ ansible-core-2.16.5/lib/ansible/cli/arguments/option_helpers.py
+@@ -16,7 +16,7 @@ from jinja2 import __version__ as j2_ver
+
+ import ansible
+ from ansible import constants as C
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.yaml import HAS_LIBYAML, yaml_load
+ from ansible.release import __version__
+ from ansible.utils.path import unfrackpath
+@@ -31,16 +31,6 @@ class SortingHelpFormatter(argparse.Help
+ super(SortingHelpFormatter, self).add_arguments(actions)
+
+
+-class ArgumentParser(argparse.ArgumentParser):
+- def add_argument(self, *args, **kwargs):
+- action = kwargs.get('action')
+- help = kwargs.get('help')
+- if help and action in {'append', 'append_const', 'count', 'extend', PrependListAction}:
+- help = f'{help.rstrip(".")}. This argument may be specified multiple times.'
+- kwargs['help'] = help
+- return super().add_argument(*args, **kwargs)
+-
+-
+ class AnsibleVersion(argparse.Action):
+ def __call__(self, parser, namespace, values, option_string=None):
+ ansible_version = to_native(version(getattr(parser, 'prog')))
+@@ -202,7 +192,7 @@ def create_base_parser(prog, usage="", d
+ Create an options parser for all ansible scripts
+ """
+ # base opts
+- parser = ArgumentParser(
++ parser = argparse.ArgumentParser(
+ prog=prog,
+ formatter_class=SortingHelpFormatter,
+ epilog=epilog,
+@@ -260,8 +250,8 @@ def add_connect_options(parser):
+ help='connect as this user (default=%s)' % C.DEFAULT_REMOTE_USER)
+ connect_group.add_argument('-c', '--connection', dest='connection', default=C.DEFAULT_TRANSPORT,
+ help="connection type to use (default=%s)" % C.DEFAULT_TRANSPORT)
+- connect_group.add_argument('-T', '--timeout', default=None, type=int, dest='timeout',
+- help="override the connection timeout in seconds (default depends on connection)")
++ connect_group.add_argument('-T', '--timeout', default=C.DEFAULT_TIMEOUT, type=int, dest='timeout',
++ help="override the connection timeout in seconds (default=%s)" % C.DEFAULT_TIMEOUT)
+
+ # ssh only
+ connect_group.add_argument('--ssh-common-args', default=None, dest='ssh_common_args',
+@@ -393,7 +383,7 @@ def add_vault_options(parser):
+ parser.add_argument('--vault-id', default=[], dest='vault_ids', action='append', type=str,
+ help='the vault identity to use')
+ base_group = parser.add_mutually_exclusive_group()
+- base_group.add_argument('-J', '--ask-vault-password', '--ask-vault-pass', default=C.DEFAULT_ASK_VAULT_PASS, dest='ask_vault_pass', action='store_true',
++ base_group.add_argument('--ask-vault-password', '--ask-vault-pass', default=C.DEFAULT_ASK_VAULT_PASS, dest='ask_vault_pass', action='store_true',
+ help='ask for vault password')
+ base_group.add_argument('--vault-password-file', '--vault-pass-file', default=[], dest='vault_password_files',
+ help="vault password file", type=unfrack_path(follow=False), action='append')
+--- ansible-core-2.16.5.orig/lib/ansible/cli/config.py
++++ ansible-core-2.16.5/lib/ansible/cli/config.py
+@@ -23,7 +23,7 @@ from ansible import constants as C
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.config.manager import ConfigManager, Setting
+ from ansible.errors import AnsibleError, AnsibleOptionsError
+-from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
++from ansible.module_utils._text import to_native, to_text, to_bytes
+ from ansible.module_utils.common.json import json_dump
+ from ansible.module_utils.six import string_types
+ from ansible.parsing.quoting import is_quoted
+@@ -67,7 +67,7 @@ class ConfigCLI(CLI):
+ desc="View ansible configuration.",
+ )
+
+- common = opt_help.ArgumentParser(add_help=False)
++ common = opt_help.argparse.ArgumentParser(add_help=False)
+ opt_help.add_verbosity_options(common)
+ common.add_argument('-c', '--config', dest='config_file',
+ help="path to configuration file, defaults to first file found in precedence.")
+@@ -187,7 +187,7 @@ class ConfigCLI(CLI):
+
+ # pylint: disable=unreachable
+ try:
+- editor = shlex.split(C.config.get_config_value('EDITOR'))
++ editor = shlex.split(os.environ.get('EDITOR', 'vi'))
+ editor.append(self.config_file)
+ subprocess.call(editor)
+ except Exception as e:
+@@ -314,7 +314,7 @@ class ConfigCLI(CLI):
+
+ return data
+
+- def _get_settings_ini(self, settings, seen):
++ def _get_settings_ini(self, settings):
+
+ sections = {}
+ for o in sorted(settings.keys()):
+@@ -327,7 +327,7 @@ class ConfigCLI(CLI):
+
+ if not opt.get('description'):
+ # its a plugin
+- new_sections = self._get_settings_ini(opt, seen)
++ new_sections = self._get_settings_ini(opt)
+ for s in new_sections:
+ if s in sections:
+ sections[s].extend(new_sections[s])
+@@ -343,45 +343,37 @@ class ConfigCLI(CLI):
+
+ if 'ini' in opt and opt['ini']:
+ entry = opt['ini'][-1]
+- if entry['section'] not in seen:
+- seen[entry['section']] = []
+ if entry['section'] not in sections:
+ sections[entry['section']] = []
+
+- # avoid dupes
+- if entry['key'] not in seen[entry['section']]:
+- seen[entry['section']].append(entry['key'])
+-
+- default = opt.get('default', '')
+- if opt.get('type', '') == 'list' and not isinstance(default, string_types):
+- # python lists are not valid ini ones
+- default = ', '.join(default)
+- elif default is None:
+- default = ''
+-
+- if context.CLIARGS['commented']:
+- entry['key'] = ';%s' % entry['key']
++ default = opt.get('default', '')
++ if opt.get('type', '') == 'list' and not isinstance(default, string_types):
++ # python lists are not valid ini ones
++ default = ', '.join(default)
++ elif default is None:
++ default = ''
+
+- key = desc + '\n%s=%s' % (entry['key'], default)
++ if context.CLIARGS['commented']:
++ entry['key'] = ';%s' % entry['key']
+
+- sections[entry['section']].append(key)
++ key = desc + '\n%s=%s' % (entry['key'], default)
++ sections[entry['section']].append(key)
+
+ return sections
+
+ def execute_init(self):
+ """Create initial configuration"""
+
+- seen = {}
+ data = []
+ config_entries = self._list_entries_from_args()
+ plugin_types = config_entries.pop('PLUGINS', None)
+
+ if context.CLIARGS['format'] == 'ini':
+- sections = self._get_settings_ini(config_entries, seen)
++ sections = self._get_settings_ini(config_entries)
+
+ if plugin_types:
+ for ptype in plugin_types:
+- plugin_sections = self._get_settings_ini(plugin_types[ptype], seen)
++ plugin_sections = self._get_settings_ini(plugin_types[ptype])
+ for s in plugin_sections:
+ if s in sections:
+ sections[s].extend(plugin_sections[s])
+--- ansible-core-2.16.5.orig/lib/ansible/cli/console.py
++++ ansible-core-2.16.5/lib/ansible/cli/console.py
+@@ -22,7 +22,7 @@ from ansible import constants as C
+ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.executor.task_queue_manager import TaskQueueManager
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.parsing.splitter import parse_kv
+ from ansible.playbook.play import Play
+@@ -39,30 +39,26 @@ class ConsoleCLI(CLI, cmd.Cmd):
+ '''
+ A REPL that allows for running ad-hoc tasks against a chosen inventory
+ from a nice shell with built-in tab completion (based on dominis'
+- ``ansible-shell``).
++ ansible-shell).
+
+ It supports several commands, and you can modify its configuration at
+ runtime:
+
+- - ``cd [pattern]``: change host/group
+- (you can use host patterns eg.: ``app*.dc*:!app01*``)
+- - ``list``: list available hosts in the current path
+- - ``list groups``: list groups included in the current path
+- - ``become``: toggle the become flag
+- - ``!``: forces shell module instead of the ansible module
+- (``!yum update -y``)
+- - ``verbosity [num]``: set the verbosity level
+- - ``forks [num]``: set the number of forks
+- - ``become_user [user]``: set the become_user
+- - ``remote_user [user]``: set the remote_user
+- - ``become_method [method]``: set the privilege escalation method
+- - ``check [bool]``: toggle check mode
+- - ``diff [bool]``: toggle diff mode
+- - ``timeout [integer]``: set the timeout of tasks in seconds
+- (0 to disable)
+- - ``help [command/module]``: display documentation for
+- the command or module
+- - ``exit``: exit ``ansible-console``
++ - `cd [pattern]`: change host/group (you can use host patterns eg.: app*.dc*:!app01*)
++ - `list`: list available hosts in the current path
++ - `list groups`: list groups included in the current path
++ - `become`: toggle the become flag
++ - `!`: forces shell module instead of the ansible module (!yum update -y)
++ - `verbosity [num]`: set the verbosity level
++ - `forks [num]`: set the number of forks
++ - `become_user [user]`: set the become_user
++ - `remote_user [user]`: set the remote_user
++ - `become_method [method]`: set the privilege escalation method
++ - `check [bool]`: toggle check mode
++ - `diff [bool]`: toggle diff mode
++ - `timeout [integer]`: set the timeout of tasks in seconds (0 to disable)
++ - `help [command/module]`: display documentation for the command or module
++ - `exit`: exit ansible-console
+ '''
+
+ name = 'ansible-console'
+--- ansible-core-2.16.5.orig/lib/ansible/cli/doc.py
++++ ansible-core-2.16.5/lib/ansible/cli/doc.py
+@@ -26,7 +26,7 @@ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.collections.list import list_collection_dirs
+ from ansible.errors import AnsibleError, AnsibleOptionsError, AnsibleParserError, AnsiblePluginNotFound
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.common.collections import is_sequence
+ from ansible.module_utils.common.json import json_dump
+ from ansible.module_utils.common.yaml import yaml_dump
+@@ -163,8 +163,8 @@ class RoleMixin(object):
+ might be fully qualified with the collection name (e.g., community.general.roleA)
+ or not (e.g., roleA).
+
+- :param collection_filter: A list of strings containing the FQCN of a collection which will
+- be used to limit results. This filter will take precedence over the name_filters.
++ :param collection_filter: A string containing the FQCN of a collection which will be
++ used to limit results. This filter will take precedence over the name_filters.
+
+ :returns: A set of tuples consisting of: role name, collection name, collection path
+ """
+@@ -362,23 +362,12 @@ class DocCLI(CLI, RoleMixin):
+ _ITALIC = re.compile(r"\bI\(([^)]+)\)")
+ _BOLD = re.compile(r"\bB\(([^)]+)\)")
+ _MODULE = re.compile(r"\bM\(([^)]+)\)")
+- _PLUGIN = re.compile(r"\bP\(([^#)]+)#([a-z]+)\)")
+ _LINK = re.compile(r"\bL\(([^)]+), *([^)]+)\)")
+ _URL = re.compile(r"\bU\(([^)]+)\)")
+ _REF = re.compile(r"\bR\(([^)]+), *([^)]+)\)")
+ _CONST = re.compile(r"\bC\(([^)]+)\)")
+- _SEM_PARAMETER_STRING = r"\(((?:[^\\)]+|\\.)+)\)"
+- _SEM_OPTION_NAME = re.compile(r"\bO" + _SEM_PARAMETER_STRING)
+- _SEM_OPTION_VALUE = re.compile(r"\bV" + _SEM_PARAMETER_STRING)
+- _SEM_ENV_VARIABLE = re.compile(r"\bE" + _SEM_PARAMETER_STRING)
+- _SEM_RET_VALUE = re.compile(r"\bRV" + _SEM_PARAMETER_STRING)
+ _RULER = re.compile(r"\bHORIZONTALLINE\b")
+
+- # helper for unescaping
+- _UNESCAPE = re.compile(r"\\(.)")
+- _FQCN_TYPE_PREFIX_RE = re.compile(r'^([^.]+\.[^.]+\.[^#]+)#([a-z]+):(.*)$')
+- _IGNORE_MARKER = 'ignore:'
+-
+ # rst specific
+ _RST_NOTE = re.compile(r".. note::")
+ _RST_SEEALSO = re.compile(r".. seealso::")
+@@ -390,40 +379,6 @@ class DocCLI(CLI, RoleMixin):
+ super(DocCLI, self).__init__(args)
+ self.plugin_list = set()
+
+- @staticmethod
+- def _tty_ify_sem_simle(matcher):
+- text = DocCLI._UNESCAPE.sub(r'\1', matcher.group(1))
+- return f"`{text}'"
+-
+- @staticmethod
+- def _tty_ify_sem_complex(matcher):
+- text = DocCLI._UNESCAPE.sub(r'\1', matcher.group(1))
+- value = None
+- if '=' in text:
+- text, value = text.split('=', 1)
+- m = DocCLI._FQCN_TYPE_PREFIX_RE.match(text)
+- if m:
+- plugin_fqcn = m.group(1)
+- plugin_type = m.group(2)
+- text = m.group(3)
+- elif text.startswith(DocCLI._IGNORE_MARKER):
+- text = text[len(DocCLI._IGNORE_MARKER):]
+- plugin_fqcn = plugin_type = ''
+- else:
+- plugin_fqcn = plugin_type = ''
+- entrypoint = None
+- if ':' in text:
+- entrypoint, text = text.split(':', 1)
+- if value is not None:
+- text = f"{text}={value}"
+- if plugin_fqcn and plugin_type:
+- plugin_suffix = '' if plugin_type in ('role', 'module', 'playbook') else ' plugin'
+- plugin = f"{plugin_type}{plugin_suffix} {plugin_fqcn}"
+- if plugin_type == 'role' and entrypoint is not None:
+- plugin = f"{plugin}, {entrypoint} entrypoint"
+- return f"`{text}' (of {plugin})"
+- return f"`{text}'"
+-
+ @classmethod
+ def find_plugins(cls, path, internal, plugin_type, coll_filter=None):
+ display.deprecated("find_plugins method as it is incomplete/incorrect. use ansible.plugins.list functions instead.", version='2.17')
+@@ -438,13 +393,8 @@ class DocCLI(CLI, RoleMixin):
+ t = cls._MODULE.sub("[" + r"\1" + "]", t) # M(word) => [word]
+ t = cls._URL.sub(r"\1", t) # U(word) => word
+ t = cls._LINK.sub(r"\1 <\2>", t) # L(word, url) => word <url>
+- t = cls._PLUGIN.sub("[" + r"\1" + "]", t) # P(word#type) => [word]
+ t = cls._REF.sub(r"\1", t) # R(word, sphinx-ref) => word
+ t = cls._CONST.sub(r"`\1'", t) # C(word) => `word'
+- t = cls._SEM_OPTION_NAME.sub(cls._tty_ify_sem_complex, t) # O(expr)
+- t = cls._SEM_OPTION_VALUE.sub(cls._tty_ify_sem_simle, t) # V(expr)
+- t = cls._SEM_ENV_VARIABLE.sub(cls._tty_ify_sem_simle, t) # E(expr)
+- t = cls._SEM_RET_VALUE.sub(cls._tty_ify_sem_complex, t) # RV(expr)
+ t = cls._RULER.sub("\n{0}\n".format("-" * 13), t) # HORIZONTALLINE => -------
+
+ # remove rst
+@@ -545,9 +495,7 @@ class DocCLI(CLI, RoleMixin):
+ desc = desc[:linelimit] + '...'
+
+ pbreak = plugin.split('.')
+- # TODO: add mark for deprecated collection plugins
+- if pbreak[-1].startswith('_') and plugin.startswith(('ansible.builtin.', 'ansible.legacy.')):
+- # Handle deprecated ansible.builtin plugins
++ if pbreak[-1].startswith('_'): # Handle deprecated # TODO: add mark for deprecated collection plugins
+ pbreak[-1] = pbreak[-1][1:]
+ plugin = '.'.join(pbreak)
+ deprecated.append("%-*s %-*.*s" % (displace, plugin, linelimit, len(desc), desc))
+@@ -678,11 +626,12 @@ class DocCLI(CLI, RoleMixin):
+ def _get_collection_filter(self):
+
+ coll_filter = None
+- if len(context.CLIARGS['args']) >= 1:
+- coll_filter = context.CLIARGS['args']
+- for coll_name in coll_filter:
+- if not AnsibleCollectionRef.is_valid_collection_name(coll_name):
+- raise AnsibleError('Invalid collection name (must be of the form namespace.collection): {0}'.format(coll_name))
++ if len(context.CLIARGS['args']) == 1:
++ coll_filter = context.CLIARGS['args'][0]
++ if not AnsibleCollectionRef.is_valid_collection_name(coll_filter):
++ raise AnsibleError('Invalid collection name (must be of the form namespace.collection): {0}'.format(coll_filter))
++ elif len(context.CLIARGS['args']) > 1:
++ raise AnsibleOptionsError("Only a single collection filter is supported.")
+
+ return coll_filter
+
+@@ -1302,20 +1251,6 @@ class DocCLI(CLI, RoleMixin):
+ relative_url = 'collections/%s_module.html' % item['module'].replace('.', '/', 2)
+ text.append(textwrap.fill(DocCLI.tty_ify(get_versioned_doclink(relative_url)),
+ limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent))
+- elif 'plugin' in item and 'plugin_type' in item:
+- plugin_suffix = ' plugin' if item['plugin_type'] not in ('module', 'role') else ''
+- text.append(textwrap.fill(DocCLI.tty_ify('%s%s %s' % (item['plugin_type'].title(), plugin_suffix, item['plugin'])),
+- limit - 6, initial_indent=opt_indent[:-2] + "* ", subsequent_indent=opt_indent))
+- description = item.get('description')
+- if description is None and item['plugin'].startswith('ansible.builtin.'):
+- description = 'The official documentation on the %s %s%s.' % (item['plugin'], item['plugin_type'], plugin_suffix)
+- if description is not None:
+- text.append(textwrap.fill(DocCLI.tty_ify(description),
+- limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent + ' '))
+- if item['plugin'].startswith('ansible.builtin.'):
+- relative_url = 'collections/%s_%s.html' % (item['plugin'].replace('.', '/', 2), item['plugin_type'])
+- text.append(textwrap.fill(DocCLI.tty_ify(get_versioned_doclink(relative_url)),
+- limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent))
+ elif 'name' in item and 'link' in item and 'description' in item:
+ text.append(textwrap.fill(DocCLI.tty_ify(item['name']),
+ limit - 6, initial_indent=opt_indent[:-2] + "* ", subsequent_indent=opt_indent))
+--- ansible-core-2.16.5.orig/lib/ansible/cli/galaxy.py
++++ ansible-core-2.16.5/lib/ansible/cli/galaxy.py
+@@ -10,11 +10,9 @@ __metaclass__ = type
+ # ansible.cli needs to be imported first, to ensure the source bin/* scripts run that code first
+ from ansible.cli import CLI
+
+-import argparse
+ import functools
+ import json
+ import os.path
+-import pathlib
+ import re
+ import shutil
+ import sys
+@@ -53,7 +51,7 @@ from ansible.galaxy.token import BasicAu
+ from ansible.module_utils.ansible_release import __version__ as ansible_version
+ from ansible.module_utils.common.collections import is_iterable
+ from ansible.module_utils.common.yaml import yaml_dump, yaml_load
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils import six
+ from ansible.parsing.dataloader import DataLoader
+ from ansible.parsing.yaml.loader import AnsibleLoader
+@@ -73,7 +71,7 @@ SERVER_DEF = [
+ ('password', False, 'str'),
+ ('token', False, 'str'),
+ ('auth_url', False, 'str'),
+- ('api_version', False, 'int'),
++ ('v3', False, 'bool'),
+ ('validate_certs', False, 'bool'),
+ ('client_id', False, 'str'),
+ ('timeout', False, 'int'),
+@@ -81,9 +79,9 @@ SERVER_DEF = [
+
+ # config definition fields
+ SERVER_ADDITIONAL = {
+- 'api_version': {'default': None, 'choices': [2, 3]},
++ 'v3': {'default': 'False'},
+ 'validate_certs': {'cli': [{'name': 'validate_certs'}]},
+- 'timeout': {'default': C.GALAXY_SERVER_TIMEOUT, 'cli': [{'name': 'timeout'}]},
++ 'timeout': {'default': '60', 'cli': [{'name': 'timeout'}]},
+ 'token': {'default': None},
+ }
+
+@@ -101,8 +99,7 @@ def with_collection_artifacts_manager(wr
+ return wrapped_method(*args, **kwargs)
+
+ # FIXME: use validate_certs context from Galaxy servers when downloading collections
+- # .get used here for when this is used in a non-CLI context
+- artifacts_manager_kwargs = {'validate_certs': context.CLIARGS.get('resolved_validate_certs', True)}
++ artifacts_manager_kwargs = {'validate_certs': context.CLIARGS['resolved_validate_certs']}
+
+ keyring = context.CLIARGS.get('keyring', None)
+ if keyring is not None:
+@@ -159,8 +156,8 @@ def _get_collection_widths(collections):
+ fqcn_set = {to_text(c.fqcn) for c in collections}
+ version_set = {to_text(c.ver) for c in collections}
+
+- fqcn_length = len(max(fqcn_set or [''], key=len))
+- version_length = len(max(version_set or [''], key=len))
++ fqcn_length = len(max(fqcn_set, key=len))
++ version_length = len(max(version_set, key=len))
+
+ return fqcn_length, version_length
+
+@@ -241,49 +238,45 @@ class GalaxyCLI(CLI):
+ )
+
+ # Common arguments that apply to more than 1 action
+- common = opt_help.ArgumentParser(add_help=False)
++ common = opt_help.argparse.ArgumentParser(add_help=False)
+ common.add_argument('-s', '--server', dest='api_server', help='The Galaxy API server URL')
+- common.add_argument('--api-version', type=int, choices=[2, 3], help=argparse.SUPPRESS) # Hidden argument that should only be used in our tests
+ common.add_argument('--token', '--api-key', dest='api_key',
+ help='The Ansible Galaxy API key which can be found at '
+ 'https://galaxy.ansible.com/me/preferences.')
+ common.add_argument('-c', '--ignore-certs', action='store_true', dest='ignore_certs', help='Ignore SSL certificate validation errors.', default=None)
+-
+- # --timeout uses the default None to handle two different scenarios.
+- # * --timeout > C.GALAXY_SERVER_TIMEOUT for non-configured servers
+- # * --timeout > server-specific timeout > C.GALAXY_SERVER_TIMEOUT for configured servers.
+- common.add_argument('--timeout', dest='timeout', type=int,
++ common.add_argument('--timeout', dest='timeout', type=int, default=60,
+ help="The time to wait for operations against the galaxy server, defaults to 60s.")
+
+ opt_help.add_verbosity_options(common)
+
+- force = opt_help.ArgumentParser(add_help=False)
++ force = opt_help.argparse.ArgumentParser(add_help=False)
+ force.add_argument('-f', '--force', dest='force', action='store_true', default=False,
+ help='Force overwriting an existing role or collection')
+
+- github = opt_help.ArgumentParser(add_help=False)
++ github = opt_help.argparse.ArgumentParser(add_help=False)
+ github.add_argument('github_user', help='GitHub username')
+ github.add_argument('github_repo', help='GitHub repository')
+
+- offline = opt_help.ArgumentParser(add_help=False)
++ offline = opt_help.argparse.ArgumentParser(add_help=False)
+ offline.add_argument('--offline', dest='offline', default=False, action='store_true',
+ help="Don't query the galaxy API when creating roles")
+
+ default_roles_path = C.config.get_configuration_definition('DEFAULT_ROLES_PATH').get('default', '')
+- roles_path = opt_help.ArgumentParser(add_help=False)
++ roles_path = opt_help.argparse.ArgumentParser(add_help=False)
+ roles_path.add_argument('-p', '--roles-path', dest='roles_path', type=opt_help.unfrack_path(pathsep=True),
+ default=C.DEFAULT_ROLES_PATH, action=opt_help.PrependListAction,
+ help='The path to the directory containing your roles. The default is the first '
+ 'writable one configured via DEFAULT_ROLES_PATH: %s ' % default_roles_path)
+
+- collections_path = opt_help.ArgumentParser(add_help=False)
++ collections_path = opt_help.argparse.ArgumentParser(add_help=False)
+ collections_path.add_argument('-p', '--collections-path', dest='collections_path', type=opt_help.unfrack_path(pathsep=True),
++ default=AnsibleCollectionConfig.collection_paths,
+ action=opt_help.PrependListAction,
+ help="One or more directories to search for collections in addition "
+ "to the default COLLECTIONS_PATHS. Separate multiple paths "
+ "with '{0}'.".format(os.path.pathsep))
+
+- cache_options = opt_help.ArgumentParser(add_help=False)
++ cache_options = opt_help.argparse.ArgumentParser(add_help=False)
+ cache_options.add_argument('--clear-response-cache', dest='clear_response_cache', action='store_true',
+ default=False, help='Clear the existing server response cache.')
+ cache_options.add_argument('--no-cache', dest='no_cache', action='store_true', default=False,
+@@ -467,15 +460,12 @@ class GalaxyCLI(CLI):
+ valid_signature_count_help = 'The number of signatures that must successfully verify the collection. This should be a positive integer ' \
+ 'or all to signify that all signatures must be used to verify the collection. ' \
+ 'Prepend the value with + to fail if no valid signatures are found for the collection (e.g. +all).'
+- ignore_gpg_status_help = 'A space separated list of status codes to ignore during signature verification (for example, NO_PUBKEY FAILURE). ' \
+- 'Descriptions for the choices can be seen at L(https://github.com/gpg/gnupg/blob/master/doc/DETAILS#general-status-codes).' \
+- 'Note: specify these after positional arguments or use -- to separate them.'
++ ignore_gpg_status_help = 'A status code to ignore during signature verification (for example, NO_PUBKEY). ' \
++ 'Provide this option multiple times to ignore a list of status codes. ' \
++ 'Descriptions for the choices can be seen at L(https://github.com/gpg/gnupg/blob/master/doc/DETAILS#general-status-codes).'
+ verify_parser.add_argument('--required-valid-signature-count', dest='required_valid_signature_count', type=validate_signature_count,
+ help=valid_signature_count_help, default=C.GALAXY_REQUIRED_VALID_SIGNATURE_COUNT)
+ verify_parser.add_argument('--ignore-signature-status-code', dest='ignore_gpg_errors', type=str, action='append',
+- help=opt_help.argparse.SUPPRESS, default=C.GALAXY_IGNORE_INVALID_SIGNATURE_STATUS_CODES,
+- choices=list(GPG_ERROR_MAP.keys()))
+- verify_parser.add_argument('--ignore-signature-status-codes', dest='ignore_gpg_errors', type=str, action='extend', nargs='+',
+ help=ignore_gpg_status_help, default=C.GALAXY_IGNORE_INVALID_SIGNATURE_STATUS_CODES,
+ choices=list(GPG_ERROR_MAP.keys()))
+
+@@ -511,9 +501,9 @@ class GalaxyCLI(CLI):
+ valid_signature_count_help = 'The number of signatures that must successfully verify the collection. This should be a positive integer ' \
+ 'or -1 to signify that all signatures must be used to verify the collection. ' \
+ 'Prepend the value with + to fail if no valid signatures are found for the collection (e.g. +all).'
+- ignore_gpg_status_help = 'A space separated list of status codes to ignore during signature verification (for example, NO_PUBKEY FAILURE). ' \
+- 'Descriptions for the choices can be seen at L(https://github.com/gpg/gnupg/blob/master/doc/DETAILS#general-status-codes).' \
+- 'Note: specify these after positional arguments or use -- to separate them.'
++ ignore_gpg_status_help = 'A status code to ignore during signature verification (for example, NO_PUBKEY). ' \
++ 'Provide this option multiple times to ignore a list of status codes. ' \
++ 'Descriptions for the choices can be seen at L(https://github.com/gpg/gnupg/blob/master/doc/DETAILS#general-status-codes).'
+
+ if galaxy_type == 'collection':
+ install_parser.add_argument('-p', '--collections-path', dest='collections_path',
+@@ -537,9 +527,6 @@ class GalaxyCLI(CLI):
+ install_parser.add_argument('--required-valid-signature-count', dest='required_valid_signature_count', type=validate_signature_count,
+ help=valid_signature_count_help, default=C.GALAXY_REQUIRED_VALID_SIGNATURE_COUNT)
+ install_parser.add_argument('--ignore-signature-status-code', dest='ignore_gpg_errors', type=str, action='append',
+- help=opt_help.argparse.SUPPRESS, default=C.GALAXY_IGNORE_INVALID_SIGNATURE_STATUS_CODES,
+- choices=list(GPG_ERROR_MAP.keys()))
+- install_parser.add_argument('--ignore-signature-status-codes', dest='ignore_gpg_errors', type=str, action='extend', nargs='+',
+ help=ignore_gpg_status_help, default=C.GALAXY_IGNORE_INVALID_SIGNATURE_STATUS_CODES,
+ choices=list(GPG_ERROR_MAP.keys()))
+ install_parser.add_argument('--offline', dest='offline', action='store_true', default=False,
+@@ -564,9 +551,6 @@ class GalaxyCLI(CLI):
+ install_parser.add_argument('--required-valid-signature-count', dest='required_valid_signature_count', type=validate_signature_count,
+ help=valid_signature_count_help, default=C.GALAXY_REQUIRED_VALID_SIGNATURE_COUNT)
+ install_parser.add_argument('--ignore-signature-status-code', dest='ignore_gpg_errors', type=str, action='append',
+- help=opt_help.argparse.SUPPRESS, default=C.GALAXY_IGNORE_INVALID_SIGNATURE_STATUS_CODES,
+- choices=list(GPG_ERROR_MAP.keys()))
+- install_parser.add_argument('--ignore-signature-status-codes', dest='ignore_gpg_errors', type=str, action='extend', nargs='+',
+ help=ignore_gpg_status_help, default=C.GALAXY_IGNORE_INVALID_SIGNATURE_STATUS_CODES,
+ choices=list(GPG_ERROR_MAP.keys()))
+
+@@ -638,7 +622,7 @@ class GalaxyCLI(CLI):
+ return config_def
+
+ galaxy_options = {}
+- for optional_key in ['clear_response_cache', 'no_cache']:
++ for optional_key in ['clear_response_cache', 'no_cache', 'timeout']:
+ if optional_key in context.CLIARGS:
+ galaxy_options[optional_key] = context.CLIARGS[optional_key]
+
+@@ -663,22 +647,17 @@ class GalaxyCLI(CLI):
+ client_id = server_options.pop('client_id')
+ token_val = server_options['token'] or NoTokenSentinel
+ username = server_options['username']
+- api_version = server_options.pop('api_version')
++ v3 = server_options.pop('v3')
+ if server_options['validate_certs'] is None:
+ server_options['validate_certs'] = context.CLIARGS['resolved_validate_certs']
+ validate_certs = server_options['validate_certs']
+
+- # This allows a user to explicitly force use of an API version when
+- # multiple versions are supported. This was added for testing
+- # against pulp_ansible and I'm not sure it has a practical purpose
+- # outside of this use case. As such, this option is not documented
+- # as of now
+- if api_version:
+- display.warning(
+- f'The specified "api_version" configuration for the galaxy server "{server_key}" is '
+- 'not a public configuration, and may be removed at any time without warning.'
+- )
+- server_options['available_api_versions'] = {'v%s' % api_version: '/v%s' % api_version}
++ if v3:
++ # This allows a user to explicitly indicate the server uses the /v3 API
++ # This was added for testing against pulp_ansible and I'm not sure it has
++ # a practical purpose outside of this use case. As such, this option is not
++ # documented as of now
++ server_options['available_api_versions'] = {'v3': '/v3'}
+
+ # default case if no auth info is provided.
+ server_options['token'] = None
+@@ -704,17 +683,9 @@ class GalaxyCLI(CLI):
+ ))
+
+ cmd_server = context.CLIARGS['api_server']
+- if context.CLIARGS['api_version']:
+- api_version = context.CLIARGS['api_version']
+- display.warning(
+- 'The --api-version is not a public argument, and may be removed at any time without warning.'
+- )
+- galaxy_options['available_api_versions'] = {'v%s' % api_version: '/v%s' % api_version}
+-
+ cmd_token = GalaxyToken(token=context.CLIARGS['api_key'])
+
+ validate_certs = context.CLIARGS['resolved_validate_certs']
+- default_server_timeout = context.CLIARGS['timeout'] if context.CLIARGS['timeout'] is not None else C.GALAXY_SERVER_TIMEOUT
+ if cmd_server:
+ # Cmd args take precedence over the config entry but fist check if the arg was a name and use that config
+ # entry, otherwise create a new API entry for the server specified.
+@@ -726,7 +697,6 @@ class GalaxyCLI(CLI):
+ self.galaxy, 'cmd_arg', cmd_server, token=cmd_token,
+ priority=len(config_servers) + 1,
+ validate_certs=validate_certs,
+- timeout=default_server_timeout,
+ **galaxy_options
+ ))
+ else:
+@@ -738,7 +708,6 @@ class GalaxyCLI(CLI):
+ self.galaxy, 'default', C.GALAXY_SERVER, token=cmd_token,
+ priority=0,
+ validate_certs=validate_certs,
+- timeout=default_server_timeout,
+ **galaxy_options
+ ))
+
+@@ -835,7 +804,7 @@ class GalaxyCLI(CLI):
+ for role_req in file_requirements:
+ requirements['roles'] += parse_role_req(role_req)
+
+- elif isinstance(file_requirements, dict):
++ else:
+ # Newer format with a collections and/or roles key
+ extra_keys = set(file_requirements.keys()).difference(set(['roles', 'collections']))
+ if extra_keys:
+@@ -854,9 +823,6 @@ class GalaxyCLI(CLI):
+ for collection_req in file_requirements.get('collections') or []
+ ]
+
+- else:
+- raise AnsibleError(f"Expecting requirements yaml to be a list or dictionary but got {type(file_requirements).__name__}")
+-
+ return requirements
+
+ def _init_coll_req_dict(self, coll_req):
+@@ -1220,16 +1186,11 @@ class GalaxyCLI(CLI):
+ df.write(b_rendered)
+ else:
+ f_rel_path = os.path.relpath(os.path.join(root, f), obj_skeleton)
+- shutil.copyfile(os.path.join(root, f), os.path.join(obj_path, f_rel_path), follow_symlinks=False)
++ shutil.copyfile(os.path.join(root, f), os.path.join(obj_path, f_rel_path))
+
+ for d in dirs:
+ b_dir_path = to_bytes(os.path.join(obj_path, rel_root, d), errors='surrogate_or_strict')
+- if os.path.exists(b_dir_path):
+- continue
+- b_src_dir = to_bytes(os.path.join(root, d), errors='surrogate_or_strict')
+- if os.path.islink(b_src_dir):
+- shutil.copyfile(b_src_dir, b_dir_path, follow_symlinks=False)
+- else:
++ if not os.path.exists(b_dir_path):
+ os.makedirs(b_dir_path)
+
+ display.display("- %s %s was created successfully" % (galaxy_type.title(), obj_name))
+@@ -1293,7 +1254,7 @@ class GalaxyCLI(CLI):
+ """Compare checksums with the collection(s) found on the server and the installed copy. This does not verify dependencies."""
+
+ collections = context.CLIARGS['args']
+- search_paths = AnsibleCollectionConfig.collection_paths
++ search_paths = context.CLIARGS['collections_path']
+ ignore_errors = context.CLIARGS['ignore_errors']
+ local_verify_only = context.CLIARGS['offline']
+ requirements_file = context.CLIARGS['requirements']
+@@ -1433,19 +1394,7 @@ class GalaxyCLI(CLI):
+ upgrade = context.CLIARGS.get('upgrade', False)
+
+ collections_path = C.COLLECTIONS_PATHS
+-
+- managed_paths = set(validate_collection_path(p) for p in C.COLLECTIONS_PATHS)
+- read_req_paths = set(validate_collection_path(p) for p in AnsibleCollectionConfig.collection_paths)
+-
+- unexpected_path = C.GALAXY_COLLECTIONS_PATH_WARNING and not any(p.startswith(path) for p in managed_paths)
+- if unexpected_path and any(p.startswith(path) for p in read_req_paths):
+- display.warning(
+- f"The specified collections path '{path}' appears to be part of the pip Ansible package. "
+- "Managing these directly with ansible-galaxy could break the Ansible package. "
+- "Install collections to a configured collections path, which will take precedence over "
+- "collections found in the PYTHONPATH."
+- )
+- elif unexpected_path:
++ if len([p for p in collections_path if p.startswith(path)]) == 0:
+ display.warning("The specified collections path '%s' is not part of the configured Ansible "
+ "collections paths '%s'. The installed collection will not be picked up in an Ansible "
+ "run, unless within a playbook-adjacent collections directory." % (to_text(path), to_text(":".join(collections_path))))
+@@ -1462,7 +1411,6 @@ class GalaxyCLI(CLI):
+ artifacts_manager=artifacts_manager,
+ disable_gpg_verify=disable_gpg_verify,
+ offline=context.CLIARGS.get('offline', False),
+- read_requirement_paths=read_req_paths,
+ )
+
+ return 0
+@@ -1631,9 +1579,7 @@ class GalaxyCLI(CLI):
+ display.warning(w)
+
+ if not path_found:
+- raise AnsibleOptionsError(
+- "- None of the provided paths were usable. Please specify a valid path with --{0}s-path".format(context.CLIARGS['type'])
+- )
++ raise AnsibleOptionsError("- None of the provided paths were usable. Please specify a valid path with --{0}s-path".format(context.CLIARGS['type']))
+
+ return 0
+
+@@ -1648,65 +1594,100 @@ class GalaxyCLI(CLI):
+ artifacts_manager.require_build_metadata = False
+
+ output_format = context.CLIARGS['output_format']
++ collections_search_paths = set(context.CLIARGS['collections_path'])
+ collection_name = context.CLIARGS['collection']
+- default_collections_path = set(C.COLLECTIONS_PATHS)
+- collections_search_paths = (
+- set(context.CLIARGS['collections_path'] or []) | default_collections_path | set(AnsibleCollectionConfig.collection_paths)
+- )
++ default_collections_path = AnsibleCollectionConfig.collection_paths
+ collections_in_paths = {}
+
+ warnings = []
+ path_found = False
+ collection_found = False
+-
+- namespace_filter = None
+- collection_filter = None
+- if collection_name:
+- # list a specific collection
+-
+- validate_collection_name(collection_name)
+- namespace_filter, collection_filter = collection_name.split('.')
+-
+- collections = list(find_existing_collections(
+- list(collections_search_paths),
+- artifacts_manager,
+- namespace_filter=namespace_filter,
+- collection_filter=collection_filter,
+- dedupe=False
+- ))
+-
+- seen = set()
+- fqcn_width, version_width = _get_collection_widths(collections)
+- for collection in sorted(collections, key=lambda c: c.src):
+- collection_found = True
+- collection_path = pathlib.Path(to_text(collection.src)).parent.parent.as_posix()
+-
+- if output_format in {'yaml', 'json'}:
+- collections_in_paths.setdefault(collection_path, {})
+- collections_in_paths[collection_path][collection.fqcn] = {'version': collection.ver}
+- else:
+- if collection_path not in seen:
+- _display_header(
+- collection_path,
+- 'Collection',
+- 'Version',
+- fqcn_width,
+- version_width
+- )
+- seen.add(collection_path)
+- _display_collection(collection, fqcn_width, version_width)
+-
+- path_found = False
+ for path in collections_search_paths:
++ collection_path = GalaxyCLI._resolve_path(path)
+ if not os.path.exists(path):
+ if path in default_collections_path:
+ # don't warn for missing default paths
+ continue
+- warnings.append("- the configured path {0} does not exist.".format(path))
+- elif os.path.exists(path) and not os.path.isdir(path):
+- warnings.append("- the configured path {0}, exists, but it is not a directory.".format(path))
++ warnings.append("- the configured path {0} does not exist.".format(collection_path))
++ continue
++
++ if not os.path.isdir(collection_path):
++ warnings.append("- the configured path {0}, exists, but it is not a directory.".format(collection_path))
++ continue
++
++ path_found = True
++
++ if collection_name:
++ # list a specific collection
++
++ validate_collection_name(collection_name)
++ namespace, collection = collection_name.split('.')
++
++ collection_path = validate_collection_path(collection_path)
++ b_collection_path = to_bytes(os.path.join(collection_path, namespace, collection), errors='surrogate_or_strict')
++
++ if not os.path.exists(b_collection_path):
++ warnings.append("- unable to find {0} in collection paths".format(collection_name))
++ continue
++
++ if not os.path.isdir(collection_path):
++ warnings.append("- the configured path {0}, exists, but it is not a directory.".format(collection_path))
++ continue
++
++ collection_found = True
++
++ try:
++ collection = Requirement.from_dir_path_as_unknown(
++ b_collection_path,
++ artifacts_manager,
++ )
++ except ValueError as val_err:
++ six.raise_from(AnsibleError(val_err), val_err)
++
++ if output_format in {'yaml', 'json'}:
++ collections_in_paths[collection_path] = {
++ collection.fqcn: {'version': collection.ver}
++ }
++
++ continue
++
++ fqcn_width, version_width = _get_collection_widths([collection])
++
++ _display_header(collection_path, 'Collection', 'Version', fqcn_width, version_width)
++ _display_collection(collection, fqcn_width, version_width)
++
+ else:
+- path_found = True
++ # list all collections
++ collection_path = validate_collection_path(path)
++ if os.path.isdir(collection_path):
++ display.vvv("Searching {0} for collections".format(collection_path))
++ collections = list(find_existing_collections(
++ collection_path, artifacts_manager,
++ ))
++ else:
++ # There was no 'ansible_collections/' directory in the path, so there
++ # or no collections here.
++ display.vvv("No 'ansible_collections' directory found at {0}".format(collection_path))
++ continue
++
++ if not collections:
++ display.vvv("No collections found at {0}".format(collection_path))
++ continue
++
++ if output_format in {'yaml', 'json'}:
++ collections_in_paths[collection_path] = {
++ collection.fqcn: {'version': collection.ver} for collection in collections
++ }
++
++ continue
++
++ # Display header
++ fqcn_width, version_width = _get_collection_widths(collections)
++ _display_header(collection_path, 'Collection', 'Version', fqcn_width, version_width)
++
++ # Sort collections by the namespace and name
++ for collection in sorted(collections, key=to_text):
++ _display_collection(collection, fqcn_width, version_width)
+
+ # Do not warn if the specific collection was found in any of the search paths
+ if collection_found and collection_name:
+@@ -1715,10 +1696,8 @@ class GalaxyCLI(CLI):
+ for w in warnings:
+ display.warning(w)
+
+- if not collections and not path_found:
+- raise AnsibleOptionsError(
+- "- None of the provided paths were usable. Please specify a valid path with --{0}s-path".format(context.CLIARGS['type'])
+- )
++ if not path_found:
++ raise AnsibleOptionsError("- None of the provided paths were usable. Please specify a valid path with --{0}s-path".format(context.CLIARGS['type']))
+
+ if output_format == 'json':
+ display.display(json.dumps(collections_in_paths))
+@@ -1752,8 +1731,8 @@ class GalaxyCLI(CLI):
+ tags=context.CLIARGS['galaxy_tags'], author=context.CLIARGS['author'], page_size=page_size)
+
+ if response['count'] == 0:
+- display.warning("No roles match your search.")
+- return 0
++ display.display("No roles match your search.", color=C.COLOR_ERROR)
++ return 1
+
+ data = [u'']
+
+@@ -1792,7 +1771,6 @@ class GalaxyCLI(CLI):
+ github_user = to_text(context.CLIARGS['github_user'], errors='surrogate_or_strict')
+ github_repo = to_text(context.CLIARGS['github_repo'], errors='surrogate_or_strict')
+
+- rc = 0
+ if context.CLIARGS['check_status']:
+ task = self.api.get_import_task(github_user=github_user, github_repo=github_repo)
+ else:
+@@ -1810,7 +1788,7 @@ class GalaxyCLI(CLI):
+ display.display('%s.%s' % (t['summary_fields']['role']['namespace'], t['summary_fields']['role']['name']), color=C.COLOR_CHANGED)
+ display.display(u'\nTo properly namespace this role, remove each of the above and re-import %s/%s from scratch' % (github_user, github_repo),
+ color=C.COLOR_CHANGED)
+- return rc
++ return 0
+ # found a single role as expected
+ display.display("Successfully submitted import request %d" % task[0]['id'])
+ if not context.CLIARGS['wait']:
+@@ -1827,13 +1805,12 @@ class GalaxyCLI(CLI):
+ if msg['id'] not in msg_list:
+ display.display(msg['message_text'], color=colors[msg['message_type']])
+ msg_list.append(msg['id'])
+- if (state := task[0]['state']) in ['SUCCESS', 'FAILED']:
+- rc = ['SUCCESS', 'FAILED'].index(state)
++ if task[0]['state'] in ['SUCCESS', 'FAILED']:
+ finished = True
+ else:
+ time.sleep(10)
+
+- return rc
++ return 0
+
+ def execute_setup(self):
+ """ Setup an integration from Github or Travis for Ansible Galaxy roles"""
+--- ansible-core-2.16.5.orig/lib/ansible/cli/inventory.py
++++ ansible-core-2.16.5/lib/ansible/cli/inventory.py
+@@ -18,7 +18,7 @@ from ansible import constants as C
+ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.errors import AnsibleError, AnsibleOptionsError
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.utils.vars import combine_vars
+ from ansible.utils.display import Display
+ from ansible.vars.plugins import get_vars_from_inventory_sources, get_vars_from_path
+@@ -72,6 +72,7 @@ class InventoryCLI(CLI):
+ opt_help.add_runtask_options(self.parser)
+
+ # remove unused default options
++ self.parser.add_argument('-l', '--limit', help=argparse.SUPPRESS, action=opt_help.UnrecognizedArgument, nargs='?')
+ self.parser.add_argument('--list-hosts', help=argparse.SUPPRESS, action=opt_help.UnrecognizedArgument)
+
+ self.parser.add_argument('args', metavar='host|group', nargs='?')
+@@ -79,10 +80,9 @@ class InventoryCLI(CLI):
+ # Actions
+ action_group = self.parser.add_argument_group("Actions", "One of following must be used on invocation, ONLY ONE!")
+ action_group.add_argument("--list", action="store_true", default=False, dest='list', help='Output all hosts info, works as inventory script')
+- action_group.add_argument("--host", action="store", default=None, dest='host',
+- help='Output specific host info, works as inventory script. It will ignore limit')
++ action_group.add_argument("--host", action="store", default=None, dest='host', help='Output specific host info, works as inventory script')
+ action_group.add_argument("--graph", action="store_true", default=False, dest='graph',
+- help='create inventory graph, if supplying pattern it must be a valid group name. It will ignore limit')
++ help='create inventory graph, if supplying pattern it must be a valid group name')
+ self.parser.add_argument_group(action_group)
+
+ # graph
+@@ -144,22 +144,17 @@ class InventoryCLI(CLI):
+ # FIXME: should we template first?
+ results = self.dump(myvars)
+
+- else:
+- if context.CLIARGS['subset']:
+- # not doing single host, set limit in general if given
+- self.inventory.subset(context.CLIARGS['subset'])
+-
+- if context.CLIARGS['graph']:
+- results = self.inventory_graph()
+- elif context.CLIARGS['list']:
+- top = self._get_group('all')
+- if context.CLIARGS['yaml']:
+- results = self.yaml_inventory(top)
+- elif context.CLIARGS['toml']:
+- results = self.toml_inventory(top)
+- else:
+- results = self.json_inventory(top)
+- results = self.dump(results)
++ elif context.CLIARGS['graph']:
++ results = self.inventory_graph()
++ elif context.CLIARGS['list']:
++ top = self._get_group('all')
++ if context.CLIARGS['yaml']:
++ results = self.yaml_inventory(top)
++ elif context.CLIARGS['toml']:
++ results = self.toml_inventory(top)
++ else:
++ results = self.json_inventory(top)
++ results = self.dump(results)
+
+ if results:
+ outfile = context.CLIARGS['output_file']
+@@ -254,7 +249,7 @@ class InventoryCLI(CLI):
+ return dump
+
+ @staticmethod
+- def _remove_empty_keys(dump):
++ def _remove_empty(dump):
+ # remove empty keys
+ for x in ('hosts', 'vars', 'children'):
+ if x in dump and not dump[x]:
+@@ -301,34 +296,33 @@ class InventoryCLI(CLI):
+
+ def json_inventory(self, top):
+
+- seen_groups = set()
++ seen = set()
+
+- def format_group(group, available_hosts):
++ def format_group(group):
+ results = {}
+ results[group.name] = {}
+ if group.name != 'all':
+- results[group.name]['hosts'] = [h.name for h in group.hosts if h.name in available_hosts]
++ results[group.name]['hosts'] = [h.name for h in group.hosts]
+ results[group.name]['children'] = []
+ for subgroup in group.child_groups:
+ results[group.name]['children'].append(subgroup.name)
+- if subgroup.name not in seen_groups:
+- results.update(format_group(subgroup, available_hosts))
+- seen_groups.add(subgroup.name)
++ if subgroup.name not in seen:
++ results.update(format_group(subgroup))
++ seen.add(subgroup.name)
+ if context.CLIARGS['export']:
+ results[group.name]['vars'] = self._get_group_variables(group)
+
+- self._remove_empty_keys(results[group.name])
+- # remove empty groups
++ self._remove_empty(results[group.name])
+ if not results[group.name]:
+ del results[group.name]
+
+ return results
+
+- hosts = self.inventory.get_hosts(top.name)
+- results = format_group(top, frozenset(h.name for h in hosts))
++ results = format_group(top)
+
+ # populate meta
+ results['_meta'] = {'hostvars': {}}
++ hosts = self.inventory.get_hosts()
+ for host in hosts:
+ hvars = self._get_host_variables(host)
+ if hvars:
+@@ -338,10 +332,9 @@ class InventoryCLI(CLI):
+
+ def yaml_inventory(self, top):
+
+- seen_hosts = set()
+- seen_groups = set()
++ seen = []
+
+- def format_group(group, available_hosts):
++ def format_group(group):
+ results = {}
+
+ # initialize group + vars
+@@ -351,21 +344,15 @@ class InventoryCLI(CLI):
+ results[group.name]['children'] = {}
+ for subgroup in group.child_groups:
+ if subgroup.name != 'all':
+- if subgroup.name in seen_groups:
+- results[group.name]['children'].update({subgroup.name: {}})
+- else:
+- results[group.name]['children'].update(format_group(subgroup, available_hosts))
+- seen_groups.add(subgroup.name)
++ results[group.name]['children'].update(format_group(subgroup))
+
+ # hosts for group
+ results[group.name]['hosts'] = {}
+ if group.name != 'all':
+ for h in group.hosts:
+- if h.name not in available_hosts:
+- continue # observe limit
+ myvars = {}
+- if h.name not in seen_hosts: # avoid defining host vars more than once
+- seen_hosts.add(h.name)
++ if h.name not in seen: # avoid defining host vars more than once
++ seen.append(h.name)
+ myvars = self._get_host_variables(host=h)
+ results[group.name]['hosts'][h.name] = myvars
+
+@@ -374,22 +361,17 @@ class InventoryCLI(CLI):
+ if gvars:
+ results[group.name]['vars'] = gvars
+
+- self._remove_empty_keys(results[group.name])
+- # remove empty groups
+- if not results[group.name]:
+- del results[group.name]
++ self._remove_empty(results[group.name])
+
+ return results
+
+- available_hosts = frozenset(h.name for h in self.inventory.get_hosts(top.name))
+- return format_group(top, available_hosts)
++ return format_group(top)
+
+ def toml_inventory(self, top):
+- seen_hosts = set()
+- seen_hosts = set()
++ seen = set()
+ has_ungrouped = bool(next(g.hosts for g in top.child_groups if g.name == 'ungrouped'))
+
+- def format_group(group, available_hosts):
++ def format_group(group):
+ results = {}
+ results[group.name] = {}
+
+@@ -399,14 +381,12 @@ class InventoryCLI(CLI):
+ continue
+ if group.name != 'all':
+ results[group.name]['children'].append(subgroup.name)
+- results.update(format_group(subgroup, available_hosts))
++ results.update(format_group(subgroup))
+
+ if group.name != 'all':
+ for host in group.hosts:
+- if host.name not in available_hosts:
+- continue
+- if host.name not in seen_hosts:
+- seen_hosts.add(host.name)
++ if host.name not in seen:
++ seen.add(host.name)
+ host_vars = self._get_host_variables(host=host)
+ else:
+ host_vars = {}
+@@ -418,15 +398,13 @@ class InventoryCLI(CLI):
+ if context.CLIARGS['export']:
+ results[group.name]['vars'] = self._get_group_variables(group)
+
+- self._remove_empty_keys(results[group.name])
+- # remove empty groups
++ self._remove_empty(results[group.name])
+ if not results[group.name]:
+ del results[group.name]
+
+ return results
+
+- available_hosts = frozenset(h.name for h in self.inventory.get_hosts(top.name))
+- results = format_group(top, available_hosts)
++ results = format_group(top)
+
+ return results
+
+--- ansible-core-2.16.5.orig/lib/ansible/cli/playbook.py
++++ ansible-core-2.16.5/lib/ansible/cli/playbook.py
+@@ -18,7 +18,7 @@ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.errors import AnsibleError
+ from ansible.executor.playbook_executor import PlaybookExecutor
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+ from ansible.playbook.block import Block
+ from ansible.plugins.loader import add_all_plugin_dirs
+ from ansible.utils.collection_loader import AnsibleCollectionConfig
+@@ -67,19 +67,8 @@ class PlaybookCLI(CLI):
+ self.parser.add_argument('args', help='Playbook(s)', metavar='playbook', nargs='+')
+
+ def post_process_args(self, options):
+-
+- # for listing, we need to know if user had tag input
+- # capture here as parent function sets defaults for tags
+- havetags = bool(options.tags or options.skip_tags)
+-
+ options = super(PlaybookCLI, self).post_process_args(options)
+
+- if options.listtags:
+- # default to all tags (including never), when listing tags
+- # unless user specified tags
+- if not havetags:
+- options.tags = ['never', 'all']
+-
+ display.verbosity = options.verbosity
+ self.validate_conflicts(options, runas_opts=True, fork_opts=True)
+
+--- ansible-core-2.16.5.orig/lib/ansible/cli/pull.py
++++ ansible-core-2.16.5/lib/ansible/cli/pull.py
+@@ -24,7 +24,7 @@ from ansible import constants as C
+ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.errors import AnsibleOptionsError
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.plugins.loader import module_loader
+ from ansible.utils.cmd_functions import run_cmd
+ from ansible.utils.display import Display
+@@ -81,7 +81,7 @@ class PullCLI(CLI):
+
+ super(PullCLI, self).init_parser(
+ usage='%prog -U <repository> [options] [<playbook.yml>]',
+- desc="pulls playbooks from a VCS repo and executes them on target host")
++ desc="pulls playbooks from a VCS repo and executes them for the local host")
+
+ # Do not add check_options as there's a conflict with --checkout/-C
+ opt_help.add_connect_options(self.parser)
+@@ -275,15 +275,8 @@ class PullCLI(CLI):
+ for vault_id in context.CLIARGS['vault_ids']:
+ cmd += " --vault-id=%s" % vault_id
+
+- if context.CLIARGS['become_password_file']:
+- cmd += " --become-password-file=%s" % context.CLIARGS['become_password_file']
+-
+- if context.CLIARGS['connection_password_file']:
+- cmd += " --connection-password-file=%s" % context.CLIARGS['connection_password_file']
+-
+ for ev in context.CLIARGS['extra_vars']:
+ cmd += ' -e %s' % shlex.quote(ev)
+-
+ if context.CLIARGS['become_ask_pass']:
+ cmd += ' --ask-become-pass'
+ if context.CLIARGS['skip_tags']:
+--- ansible-core-2.16.5.orig/lib/ansible/cli/scripts/ansible_connection_cli_stub.py
++++ ansible-core-2.16.5/lib/ansible/cli/scripts/ansible_connection_cli_stub.py
+@@ -6,6 +6,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+
++import argparse
+ import fcntl
+ import hashlib
+ import io
+@@ -23,12 +24,12 @@ from contextlib import contextmanager
+
+ from ansible import constants as C
+ from ansible.cli.arguments import option_helpers as opt_help
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.module_utils.connection import Connection, ConnectionError, send_data, recv_data
+ from ansible.module_utils.service import fork_process
+ from ansible.parsing.ajson import AnsibleJSONEncoder, AnsibleJSONDecoder
+ from ansible.playbook.play_context import PlayContext
+-from ansible.plugins.loader import connection_loader, init_plugin_loader
++from ansible.plugins.loader import connection_loader
+ from ansible.utils.path import unfrackpath, makedirs_safe
+ from ansible.utils.display import Display
+ from ansible.utils.jsonrpc import JsonRpcServer
+@@ -229,7 +230,6 @@ def main(args=None):
+ parser.add_argument('playbook_pid')
+ parser.add_argument('task_uuid')
+ args = parser.parse_args(args[1:] if args is not None else args)
+- init_plugin_loader()
+
+ # initialize verbosity
+ display.verbosity = args.verbosity
+--- ansible-core-2.16.5.orig/lib/ansible/cli/vault.py
++++ ansible-core-2.16.5/lib/ansible/cli/vault.py
+@@ -17,7 +17,7 @@ from ansible import constants as C
+ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
+ from ansible.errors import AnsibleOptionsError
+-from ansible.module_utils.common.text.converters import to_text, to_bytes
++from ansible.module_utils._text import to_text, to_bytes
+ from ansible.parsing.dataloader import DataLoader
+ from ansible.parsing.vault import VaultEditor, VaultLib, match_encrypt_secret
+ from ansible.utils.display import Display
+@@ -61,20 +61,20 @@ class VaultCLI(CLI):
+ epilog="\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0])
+ )
+
+- common = opt_help.ArgumentParser(add_help=False)
++ common = opt_help.argparse.ArgumentParser(add_help=False)
+ opt_help.add_vault_options(common)
+ opt_help.add_verbosity_options(common)
+
+ subparsers = self.parser.add_subparsers(dest='action')
+ subparsers.required = True
+
+- output = opt_help.ArgumentParser(add_help=False)
++ output = opt_help.argparse.ArgumentParser(add_help=False)
+ output.add_argument('--output', default=None, dest='output_file',
+ help='output file name for encrypt or decrypt; use - for stdout',
+ type=opt_help.unfrack_path())
+
+ # For encrypting actions, we can also specify which of multiple vault ids should be used for encrypting
+- vault_id = opt_help.ArgumentParser(add_help=False)
++ vault_id = opt_help.argparse.ArgumentParser(add_help=False)
+ vault_id.add_argument('--encrypt-vault-id', default=[], dest='encrypt_vault_id',
+ action='store', type=str,
+ help='the vault id used to encrypt (required if more than one vault-id is provided)')
+@@ -82,8 +82,6 @@ class VaultCLI(CLI):
+ create_parser = subparsers.add_parser('create', help='Create new vault encrypted file', parents=[vault_id, common])
+ create_parser.set_defaults(func=self.execute_create)
+ create_parser.add_argument('args', help='Filename', metavar='file_name', nargs='*')
+- create_parser.add_argument('--skip-tty-check', default=False, help='allows editor to be opened when no tty attached',
+- dest='skip_tty_check', action='store_true')
+
+ decrypt_parser = subparsers.add_parser('decrypt', help='Decrypt vault encrypted file', parents=[output, common])
+ decrypt_parser.set_defaults(func=self.execute_decrypt)
+@@ -386,11 +384,6 @@ class VaultCLI(CLI):
+ sys.stderr.write(err)
+ b_outs.append(to_bytes(out))
+
+- # The output must end with a newline to play nice with terminal representation.
+- # Refs:
+- # * https://stackoverflow.com/a/729795/595220
+- # * https://github.com/ansible/ansible/issues/78932
+- b_outs.append(b'')
+ self.editor.write_data(b'\n'.join(b_outs), context.CLIARGS['output_file'] or '-')
+
+ if sys.stdout.isatty():
+@@ -449,11 +442,8 @@ class VaultCLI(CLI):
+ if len(context.CLIARGS['args']) != 1:
+ raise AnsibleOptionsError("ansible-vault create can take only one filename argument")
+
+- if sys.stdout.isatty() or context.CLIARGS['skip_tty_check']:
+- self.editor.create_file(context.CLIARGS['args'][0], self.encrypt_secret,
+- vault_id=self.encrypt_vault_id)
+- else:
+- raise AnsibleOptionsError("not a tty, editor cannot be opened")
++ self.editor.create_file(context.CLIARGS['args'][0], self.encrypt_secret,
++ vault_id=self.encrypt_vault_id)
+
+ def execute_edit(self):
+ ''' open and decrypt an existing vaulted file in an editor, that will be encrypted again when closed'''
+--- ansible-core-2.16.5.orig/lib/ansible/collections/__init__.py
++++ ansible-core-2.16.5/lib/ansible/collections/__init__.py
+@@ -0,0 +1,29 @@
++# (c) 2019 Ansible Project
++# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
++
++from __future__ import (absolute_import, division, print_function)
++__metaclass__ = type
++
++import os
++
++from ansible.module_utils._text import to_bytes
++
++B_FLAG_FILES = frozenset([b'MANIFEST.json', b'galaxy.yml'])
++
++
++def is_collection_path(path):
++ """
++ Verify that a path meets min requirements to be a collection
++ :param path: byte-string path to evaluate for collection containment
++ :return: boolean signifying 'collectionness'
++ """
++
++ is_coll = False
++ b_path = to_bytes(path)
++ if os.path.isdir(b_path):
++ for b_flag in B_FLAG_FILES:
++ if os.path.exists(os.path.join(b_path, b_flag)):
++ is_coll = True
++ break
++
++ return is_coll
+--- ansible-core-2.16.5.orig/lib/ansible/collections/list.py
++++ ansible-core-2.16.5/lib/ansible/collections/list.py
+@@ -1,28 +1,65 @@
+ # (c) 2019 Ansible Project
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
++from __future__ import (absolute_import, division, print_function)
++__metaclass__ = type
++
++import os
++
++from collections import defaultdict
++
+ from ansible.errors import AnsibleError
+-from ansible.cli.galaxy import with_collection_artifacts_manager
+-from ansible.galaxy.collection import find_existing_collections
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.collections import is_collection_path
++from ansible.module_utils._text import to_bytes
++from ansible.utils.collection_loader import AnsibleCollectionConfig
+ from ansible.utils.collection_loader._collection_finder import _get_collection_name_from_path
+ from ansible.utils.display import Display
+
+ display = Display()
+
+
+-@with_collection_artifacts_manager
+-def list_collections(coll_filter=None, search_paths=None, dedupe=True, artifacts_manager=None):
++def list_collections(coll_filter=None, search_paths=None, dedupe=False):
+
+ collections = {}
+- for candidate in list_collection_dirs(search_paths=search_paths, coll_filter=coll_filter, artifacts_manager=artifacts_manager, dedupe=dedupe):
+- collection = _get_collection_name_from_path(candidate)
+- collections[collection] = candidate
++ for candidate in list_collection_dirs(search_paths=search_paths, coll_filter=coll_filter):
++ if os.path.exists(candidate):
++ collection = _get_collection_name_from_path(candidate)
++ if collection not in collections or not dedupe:
++ collections[collection] = candidate
+ return collections
+
+
+-@with_collection_artifacts_manager
+-def list_collection_dirs(search_paths=None, coll_filter=None, artifacts_manager=None, dedupe=True):
++def list_valid_collection_paths(search_paths=None, warn=False):
++ """
++ Filter out non existing or invalid search_paths for collections
++ :param search_paths: list of text-string paths, if none load default config
++ :param warn: display warning if search_path does not exist
++ :return: subset of original list
++ """
++
++ if search_paths is None:
++ search_paths = []
++
++ search_paths.extend(AnsibleCollectionConfig.collection_paths)
++
++ for path in search_paths:
++
++ b_path = to_bytes(path)
++ if not os.path.exists(b_path):
++ # warn for missing, but not if default
++ if warn:
++ display.warning("The configured collection path {0} does not exist.".format(path))
++ continue
++
++ if not os.path.isdir(b_path):
++ if warn:
++ display.warning("The configured collection path {0}, exists, but it is not a directory.".format(path))
++ continue
++
++ yield path
++
++
++def list_collection_dirs(search_paths=None, coll_filter=None):
+ """
+ Return paths for the specific collections found in passed or configured search paths
+ :param search_paths: list of text-string paths, if none load default config
+@@ -30,33 +67,48 @@ def list_collection_dirs(search_paths=No
+ :return: list of collection directory paths
+ """
+
+- namespace_filter = None
+- collection_filter = None
+- has_pure_namespace_filter = False # whether at least one coll_filter is a namespace-only filter
++ collection = None
++ namespace = None
+ if coll_filter is not None:
+- if isinstance(coll_filter, str):
+- coll_filter = [coll_filter]
+- namespace_filter = set()
+- for coll_name in coll_filter:
+- if '.' in coll_name:
+- try:
+- namespace, collection = coll_name.split('.')
+- except ValueError:
+- raise AnsibleError("Invalid collection pattern supplied: %s" % coll_name)
+- namespace_filter.add(namespace)
+- if not has_pure_namespace_filter:
+- if collection_filter is None:
+- collection_filter = []
+- collection_filter.append(collection)
++ if '.' in coll_filter:
++ try:
++ (namespace, collection) = coll_filter.split('.')
++ except ValueError:
++ raise AnsibleError("Invalid collection pattern supplied: %s" % coll_filter)
++ else:
++ namespace = coll_filter
++
++ collections = defaultdict(dict)
++ for path in list_valid_collection_paths(search_paths):
++
++ if os.path.basename(path) != 'ansible_collections':
++ path = os.path.join(path, 'ansible_collections')
++
++ b_coll_root = to_bytes(path, errors='surrogate_or_strict')
++
++ if os.path.exists(b_coll_root) and os.path.isdir(b_coll_root):
++
++ if namespace is None:
++ namespaces = os.listdir(b_coll_root)
+ else:
+- namespace_filter.add(coll_name)
+- has_pure_namespace_filter = True
+- collection_filter = None
+- namespace_filter = sorted(namespace_filter)
++ namespaces = [namespace]
+
+- for req in find_existing_collections(search_paths, artifacts_manager, namespace_filter=namespace_filter,
+- collection_filter=collection_filter, dedupe=dedupe):
++ for ns in namespaces:
++ b_namespace_dir = os.path.join(b_coll_root, to_bytes(ns))
+
+- if not has_pure_namespace_filter and coll_filter is not None and req.fqcn not in coll_filter:
+- continue
+- yield to_bytes(req.src)
++ if os.path.isdir(b_namespace_dir):
++
++ if collection is None:
++ colls = os.listdir(b_namespace_dir)
++ else:
++ colls = [collection]
++
++ for mycoll in colls:
++
++ # skip dupe collections as they will be masked in execution
++ if mycoll not in collections[ns]:
++ b_coll = to_bytes(mycoll)
++ b_coll_dir = os.path.join(b_namespace_dir, b_coll)
++ if is_collection_path(b_coll_dir):
++ collections[ns][mycoll] = b_coll_dir
++ yield b_coll_dir
+--- ansible-core-2.16.5.orig/lib/ansible/config/ansible_builtin_runtime.yml
++++ ansible-core-2.16.5/lib/ansible/config/ansible_builtin_runtime.yml
+@@ -2162,7 +2162,7 @@ plugin_routing:
+ redirect: community.network.exos_vlans
+ bigip_asm_policy:
+ tombstone:
+- removal_date: "2019-11-06"
++ removal_date: 2019-11-06
+ warning_text: bigip_asm_policy has been removed please use bigip_asm_policy_manage instead.
+ bigip_device_facts:
+ redirect: f5networks.f5_modules.bigip_device_info
+@@ -2176,11 +2176,11 @@ plugin_routing:
+ redirect: f5networks.f5_modules.bigip_device_traffic_group
+ bigip_facts:
+ tombstone:
+- removal_date: "2019-11-06"
++ removal_date: 2019-11-06
+ warning_text: bigip_facts has been removed please use bigip_device_info module.
+ bigip_gtm_facts:
+ tombstone:
+- removal_date: "2019-11-06"
++ removal_date: 2019-11-06
+ warning_text: bigip_gtm_facts has been removed please use bigip_device_info module.
+ faz_device:
+ redirect: community.fortios.faz_device
+@@ -7641,7 +7641,7 @@ plugin_routing:
+ redirect: ngine_io.exoscale.exoscale
+ f5_utils:
+ tombstone:
+- removal_date: "2019-11-06"
++ removal_date: 2019-11-06
+ firewalld:
+ redirect: ansible.posix.firewalld
+ gcdns:
+@@ -9084,10 +9084,6 @@ plugin_routing:
+ redirect: dellemc.os6.os6
+ vyos:
+ redirect: vyos.vyos.vyos
+- include:
+- tombstone:
+- removal_date: "2023-05-16"
+- warning_text: Use include_tasks or import_tasks instead.
+ become:
+ doas:
+ redirect: community.general.doas
+--- ansible-core-2.16.5.orig/lib/ansible/config/base.yml
++++ ansible-core-2.16.5/lib/ansible/config/base.yml
+@@ -37,9 +37,20 @@ ANSIBLE_COW_ACCEPTLIST:
+ default: ['bud-frogs', 'bunny', 'cheese', 'daemon', 'default', 'dragon', 'elephant-in-snake', 'elephant', 'eyes', 'hellokitty', 'kitty', 'luke-koala', 'meow', 'milk', 'moofasa', 'moose', 'ren', 'sheep', 'small', 'stegosaurus', 'stimpy', 'supermilker', 'three-eyes', 'turkey', 'turtle', 'tux', 'udder', 'vader-koala', 'vader', 'www']
+ description: Accept list of cowsay templates that are 'safe' to use, set to empty list if you want to enable all installed templates.
+ env:
++ - name: ANSIBLE_COW_WHITELIST
++ deprecated:
++ why: normalizing names to new standard
++ version: "2.15"
++ alternatives: 'ANSIBLE_COW_ACCEPTLIST'
+ - name: ANSIBLE_COW_ACCEPTLIST
+ version_added: '2.11'
+ ini:
++ - key: cow_whitelist
++ section: defaults
++ deprecated:
++ why: normalizing names to new standard
++ version: "2.15"
++ alternatives: 'cowsay_enabled_stencils'
+ - key: cowsay_enabled_stencils
+ section: defaults
+ version_added: '2.11'
+@@ -200,18 +211,12 @@ COLLECTIONS_PATHS:
+ default: '{{ ANSIBLE_HOME ~ "/collections:/usr/share/ansible/collections" }}'
+ type: pathspec
+ env:
+- - name: ANSIBLE_COLLECTIONS_PATHS
+- deprecated:
+- why: does not fit var naming standard, use the singular form ANSIBLE_COLLECTIONS_PATH instead
+- version: "2.19"
++ - name: ANSIBLE_COLLECTIONS_PATHS # TODO: Deprecate this and ini once PATH has been in a few releases.
+ - name: ANSIBLE_COLLECTIONS_PATH
+ version_added: '2.10'
+ ini:
+ - key: collections_paths
+ section: defaults
+- deprecated:
+- why: does not fit var naming standard, use the singular form collections_path instead
+- version: "2.19"
+ - key: collections_path
+ section: defaults
+ version_added: '2.10'
+@@ -226,7 +231,11 @@ COLLECTIONS_ON_ANSIBLE_VERSION_MISMATCH:
+ warning: issue a warning but continue
+ ignore: just continue silently
+ default: warning
++_COLOR_DEFAULTS: &color
++ name: placeholder for color settings' defaults
++ choices: ['black', 'bright gray', 'blue', 'white', 'green', 'bright blue', 'cyan', 'bright green', 'red', 'bright cyan', 'purple', 'bright red', 'yellow', 'bright purple', 'dark gray', 'bright yellow', 'magenta', 'bright magenta', 'normal']
+ COLOR_CHANGED:
++ <<: *color
+ name: Color for 'changed' task status
+ default: yellow
+ description: Defines the color to use on 'Changed' task status
+@@ -234,6 +243,7 @@ COLOR_CHANGED:
+ ini:
+ - {key: changed, section: colors}
+ COLOR_CONSOLE_PROMPT:
++ <<: *color
+ name: "Color for ansible-console's prompt task status"
+ default: white
+ description: Defines the default color to use for ansible-console
+@@ -242,6 +252,7 @@ COLOR_CONSOLE_PROMPT:
+ - {key: console_prompt, section: colors}
+ version_added: "2.7"
+ COLOR_DEBUG:
++ <<: *color
+ name: Color for debug statements
+ default: dark gray
+ description: Defines the color to use when emitting debug messages
+@@ -249,6 +260,7 @@ COLOR_DEBUG:
+ ini:
+ - {key: debug, section: colors}
+ COLOR_DEPRECATE:
++ <<: *color
+ name: Color for deprecation messages
+ default: purple
+ description: Defines the color to use when emitting deprecation messages
+@@ -256,6 +268,7 @@ COLOR_DEPRECATE:
+ ini:
+ - {key: deprecate, section: colors}
+ COLOR_DIFF_ADD:
++ <<: *color
+ name: Color for diff added display
+ default: green
+ description: Defines the color to use when showing added lines in diffs
+@@ -264,6 +277,7 @@ COLOR_DIFF_ADD:
+ - {key: diff_add, section: colors}
+ yaml: {key: display.colors.diff.add}
+ COLOR_DIFF_LINES:
++ <<: *color
+ name: Color for diff lines display
+ default: cyan
+ description: Defines the color to use when showing diffs
+@@ -271,6 +285,7 @@ COLOR_DIFF_LINES:
+ ini:
+ - {key: diff_lines, section: colors}
+ COLOR_DIFF_REMOVE:
++ <<: *color
+ name: Color for diff removed display
+ default: red
+ description: Defines the color to use when showing removed lines in diffs
+@@ -278,6 +293,7 @@ COLOR_DIFF_REMOVE:
+ ini:
+ - {key: diff_remove, section: colors}
+ COLOR_ERROR:
++ <<: *color
+ name: Color for error messages
+ default: red
+ description: Defines the color to use when emitting error messages
+@@ -286,6 +302,7 @@ COLOR_ERROR:
+ - {key: error, section: colors}
+ yaml: {key: colors.error}
+ COLOR_HIGHLIGHT:
++ <<: *color
+ name: Color for highlighting
+ default: white
+ description: Defines the color to use for highlighting
+@@ -293,6 +310,7 @@ COLOR_HIGHLIGHT:
+ ini:
+ - {key: highlight, section: colors}
+ COLOR_OK:
++ <<: *color
+ name: Color for 'ok' task status
+ default: green
+ description: Defines the color to use when showing 'OK' task status
+@@ -300,6 +318,7 @@ COLOR_OK:
+ ini:
+ - {key: ok, section: colors}
+ COLOR_SKIP:
++ <<: *color
+ name: Color for 'skip' task status
+ default: cyan
+ description: Defines the color to use when showing 'Skipped' task status
+@@ -307,6 +326,7 @@ COLOR_SKIP:
+ ini:
+ - {key: skip, section: colors}
+ COLOR_UNREACHABLE:
++ <<: *color
+ name: Color for 'unreachable' host state
+ default: bright red
+ description: Defines the color to use on 'Unreachable' status
+@@ -314,6 +334,7 @@ COLOR_UNREACHABLE:
+ ini:
+ - {key: unreachable, section: colors}
+ COLOR_VERBOSE:
++ <<: *color
+ name: Color for verbose messages
+ default: blue
+ description: Defines the color to use when emitting verbose messages. i.e those that show with '-v's.
+@@ -321,6 +342,7 @@ COLOR_VERBOSE:
+ ini:
+ - {key: verbose, section: colors}
+ COLOR_WARN:
++ <<: *color
+ name: Color for warning messages
+ default: bright purple
+ description: Defines the color to use when emitting warning messages
+@@ -480,7 +502,7 @@ DEFAULT_BECOME_EXE:
+ - {key: become_exe, section: privilege_escalation}
+ DEFAULT_BECOME_FLAGS:
+ name: Set 'become' executable options
+- default: ''
++ default: ~
+ description: Flags to pass to the privilege escalation executable.
+ env: [{name: ANSIBLE_BECOME_FLAGS}]
+ ini:
+@@ -527,9 +549,20 @@ CALLBACKS_ENABLED:
+ - "List of enabled callbacks, not all callbacks need enabling,
+ but many of those shipped with Ansible do as we don't want them activated by default."
+ env:
++ - name: ANSIBLE_CALLBACK_WHITELIST
++ deprecated:
++ why: normalizing names to new standard
++ version: "2.15"
++ alternatives: 'ANSIBLE_CALLBACKS_ENABLED'
+ - name: ANSIBLE_CALLBACKS_ENABLED
+ version_added: '2.11'
+ ini:
++ - key: callback_whitelist
++ section: defaults
++ deprecated:
++ why: normalizing names to new standard
++ version: "2.15"
++ alternatives: 'callbacks_enabled'
+ - key: callbacks_enabled
+ section: defaults
+ version_added: '2.11'
+@@ -934,9 +967,9 @@ DEFAULT_PRIVATE_ROLE_VARS:
+ name: Private role variables
+ default: False
+ description:
+- - By default, imported roles publish their variables to the play and other roles, this setting can avoid that.
+- - This was introduced as a way to reset role variables to default values if a role is used more than once in a playbook.
+- - Included roles only make their variables public at execution, unlike imported roles which happen at playbook compile time.
++ - Makes role variables inaccessible from other roles.
++ - This was introduced as a way to reset role variables to default values if
++ a role is used more than once in a playbook.
+ env: [{name: ANSIBLE_PRIVATE_ROLE_VARS}]
+ ini:
+ - {key: private_role_vars, section: defaults}
+@@ -992,19 +1025,6 @@ DEFAULT_STDOUT_CALLBACK:
+ env: [{name: ANSIBLE_STDOUT_CALLBACK}]
+ ini:
+ - {key: stdout_callback, section: defaults}
+-EDITOR:
+- name: editor application touse
+- default: vi
+- descrioption:
+- - for the cases in which Ansible needs to return a file within an editor, this chooses the application to use
+- ini:
+- - section: defaults
+- key: editor
+- version_added: '2.15'
+- env:
+- - name: ANSIBLE_EDITOR
+- version_added: '2.15'
+- - name: EDITOR
+ ENABLE_TASK_DEBUGGER:
+ name: Whether to enable the task debugger
+ default: False
+@@ -1085,11 +1105,10 @@ DEFAULT_TIMEOUT:
+ - {key: timeout, section: defaults}
+ type: integer
+ DEFAULT_TRANSPORT:
++ # note that ssh_utils refs this and needs to be updated if removed
+ name: Connection plugin
+- default: ssh
+- description:
+- - Can be any connection plugin available to your ansible installation.
+- - There is also a (DEPRECATED) special 'smart' option, that will toggle between 'ssh' and 'paramiko' depending on controller OS and ssh versions.
++ default: smart
++ description: "Default connection plugin to use, the 'smart' option will toggle between 'ssh' and 'paramiko' depending on controller OS and ssh versions"
+ env: [{name: ANSIBLE_TRANSPORT}]
+ ini:
+ - {key: transport, section: defaults}
+@@ -1137,14 +1156,6 @@ DEFAULT_VAULT_IDENTITY:
+ ini:
+ - {key: vault_identity, section: defaults}
+ yaml: {key: defaults.vault_identity}
+-VAULT_ENCRYPT_SALT:
+- name: Vault salt to use for encryption
+- default: ~
+- description: 'The salt to use for the vault encryption. If it is not provided, a random salt will be used.'
+- env: [{name: ANSIBLE_VAULT_ENCRYPT_SALT}]
+- ini:
+- - {key: vault_encrypt_salt, section: defaults}
+- version_added: '2.15'
+ DEFAULT_VAULT_ENCRYPT_IDENTITY:
+ name: Vault id to use for encryption
+ description: 'The vault_id to use for encrypting by default. If multiple vault_ids are provided, this specifies which to use for encryption. The --encrypt-vault-id cli option overrides the configured value.'
+@@ -1326,15 +1337,6 @@ GALAXY_IGNORE_CERTS:
+ ini:
+ - {key: ignore_certs, section: galaxy}
+ type: boolean
+-GALAXY_SERVER_TIMEOUT:
+- name: Default timeout to use for API calls
+- description:
+- - The default timeout for Galaxy API calls. Galaxy servers that don't configure a specific timeout will fall back to this value.
+- env: [{name: ANSIBLE_GALAXY_SERVER_TIMEOUT}]
+- default: 60
+- ini:
+- - {key: server_timeout, section: galaxy}
+- type: int
+ GALAXY_ROLE_SKELETON:
+ name: Galaxy role skeleton directory
+ description: Role skeleton directory to use as a template for the ``init`` action in ``ansible-galaxy``/``ansible-galaxy role``, same as ``--role-skeleton``.
+@@ -1365,15 +1367,6 @@ GALAXY_COLLECTION_SKELETON_IGNORE:
+ ini:
+ - {key: collection_skeleton_ignore, section: galaxy}
+ type: list
+-GALAXY_COLLECTIONS_PATH_WARNING:
+- name: "ansible-galaxy collection install colections path warnings"
+- description: "whether ``ansible-galaxy collection install`` should warn about ``--collections-path`` missing from configured :ref:`collections_paths`"
+- default: true
+- type: bool
+- env: [{name: ANSIBLE_GALAXY_COLLECTIONS_PATH_WARNING}]
+- ini:
+- - {key: collections_path_warning, section: galaxy}
+- version_added: "2.16"
+ # TODO: unused?
+ #GALAXY_SCMS:
+ # name: Galaxy SCMS
+@@ -1414,7 +1407,7 @@ GALAXY_DISPLAY_PROGRESS:
+ default: ~
+ description:
+ - Some steps in ``ansible-galaxy`` display a progress wheel which can cause issues on certain displays or when
+- outputting the stdout to a file.
++ outputing the stdout to a file.
+ - This config option controls whether the display wheel is shown or not.
+ - The default is to show the display wheel if stdout has a tty.
+ env: [{name: ANSIBLE_GALAXY_DISPLAY_PROGRESS}]
+@@ -1556,13 +1549,13 @@ _INTERPRETER_PYTHON_DISTRO_MAP:
+ INTERPRETER_PYTHON_FALLBACK:
+ name: Ordered list of Python interpreters to check for in discovery
+ default:
+- - python3.12
+ - python3.11
+ - python3.10
+ - python3.9
+ - python3.8
+ - python3.7
+ - python3.6
++ - python3.5
+ - /usr/bin/python3
+ - /usr/libexec/platform-python
+ - python2.7
+@@ -1599,7 +1592,7 @@ INVALID_TASK_ATTRIBUTE_FAILED:
+ section: defaults
+ version_added: "2.7"
+ INVENTORY_ANY_UNPARSED_IS_FAILED:
+- name: Controls whether any unparsable inventory source is a fatal error
++ name: Controls whether any unparseable inventory source is a fatal error
+ default: False
+ description: >
+ If 'true', it is a fatal error when any given inventory source
+@@ -1760,38 +1753,14 @@ MODULE_IGNORE_EXTS:
+ ini:
+ - {key: module_ignore_exts, section: defaults}
+ type: list
+-MODULE_STRICT_UTF8_RESPONSE:
+- name: Module strict UTF-8 response
+- description:
+- - Enables whether module responses are evaluated for containing non UTF-8 data
+- - Disabling this may result in unexpected behavior
+- - Only ansible-core should evaluate this configuration
+- env: [{name: ANSIBLE_MODULE_STRICT_UTF8_RESPONSE}]
+- ini:
+- - {key: module_strict_utf8_response, section: defaults}
+- type: bool
+- default: True
+ OLD_PLUGIN_CACHE_CLEARING:
+- description: Previously Ansible would only clear some of the plugin loading caches when loading new roles, this led to some behaviours in which a plugin loaded in previous plays would be unexpectedly 'sticky'. This setting allows to return to that behaviour.
++ description: Previously Ansible would only clear some of the plugin loading caches when loading new roles, this led to some behaviours in which a plugin loaded in prevoius plays would be unexpectedly 'sticky'. This setting allows to return to that behaviour.
+ env: [{name: ANSIBLE_OLD_PLUGIN_CACHE_CLEAR}]
+ ini:
+ - {key: old_plugin_cache_clear, section: defaults}
+ type: boolean
+ default: False
+ version_added: "2.8"
+-PAGER:
+- name: pager application to use
+- default: less
+- descrioption:
+- - for the cases in which Ansible needs to return output in pageable fashion, this chooses the application to use
+- ini:
+- - section: defaults
+- key: pager
+- version_added: '2.15'
+- env:
+- - name: ANSIBLE_PAGER
+- version_added: '2.15'
+- - name: PAGER
+ PARAMIKO_HOST_KEY_AUTO_ADD:
+ # TODO: move to plugin
+ default: False
+@@ -2073,10 +2042,6 @@ STRING_CONVERSION_ACTION:
+ - section: defaults
+ key: string_conversion_action
+ type: string
+- deprecated:
+- why: This option is no longer used in the Ansible Core code base.
+- version: "2.19"
+- alternatives: There is no alternative at the moment. A different mechanism would have to be implemented in the current code base.
+ VALIDATE_ACTION_GROUP_METADATA:
+ version_added: '2.12'
+ description:
+--- ansible-core-2.16.5.orig/lib/ansible/config/manager.py
++++ ansible-core-2.16.5/lib/ansible/config/manager.py
+@@ -11,13 +11,14 @@ import os.path
+ import sys
+ import stat
+ import tempfile
++import traceback
+
+ from collections import namedtuple
+ from collections.abc import Mapping, Sequence
+ from jinja2.nativetypes import NativeEnvironment
+
+ from ansible.errors import AnsibleOptionsError, AnsibleError
+-from ansible.module_utils.common.text.converters import to_text, to_bytes, to_native
++from ansible.module_utils._text import to_text, to_bytes, to_native
+ from ansible.module_utils.common.yaml import yaml_load
+ from ansible.module_utils.six import string_types
+ from ansible.module_utils.parsing.convert_bool import boolean
+@@ -63,7 +64,7 @@ def ensure_type(value, value_type, origi
+ :temppath: Same as 'tmppath'
+ :tmp: Same as 'tmppath'
+ :pathlist: Treat the value as a typical PATH string. (On POSIX, this
+- means comma separated strings.) Split the value and then expand
++ means colon separated strings.) Split the value and then expand
+ each part for environment variables and tildes.
+ :pathspec: Treat the value as a PATH string. Expands any environment variables
+ tildes's in the value.
+@@ -143,17 +144,13 @@ def ensure_type(value, value_type, origi
+
+ elif value_type in ('str', 'string'):
+ if isinstance(value, (string_types, AnsibleVaultEncryptedUnicode, bool, int, float, complex)):
+- value = to_text(value, errors='surrogate_or_strict')
+- if origin == 'ini':
+- value = unquote(value)
++ value = unquote(to_text(value, errors='surrogate_or_strict'))
+ else:
+ errmsg = 'string'
+
+ # defaults to string type
+ elif isinstance(value, (string_types, AnsibleVaultEncryptedUnicode)):
+- value = to_text(value, errors='surrogate_or_strict')
+- if origin == 'ini':
+- value = unquote(value)
++ value = unquote(to_text(value, errors='surrogate_or_strict'))
+
+ if errmsg:
+ raise ValueError('Invalid type provided for "%s": %s' % (errmsg, to_native(value)))
+--- ansible-core-2.16.5.orig/lib/ansible/constants.py
++++ ansible-core-2.16.5/lib/ansible/constants.py
+@@ -10,7 +10,7 @@ import re
+ from string import ascii_letters, digits
+
+ from ansible.config.manager import ConfigManager
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.common.collections import Sequence
+ from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE
+ from ansible.release import __version__
+@@ -64,6 +64,7 @@ _ACTION_DEBUG = add_internal_fqcns(('deb
+ _ACTION_IMPORT_PLAYBOOK = add_internal_fqcns(('import_playbook', ))
+ _ACTION_IMPORT_ROLE = add_internal_fqcns(('import_role', ))
+ _ACTION_IMPORT_TASKS = add_internal_fqcns(('import_tasks', ))
++_ACTION_INCLUDE = add_internal_fqcns(('include', ))
+ _ACTION_INCLUDE_ROLE = add_internal_fqcns(('include_role', ))
+ _ACTION_INCLUDE_TASKS = add_internal_fqcns(('include_tasks', ))
+ _ACTION_INCLUDE_VARS = add_internal_fqcns(('include_vars', ))
+@@ -73,11 +74,12 @@ _ACTION_SET_FACT = add_internal_fqcns(('
+ _ACTION_SETUP = add_internal_fqcns(('setup', ))
+ _ACTION_HAS_CMD = add_internal_fqcns(('command', 'shell', 'script'))
+ _ACTION_ALLOWS_RAW_ARGS = _ACTION_HAS_CMD + add_internal_fqcns(('raw', ))
+-_ACTION_ALL_INCLUDES = _ACTION_INCLUDE_TASKS + _ACTION_INCLUDE_ROLE
+-_ACTION_ALL_INCLUDE_IMPORT_TASKS = _ACTION_INCLUDE_TASKS + _ACTION_IMPORT_TASKS
++_ACTION_ALL_INCLUDES = _ACTION_INCLUDE + _ACTION_INCLUDE_TASKS + _ACTION_INCLUDE_ROLE
++_ACTION_ALL_INCLUDE_IMPORT_TASKS = _ACTION_INCLUDE + _ACTION_INCLUDE_TASKS + _ACTION_IMPORT_TASKS
+ _ACTION_ALL_PROPER_INCLUDE_IMPORT_ROLES = _ACTION_INCLUDE_ROLE + _ACTION_IMPORT_ROLE
+ _ACTION_ALL_PROPER_INCLUDE_IMPORT_TASKS = _ACTION_INCLUDE_TASKS + _ACTION_IMPORT_TASKS
+ _ACTION_ALL_INCLUDE_ROLE_TASKS = _ACTION_INCLUDE_ROLE + _ACTION_INCLUDE_TASKS
++_ACTION_ALL_INCLUDE_TASKS = _ACTION_INCLUDE + _ACTION_INCLUDE_TASKS
+ _ACTION_FACT_GATHERING = _ACTION_SETUP + add_internal_fqcns(('gather_facts', ))
+ _ACTION_WITH_CLEAN_FACTS = _ACTION_SET_FACT + _ACTION_INCLUDE_VARS
+
+--- ansible-core-2.16.5.orig/lib/ansible/errors/__init__.py
++++ ansible-core-2.16.5/lib/ansible/errors/__init__.py
+@@ -34,7 +34,7 @@ from ansible.errors.yaml_strings import
+ YAML_POSITION_DETAILS,
+ YAML_AND_SHORTHAND_ERROR,
+ )
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+
+
+ class AnsibleError(Exception):
+@@ -211,14 +211,6 @@ class AnsibleError(Exception):
+ return error_message
+
+
+-class AnsiblePromptInterrupt(AnsibleError):
+- '''User interrupt'''
+-
+-
+-class AnsiblePromptNoninteractive(AnsibleError):
+- '''Unable to get user input'''
+-
+-
+ class AnsibleAssertionError(AnsibleError, AssertionError):
+ '''Invalid assertion'''
+ pass
+--- ansible-core-2.16.5.orig/lib/ansible/executor/action_write_locks.py
++++ ansible-core-2.16.5/lib/ansible/executor/action_write_locks.py
+@@ -15,7 +15,9 @@
+ # You should have received a copy of the GNU General Public License
+ # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+-from __future__ import annotations
++# Make coding more python3-ish
++from __future__ import (absolute_import, division, print_function)
++__metaclass__ = type
+
+ import multiprocessing.synchronize
+
+@@ -27,7 +29,7 @@ if 'action_write_locks' not in globals()
+ # Do not initialize this more than once because it seems to bash
+ # the existing one. multiprocessing must be reloading the module
+ # when it forks?
+- action_write_locks: dict[str | None, multiprocessing.synchronize.Lock] = dict()
++ action_write_locks = dict() # type: dict[str | None, multiprocessing.synchronize.Lock]
+
+ # Below is a Lock for use when we weren't expecting a named module. It gets used when an action
+ # plugin invokes a module whose name does not match with the action's name. Slightly less
+--- ansible-core-2.16.5.orig/lib/ansible/executor/interpreter_discovery.py
++++ ansible-core-2.16.5/lib/ansible/executor/interpreter_discovery.py
+@@ -10,7 +10,7 @@ import pkgutil
+ import re
+
+ from ansible import constants as C
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.distro import LinuxDistribution
+ from ansible.utils.display import Display
+ from ansible.utils.plugin_docs import get_versioned_doclink
+--- ansible-core-2.16.5.orig/lib/ansible/executor/module_common.py
++++ ansible-core-2.16.5/lib/ansible/executor/module_common.py
+@@ -26,7 +26,6 @@ import datetime
+ import json
+ import os
+ import shlex
+-import time
+ import zipfile
+ import re
+ import pkgutil
+@@ -167,7 +166,7 @@ def _ansiballz_main():
+ else:
+ PY3 = True
+
+- ZIPDATA = %(zipdata)r
++ ZIPDATA = """%(zipdata)s"""
+
+ # Note: temp_path isn't needed once we switch to zipimport
+ def invoke_module(modlib_path, temp_path, json_params):
+@@ -178,13 +177,13 @@ def _ansiballz_main():
+ z = zipfile.ZipFile(modlib_path, mode='a')
+
+ # py3: modlib_path will be text, py2: it's bytes. Need bytes at the end
+- sitecustomize = u'import sys\\nsys.path.insert(0,"%%s")\\n' %% modlib_path
++ sitecustomize = u'import sys\\nsys.path.insert(0,"%%s")\\n' %% modlib_path
+ sitecustomize = sitecustomize.encode('utf-8')
+ # Use a ZipInfo to work around zipfile limitation on hosts with
+ # clocks set to a pre-1980 year (for instance, Raspberry Pi)
+ zinfo = zipfile.ZipInfo()
+ zinfo.filename = 'sitecustomize.py'
+- zinfo.date_time = %(date_time)s
++ zinfo.date_time = ( %(year)i, %(month)i, %(day)i, %(hour)i, %(minute)i, %(second)i)
+ z.writestr(zinfo, sitecustomize)
+ z.close()
+
+@@ -197,7 +196,7 @@ def _ansiballz_main():
+ basic._ANSIBLE_ARGS = json_params
+ %(coverage)s
+ # Run the module! By importing it as '__main__', it thinks it is executing as a script
+- runpy.run_module(mod_name=%(module_fqn)r, init_globals=dict(_module_fqn=%(module_fqn)r, _modlib_path=modlib_path),
++ runpy.run_module(mod_name='%(module_fqn)s', init_globals=dict(_module_fqn='%(module_fqn)s', _modlib_path=modlib_path),
+ run_name='__main__', alter_sys=True)
+
+ # Ansible modules must exit themselves
+@@ -288,7 +287,7 @@ def _ansiballz_main():
+ basic._ANSIBLE_ARGS = json_params
+
+ # Run the module! By importing it as '__main__', it thinks it is executing as a script
+- runpy.run_module(mod_name=%(module_fqn)r, init_globals=None, run_name='__main__', alter_sys=True)
++ runpy.run_module(mod_name='%(module_fqn)s', init_globals=None, run_name='__main__', alter_sys=True)
+
+ # Ansible modules must exit themselves
+ print('{"msg": "New-style module did not handle its own exit", "failed": true}')
+@@ -313,9 +312,9 @@ def _ansiballz_main():
+ # store this in remote_tmpdir (use system tempdir instead)
+ # Only need to use [ansible_module]_payload_ in the temp_path until we move to zipimport
+ # (this helps ansible-test produce coverage stats)
+- temp_path = tempfile.mkdtemp(prefix='ansible_' + %(ansible_module)r + '_payload_')
++ temp_path = tempfile.mkdtemp(prefix='ansible_%(ansible_module)s_payload_')
+
+- zipped_mod = os.path.join(temp_path, 'ansible_' + %(ansible_module)r + '_payload.zip')
++ zipped_mod = os.path.join(temp_path, 'ansible_%(ansible_module)s_payload.zip')
+
+ with open(zipped_mod, 'wb') as modlib:
+ modlib.write(base64.b64decode(ZIPDATA))
+@@ -338,7 +337,7 @@ if __name__ == '__main__':
+ '''
+
+ ANSIBALLZ_COVERAGE_TEMPLATE = '''
+- os.environ['COVERAGE_FILE'] = %(coverage_output)r + '=python-%%s=coverage' %% '.'.join(str(v) for v in sys.version_info[:2])
++ os.environ['COVERAGE_FILE'] = '%(coverage_output)s=python-%%s=coverage' %% '.'.join(str(v) for v in sys.version_info[:2])
+
+ import atexit
+
+@@ -348,7 +347,7 @@ ANSIBALLZ_COVERAGE_TEMPLATE = '''
+ print('{"msg": "Could not import `coverage` module.", "failed": true}')
+ sys.exit(1)
+
+- cov = coverage.Coverage(config_file=%(coverage_config)r)
++ cov = coverage.Coverage(config_file='%(coverage_config)s')
+
+ def atexit_coverage():
+ cov.stop()
+@@ -871,17 +870,7 @@ class CollectionModuleUtilLocator(Module
+ return name_parts[5:] # eg, foo.bar for ansible_collections.ns.coll.plugins.module_utils.foo.bar
+
+
+-def _make_zinfo(filename, date_time, zf=None):
+- zinfo = zipfile.ZipInfo(
+- filename=filename,
+- date_time=date_time
+- )
+- if zf:
+- zinfo.compress_type = zf.compression
+- return zinfo
+-
+-
+-def recursive_finder(name, module_fqn, module_data, zf, date_time=None):
++def recursive_finder(name, module_fqn, module_data, zf):
+ """
+ Using ModuleDepFinder, make sure we have all of the module_utils files that
+ the module and its module_utils files needs. (no longer actually recursive)
+@@ -891,8 +880,6 @@ def recursive_finder(name, module_fqn, m
+ :arg zf: An open :python:class:`zipfile.ZipFile` object that holds the Ansible module payload
+ which we're assembling
+ """
+- if date_time is None:
+- date_time = time.gmtime()[:6]
+
+ # py_module_cache maps python module names to a tuple of the code in the module
+ # and the pathname to the module.
+@@ -989,10 +976,7 @@ def recursive_finder(name, module_fqn, m
+ for py_module_name in py_module_cache:
+ py_module_file_name = py_module_cache[py_module_name][1]
+
+- zf.writestr(
+- _make_zinfo(py_module_file_name, date_time, zf=zf),
+- py_module_cache[py_module_name][0]
+- )
++ zf.writestr(py_module_file_name, py_module_cache[py_module_name][0])
+ mu_file = to_text(py_module_file_name, errors='surrogate_or_strict')
+ display.vvvvv("Including module_utils file %s" % mu_file)
+
+@@ -1036,16 +1020,13 @@ def _get_ansible_module_fqn(module_path)
+ return remote_module_fqn
+
+
+-def _add_module_to_zip(zf, date_time, remote_module_fqn, b_module_data):
++def _add_module_to_zip(zf, remote_module_fqn, b_module_data):
+ """Add a module from ansible or from an ansible collection into the module zip"""
+ module_path_parts = remote_module_fqn.split('.')
+
+ # Write the module
+ module_path = '/'.join(module_path_parts) + '.py'
+- zf.writestr(
+- _make_zinfo(module_path, date_time, zf=zf),
+- b_module_data
+- )
++ zf.writestr(module_path, b_module_data)
+
+ # Write the __init__.py's necessary to get there
+ if module_path_parts[0] == 'ansible':
+@@ -1064,10 +1045,7 @@ def _add_module_to_zip(zf, date_time, re
+ continue
+ # Note: We don't want to include more than one ansible module in a payload at this time
+ # so no need to fill the __init__.py with namespace code
+- zf.writestr(
+- _make_zinfo(package_path, date_time, zf=zf),
+- b''
+- )
++ zf.writestr(package_path, b'')
+
+
+ def _find_module_utils(module_name, b_module_data, module_path, module_args, task_vars, templar, module_compression, async_timeout, become,
+@@ -1132,10 +1110,6 @@ def _find_module_utils(module_name, b_mo
+ remote_module_fqn = 'ansible.modules.%s' % module_name
+
+ if module_substyle == 'python':
+- date_time = time.gmtime()[:6]
+- if date_time[0] < 1980:
+- date_string = datetime.datetime(*date_time, tzinfo=datetime.timezone.utc).strftime('%c')
+- raise AnsibleError(f'Cannot create zipfile due to pre-1980 configured date: {date_string}')
+ params = dict(ANSIBLE_MODULE_ARGS=module_args,)
+ try:
+ python_repred_params = repr(json.dumps(params, cls=AnsibleJSONEncoder, vault_to_text=True))
+@@ -1181,10 +1155,10 @@ def _find_module_utils(module_name, b_mo
+ zf = zipfile.ZipFile(zipoutput, mode='w', compression=compression_method)
+
+ # walk the module imports, looking for module_utils to send- they'll be added to the zipfile
+- recursive_finder(module_name, remote_module_fqn, b_module_data, zf, date_time)
++ recursive_finder(module_name, remote_module_fqn, b_module_data, zf)
+
+ display.debug('ANSIBALLZ: Writing module into payload')
+- _add_module_to_zip(zf, date_time, remote_module_fqn, b_module_data)
++ _add_module_to_zip(zf, remote_module_fqn, b_module_data)
+
+ zf.close()
+ zipdata = base64.b64encode(zipoutput.getvalue())
+@@ -1267,6 +1241,7 @@ def _find_module_utils(module_name, b_mo
+ else:
+ coverage = ''
+
++ now = datetime.datetime.utcnow()
+ output.write(to_bytes(ACTIVE_ANSIBALLZ_TEMPLATE % dict(
+ zipdata=zipdata,
+ ansible_module=module_name,
+@@ -1274,7 +1249,12 @@ def _find_module_utils(module_name, b_mo
+ params=python_repred_params,
+ shebang=shebang,
+ coding=ENCODING_STRING,
+- date_time=date_time,
++ year=now.year,
++ month=now.month,
++ day=now.day,
++ hour=now.hour,
++ minute=now.minute,
++ second=now.second,
+ coverage=coverage,
+ rlimit=rlimit,
+ )))
+@@ -1397,7 +1377,20 @@ def modify_module(module_name, module_pa
+ return (b_module_data, module_style, shebang)
+
+
+-def get_action_args_with_defaults(action, args, defaults, templar, action_groups=None):
++def get_action_args_with_defaults(action, args, defaults, templar, redirected_names=None, action_groups=None):
++ if redirected_names:
++ resolved_action_name = redirected_names[-1]
++ else:
++ resolved_action_name = action
++
++ if redirected_names is not None:
++ msg = (
++ "Finding module_defaults for the action %s. "
++ "The caller passed a list of redirected action names, which is deprecated. "
++ "The task's resolved action should be provided as the first argument instead."
++ )
++ display.deprecated(msg % resolved_action_name, version='2.16')
++
+ # Get the list of groups that contain this action
+ if action_groups is None:
+ msg = (
+@@ -1408,7 +1401,7 @@ def get_action_args_with_defaults(action
+ display.warning(msg=msg)
+ group_names = []
+ else:
+- group_names = action_groups.get(action, [])
++ group_names = action_groups.get(resolved_action_name, [])
+
+ tmp_args = {}
+ module_defaults = {}
+@@ -1427,7 +1420,7 @@ def get_action_args_with_defaults(action
+ tmp_args.update((module_defaults.get('group/%s' % group_name) or {}).copy())
+
+ # handle specific action defaults
+- tmp_args.update(module_defaults.get(action, {}).copy())
++ tmp_args.update(module_defaults.get(resolved_action_name, {}).copy())
+
+ # direct args override all
+ tmp_args.update(args)
+--- ansible-core-2.16.5.orig/lib/ansible/executor/play_iterator.py
++++ ansible-core-2.16.5/lib/ansible/executor/play_iterator.py
+@@ -52,7 +52,7 @@ class FailedStates(IntFlag):
+ TASKS = 2
+ RESCUE = 4
+ ALWAYS = 8
+- HANDLERS = 16 # NOTE not in use anymore
++ HANDLERS = 16
+
+
+ class HostState:
+@@ -60,8 +60,6 @@ class HostState:
+ self._blocks = blocks[:]
+ self.handlers = []
+
+- self.handler_notifications = []
+-
+ self.cur_block = 0
+ self.cur_regular_task = 0
+ self.cur_rescue_task = 0
+@@ -122,7 +120,6 @@ class HostState:
+ def copy(self):
+ new_state = HostState(self._blocks)
+ new_state.handlers = self.handlers[:]
+- new_state.handler_notifications = self.handler_notifications[:]
+ new_state.cur_block = self.cur_block
+ new_state.cur_regular_task = self.cur_regular_task
+ new_state.cur_rescue_task = self.cur_rescue_task
+@@ -241,6 +238,13 @@ class PlayIterator:
+
+ return self._host_states[host.name].copy()
+
++ def cache_block_tasks(self, block):
++ display.deprecated(
++ 'PlayIterator.cache_block_tasks is now noop due to the changes '
++ 'in the way tasks are cached and is deprecated.',
++ version=2.16
++ )
++
+ def get_next_task_for_host(self, host, peek=False):
+
+ display.debug("getting the next task for host %s" % host.name)
+@@ -431,18 +435,22 @@ class PlayIterator:
+ state.update_handlers = False
+ state.cur_handlers_task = 0
+
+- while True:
+- try:
+- task = state.handlers[state.cur_handlers_task]
+- except IndexError:
+- task = None
+- state.run_state = state.pre_flushing_run_state
+- state.update_handlers = True
+- break
+- else:
+- state.cur_handlers_task += 1
+- if task.is_host_notified(host):
++ if state.fail_state & FailedStates.HANDLERS == FailedStates.HANDLERS:
++ state.update_handlers = True
++ state.run_state = IteratingStates.COMPLETE
++ else:
++ while True:
++ try:
++ task = state.handlers[state.cur_handlers_task]
++ except IndexError:
++ task = None
++ state.run_state = state.pre_flushing_run_state
++ state.update_handlers = True
+ break
++ else:
++ state.cur_handlers_task += 1
++ if task.is_host_notified(host):
++ break
+
+ elif state.run_state == IteratingStates.COMPLETE:
+ return (state, None)
+@@ -483,16 +491,20 @@ class PlayIterator:
+ else:
+ state.fail_state |= FailedStates.ALWAYS
+ state.run_state = IteratingStates.COMPLETE
++ elif state.run_state == IteratingStates.HANDLERS:
++ state.fail_state |= FailedStates.HANDLERS
++ state.update_handlers = True
++ if state._blocks[state.cur_block].rescue:
++ state.run_state = IteratingStates.RESCUE
++ elif state._blocks[state.cur_block].always:
++ state.run_state = IteratingStates.ALWAYS
++ else:
++ state.run_state = IteratingStates.COMPLETE
+ return state
+
+ def mark_host_failed(self, host):
+ s = self.get_host_state(host)
+ display.debug("marking host %s failed, current state: %s" % (host, s))
+- if s.run_state == IteratingStates.HANDLERS:
+- # we are failing `meta: flush_handlers`, so just reset the state to whatever
+- # it was before and let `_set_failed_state` figure out the next state
+- s.run_state = s.pre_flushing_run_state
+- s.update_handlers = True
+ s = self._set_failed_state(s)
+ display.debug("^ failed state is now: %s" % s)
+ self.set_state_for_host(host.name, s)
+@@ -508,6 +520,8 @@ class PlayIterator:
+ return True
+ elif state.run_state == IteratingStates.ALWAYS and self._check_failed_state(state.always_child_state):
+ return True
++ elif state.run_state == IteratingStates.HANDLERS and state.fail_state & FailedStates.HANDLERS == FailedStates.HANDLERS:
++ return True
+ elif state.fail_state != FailedStates.NONE:
+ if state.run_state == IteratingStates.RESCUE and state.fail_state & FailedStates.RESCUE == 0:
+ return False
+@@ -567,6 +581,14 @@ class PlayIterator:
+ return self.is_any_block_rescuing(state.always_child_state)
+ return False
+
++ def get_original_task(self, host, task):
++ display.deprecated(
++ 'PlayIterator.get_original_task is now noop due to the changes '
++ 'in the way tasks are cached and is deprecated.',
++ version=2.16
++ )
++ return (None, None)
++
+ def _insert_tasks_into_state(self, state, task_list):
+ # if we've failed at all, or if the task list is empty, just return the current state
+ if (state.fail_state != FailedStates.NONE and state.run_state == IteratingStates.TASKS) or not task_list:
+@@ -628,12 +650,3 @@ class PlayIterator:
+ if not isinstance(fail_state, FailedStates):
+ raise AnsibleAssertionError('Expected fail_state to be a FailedStates but was %s' % (type(fail_state)))
+ self._host_states[hostname].fail_state = fail_state
+-
+- def add_notification(self, hostname: str, notification: str) -> None:
+- # preserve order
+- host_state = self._host_states[hostname]
+- if notification not in host_state.handler_notifications:
+- host_state.handler_notifications.append(notification)
+-
+- def clear_notification(self, hostname: str, notification: str) -> None:
+- self._host_states[hostname].handler_notifications.remove(notification)
+--- ansible-core-2.16.5.orig/lib/ansible/executor/playbook_executor.py
++++ ansible-core-2.16.5/lib/ansible/executor/playbook_executor.py
+@@ -24,7 +24,7 @@ import os
+ from ansible import constants as C
+ from ansible import context
+ from ansible.executor.task_queue_manager import TaskQueueManager, AnsibleEndPlay
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.plugins.loader import become_loader, connection_loader, shell_loader
+ from ansible.playbook import Playbook
+@@ -99,11 +99,11 @@ class PlaybookExecutor:
+ playbook_collection = resource[2]
+ else:
+ playbook_path = playbook
+- # not fqcn, but might still be collection playbook
++ # not fqcn, but might still be colleciotn playbook
+ playbook_collection = _get_collection_name_from_path(playbook)
+
+ if playbook_collection:
+- display.v("running playbook inside collection {0}".format(playbook_collection))
++ display.warning("running playbook inside collection {0}".format(playbook_collection))
+ AnsibleCollectionConfig.default_collection = playbook_collection
+ else:
+ AnsibleCollectionConfig.default_collection = None
+@@ -148,7 +148,7 @@ class PlaybookExecutor:
+ encrypt = var.get("encrypt", None)
+ salt_size = var.get("salt_size", None)
+ salt = var.get("salt", None)
+- unsafe = boolean(var.get("unsafe", False))
++ unsafe = var.get("unsafe", None)
+
+ if vname not in self._variable_manager.extra_vars:
+ if self._tqm:
+@@ -238,7 +238,7 @@ class PlaybookExecutor:
+ else:
+ basedir = '~/'
+
+- (retry_name, ext) = os.path.splitext(os.path.basename(playbook_path))
++ (retry_name, _) = os.path.splitext(os.path.basename(playbook_path))
+ filename = os.path.join(basedir, "%s.retry" % retry_name)
+ if self._generate_retry_inventory(filename, retries):
+ display.display("\tto retry, use: --limit @%s\n" % filename)
+--- ansible-core-2.16.5.orig/lib/ansible/executor/powershell/async_wrapper.ps1
++++ ansible-core-2.16.5/lib/ansible/executor/powershell/async_wrapper.ps1
+@@ -135,11 +135,11 @@ try {
+
+ # populate initial results before we send the async data to avoid result race
+ $result = @{
+- started = 1
+- finished = 0
+- results_file = $results_path
+- ansible_job_id = $local_jid
+- _ansible_suppress_tmpdir_delete = $true
++ started = 1;
++ finished = 0;
++ results_file = $results_path;
++ ansible_job_id = $local_jid;
++ _ansible_suppress_tmpdir_delete = $true;
+ ansible_async_watchdog_pid = $watchdog_pid
+ }
+
+--- ansible-core-2.16.5.orig/lib/ansible/executor/powershell/module_manifest.py
++++ ansible-core-2.16.5/lib/ansible/executor/powershell/module_manifest.py
+@@ -16,7 +16,7 @@ from ansible.module_utils.compat.version
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.compat.importlib import import_module
+ from ansible.plugins.loader import ps_module_utils_loader
+ from ansible.utils.collection_loader import resource_from_fqcr
+--- ansible-core-2.16.5.orig/lib/ansible/executor/powershell/module_wrapper.ps1
++++ ansible-core-2.16.5/lib/ansible/executor/powershell/module_wrapper.ps1
+@@ -207,10 +207,7 @@ if ($null -ne $rc) {
+ # with the trap handler that's now in place, this should only write to the output if
+ # $ErrorActionPreference != "Stop", that's ok because this is sent to the stderr output
+ # for a user to manually debug if something went horribly wrong
+-if (
+- $ps.Streams.Error.Count -and
+- ($ps.HadErrors -or $PSVersionTable.PSVersion.Major -lt 4)
+-) {
++if ($ps.HadErrors -or ($PSVersionTable.PSVersion.Major -lt 4 -and $ps.Streams.Error.Count -gt 0)) {
+ Write-AnsibleLog "WARN - module had errors, outputting error info $ModuleName" "module_wrapper"
+ # if the rc wasn't explicitly set, we return an exit code of 1
+ if ($null -eq $rc) {
+--- ansible-core-2.16.5.orig/lib/ansible/executor/process/worker.py
++++ ansible-core-2.16.5/lib/ansible/executor/process/worker.py
+@@ -24,11 +24,10 @@ import sys
+ import traceback
+
+ from jinja2.exceptions import TemplateNotFound
+-from multiprocessing.queues import Queue
+
+-from ansible.errors import AnsibleConnectionFailure, AnsibleError
++from ansible.errors import AnsibleConnectionFailure
+ from ansible.executor.task_executor import TaskExecutor
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.utils.display import Display
+ from ansible.utils.multiprocessing import context as multiprocessing_context
+
+@@ -36,17 +35,6 @@ __all__ = ['WorkerProcess']
+
+ display = Display()
+
+-current_worker = None
+-
+-
+-class WorkerQueue(Queue):
+- """Queue that raises AnsibleError items on get()."""
+- def get(self, *args, **kwargs):
+- result = super(WorkerQueue, self).get(*args, **kwargs)
+- if isinstance(result, AnsibleError):
+- raise result
+- return result
+-
+
+ class WorkerProcess(multiprocessing_context.Process): # type: ignore[name-defined]
+ '''
+@@ -55,7 +43,7 @@ class WorkerProcess(multiprocessing_cont
+ for reading later.
+ '''
+
+- def __init__(self, final_q, task_vars, host, task, play_context, loader, variable_manager, shared_loader_obj, worker_id):
++ def __init__(self, final_q, task_vars, host, task, play_context, loader, variable_manager, shared_loader_obj):
+
+ super(WorkerProcess, self).__init__()
+ # takes a task queue manager as the sole param:
+@@ -72,9 +60,6 @@ class WorkerProcess(multiprocessing_cont
+ # clear var to ensure we only delete files for this child
+ self._loader._tempfiles = set()
+
+- self.worker_queue = WorkerQueue(ctx=multiprocessing_context)
+- self.worker_id = worker_id
+-
+ def _save_stdin(self):
+ self._new_stdin = None
+ try:
+@@ -170,9 +155,6 @@ class WorkerProcess(multiprocessing_cont
+ # Set the queue on Display so calls to Display.display are proxied over the queue
+ display.set_queue(self._final_q)
+
+- global current_worker
+- current_worker = self
+-
+ try:
+ # execute the task and build a TaskResult from the result
+ display.debug("running TaskExecutor() for %s/%s" % (self._host, self._task))
+@@ -184,8 +166,7 @@ class WorkerProcess(multiprocessing_cont
+ self._new_stdin,
+ self._loader,
+ self._shared_loader_obj,
+- self._final_q,
+- self._variable_manager,
++ self._final_q
+ ).run()
+
+ display.debug("done running TaskExecutor() for %s/%s [%s]" % (self._host, self._task, self._task._uuid))
+@@ -194,27 +175,12 @@ class WorkerProcess(multiprocessing_cont
+
+ # put the result on the result queue
+ display.debug("sending task result for task %s" % self._task._uuid)
+- try:
+- self._final_q.send_task_result(
+- self._host.name,
+- self._task._uuid,
+- executor_result,
+- task_fields=self._task.dump_attrs(),
+- )
+- except Exception as e:
+- display.debug(f'failed to send task result ({e}), sending surrogate result')
+- self._final_q.send_task_result(
+- self._host.name,
+- self._task._uuid,
+- # Overriding the task result, to represent the failure
+- {
+- 'failed': True,
+- 'msg': f'{e}',
+- 'exception': traceback.format_exc(),
+- },
+- # The failure pickling may have been caused by the task attrs, omit for safety
+- {},
+- )
++ self._final_q.send_task_result(
++ self._host.name,
++ self._task._uuid,
++ executor_result,
++ task_fields=self._task.dump_attrs(),
++ )
+ display.debug("done sending task result for task %s" % self._task._uuid)
+
+ except AnsibleConnectionFailure:
+--- ansible-core-2.16.5.orig/lib/ansible/executor/task_executor.py
++++ ansible-core-2.16.5/lib/ansible/executor/task_executor.py
+@@ -20,14 +20,14 @@ from ansible.executor.task_result import
+ from ansible.executor.module_common import get_action_args_with_defaults
+ from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.module_utils.six import binary_type
+-from ansible.module_utils.common.text.converters import to_text, to_native
++from ansible.module_utils._text import to_text, to_native
+ from ansible.module_utils.connection import write_to_file_descriptor
+ from ansible.playbook.conditional import Conditional
+ from ansible.playbook.task import Task
+ from ansible.plugins import get_plugin_class
+ from ansible.plugins.loader import become_loader, cliconf_loader, connection_loader, httpapi_loader, netconf_loader, terminal_loader
+ from ansible.template import Templar
+-from ansible.utils.collection_loader import AnsibleCollectionConfig
++from ansible.utils.collection_loader import AnsibleCollectionConfig, AnsibleCollectionRef
+ from ansible.utils.listify import listify_lookup_plugin_terms
+ from ansible.utils.unsafe_proxy import to_unsafe_text, wrap_var
+ from ansible.vars.clean import namespace_facts, clean_facts
+@@ -82,7 +82,7 @@ class TaskExecutor:
+ class.
+ '''
+
+- def __init__(self, host, task, job_vars, play_context, new_stdin, loader, shared_loader_obj, final_q, variable_manager):
++ def __init__(self, host, task, job_vars, play_context, new_stdin, loader, shared_loader_obj, final_q):
+ self._host = host
+ self._task = task
+ self._job_vars = job_vars
+@@ -92,7 +92,6 @@ class TaskExecutor:
+ self._shared_loader_obj = shared_loader_obj
+ self._connection = None
+ self._final_q = final_q
+- self._variable_manager = variable_manager
+ self._loop_eval_error = None
+
+ self._task.squash()
+@@ -137,12 +136,6 @@ class TaskExecutor:
+ self._task.ignore_errors = item_ignore
+ elif self._task.ignore_errors and not item_ignore:
+ self._task.ignore_errors = item_ignore
+- if 'unreachable' in item and item['unreachable']:
+- item_ignore_unreachable = item.pop('_ansible_ignore_unreachable')
+- if not res.get('unreachable'):
+- self._task.ignore_unreachable = item_ignore_unreachable
+- elif self._task.ignore_unreachable and not item_ignore_unreachable:
+- self._task.ignore_unreachable = item_ignore_unreachable
+
+ # ensure to accumulate these
+ for array in ['warnings', 'deprecations']:
+@@ -222,13 +215,21 @@ class TaskExecutor:
+
+ templar = Templar(loader=self._loader, variables=self._job_vars)
+ items = None
+- if self._task.loop_with:
++ loop_cache = self._job_vars.get('_ansible_loop_cache')
++ if loop_cache is not None:
++ # _ansible_loop_cache may be set in `get_vars` when calculating `delegate_to`
++ # to avoid reprocessing the loop
++ items = loop_cache
++ elif self._task.loop_with:
+ if self._task.loop_with in self._shared_loader_obj.lookup_loader:
++ fail = True
++ if self._task.loop_with == 'first_found':
++ # first_found loops are special. If the item is undefined then we want to fall through to the next value rather than failing.
++ fail = False
+
+- # TODO: hardcoded so it fails for non first_found lookups, but thhis shoudl be generalized for those that don't do their own templating
+- # lookup prop/attribute?
+- fail = bool(self._task.loop_with != 'first_found')
+ loop_terms = listify_lookup_plugin_terms(terms=self._task.loop, templar=templar, fail_on_undefined=fail, convert_bare=False)
++ if not fail:
++ loop_terms = [t for t in loop_terms if not templar.is_template(t)]
+
+ # get lookup
+ mylookup = self._shared_loader_obj.lookup_loader.get(self._task.loop_with, loader=self._loader, templar=templar)
+@@ -280,7 +281,6 @@ class TaskExecutor:
+ u" to something else to avoid variable collisions and unexpected behavior." % (self._task, loop_var))
+
+ ran_once = False
+- task_fields = None
+ no_log = False
+ items_len = len(items)
+ results = []
+@@ -352,7 +352,6 @@ class TaskExecutor:
+
+ res['_ansible_item_result'] = True
+ res['_ansible_ignore_errors'] = task_fields.get('ignore_errors')
+- res['_ansible_ignore_unreachable'] = task_fields.get('ignore_unreachable')
+
+ # gets templated here unlike rest of loop_control fields, depends on loop_var above
+ try:
+@@ -397,25 +396,9 @@ class TaskExecutor:
+ del task_vars[var]
+
+ self._task.no_log = no_log
+- # NOTE: run_once cannot contain loop vars because it's templated earlier also
+- # This is saving the post-validated field from the last loop so the strategy can use the templated value post task execution
+- self._task.run_once = task_fields.get('run_once')
+- self._task.action = task_fields.get('action')
+
+ return results
+
+- def _calculate_delegate_to(self, templar, variables):
+- """This method is responsible for effectively pre-validating Task.delegate_to and will
+- happen before Task.post_validate is executed
+- """
+- delegated_vars, delegated_host_name = self._variable_manager.get_delegated_vars_and_hostname(templar, self._task, variables)
+- # At the point this is executed it is safe to mutate self._task,
+- # since `self._task` is either a copy referred to by `tmp_task` in `_run_loop`
+- # or just a singular non-looped task
+- if delegated_host_name:
+- self._task.delegate_to = delegated_host_name
+- variables.update(delegated_vars)
+-
+ def _execute(self, variables=None):
+ '''
+ The primary workhorse of the executor system, this runs the task
+@@ -428,8 +411,6 @@ class TaskExecutor:
+
+ templar = Templar(loader=self._loader, variables=variables)
+
+- self._calculate_delegate_to(templar, variables)
+-
+ context_validation_error = None
+
+ # a certain subset of variables exist.
+@@ -469,11 +450,9 @@ class TaskExecutor:
+ # the fact that the conditional may specify that the task be skipped due to a
+ # variable not being present which would otherwise cause validation to fail
+ try:
+- conditional_result, false_condition = self._task.evaluate_conditional_with_result(templar, tempvars)
+- if not conditional_result:
++ if not self._task.evaluate_conditional(templar, tempvars):
+ display.debug("when evaluation is False, skipping this task")
+- return dict(changed=False, skipped=True, skip_reason='Conditional result was False',
+- false_condition=false_condition, _ansible_no_log=no_log)
++ return dict(changed=False, skipped=True, skip_reason='Conditional result was False', _ansible_no_log=no_log)
+ except AnsibleError as e:
+ # loop error takes precedence
+ if self._loop_eval_error is not None:
+@@ -507,7 +486,7 @@ class TaskExecutor:
+
+ # if this task is a TaskInclude, we just return now with a success code so the
+ # main thread can expand the task list for the given host
+- if self._task.action in C._ACTION_INCLUDE_TASKS:
++ if self._task.action in C._ACTION_ALL_INCLUDE_TASKS:
+ include_args = self._task.args.copy()
+ include_file = include_args.pop('_raw_params', None)
+ if not include_file:
+@@ -591,14 +570,25 @@ class TaskExecutor:
+ # feed back into pc to ensure plugins not using get_option can get correct value
+ self._connection._play_context = self._play_context.set_task_and_variable_override(task=self._task, variables=vars_copy, templar=templar)
+
+- # TODO: eventually remove this block as this should be a 'consequence' of 'forced_local' modules, right now rely on remote_is_local connection
++ # for persistent connections, initialize socket path and start connection manager
++ if any(((self._connection.supports_persistence and C.USE_PERSISTENT_CONNECTIONS), self._connection.force_persistence)):
++ self._play_context.timeout = self._connection.get_option('persistent_command_timeout')
++ display.vvvv('attempting to start connection', host=self._play_context.remote_addr)
++ display.vvvv('using connection plugin %s' % self._connection.transport, host=self._play_context.remote_addr)
++
++ options = self._connection.get_options()
++ socket_path = start_connection(self._play_context, options, self._task._uuid)
++ display.vvvv('local domain socket path is %s' % socket_path, host=self._play_context.remote_addr)
++ setattr(self._connection, '_socket_path', socket_path)
++
++ # TODO: eventually remove this block as this should be a 'consequence' of 'forced_local' modules
+ # special handling for python interpreter for network_os, default to ansible python unless overridden
+- if 'ansible_python_interpreter' not in cvars and 'ansible_network_os' in cvars and getattr(self._connection, '_remote_is_local', False):
++ if 'ansible_network_os' in cvars and 'ansible_python_interpreter' not in cvars:
+ # this also avoids 'python discovery'
+ cvars['ansible_python_interpreter'] = sys.executable
+
+ # get handler
+- self._handler, module_context = self._get_action_handler_with_module_context(templar=templar)
++ self._handler, module_context = self._get_action_handler_with_module_context(connection=self._connection, templar=templar)
+
+ if module_context is not None:
+ module_defaults_fqcn = module_context.resolved_fqcn
+@@ -616,11 +606,17 @@ class TaskExecutor:
+ if omit_token is not None:
+ self._task.args = remove_omit(self._task.args, omit_token)
+
+- retries = 1 # includes the default actual run + retries set by user/default
+- if self._task.retries is not None:
+- retries += max(0, self._task.retries)
+- elif self._task.until:
+- retries += 3 # the default is not set in FA because we need to differentiate "unset" value
++ # Read some values from the task, so that we can modify them if need be
++ if self._task.until:
++ retries = self._task.retries
++ if retries is None:
++ retries = 3
++ elif retries <= 0:
++ retries = 1
++ else:
++ retries += 1
++ else:
++ retries = 1
+
+ delay = self._task.delay
+ if delay < 0:
+@@ -726,7 +722,7 @@ class TaskExecutor:
+ result['failed'] = False
+
+ # Make attempts and retries available early to allow their use in changed/failed_when
+- if retries > 1:
++ if self._task.until:
+ result['attempts'] = attempt
+
+ # set the changed property if it was missing.
+@@ -758,7 +754,7 @@ class TaskExecutor:
+
+ if retries > 1:
+ cond = Conditional(loader=self._loader)
+- cond.when = self._task.until or [not result['failed']]
++ cond.when = self._task.until
+ if cond.evaluate_conditional(templar, vars_copy):
+ break
+ else:
+@@ -777,7 +773,7 @@ class TaskExecutor:
+ )
+ )
+ time.sleep(delay)
+- self._handler = self._get_action_handler(templar=templar)
++ self._handler = self._get_action_handler(connection=self._connection, templar=templar)
+ else:
+ if retries > 1:
+ # we ran out of attempts, so mark the result as failed
+@@ -1095,13 +1091,13 @@ class TaskExecutor:
+
+ return varnames
+
+- def _get_action_handler(self, templar):
++ def _get_action_handler(self, connection, templar):
+ '''
+ Returns the correct action plugin to handle the requestion task action
+ '''
+- return self._get_action_handler_with_module_context(templar)[0]
++ return self._get_action_handler_with_module_context(connection, templar)[0]
+
+- def _get_action_handler_with_module_context(self, templar):
++ def _get_action_handler_with_module_context(self, connection, templar):
+ '''
+ Returns the correct action plugin to handle the requestion task action and the module context
+ '''
+@@ -1138,29 +1134,10 @@ class TaskExecutor:
+ handler_name = 'ansible.legacy.normal'
+ collections = None # until then, we don't want the task's collection list to be consulted; use the builtin
+
+- # networking/psersistent connections handling
+- if any(((self._connection.supports_persistence and C.USE_PERSISTENT_CONNECTIONS), self._connection.force_persistence)):
+-
+- # check handler in case we dont need to do all the work to setup persistent connection
+- handler_class = self._shared_loader_obj.action_loader.get(handler_name, class_only=True)
+- if getattr(handler_class, '_requires_connection', True):
+- # for persistent connections, initialize socket path and start connection manager
+- self._play_context.timeout = self._connection.get_option('persistent_command_timeout')
+- display.vvvv('attempting to start connection', host=self._play_context.remote_addr)
+- display.vvvv('using connection plugin %s' % self._connection.transport, host=self._play_context.remote_addr)
+-
+- options = self._connection.get_options()
+- socket_path = start_connection(self._play_context, options, self._task._uuid)
+- display.vvvv('local domain socket path is %s' % socket_path, host=self._play_context.remote_addr)
+- setattr(self._connection, '_socket_path', socket_path)
+- else:
+- # TODO: set self._connection to dummy/noop connection, using local for now
+- self._connection = self._get_connection({}, templar, 'local')
+-
+ handler = self._shared_loader_obj.action_loader.get(
+ handler_name,
+ task=self._task,
+- connection=self._connection,
++ connection=connection,
+ play_context=self._play_context,
+ loader=self._loader,
+ templar=templar,
+@@ -1236,7 +1213,8 @@ def start_connection(play_context, optio
+ else:
+ try:
+ result = json.loads(to_text(stderr, errors='surrogate_then_replace'))
+- except json.decoder.JSONDecodeError:
++ except getattr(json.decoder, 'JSONDecodeError', ValueError):
++ # JSONDecodeError only available on Python 3.5+
+ result = {'error': to_text(stderr, errors='surrogate_then_replace')}
+
+ if 'messages' in result:
+--- ansible-core-2.16.5.orig/lib/ansible/executor/task_queue_manager.py
++++ ansible-core-2.16.5/lib/ansible/executor/task_queue_manager.py
+@@ -24,7 +24,6 @@ import sys
+ import tempfile
+ import threading
+ import time
+-import typing as t
+ import multiprocessing.queues
+
+ from ansible import constants as C
+@@ -34,7 +33,7 @@ from ansible.executor.play_iterator impo
+ from ansible.executor.stats import AggregateStats
+ from ansible.executor.task_result import TaskResult
+ from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_text, to_native
++from ansible.module_utils._text import to_text, to_native
+ from ansible.playbook.play_context import PlayContext
+ from ansible.playbook.task import Task
+ from ansible.plugins.loader import callback_loader, strategy_loader, module_loader
+@@ -46,7 +45,6 @@ from ansible.utils.display import Displa
+ from ansible.utils.lock import lock_decorator
+ from ansible.utils.multiprocessing import context as multiprocessing_context
+
+-from dataclasses import dataclass
+
+ __all__ = ['TaskQueueManager']
+
+@@ -61,30 +59,20 @@ class CallbackSend:
+
+
+ class DisplaySend:
+- def __init__(self, method, *args, **kwargs):
+- self.method = method
++ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+
+
+-@dataclass
+-class PromptSend:
+- worker_id: int
+- prompt: str
+- private: bool = True
+- seconds: int = None
+- interrupt_input: t.Iterable[bytes] = None
+- complete_input: t.Iterable[bytes] = None
+-
+-
+-class FinalQueue(multiprocessing.queues.SimpleQueue):
++class FinalQueue(multiprocessing.queues.Queue):
+ def __init__(self, *args, **kwargs):
+ kwargs['ctx'] = multiprocessing_context
+- super().__init__(*args, **kwargs)
++ super(FinalQueue, self).__init__(*args, **kwargs)
+
+ def send_callback(self, method_name, *args, **kwargs):
+ self.put(
+ CallbackSend(method_name, *args, **kwargs),
++ block=False
+ )
+
+ def send_task_result(self, *args, **kwargs):
+@@ -94,16 +82,13 @@ class FinalQueue(multiprocessing.queues.
+ tr = TaskResult(*args, **kwargs)
+ self.put(
+ tr,
++ block=False
+ )
+
+- def send_display(self, method, *args, **kwargs):
+- self.put(
+- DisplaySend(method, *args, **kwargs),
+- )
+-
+- def send_prompt(self, **kwargs):
++ def send_display(self, *args, **kwargs):
+ self.put(
+- PromptSend(**kwargs),
++ DisplaySend(*args, **kwargs),
++ block=False
+ )
+
+
+@@ -232,7 +217,7 @@ class TaskQueueManager:
+ callback_name = cnames[0]
+ else:
+ # fallback to 'old loader name'
+- (callback_name, ext) = os.path.splitext(os.path.basename(callback_plugin._original_path))
++ (callback_name, _) = os.path.splitext(os.path.basename(callback_plugin._original_path))
+
+ display.vvvvv("Attempting to use '%s' callback." % (callback_name))
+ if callback_type == 'stdout':
+--- ansible-core-2.16.5.orig/lib/ansible/galaxy/__init__.py
++++ ansible-core-2.16.5/lib/ansible/galaxy/__init__.py
+@@ -27,7 +27,7 @@ import os
+
+ import ansible.constants as C
+ from ansible import context
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+ from ansible.module_utils.common.yaml import yaml_load
+
+ # default_readme_template
+--- ansible-core-2.16.5.orig/lib/ansible/galaxy/api.py
++++ ansible-core-2.16.5/lib/ansible/galaxy/api.py
+@@ -11,6 +11,7 @@ import functools
+ import hashlib
+ import json
+ import os
++import socket
+ import stat
+ import tarfile
+ import time
+@@ -27,7 +28,7 @@ from ansible.galaxy.user_agent import us
+ from ansible.module_utils.api import retry_with_delays_and_condition
+ from ansible.module_utils.api import generate_jittered_backoff
+ from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.urls import open_url, prepare_multipart
+ from ansible.utils.display import Display
+ from ansible.utils.hashing import secure_hash_s
+@@ -65,7 +66,7 @@ def should_retry_error(exception):
+
+ # Handle common URL related errors such as TimeoutError, and BadStatusLine
+ # Note: socket.timeout is only required for Py3.9
+- if isinstance(orig_exc, (TimeoutError, BadStatusLine, IncompleteRead)):
++ if isinstance(orig_exc, (TimeoutError, BadStatusLine, IncompleteRead, socket.timeout)):
+ return True
+
+ return False
+@@ -359,8 +360,7 @@ class GalaxyAPI:
+ valid = False
+ if cache_key in server_cache:
+ expires = datetime.datetime.strptime(server_cache[cache_key]['expires'], iso_datetime_format)
+- expires = expires.replace(tzinfo=datetime.timezone.utc)
+- valid = datetime.datetime.now(datetime.timezone.utc) < expires
++ valid = datetime.datetime.utcnow() < expires
+
+ is_paginated_url = 'page' in query or 'offset' in query
+ if valid and not is_paginated_url:
+@@ -385,7 +385,7 @@ class GalaxyAPI:
+
+ elif not is_paginated_url:
+ # The cache entry had expired or does not exist, start a new blank entry to be filled later.
+- expires = datetime.datetime.now(datetime.timezone.utc)
++ expires = datetime.datetime.utcnow()
+ expires += datetime.timedelta(days=1)
+ server_cache[cache_key] = {
+ 'expires': expires.strftime(iso_datetime_format),
+@@ -483,6 +483,8 @@ class GalaxyAPI:
+ }
+ if role_name:
+ args['alternate_role_name'] = role_name
++ elif github_repo.startswith('ansible-role'):
++ args['alternate_role_name'] = github_repo[len('ansible-role') + 1:]
+ data = self._call_galaxy(url, args=urlencode(args), method="POST")
+ if data.get('results', None):
+ return data['results']
+@@ -921,7 +923,10 @@ class GalaxyAPI:
+ data = self._call_galaxy(n_collection_url, error_context_msg=error_context_msg, cache=True)
+ self._set_cache()
+
+- signatures = [signature_info["signature"] for signature_info in data.get("signatures") or []]
+- if not signatures:
++ try:
++ signatures = data["signatures"]
++ except KeyError:
+ display.vvvv(f"Server {self.api_server} has not signed {namespace}.{name}:{version}")
+- return signatures
++ return []
++ else:
++ return [signature_info["signature"] for signature_info in signatures]
+--- ansible-core-2.16.5.orig/lib/ansible/galaxy/collection/__init__.py
++++ ansible-core-2.16.5/lib/ansible/galaxy/collection/__init__.py
+@@ -11,7 +11,6 @@ import fnmatch
+ import functools
+ import json
+ import os
+-import pathlib
+ import queue
+ import re
+ import shutil
+@@ -84,7 +83,6 @@ if t.TYPE_CHECKING:
+ FilesManifestType = t.Dict[t.Literal['files', 'format'], t.Union[t.List[FileManifestEntryType], int]]
+
+ import ansible.constants as C
+-from ansible.compat.importlib_resources import files
+ from ansible.errors import AnsibleError
+ from ansible.galaxy.api import GalaxyAPI
+ from ansible.galaxy.collection.concrete_artifact_manager import (
+@@ -124,7 +122,8 @@ from ansible.galaxy.dependency_resolutio
+ )
+ from ansible.galaxy.dependency_resolution.versioning import meets_requirements
+ from ansible.plugins.loader import get_all_plugin_loaders
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils.six import raise_from
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.common.collections import is_sequence
+ from ansible.module_utils.common.yaml import yaml_dump
+ from ansible.utils.collection_loader import AnsibleCollectionRef
+@@ -283,8 +282,11 @@ def verify_local_collection(local_collec
+ manifest_hash = get_hash_from_validation_source(MANIFEST_FILENAME)
+ else:
+ # fetch remote
+- # NOTE: AnsibleError is raised on URLError
+- b_temp_tar_path = artifacts_manager.get_artifact_path_from_unknown(remote_collection)
++ b_temp_tar_path = ( # NOTE: AnsibleError is raised on URLError
++ artifacts_manager.get_artifact_path
++ if remote_collection.is_concrete_artifact
++ else artifacts_manager.get_galaxy_artifact_path
++ )(remote_collection)
+
+ display.vvv(
+ u"Remote collection cached as '{path!s}'".format(path=to_text(b_temp_tar_path))
+@@ -468,7 +470,7 @@ def build_collection(u_collection_path,
+ try:
+ collection_meta = _get_meta_from_src_dir(b_collection_path)
+ except LookupError as lookup_err:
+- raise AnsibleError(to_native(lookup_err)) from lookup_err
++ raise_from(AnsibleError(to_native(lookup_err)), lookup_err)
+
+ collection_manifest = _build_manifest(**collection_meta)
+ file_manifest = _build_files_manifest(
+@@ -477,7 +479,6 @@ def build_collection(u_collection_path,
+ collection_meta['name'], # type: ignore[arg-type]
+ collection_meta['build_ignore'], # type: ignore[arg-type]
+ collection_meta['manifest'], # type: ignore[arg-type]
+- collection_meta['license_file'], # type: ignore[arg-type]
+ )
+
+ artifact_tarball_file_name = '{ns!s}-{name!s}-{ver!s}.tar.gz'.format(
+@@ -544,7 +545,7 @@ def download_collections(
+ for fqcn, concrete_coll_pin in dep_map.copy().items(): # FIXME: move into the provider
+ if concrete_coll_pin.is_virtual:
+ display.display(
+- 'Virtual collection {coll!s} is not downloadable'.
++ '{coll!s} is not downloadable'.
+ format(coll=to_text(concrete_coll_pin)),
+ )
+ continue
+@@ -554,7 +555,11 @@ def download_collections(
+ format(coll=to_text(concrete_coll_pin), path=to_text(b_output_path)),
+ )
+
+- b_src_path = artifacts_manager.get_artifact_path_from_unknown(concrete_coll_pin)
++ b_src_path = (
++ artifacts_manager.get_artifact_path
++ if concrete_coll_pin.is_concrete_artifact
++ else artifacts_manager.get_galaxy_artifact_path
++ )(concrete_coll_pin)
+
+ b_dest_path = os.path.join(
+ b_output_path,
+@@ -654,7 +659,6 @@ def install_collections(
+ artifacts_manager, # type: ConcreteArtifactsManager
+ disable_gpg_verify, # type: bool
+ offline, # type: bool
+- read_requirement_paths, # type: set[str]
+ ): # type: (...) -> None
+ """Install Ansible collections to the path specified.
+
+@@ -669,14 +673,13 @@ def install_collections(
+ """
+ existing_collections = {
+ Requirement(coll.fqcn, coll.ver, coll.src, coll.type, None)
+- for path in {output_path} | read_requirement_paths
+- for coll in find_existing_collections(path, artifacts_manager)
++ for coll in find_existing_collections(output_path, artifacts_manager)
+ }
+
+ unsatisfied_requirements = set(
+ chain.from_iterable(
+ (
+- Requirement.from_dir_path(to_bytes(sub_coll), artifacts_manager)
++ Requirement.from_dir_path(sub_coll, artifacts_manager)
+ for sub_coll in (
+ artifacts_manager.
+ get_direct_collection_dependencies(install_req).
+@@ -741,7 +744,7 @@ def install_collections(
+ for fqcn, concrete_coll_pin in dependency_map.items():
+ if concrete_coll_pin.is_virtual:
+ display.vvvv(
+- "'{coll!s}' is virtual, skipping.".
++ "Encountered {coll!s}, skipping.".
+ format(coll=to_text(concrete_coll_pin)),
+ )
+ continue
+@@ -1062,9 +1065,8 @@ def _make_entry(name, ftype, chksum_type
+ }
+
+
+-def _build_files_manifest(b_collection_path, namespace, name, ignore_patterns,
+- manifest_control, license_file):
+- # type: (bytes, str, str, list[str], dict[str, t.Any], t.Optional[str]) -> FilesManifestType
++def _build_files_manifest(b_collection_path, namespace, name, ignore_patterns, manifest_control):
++ # type: (bytes, str, str, list[str], dict[str, t.Any]) -> FilesManifestType
+ if ignore_patterns and manifest_control is not Sentinel:
+ raise AnsibleError('"build_ignore" and "manifest" are mutually exclusive')
+
+@@ -1074,15 +1076,14 @@ def _build_files_manifest(b_collection_p
+ namespace,
+ name,
+ manifest_control,
+- license_file,
+ )
+
+ return _build_files_manifest_walk(b_collection_path, namespace, name, ignore_patterns)
+
+
+-def _build_files_manifest_distlib(b_collection_path, namespace, name, manifest_control,
+- license_file):
+- # type: (bytes, str, str, dict[str, t.Any], t.Optional[str]) -> FilesManifestType
++def _build_files_manifest_distlib(b_collection_path, namespace, name, manifest_control):
++ # type: (bytes, str, str, dict[str, t.Any]) -> FilesManifestType
++
+ if not HAS_DISTLIB:
+ raise AnsibleError('Use of "manifest" requires the python "distlib" library')
+
+@@ -1115,20 +1116,15 @@ def _build_files_manifest_distlib(b_coll
+ else:
+ directives.extend([
+ 'include meta/*.yml',
+- 'include *.txt *.md *.rst *.license COPYING LICENSE',
+- 'recursive-include .reuse **',
+- 'recursive-include LICENSES **',
++ 'include *.txt *.md *.rst COPYING LICENSE',
+ 'recursive-include tests **',
+- 'recursive-include docs **.rst **.yml **.yaml **.json **.j2 **.txt **.license',
+- 'recursive-include roles **.yml **.yaml **.json **.j2 **.license',
+- 'recursive-include playbooks **.yml **.yaml **.json **.license',
+- 'recursive-include changelogs **.yml **.yaml **.license',
+- 'recursive-include plugins */**.py */**.license',
++ 'recursive-include docs **.rst **.yml **.yaml **.json **.j2 **.txt',
++ 'recursive-include roles **.yml **.yaml **.json **.j2',
++ 'recursive-include playbooks **.yml **.yaml **.json',
++ 'recursive-include changelogs **.yml **.yaml',
++ 'recursive-include plugins */**.py',
+ ])
+
+- if license_file:
+- directives.append(f'include {license_file}')
+-
+ plugins = set(l.package.split('.')[-1] for d, l in get_all_plugin_loaders())
+ for plugin in sorted(plugins):
+ if plugin in ('modules', 'module_utils'):
+@@ -1139,8 +1135,8 @@ def _build_files_manifest_distlib(b_coll
+ )
+
+ directives.extend([
+- 'recursive-include plugins/modules **.ps1 **.yml **.yaml **.license',
+- 'recursive-include plugins/module_utils **.ps1 **.psm1 **.cs **.license',
++ 'recursive-include plugins/modules **.ps1 **.yml **.yaml',
++ 'recursive-include plugins/module_utils **.ps1 **.psm1 **.cs',
+ ])
+
+ directives.extend(control.directives)
+@@ -1148,7 +1144,7 @@ def _build_files_manifest_distlib(b_coll
+ directives.extend([
+ f'exclude galaxy.yml galaxy.yaml MANIFEST.json FILES.json {namespace}-{name}-*.tar.gz',
+ 'recursive-exclude tests/output **',
+- 'global-exclude /.* /__pycache__ *.pyc *.pyo *.bak *~ *.swp',
++ 'global-exclude /.* /__pycache__',
+ ])
+
+ display.vvv('Manifest Directives:')
+@@ -1325,8 +1321,6 @@ def _build_collection_tar(
+
+ if os.path.islink(b_src_path):
+ b_link_target = os.path.realpath(b_src_path)
+- if not os.path.exists(b_link_target):
+- raise AnsibleError(f"Failed to find the target path '{to_native(b_link_target)}' for the symlink '{to_native(b_src_path)}'.")
+ if _is_child_path(b_link_target, b_collection_path):
+ b_rel_path = os.path.relpath(b_link_target, start=os.path.dirname(b_src_path))
+
+@@ -1381,101 +1375,51 @@ def _build_collection_dir(b_collection_p
+ src_file = os.path.join(b_collection_path, to_bytes(file_info['name'], errors='surrogate_or_strict'))
+ dest_file = os.path.join(b_collection_output, to_bytes(file_info['name'], errors='surrogate_or_strict'))
+
+- existing_is_exec = os.stat(src_file, follow_symlinks=False).st_mode & stat.S_IXUSR
++ existing_is_exec = os.stat(src_file).st_mode & stat.S_IXUSR
+ mode = 0o0755 if existing_is_exec else 0o0644
+
+- # ensure symlinks to dirs are not translated to empty dirs
+- if os.path.isdir(src_file) and not os.path.islink(src_file):
++ if os.path.isdir(src_file):
+ mode = 0o0755
+ base_directories.append(src_file)
+ os.mkdir(dest_file, mode)
+ else:
+- # do not follow symlinks to ensure the original link is used
+- shutil.copyfile(src_file, dest_file, follow_symlinks=False)
+-
+- # avoid setting specific permission on symlinks since it does not
+- # support avoid following symlinks and will thrown an exception if the
+- # symlink target does not exist
+- if not os.path.islink(dest_file):
+- os.chmod(dest_file, mode)
++ shutil.copyfile(src_file, dest_file)
+
++ os.chmod(dest_file, mode)
+ collection_output = to_text(b_collection_output)
+ return collection_output
+
+
+-def _normalize_collection_path(path):
+- str_path = path.as_posix() if isinstance(path, pathlib.Path) else path
+- return pathlib.Path(
+- # This is annoying, but GalaxyCLI._resolve_path did it
+- os.path.expandvars(str_path)
+- ).expanduser().absolute()
+-
+-
+-def find_existing_collections(path_filter, artifacts_manager, namespace_filter=None, collection_filter=None, dedupe=True):
++def find_existing_collections(path, artifacts_manager):
+ """Locate all collections under a given path.
+
+ :param path: Collection dirs layout search path.
+ :param artifacts_manager: Artifacts manager.
+ """
+- if files is None:
+- raise AnsibleError('importlib_resources is not installed and is required')
+-
+- if path_filter and not is_sequence(path_filter):
+- path_filter = [path_filter]
+- if namespace_filter and not is_sequence(namespace_filter):
+- namespace_filter = [namespace_filter]
+- if collection_filter and not is_sequence(collection_filter):
+- collection_filter = [collection_filter]
+-
+- paths = set()
+- for path in files('ansible_collections').glob('*/*/'):
+- path = _normalize_collection_path(path)
+- if not path.is_dir():
+- continue
+- if path_filter:
+- for pf in path_filter:
+- try:
+- path.relative_to(_normalize_collection_path(pf))
+- except ValueError:
+- continue
+- break
+- else:
+- continue
+- paths.add(path)
++ b_path = to_bytes(path, errors='surrogate_or_strict')
+
+- seen = set()
+- for path in paths:
+- namespace = path.parent.name
+- name = path.name
+- if namespace_filter and namespace not in namespace_filter:
+- continue
+- if collection_filter and name not in collection_filter:
++ # FIXME: consider using `glob.glob()` to simplify looping
++ for b_namespace in os.listdir(b_path):
++ b_namespace_path = os.path.join(b_path, b_namespace)
++ if os.path.isfile(b_namespace_path):
+ continue
+
+- if dedupe:
+- try:
+- collection_path = files(f'ansible_collections.{namespace}.{name}')
+- except ImportError:
++ # FIXME: consider feeding b_namespace_path to Candidate.from_dir_path to get subdirs automatically
++ for b_collection in os.listdir(b_namespace_path):
++ b_collection_path = os.path.join(b_namespace_path, b_collection)
++ if not os.path.isdir(b_collection_path):
+ continue
+- if collection_path in seen:
+- continue
+- seen.add(collection_path)
+- else:
+- collection_path = path
+
+- b_collection_path = to_bytes(collection_path.as_posix())
+-
+- try:
+- req = Candidate.from_dir_path_as_unknown(b_collection_path, artifacts_manager)
+- except ValueError as val_err:
+- display.warning(f'{val_err}')
+- continue
+-
+- display.vvv(
+- u"Found installed collection {coll!s} at '{path!s}'".
+- format(coll=to_text(req), path=to_text(req.src))
+- )
+- yield req
++ try:
++ req = Candidate.from_dir_path_as_unknown(b_collection_path, artifacts_manager)
++ except ValueError as val_err:
++ raise_from(AnsibleError(val_err), val_err)
++
++ display.vvv(
++ u"Found installed collection {coll!s} at '{path!s}'".
++ format(coll=to_text(req), path=to_text(req.src))
++ )
++ yield req
+
+
+ def install(collection, path, artifacts_manager): # FIXME: mv to dataclasses?
+@@ -1486,7 +1430,10 @@ def install(collection, path, artifacts_
+ :param path: Collection dirs layout path.
+ :param artifacts_manager: Artifacts manager.
+ """
+- b_artifact_path = artifacts_manager.get_artifact_path_from_unknown(collection)
++ b_artifact_path = (
++ artifacts_manager.get_artifact_path if collection.is_concrete_artifact
++ else artifacts_manager.get_galaxy_artifact_path
++ )(collection)
+
+ collection_path = os.path.join(path, collection.namespace, collection.name)
+ b_collection_path = to_bytes(collection_path, errors='surrogate_or_strict')
+@@ -1640,7 +1587,6 @@ def install_src(collection, b_collection
+ collection_meta['namespace'], collection_meta['name'],
+ collection_meta['build_ignore'],
+ collection_meta['manifest'],
+- collection_meta['license_file'],
+ )
+
+ collection_output_path = _build_collection_dir(
+@@ -1817,15 +1763,10 @@ def _resolve_depenency_map(
+ elif not req.specifier.contains(RESOLVELIB_VERSION.vstring):
+ raise AnsibleError(f"ansible-galaxy requires {req.name}{req.specifier}")
+
+- pre_release_hint = '' if allow_pre_release else (
+- 'Hint: Pre-releases hosted on Galaxy or Automation Hub are not '
+- 'installed by default unless a specific version is requested. '
+- 'To enable pre-releases globally, use --pre.'
+- )
+-
+ collection_dep_resolver = build_collection_dependency_resolver(
+ galaxy_apis=galaxy_apis,
+ concrete_artifacts_manager=concrete_artifacts_manager,
++ user_requirements=requested_requirements,
+ preferred_candidates=preferred_candidates,
+ with_deps=not no_deps,
+ with_pre_releases=allow_pre_release,
+@@ -1857,12 +1798,13 @@ def _resolve_depenency_map(
+ ),
+ conflict_causes,
+ ))
+- error_msg_lines.append(pre_release_hint)
+- raise AnsibleError('\n'.join(error_msg_lines)) from dep_exc
++ raise raise_from( # NOTE: Leading "raise" is a hack for mypy bug #9717
++ AnsibleError('\n'.join(error_msg_lines)),
++ dep_exc,
++ )
+ except CollectionDependencyInconsistentCandidate as dep_exc:
+ parents = [
+- "%s.%s:%s" % (p.namespace, p.name, p.ver)
+- for p in dep_exc.criterion.iter_parent()
++ str(p) for p in dep_exc.criterion.iter_parent()
+ if p is not None
+ ]
+
+@@ -1884,8 +1826,10 @@ def _resolve_depenency_map(
+ error_msg_lines.append(
+ '* {req.fqcn!s}:{req.ver!s}'.format(req=req)
+ )
+- error_msg_lines.append(pre_release_hint)
+
+- raise AnsibleError('\n'.join(error_msg_lines)) from dep_exc
++ raise raise_from( # NOTE: Leading "raise" is a hack for mypy bug #9717
++ AnsibleError('\n'.join(error_msg_lines)),
++ dep_exc,
++ )
+ except ValueError as exc:
+ raise AnsibleError(to_native(exc)) from exc
+--- ansible-core-2.16.5.orig/lib/ansible/galaxy/collection/concrete_artifact_manager.py
++++ ansible-core-2.16.5/lib/ansible/galaxy/collection/concrete_artifact_manager.py
+@@ -21,7 +21,7 @@ from tempfile import mkdtemp
+
+ if t.TYPE_CHECKING:
+ from ansible.galaxy.dependency_resolution.dataclasses import (
+- Candidate, Collection, Requirement,
++ Candidate, Requirement,
+ )
+ from ansible.galaxy.token import GalaxyToken
+
+@@ -30,11 +30,13 @@ from ansible.galaxy import get_collectio
+ from ansible.galaxy.api import should_retry_error
+ from ansible.galaxy.dependency_resolution.dataclasses import _GALAXY_YAML
+ from ansible.galaxy.user_agent import user_agent
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.api import retry_with_delays_and_condition
+ from ansible.module_utils.api import generate_jittered_backoff
+ from ansible.module_utils.common.process import get_bin_path
++from ansible.module_utils.common._collections_compat import MutableMapping
+ from ansible.module_utils.common.yaml import yaml_load
++from ansible.module_utils.six import raise_from
+ from ansible.module_utils.urls import open_url
+ from ansible.utils.display import Display
+ from ansible.utils.sentinel import Sentinel
+@@ -139,10 +141,13 @@ class ConcreteArtifactsManager:
+ try:
+ url, sha256_hash, token = self._galaxy_collection_cache[collection]
+ except KeyError as key_err:
+- raise RuntimeError(
+- 'There is no known source for {coll!s}'.
+- format(coll=collection),
+- ) from key_err
++ raise_from(
++ RuntimeError(
++ 'The is no known source for {coll!s}'.
++ format(coll=collection),
++ ),
++ key_err,
++ )
+
+ display.vvvv(
+ "Fetching a collection tarball for '{collection!s}' from "
+@@ -190,7 +195,7 @@ class ConcreteArtifactsManager:
+ return b_artifact_path
+
+ def get_artifact_path(self, collection):
+- # type: (Collection) -> bytes
++ # type: (t.Union[Candidate, Requirement]) -> bytes
+ """Given a concrete collection pointer, return a cached path.
+
+ If it's not yet on disk, this method downloads the artifact first.
+@@ -225,14 +230,17 @@ class ConcreteArtifactsManager:
+ timeout=self.timeout
+ )
+ except Exception as err:
+- raise AnsibleError(
+- 'Failed to download collection tar '
+- "from '{coll_src!s}': {download_err!s}".
+- format(
+- coll_src=to_native(collection.src),
+- download_err=to_native(err),
++ raise_from(
++ AnsibleError(
++ 'Failed to download collection tar '
++ "from '{coll_src!s}': {download_err!s}".
++ format(
++ coll_src=to_native(collection.src),
++ download_err=to_native(err),
++ ),
+ ),
+- ) from err
++ err,
++ )
+ elif collection.is_scm:
+ b_artifact_path = _extract_collection_from_git(
+ collection.src,
+@@ -251,22 +259,16 @@ class ConcreteArtifactsManager:
+ self._artifact_cache[collection.src] = b_artifact_path
+ return b_artifact_path
+
+- def get_artifact_path_from_unknown(self, collection):
+- # type: (Candidate) -> bytes
+- if collection.is_concrete_artifact:
+- return self.get_artifact_path(collection)
+- return self.get_galaxy_artifact_path(collection)
+-
+ def _get_direct_collection_namespace(self, collection):
+ # type: (Candidate) -> t.Optional[str]
+ return self.get_direct_collection_meta(collection)['namespace'] # type: ignore[return-value]
+
+ def _get_direct_collection_name(self, collection):
+- # type: (Collection) -> t.Optional[str]
++ # type: (Candidate) -> t.Optional[str]
+ return self.get_direct_collection_meta(collection)['name'] # type: ignore[return-value]
+
+ def get_direct_collection_fqcn(self, collection):
+- # type: (Collection) -> t.Optional[str]
++ # type: (Candidate) -> t.Optional[str]
+ """Extract FQCN from the given on-disk collection artifact.
+
+ If the collection is virtual, ``None`` is returned instead
+@@ -282,7 +284,7 @@ class ConcreteArtifactsManager:
+ ))
+
+ def get_direct_collection_version(self, collection):
+- # type: (Collection) -> str
++ # type: (t.Union[Candidate, Requirement]) -> str
+ """Extract version from the given on-disk collection artifact."""
+ return self.get_direct_collection_meta(collection)['version'] # type: ignore[return-value]
+
+@@ -295,7 +297,7 @@ class ConcreteArtifactsManager:
+ return collection_dependencies # type: ignore[return-value]
+
+ def get_direct_collection_meta(self, collection):
+- # type: (Collection) -> dict[str, t.Union[str, dict[str, str], list[str], None, t.Type[Sentinel]]]
++ # type: (t.Union[Candidate, Requirement]) -> dict[str, t.Union[str, dict[str, str], list[str], None, t.Type[Sentinel]]]
+ """Extract meta from the given on-disk collection artifact."""
+ try: # FIXME: use unique collection identifier as a cache key?
+ return self._artifact_meta_cache[collection.src]
+@@ -309,10 +311,13 @@ class ConcreteArtifactsManager:
+ try:
+ collection_meta = _get_meta_from_dir(b_artifact_path, self.require_build_metadata)
+ except LookupError as lookup_err:
+- raise AnsibleError(
+- 'Failed to find the collection dir deps: {err!s}'.
+- format(err=to_native(lookup_err)),
+- ) from lookup_err
++ raise_from(
++ AnsibleError(
++ 'Failed to find the collection dir deps: {err!s}'.
++ format(err=to_native(lookup_err)),
++ ),
++ lookup_err,
++ )
+ elif collection.is_scm:
+ collection_meta = {
+ 'name': None,
+@@ -434,23 +439,29 @@ def _extract_collection_from_git(repo_ur
+ try:
+ subprocess.check_call(git_clone_cmd)
+ except subprocess.CalledProcessError as proc_err:
+- raise AnsibleError( # should probably be LookupError
+- 'Failed to clone a Git repository from `{repo_url!s}`.'.
+- format(repo_url=to_native(git_url)),
+- ) from proc_err
++ raise_from(
++ AnsibleError( # should probably be LookupError
++ 'Failed to clone a Git repository from `{repo_url!s}`.'.
++ format(repo_url=to_native(git_url)),
++ ),
++ proc_err,
++ )
+
+ git_switch_cmd = git_executable, 'checkout', to_text(version)
+ try:
+ subprocess.check_call(git_switch_cmd, cwd=b_checkout_path)
+ except subprocess.CalledProcessError as proc_err:
+- raise AnsibleError( # should probably be LookupError
+- 'Failed to switch a cloned Git repo `{repo_url!s}` '
+- 'to the requested revision `{commitish!s}`.'.
+- format(
+- commitish=to_native(version),
+- repo_url=to_native(git_url),
++ raise_from(
++ AnsibleError( # should probably be LookupError
++ 'Failed to switch a cloned Git repo `{repo_url!s}` '
++ 'to the requested revision `{commitish!s}`.'.
++ format(
++ commitish=to_native(version),
++ repo_url=to_native(git_url),
++ ),
+ ),
+- ) from proc_err
++ proc_err,
++ )
+
+ return (
+ os.path.join(b_checkout_path, to_bytes(fragment))
+@@ -626,14 +637,17 @@ def _get_meta_from_src_dir(
+ try:
+ manifest = yaml_load(manifest_file_obj)
+ except yaml.error.YAMLError as yaml_err:
+- raise AnsibleError(
+- "Failed to parse the galaxy.yml at '{path!s}' with "
+- 'the following error:\n{err_txt!s}'.
+- format(
+- path=to_native(galaxy_yml),
+- err_txt=to_native(yaml_err),
++ raise_from(
++ AnsibleError(
++ "Failed to parse the galaxy.yml at '{path!s}' with "
++ 'the following error:\n{err_txt!s}'.
++ format(
++ path=to_native(galaxy_yml),
++ err_txt=to_native(yaml_err),
++ ),
+ ),
+- ) from yaml_err
++ yaml_err,
++ )
+
+ if not isinstance(manifest, dict):
+ if require_build_metadata:
+@@ -702,11 +716,6 @@ def _get_meta_from_installed_dir(
+ def _get_meta_from_tar(
+ b_path, # type: bytes
+ ): # type: (...) -> dict[str, t.Union[str, list[str], dict[str, str], None, t.Type[Sentinel]]]
+- if not os.path.exists(b_path):
+- raise AnsibleError(
+- f"Unable to find collection artifact file at '{to_native(b_path)}'."
+- )
+-
+ if not tarfile.is_tarfile(b_path):
+ raise AnsibleError(
+ "Collection artifact at '{path!s}' is not a valid tar file.".
+--- ansible-core-2.16.5.orig/lib/ansible/galaxy/collection/galaxy_api_proxy.py
++++ ansible-core-2.16.5/lib/ansible/galaxy/collection/galaxy_api_proxy.py
+@@ -18,7 +18,7 @@ if t.TYPE_CHECKING:
+ )
+
+ from ansible.galaxy.api import GalaxyAPI, GalaxyError
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.utils.display import Display
+
+
+--- ansible-core-2.16.5.orig/lib/ansible/galaxy/data/container/README.md
++++ ansible-core-2.16.5/lib/ansible/galaxy/data/container/README.md
+@@ -3,7 +3,7 @@
+ Adds a <SERVICE_NAME> service to your [Ansible Container](https://github.com/ansible/ansible-container) project. Run the following commands
+ to install the service:
+
+-```shell
++```
+ # Set the working directory to your Ansible Container project root
+ $ cd myproject
+
+@@ -15,8 +15,7 @@ $ ansible-container install <USERNAME.RO
+
+ - [Ansible Container](https://github.com/ansible/ansible-container)
+ - An existing Ansible Container project. To create a project, simply run the following:
+-
+- ```shell
++ ```
+ # Create an empty project directory
+ $ mkdir myproject
+
+@@ -29,6 +28,7 @@ $ ansible-container install <USERNAME.RO
+
+ - Continue listing any prerequisites here...
+
++
+ ## Role Variables
+
+ A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set
+@@ -45,3 +45,5 @@ BSD
+ ## Author Information
+
+ An optional section for the role authors to include contact information, or a website (HTML is not allowed).
++
++
+--- ansible-core-2.16.5.orig/lib/ansible/galaxy/dependency_resolution/__init__.py
++++ ansible-core-2.16.5/lib/ansible/galaxy/dependency_resolution/__init__.py
+@@ -13,7 +13,10 @@ if t.TYPE_CHECKING:
+ from ansible.galaxy.collection.concrete_artifact_manager import (
+ ConcreteArtifactsManager,
+ )
+- from ansible.galaxy.dependency_resolution.dataclasses import Candidate
++ from ansible.galaxy.dependency_resolution.dataclasses import (
++ Candidate,
++ Requirement,
++ )
+
+ from ansible.galaxy.collection.galaxy_api_proxy import MultiGalaxyAPIProxy
+ from ansible.galaxy.dependency_resolution.providers import CollectionDependencyProvider
+@@ -24,6 +27,7 @@ from ansible.galaxy.dependency_resolutio
+ def build_collection_dependency_resolver(
+ galaxy_apis, # type: t.Iterable[GalaxyAPI]
+ concrete_artifacts_manager, # type: ConcreteArtifactsManager
++ user_requirements, # type: t.Iterable[Requirement]
+ preferred_candidates=None, # type: t.Iterable[Candidate]
+ with_deps=True, # type: bool
+ with_pre_releases=False, # type: bool
+@@ -40,6 +44,7 @@ def build_collection_dependency_resolver
+ CollectionDependencyProvider(
+ apis=MultiGalaxyAPIProxy(galaxy_apis, concrete_artifacts_manager, offline=offline),
+ concrete_artifacts_manager=concrete_artifacts_manager,
++ user_requirements=user_requirements,
+ preferred_candidates=preferred_candidates,
+ with_deps=with_deps,
+ with_pre_releases=with_pre_releases,
+--- ansible-core-2.16.5.orig/lib/ansible/galaxy/dependency_resolution/dataclasses.py
++++ ansible-core-2.16.5/lib/ansible/galaxy/dependency_resolution/dataclasses.py
+@@ -29,8 +29,7 @@ if t.TYPE_CHECKING:
+
+ from ansible.errors import AnsibleError, AnsibleAssertionError
+ from ansible.galaxy.api import GalaxyAPI
+-from ansible.galaxy.collection import HAS_PACKAGING, PkgReq
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.common.arg_spec import ArgumentSpecValidator
+ from ansible.utils.collection_loader import AnsibleCollectionRef
+ from ansible.utils.display import Display
+@@ -216,15 +215,10 @@ class _ComputedReqKindsMixin:
+ return cls.from_dir_path_implicit(dir_path)
+
+ @classmethod
+- def from_dir_path( # type: ignore[misc]
+- cls, # type: t.Type[Collection]
+- dir_path, # type: bytes
+- art_mgr, # type: ConcreteArtifactsManager
+- ): # type: (...) -> Collection
++ def from_dir_path(cls, dir_path, art_mgr):
+ """Make collection from an directory with metadata."""
+- if dir_path.endswith(to_bytes(os.path.sep)):
+- dir_path = dir_path.rstrip(to_bytes(os.path.sep))
+- if not _is_collection_dir(dir_path):
++ b_dir_path = to_bytes(dir_path, errors='surrogate_or_strict')
++ if not _is_collection_dir(b_dir_path):
+ display.warning(
+ u"Collection at '{path!s}' does not have a {manifest_json!s} "
+ u'file, nor has it {galaxy_yml!s}: cannot detect version.'.
+@@ -273,8 +267,6 @@ class _ComputedReqKindsMixin:
+ regardless of whether any of known metadata files are present.
+ """
+ # There is no metadata, but it isn't required for a functional collection. Determine the namespace.name from the path.
+- if dir_path.endswith(to_bytes(os.path.sep)):
+- dir_path = dir_path.rstrip(to_bytes(os.path.sep))
+ u_dir_path = to_text(dir_path, errors='surrogate_or_strict')
+ path_list = u_dir_path.split(os.path.sep)
+ req_name = '.'.join(path_list[-2:])
+@@ -283,25 +275,13 @@ class _ComputedReqKindsMixin:
+ @classmethod
+ def from_string(cls, collection_input, artifacts_manager, supplemental_signatures):
+ req = {}
+- if _is_concrete_artifact_pointer(collection_input) or AnsibleCollectionRef.is_valid_collection_name(collection_input):
+- # Arg is a file path or URL to a collection, or just a collection
++ if _is_concrete_artifact_pointer(collection_input):
++ # Arg is a file path or URL to a collection
+ req['name'] = collection_input
+- elif ':' in collection_input:
++ else:
+ req['name'], _sep, req['version'] = collection_input.partition(':')
+ if not req['version']:
+ del req['version']
+- else:
+- if not HAS_PACKAGING:
+- raise AnsibleError("Failed to import packaging, check that a supported version is installed")
+- try:
+- pkg_req = PkgReq(collection_input)
+- except Exception as e:
+- # packaging doesn't know what this is, let it fly, better errors happen in from_requirement_dict
+- req['name'] = collection_input
+- else:
+- req['name'] = pkg_req.name
+- if pkg_req.specifier:
+- req['version'] = to_text(pkg_req.specifier)
+ req['signatures'] = supplemental_signatures
+
+ return cls.from_requirement_dict(req, artifacts_manager)
+@@ -434,9 +414,6 @@ class _ComputedReqKindsMixin:
+ format(not_url=req_source.api_server),
+ )
+
+- if req_type == 'dir' and req_source.endswith(os.path.sep):
+- req_source = req_source.rstrip(os.path.sep)
+-
+ tmp_inst_req = cls(req_name, req_version, req_source, req_type, req_signature_sources)
+
+ if req_type not in {'galaxy', 'subdirs'} and req_name is None:
+@@ -463,8 +440,8 @@ class _ComputedReqKindsMixin:
+ def __unicode__(self):
+ if self.fqcn is None:
+ return (
+- u'"virtual collection Git repo"' if self.is_scm
+- else u'"virtual collection namespace"'
++ f'{self.type} collection from a Git repo' if self.is_scm
++ else f'{self.type} collection from a namespace'
+ )
+
+ return (
+@@ -504,14 +481,14 @@ class _ComputedReqKindsMixin:
+ @property
+ def namespace(self):
+ if self.is_virtual:
+- raise TypeError('Virtual collections do not have a namespace')
++ raise TypeError(f'{self.type} collections do not have a namespace')
+
+ return self._get_separate_ns_n_name()[0]
+
+ @property
+ def name(self):
+ if self.is_virtual:
+- raise TypeError('Virtual collections do not have a name')
++ raise TypeError(f'{self.type} collections do not have a name')
+
+ return self._get_separate_ns_n_name()[-1]
+
+@@ -565,27 +542,6 @@ class _ComputedReqKindsMixin:
+ return not self.is_concrete_artifact
+
+ @property
+- def is_pinned(self):
+- """Indicate if the version set is considered pinned.
+-
+- This essentially computes whether the version field of the current
+- requirement explicitly requests a specific version and not an allowed
+- version range.
+-
+- It is then used to help the resolvelib-based dependency resolver judge
+- whether it's acceptable to consider a pre-release candidate version
+- despite pre-release installs not being requested by the end-user
+- explicitly.
+-
+- See https://github.com/ansible/ansible/pull/81606 for extra context.
+- """
+- version_string = self.ver[0]
+- return version_string.isdigit() or not (
+- version_string == '*' or
+- version_string.startswith(('<', '>', '!='))
+- )
+-
+- @property
+ def source_info(self):
+ return self._source_info
+
+--- ansible-core-2.16.5.orig/lib/ansible/galaxy/dependency_resolution/errors.py
++++ ansible-core-2.16.5/lib/ansible/galaxy/dependency_resolution/errors.py
+@@ -7,7 +7,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ try:
+- from resolvelib.resolvers import ( # pylint: disable=unused-import
++ from resolvelib.resolvers import (
+ ResolutionImpossible as CollectionDependencyResolutionImpossible,
+ InconsistentCandidate as CollectionDependencyInconsistentCandidate,
+ )
+--- ansible-core-2.16.5.orig/lib/ansible/galaxy/dependency_resolution/providers.py
++++ ansible-core-2.16.5/lib/ansible/galaxy/dependency_resolution/providers.py
+@@ -40,7 +40,7 @@ except ImportError:
+
+ # TODO: add python requirements to ansible-test's ansible-core distribution info and remove the hardcoded lowerbound/upperbound fallback
+ RESOLVELIB_LOWERBOUND = SemanticVersion("0.5.3")
+-RESOLVELIB_UPPERBOUND = SemanticVersion("1.1.0")
++RESOLVELIB_UPPERBOUND = SemanticVersion("0.9.0")
+ RESOLVELIB_VERSION = SemanticVersion.from_loose_version(LooseVersion(resolvelib_version))
+
+
+@@ -51,6 +51,7 @@ class CollectionDependencyProviderBase(A
+ self, # type: CollectionDependencyProviderBase
+ apis, # type: MultiGalaxyAPIProxy
+ concrete_artifacts_manager=None, # type: ConcreteArtifactsManager
++ user_requirements=None, # type: t.Iterable[Requirement]
+ preferred_candidates=None, # type: t.Iterable[Candidate]
+ with_deps=True, # type: bool
+ with_pre_releases=False, # type: bool
+@@ -86,12 +87,58 @@ class CollectionDependencyProviderBase(A
+ Requirement.from_requirement_dict,
+ art_mgr=concrete_artifacts_manager,
+ )
++ self._pinned_candidate_requests = set(
++ # NOTE: User-provided signatures are supplemental, so signatures
++ # NOTE: are not used to determine if a candidate is user-requested
++ Candidate(req.fqcn, req.ver, req.src, req.type, None)
++ for req in (user_requirements or ())
++ if req.is_concrete_artifact or (
++ req.ver != '*' and
++ not req.ver.startswith(('<', '>', '!='))
++ )
++ )
+ self._preferred_candidates = set(preferred_candidates or ())
+ self._with_deps = with_deps
+ self._with_pre_releases = with_pre_releases
+ self._upgrade = upgrade
+ self._include_signatures = include_signatures
+
++ def _is_user_requested(self, candidate): # type: (Candidate) -> bool
++ """Check if the candidate is requested by the user."""
++ if candidate in self._pinned_candidate_requests:
++ return True
++
++ if candidate.is_online_index_pointer and candidate.src is not None:
++ # NOTE: Candidate is a namedtuple, it has a source server set
++ # NOTE: to a specific GalaxyAPI instance or `None`. When the
++ # NOTE: user runs
++ # NOTE:
++ # NOTE: $ ansible-galaxy collection install ns.coll
++ # NOTE:
++ # NOTE: then it's saved in `self._pinned_candidate_requests`
++ # NOTE: as `('ns.coll', '*', None, 'galaxy')` but then
++ # NOTE: `self.find_matches()` calls `self.is_satisfied_by()`
++ # NOTE: with Candidate instances bound to each specific
++ # NOTE: server available, those look like
++ # NOTE: `('ns.coll', '*', GalaxyAPI(...), 'galaxy')` and
++ # NOTE: wouldn't match the user requests saved in
++ # NOTE: `self._pinned_candidate_requests`. This is why we
++ # NOTE: normalize the collection to have `src=None` and try
++ # NOTE: again.
++ # NOTE:
++ # NOTE: When the user request comes from `requirements.yml`
++ # NOTE: with the `source:` set, it'll match the first check
++ # NOTE: but it still can have entries with `src=None` so this
++ # NOTE: normalized check is still necessary.
++ # NOTE:
++ # NOTE: User-provided signatures are supplemental, so signatures
++ # NOTE: are not used to determine if a candidate is user-requested
++ return Candidate(
++ candidate.fqcn, candidate.ver, None, candidate.type, None
++ ) in self._pinned_candidate_requests
++
++ return False
++
+ def identify(self, requirement_or_candidate):
+ # type: (t.Union[Candidate, Requirement]) -> str
+ """Given requirement or candidate, return an identifier for it.
+@@ -143,7 +190,7 @@ class CollectionDependencyProviderBase(A
+ Mapping of identifier, list of named tuple pairs.
+ The named tuples have the entries ``requirement`` and ``parent``.
+
+- resolvelib >=0.8.0, <= 1.0.1
++ resolvelib >=0.8.0, <= 0.8.1
+
+ :param identifier: The value returned by ``identify()``.
+
+@@ -295,79 +342,25 @@ class CollectionDependencyProviderBase(A
+ latest_matches = []
+ signatures = []
+ extra_signature_sources = [] # type: list[str]
+-
+- discarding_pre_releases_acceptable = any(
+- not is_pre_release(candidate_version)
+- for candidate_version, _src_server in coll_versions
+- )
+-
+- # NOTE: The optimization of conditionally looping over the requirements
+- # NOTE: is used to skip having to compute the pinned status of all
+- # NOTE: requirements and apply version normalization to the found ones.
+- all_pinned_requirement_version_numbers = {
+- # NOTE: Pinned versions can start with a number, but also with an
+- # NOTE: equals sign. Stripping it at the beginning should be
+- # NOTE: enough. If there's a space after equals, the second strip
+- # NOTE: will take care of it.
+- # NOTE: Without this conversion, requirements versions like
+- # NOTE: '1.2.3-alpha.4' work, but '=1.2.3-alpha.4' don't.
+- requirement.ver.lstrip('=').strip()
+- for requirement in requirements
+- if requirement.is_pinned
+- } if discarding_pre_releases_acceptable else set()
+-
+ for version, src_server in coll_versions:
+ tmp_candidate = Candidate(fqcn, version, src_server, 'galaxy', None)
+
++ unsatisfied = False
+ for requirement in requirements:
+- candidate_satisfies_requirement = self.is_satisfied_by(
+- requirement, tmp_candidate,
+- )
+- if not candidate_satisfies_requirement:
+- break
+-
+- should_disregard_pre_release_candidate = (
+- # NOTE: Do not discard pre-release candidates in the
+- # NOTE: following cases:
+- # NOTE: * the end-user requested pre-releases explicitly;
+- # NOTE: * the candidate is a concrete artifact (e.g. a
+- # NOTE: Git repository, subdirs, a tarball URL, or a
+- # NOTE: local dir or file etc.);
+- # NOTE: * the candidate's pre-release version exactly
+- # NOTE: matches a version specifically requested by one
+- # NOTE: of the requirements in the current match
+- # NOTE: discovery round (i.e. matching a requirement
+- # NOTE: that is not a range but an explicit specific
+- # NOTE: version pin). This works when some requirements
+- # NOTE: request version ranges but others (possibly on
+- # NOTE: different dependency tree level depths) demand
+- # NOTE: pre-release dependency versions, even if those
+- # NOTE: dependencies are transitive.
+- is_pre_release(tmp_candidate.ver)
+- and discarding_pre_releases_acceptable
+- and not (
+- self._with_pre_releases
+- or tmp_candidate.is_concrete_artifact
+- or version in all_pinned_requirement_version_numbers
+- )
+- )
+- if should_disregard_pre_release_candidate:
+- break
+-
++ unsatisfied |= not self.is_satisfied_by(requirement, tmp_candidate)
+ # FIXME
+- # candidate_is_from_requested_source = (
+- # requirement.src is None # if this is true for some candidates but not all it will break key param - Nonetype can't be compared to str
++ # unsatisfied |= not self.is_satisfied_by(requirement, tmp_candidate) or not (
++ # requirement.src is None or # if this is true for some candidates but not all it will break key param - Nonetype can't be compared to str
+ # or requirement.src == candidate.src
+ # )
+- # if not candidate_is_from_requested_source:
+- # break
+-
++ if unsatisfied:
++ break
+ if not self._include_signatures:
+ continue
+
+ extra_signature_sources.extend(requirement.signature_sources or [])
+
+- else: # candidate satisfies requirements, `break` never happened
++ if not unsatisfied:
+ if self._include_signatures:
+ for extra_source in extra_signature_sources:
+ signatures.append(get_signature_from_source(extra_source))
+@@ -412,6 +405,21 @@ class CollectionDependencyProviderBase(A
+ :returns: Indication whether the `candidate` is a viable \
+ solution to the `requirement`.
+ """
++ # NOTE: Only allow pre-release candidates if we want pre-releases
++ # NOTE: or the req ver was an exact match with the pre-release
++ # NOTE: version. Another case where we'd want to allow
++ # NOTE: pre-releases is when there are several user requirements
++ # NOTE: and one of them is a pre-release that also matches a
++ # NOTE: transitive dependency of another requirement.
++ allow_pre_release = self._with_pre_releases or not (
++ requirement.ver == '*' or
++ requirement.ver.startswith('<') or
++ requirement.ver.startswith('>') or
++ requirement.ver.startswith('!=')
++ ) or self._is_user_requested(candidate)
++ if is_pre_release(candidate.ver) and not allow_pre_release:
++ return False
++
+ # NOTE: This is a set of Pipenv-inspired optimizations. Ref:
+ # https://github.com/sarugaku/passa/blob/2ac00f1/src/passa/models/providers.py#L58-L74
+ if (
+--- ansible-core-2.16.5.orig/lib/ansible/galaxy/role.py
++++ ansible-core-2.16.5/lib/ansible/galaxy/role.py
+@@ -36,13 +36,12 @@ from ansible import context
+ from ansible.errors import AnsibleError, AnsibleParserError
+ from ansible.galaxy.api import GalaxyAPI
+ from ansible.galaxy.user_agent import user_agent
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.common.yaml import yaml_dump, yaml_load
+ from ansible.module_utils.compat.version import LooseVersion
+ from ansible.module_utils.urls import open_url
+ from ansible.playbook.role.requirement import RoleRequirement
+ from ansible.utils.display import Display
+-from ansible.utils.path import is_subpath, unfrackpath
+
+ display = Display()
+
+@@ -212,7 +211,7 @@ class GalaxyRole(object):
+
+ info = dict(
+ version=self.version,
+- install_date=datetime.datetime.now(datetime.timezone.utc).strftime("%c"),
++ install_date=datetime.datetime.utcnow().strftime("%c"),
+ )
+ if not os.path.exists(os.path.join(self.path, 'meta')):
+ os.makedirs(os.path.join(self.path, 'meta'))
+@@ -394,41 +393,43 @@ class GalaxyRole(object):
+ # we only extract files, and remove any relative path
+ # bits that might be in the file for security purposes
+ # and drop any containing directory, as mentioned above
+- if not (member.isreg() or member.issym()):
+- continue
+-
+- for attr in ('name', 'linkname'):
+- if not (attr_value := getattr(member, attr, None)):
+- continue
+-
+- if attr_value.startswith(os.sep) and not is_subpath(attr_value, archive_parent_dir):
+- err = f"Invalid {attr} for tarfile member: path {attr_value} is not a subpath of the role {archive_parent_dir}"
+- raise AnsibleError(err)
+-
+- if attr == 'linkname':
+- # Symlinks are relative to the link
+- relative_to_archive_dir = os.path.dirname(getattr(member, 'name', ''))
+- archive_dir_path = os.path.join(archive_parent_dir, relative_to_archive_dir, attr_value)
++ if member.isreg() or member.issym():
++ for attr in ('name', 'linkname'):
++ attr_value = getattr(member, attr, None)
++ if not attr_value:
++ continue
++ n_attr_value = to_native(attr_value)
++ n_archive_parent_dir = to_native(archive_parent_dir)
++ n_parts = n_attr_value.replace(n_archive_parent_dir, "", 1).split(os.sep)
++ n_final_parts = []
++ for n_part in n_parts:
++ # TODO if the condition triggers it produces a broken installation.
++ # It will create the parent directory as an empty file and will
++ # explode if the directory contains valid files.
++ # Leaving this as is since the whole module needs a rewrite.
++ #
++ # Check if we have any files with illegal names,
++ # and display a warning if so. This could help users
++ # to debug a broken installation.
++ if not n_part:
++ continue
++ if n_part == '..':
++ display.warning(f"Illegal filename '{n_part}': '..' is not allowed")
++ continue
++ if n_part.startswith('~'):
++ display.warning(f"Illegal filename '{n_part}': names cannot start with '~'")
++ continue
++ if '$' in n_part:
++ display.warning(f"Illegal filename '{n_part}': names cannot contain '$'")
++ continue
++ n_final_parts.append(n_part)
++ setattr(member, attr, os.path.join(*n_final_parts))
++
++ if _check_working_data_filter():
++ # deprecated: description='extract fallback without filter' python_version='3.11'
++ role_tar_file.extract(member, to_native(self.path), filter='data') # type: ignore[call-arg]
+ else:
+- # Normalize paths that start with the archive dir
+- attr_value = attr_value.replace(archive_parent_dir, "", 1)
+- attr_value = os.path.join(*attr_value.split(os.sep)) # remove leading os.sep
+- archive_dir_path = os.path.join(archive_parent_dir, attr_value)
+-
+- resolved_archive = unfrackpath(archive_parent_dir)
+- resolved_path = unfrackpath(archive_dir_path)
+- if not is_subpath(resolved_path, resolved_archive):
+- err = f"Invalid {attr} for tarfile member: path {resolved_path} is not a subpath of the role {resolved_archive}"
+- raise AnsibleError(err)
+-
+- relative_path = os.path.join(*resolved_path.replace(resolved_archive, "", 1).split(os.sep)) or '.'
+- setattr(member, attr, relative_path)
+-
+- if _check_working_data_filter():
+- # deprecated: description='extract fallback without filter' python_version='3.11'
+- role_tar_file.extract(member, to_native(self.path), filter='data') # type: ignore[call-arg]
+- else:
+- role_tar_file.extract(member, to_native(self.path))
++ role_tar_file.extract(member, to_native(self.path))
+
+ # write out the install info file for later use
+ self._write_galaxy_install_info()
+--- ansible-core-2.16.5.orig/lib/ansible/galaxy/token.py
++++ ansible-core-2.16.5/lib/ansible/galaxy/token.py
+@@ -28,7 +28,7 @@ from stat import S_IRUSR, S_IWUSR
+
+ from ansible import constants as C
+ from ansible.galaxy.user_agent import user_agent
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.common.yaml import yaml_dump, yaml_load
+ from ansible.module_utils.urls import open_url
+ from ansible.utils.display import Display
+@@ -69,7 +69,7 @@ class KeycloakToken(object):
+
+ # - build a request to POST to auth_url
+ # - body is form encoded
+- # - 'refresh_token' is the offline token stored in ansible.cfg
++ # - 'request_token' is the offline token stored in ansible.cfg
+ # - 'grant_type' is 'refresh_token'
+ # - 'client_id' is 'cloud-services'
+ # - should probably be based on the contents of the
+--- ansible-core-2.16.5.orig/lib/ansible/inventory/group.py
++++ ansible-core-2.16.5/lib/ansible/inventory/group.py
+@@ -18,12 +18,11 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ from collections.abc import Mapping, MutableMapping
+-from enum import Enum
+ from itertools import chain
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.utils.display import Display
+ from ansible.utils.vars import combine_vars
+
+@@ -54,14 +53,8 @@ def to_safe_group_name(name, replacer="_
+ return name
+
+
+-class InventoryObjectType(Enum):
+- HOST = 0
+- GROUP = 1
+-
+-
+ class Group:
+ ''' a group of ansible hosts '''
+- base_type = InventoryObjectType.GROUP
+
+ # __slots__ = [ 'name', 'hosts', 'vars', 'child_groups', 'parent_groups', 'depth', '_hosts_cache' ]
+
+--- ansible-core-2.16.5.orig/lib/ansible/inventory/host.py
++++ ansible-core-2.16.5/lib/ansible/inventory/host.py
+@@ -21,7 +21,7 @@ __metaclass__ = type
+
+ from collections.abc import Mapping, MutableMapping
+
+-from ansible.inventory.group import Group, InventoryObjectType
++from ansible.inventory.group import Group
+ from ansible.parsing.utils.addresses import patterns
+ from ansible.utils.vars import combine_vars, get_unique_id
+
+@@ -31,7 +31,6 @@ __all__ = ['Host']
+
+ class Host:
+ ''' a single ansible host '''
+- base_type = InventoryObjectType.HOST
+
+ # __slots__ = [ 'name', 'vars', 'groups' ]
+
+--- ansible-core-2.16.5.orig/lib/ansible/inventory/manager.py
++++ ansible-core-2.16.5/lib/ansible/inventory/manager.py
+@@ -33,7 +33,7 @@ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleOptionsError, AnsibleParserError
+ from ansible.inventory.data import InventoryData
+ from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.parsing.utils.addresses import parse_address
+ from ansible.plugins.loader import inventory_loader
+ from ansible.utils.helpers import deduplicate_list
+--- ansible-core-2.16.5.orig/lib/ansible/keyword_desc.yml
++++ ansible-core-2.16.5/lib/ansible/keyword_desc.yml
+@@ -5,7 +5,7 @@ action: "The 'action' to execute for a t
+ args: "A secondary way to add arguments into a task. Takes a dictionary in which keys map to options and values."
+ always: List of tasks, in a block, that execute no matter if there is an error in the block or not.
+ any_errors_fatal: Force any un-handled task errors on any host to propagate to all hosts and end the play.
+-async: Run a task asynchronously if the C(action) supports this; the value is the maximum runtime in seconds.
++async: Run a task asynchronously if the C(action) supports this; value is maximum runtime in seconds.
+ become: Boolean that controls if privilege escalation is used or not on :term:`Task` execution. Implemented by the become plugin. See :ref:`become_plugins`.
+ become_exe: Path to the executable used to elevate privileges. Implemented by the become plugin. See :ref:`become_plugins`.
+ become_flags: A string of flag(s) to pass to the privilege escalation program when :term:`become` is True.
+@@ -23,25 +23,25 @@ collections: |
+
+
+ connection: Allows you to change the connection plugin used for tasks to execute on the target. See :ref:`using_connection`.
+-debugger: Enable debugging tasks based on the state of the task result. See :ref:`playbook_debugger`.
++debugger: Enable debugging tasks based on state of the task result. See :ref:`playbook_debugger`.
+ delay: Number of seconds to delay between retries. This setting is only used in combination with :term:`until`.
+ delegate_facts: Boolean that allows you to apply facts to a delegated host instead of inventory_hostname.
+ delegate_to: Host to execute task instead of the target (inventory_hostname). Connection vars from the delegated host will also be used for the task.
+ diff: "Toggle to make tasks return 'diff' information or not."
+-environment: A dictionary that gets converted into environment vars to be provided for the task upon execution. This can ONLY be used with modules. This is not supported for any other type of plugins nor Ansible itself nor its configuration, it just sets the variables for the code responsible for executing the task. This is not a recommended way to pass in confidential data.
++environment: A dictionary that gets converted into environment vars to be provided for the task upon execution. This can ONLY be used with modules. This isn't supported for any other type of plugins nor Ansible itself nor its configuration, it just sets the variables for the code responsible for executing the task. This is not a recommended way to pass in confidential data.
+ fact_path: Set the fact path option for the fact gathering plugin controlled by :term:`gather_facts`.
+ failed_when: "Conditional expression that overrides the task's normal 'failed' status."
+ force_handlers: Will force notified handler execution for hosts even if they failed during the play. Will not trigger if the play itself fails.
+ gather_facts: "A boolean that controls if the play will automatically run the 'setup' task to gather facts for the hosts."
+-gather_subset: Allows you to pass subset options to the fact gathering plugin controlled by :term:`gather_facts`.
++gather_subset: Allows you to pass subset options to the fact gathering plugin controlled by :term:`gather_facts`.
+ gather_timeout: Allows you to set the timeout for the fact gathering plugin controlled by :term:`gather_facts`.
+ handlers: "A section with tasks that are treated as handlers, these won't get executed normally, only when notified after each section of tasks is complete. A handler's `listen` field is not templatable."
+ hosts: "A list of groups, hosts or host pattern that translates into a list of hosts that are the play's target."
+ ignore_errors: Boolean that allows you to ignore task failures and continue with play. It does not affect connection errors.
+ ignore_unreachable: Boolean that allows you to ignore task failures due to an unreachable host and continue with the play. This does not affect other task errors (see :term:`ignore_errors`) but is useful for groups of volatile/ephemeral hosts.
+ loop: "Takes a list for the task to iterate over, saving each list element into the ``item`` variable (configurable via loop_control)"
+-loop_control: Several keys here allow you to modify/set loop behavior in a task. See :ref:`loop_control`.
+-max_fail_percentage: can be used to abort the run after a given percentage of hosts in the current batch has failed. This only works on linear or linear-derived strategies.
++loop_control: Several keys here allow you to modify/set loop behaviour in a task. See :ref:`loop_control`.
++max_fail_percentage: can be used to abort the run after a given percentage of hosts in the current batch has failed. This only works on linear or linear derived strategies.
+ module_defaults: Specifies default parameter values for modules.
+ name: "Identifier. Can be used for documentation, or in tasks/handlers."
+ no_log: Boolean that controls information disclosure.
+@@ -56,13 +56,13 @@ register: Name of variable that will con
+ rescue: List of tasks in a :term:`block` that run if there is a task error in the main :term:`block` list.
+ retries: "Number of retries before giving up in a :term:`until` loop. This setting is only used in combination with :term:`until`."
+ roles: List of roles to be imported into the play
+-run_once: Boolean that will bypass the host loop, forcing the task to attempt to execute on the first host available and afterward apply any results and facts to all active hosts in the same batch.
++run_once: Boolean that will bypass the host loop, forcing the task to attempt to execute on the first host available and afterwards apply any results and facts to all active hosts in the same batch.
+ serial: Explicitly define how Ansible batches the execution of the current play on the play's target. See :ref:`rolling_update_batch_size`.
+-strategy: Allows you to choose the strategy plugin to use for the play. See :ref:`strategy_plugins`.
++strategy: Allows you to choose the connection plugin to use for the play.
+ tags: Tags applied to the task or included tasks, this allows selecting subsets of tasks from the command line.
+ tasks: Main list of tasks to execute in the play, they run after :term:`roles` and before :term:`post_tasks`.
+-timeout: Time limit for the task to execute in, if exceeded Ansible will interrupt and fail the task.
+-throttle: Limit the number of concurrent task runs on task, block and playbook level. This is independent of the forks and serial settings, but cannot be set higher than those limits. For example, if forks is set to 10 and the throttle is set to 15, at most 10 hosts will be operated on in parallel.
++timeout: Time limit for task to execute in, if exceeded Ansible will interrupt and fail the task.
++throttle: Limit number of concurrent task runs on task, block and playbook level. This is independent of the forks and serial settings, but cannot be set higher than those limits. For example, if forks is set to 10 and the throttle is set to 15, at most 10 hosts will be operated on in parallel.
+ until: "This keyword implies a ':term:`retries` loop' that will go on until the condition supplied here is met or we hit the :term:`retries` limit."
+ vars: Dictionary/map of variables
+ vars_files: List of files that contain vars to include in the play.
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/_text.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/_text.py
+@@ -8,7 +8,6 @@ __metaclass__ = type
+ """
+
+ # Backwards compat for people still calling it from this package
+-# pylint: disable=unused-import
+ import codecs
+
+ from ansible.module_utils.six import PY3, text_type, binary_type
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/ansible_release.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/ansible_release.py
+@@ -19,6 +19,6 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-__version__ = '2.16.5'
++__version__ = '2.14.13'
+ __author__ = 'Ansible, Inc.'
+-__codename__ = "All My Love"
++__codename__ = "C'mon Everybody"
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/basic.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/basic.py
+@@ -5,20 +5,28 @@
+ from __future__ import absolute_import, division, print_function
+ __metaclass__ = type
+
+-import sys
+-
+-# Used for determining if the system is running a new enough python version
+-# and should only restrict on our documented minimum versions
+-_PY3_MIN = sys.version_info >= (3, 6)
+-_PY2_MIN = (2, 7) <= sys.version_info < (3,)
+-_PY_MIN = _PY3_MIN or _PY2_MIN
+-
+-if not _PY_MIN:
+- print(
+- '\n{"failed": true, '
+- '"msg": "ansible-core requires a minimum of Python2 version 2.7 or Python3 version 3.6. Current version: %s"}' % ''.join(sys.version.splitlines())
+- )
+- sys.exit(1)
++FILE_ATTRIBUTES = {
++ 'A': 'noatime',
++ 'a': 'append',
++ 'c': 'compressed',
++ 'C': 'nocow',
++ 'd': 'nodump',
++ 'D': 'dirsync',
++ 'e': 'extents',
++ 'E': 'encrypted',
++ 'h': 'blocksize',
++ 'i': 'immutable',
++ 'I': 'indexed',
++ 'j': 'journalled',
++ 'N': 'inline',
++ 's': 'zero',
++ 'S': 'synchronous',
++ 't': 'notail',
++ 'T': 'blockroot',
++ 'u': 'undelete',
++ 'X': 'compressedraw',
++ 'Z': 'compresseddirty',
++}
+
+ # Ansible modules can be written in any language.
+ # The functions available here can be used to do many common tasks,
+@@ -41,6 +49,7 @@ import shutil
+ import signal
+ import stat
+ import subprocess
++import sys
+ import tempfile
+ import time
+ import traceback
+@@ -92,49 +101,43 @@ from ansible.module_utils.common.text.fo
+ SIZE_RANGES,
+ )
+
+-import hashlib
+-
+-
+-def _get_available_hash_algorithms():
+- """Return a dictionary of available hash function names and their associated function."""
+- try:
+- # Algorithms available in Python 2.7.9+ and Python 3.2+
+- # https://docs.python.org/2.7/library/hashlib.html#hashlib.algorithms_available
+- # https://docs.python.org/3.2/library/hashlib.html#hashlib.algorithms_available
+- algorithm_names = hashlib.algorithms_available
+- except AttributeError:
+- # Algorithms in Python 2.7.x (used only for Python 2.7.0 through 2.7.8)
+- # https://docs.python.org/2.7/library/hashlib.html#hashlib.hashlib.algorithms
+- algorithm_names = set(hashlib.algorithms)
+-
+- algorithms = {}
+-
+- for algorithm_name in algorithm_names:
+- algorithm_func = getattr(hashlib, algorithm_name, None)
+-
+- if algorithm_func:
+- try:
+- # Make sure the algorithm is actually available for use.
+- # Not all algorithms listed as available are actually usable.
+- # For example, md5 is not available in FIPS mode.
+- algorithm_func()
+- except Exception:
+- pass
+- else:
+- algorithms[algorithm_name] = algorithm_func
+-
+- return algorithms
+-
+-
+-AVAILABLE_HASH_ALGORITHMS = _get_available_hash_algorithms()
+-
+ try:
+ from ansible.module_utils.common._json_compat import json
+ except ImportError as e:
+ print('\n{{"msg": "Error: ansible requires the stdlib json: {0}", "failed": true}}'.format(to_native(e)))
+ sys.exit(1)
+
+-from ansible.module_utils.six.moves.collections_abc import (
++
++AVAILABLE_HASH_ALGORITHMS = dict()
++try:
++ import hashlib
++
++ # python 2.7.9+ and 2.7.0+
++ for attribute in ('available_algorithms', 'algorithms'):
++ algorithms = getattr(hashlib, attribute, None)
++ if algorithms:
++ break
++ if algorithms is None:
++ # python 2.5+
++ algorithms = ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512')
++ for algorithm in algorithms:
++ AVAILABLE_HASH_ALGORITHMS[algorithm] = getattr(hashlib, algorithm)
++
++ # we may have been able to import md5 but it could still not be available
++ try:
++ hashlib.md5()
++ except ValueError:
++ AVAILABLE_HASH_ALGORITHMS.pop('md5', None)
++except Exception:
++ import sha
++ AVAILABLE_HASH_ALGORITHMS = {'sha1': sha.sha}
++ try:
++ import md5
++ AVAILABLE_HASH_ALGORITHMS['md5'] = md5.md5
++ except Exception:
++ pass
++
++from ansible.module_utils.common._collections_compat import (
+ KeysView,
+ Mapping, MutableMapping,
+ Sequence, MutableSequence,
+@@ -149,7 +152,6 @@ from ansible.module_utils.common.file im
+ is_executable,
+ format_attributes,
+ get_flags_from_attributes,
+- FILE_ATTRIBUTES,
+ )
+ from ansible.module_utils.common.sys_info import (
+ get_distribution,
+@@ -201,14 +203,14 @@ imap = map
+
+ try:
+ # Python 2
+- unicode # type: ignore[used-before-def] # pylint: disable=used-before-assignment
++ unicode # type: ignore[has-type] # pylint: disable=used-before-assignment
+ except NameError:
+ # Python 3
+ unicode = text_type
+
+ try:
+ # Python 2
+- basestring # type: ignore[used-before-def,has-type] # pylint: disable=used-before-assignment
++ basestring # type: ignore[has-type] # pylint: disable=used-before-assignment
+ except NameError:
+ # Python 3
+ basestring = string_types
+@@ -243,8 +245,20 @@ PASSWD_ARG_RE = re.compile(r'^[-]{0,2}pa
+
+ # Used for parsing symbolic file perms
+ MODE_OPERATOR_RE = re.compile(r'[+=-]')
+-USERS_RE = re.compile(r'^[ugo]+$')
+-PERMS_RE = re.compile(r'^[rwxXstugo]*$')
++USERS_RE = re.compile(r'[^ugo]')
++PERMS_RE = re.compile(r'[^rwxXstugo]')
++
++# Used for determining if the system is running a new enough python version
++# and should only restrict on our documented minimum versions
++_PY3_MIN = sys.version_info >= (3, 5)
++_PY2_MIN = (2, 7) <= sys.version_info < (3,)
++_PY_MIN = _PY3_MIN or _PY2_MIN
++if not _PY_MIN:
++ print(
++ '\n{"failed": true, '
++ '"msg": "ansible-core requires a minimum of Python2 version 2.7 or Python3 version 3.5. Current version: %s"}' % ''.join(sys.version.splitlines())
++ )
++ sys.exit(1)
+
+
+ #
+@@ -1041,18 +1055,18 @@ class AnsibleModule(object):
+
+ # Check if there are illegal characters in the user list
+ # They can end up in 'users' because they are not split
+- if not USERS_RE.match(users):
++ if USERS_RE.match(users):
+ raise ValueError("bad symbolic permission for mode: %s" % mode)
+
+ # Now we have two list of equal length, one contains the requested
+ # permissions and one with the corresponding operators.
+ for idx, perms in enumerate(permlist):
+ # Check if there are illegal characters in the permissions
+- if not PERMS_RE.match(perms):
++ if PERMS_RE.match(perms):
+ raise ValueError("bad symbolic permission for mode: %s" % mode)
+
+ for user in users:
+- mode_to_apply = cls._get_octal_mode_from_symbolic_perms(path_stat, user, perms, use_umask, new_mode)
++ mode_to_apply = cls._get_octal_mode_from_symbolic_perms(path_stat, user, perms, use_umask)
+ new_mode = cls._apply_operation_to_mode(user, opers[idx], mode_to_apply, new_mode)
+
+ return new_mode
+@@ -1077,9 +1091,9 @@ class AnsibleModule(object):
+ return new_mode
+
+ @staticmethod
+- def _get_octal_mode_from_symbolic_perms(path_stat, user, perms, use_umask, prev_mode=None):
+- if prev_mode is None:
+- prev_mode = stat.S_IMODE(path_stat.st_mode)
++ def _get_octal_mode_from_symbolic_perms(path_stat, user, perms, use_umask):
++ prev_mode = stat.S_IMODE(path_stat.st_mode)
++
+ is_directory = stat.S_ISDIR(path_stat.st_mode)
+ has_x_permissions = (prev_mode & EXEC_PERM_BITS) > 0
+ apply_X_permission = is_directory or has_x_permissions
+@@ -1489,19 +1503,7 @@ class AnsibleModule(object):
+ if deprecations:
+ kwargs['deprecations'] = deprecations
+
+- # preserve bools/none from no_log
+- # TODO: once python version on target high enough, dict comprh
+- preserved = {}
+- for k, v in kwargs.items():
+- if v is None or isinstance(v, bool):
+- preserved[k] = v
+-
+- # strip no_log collisions
+ kwargs = remove_values(kwargs, self.no_log_values)
+-
+- # return preserved
+- kwargs.update(preserved)
+-
+ print('\n%s' % self.jsonify(kwargs))
+
+ def exit_json(self, **kwargs):
+@@ -1705,6 +1707,14 @@ class AnsibleModule(object):
+ tmp_dest_fd, tmp_dest_name = tempfile.mkstemp(prefix=b'.ansible_tmp', dir=b_dest_dir, suffix=b_suffix)
+ except (OSError, IOError) as e:
+ error_msg = 'The destination directory (%s) is not writable by the current user. Error was: %s' % (os.path.dirname(dest), to_native(e))
++ except TypeError:
++ # We expect that this is happening because python3.4.x and
++ # below can't handle byte strings in mkstemp().
++ # Traceback would end in something like:
++ # file = _os.path.join(dir, pre + name + suf)
++ # TypeError: can't concat bytes to str
++ error_msg = ('Failed creating tmp file for atomic move. This usually happens when using Python3 less than Python3.5. '
++ 'Please use Python2.x or Python3.5 or greater.')
+ finally:
+ if error_msg:
+ if unsafe_writes:
+@@ -1834,14 +1844,6 @@ class AnsibleModule(object):
+ '''
+ Execute a command, returns rc, stdout, and stderr.
+
+- The mechanism of this method for reading stdout and stderr differs from
+- that of CPython subprocess.Popen.communicate, in that this method will
+- stop reading once the spawned command has exited and stdout and stderr
+- have been consumed, as opposed to waiting until stdout/stderr are
+- closed. This can be an important distinction, when taken into account
+- that a forked or backgrounded process may hold stdout or stderr open
+- for longer than the spawned command.
+-
+ :arg args: is the command to run
+ * If args is a list, the command will be run with shell=False.
+ * If args is a string and use_unsafe_shell=False it will split args to a list and run with shell=False
+@@ -2021,64 +2023,53 @@ class AnsibleModule(object):
+ if before_communicate_callback:
+ before_communicate_callback(cmd)
+
++ # the communication logic here is essentially taken from that
++ # of the _communicate() function in ssh.py
++
+ stdout = b''
+ stderr = b''
+-
+- # Mirror the CPython subprocess logic and preference for the selector to use.
+- # poll/select have the advantage of not requiring any extra file
+- # descriptor, contrarily to epoll/kqueue (also, they require a single
+- # syscall).
+- if hasattr(selectors, 'PollSelector'):
++ try:
++ selector = selectors.DefaultSelector()
++ except (IOError, OSError):
++ # Failed to detect default selector for the given platform
++ # Select PollSelector which is supported by major platforms
+ selector = selectors.PollSelector()
+- else:
+- selector = selectors.SelectSelector()
+-
+- if data:
+- if not binary_data:
+- data += '\n'
+- if isinstance(data, text_type):
+- data = to_bytes(data)
+
+ selector.register(cmd.stdout, selectors.EVENT_READ)
+ selector.register(cmd.stderr, selectors.EVENT_READ)
+-
+ if os.name == 'posix':
+ fcntl.fcntl(cmd.stdout.fileno(), fcntl.F_SETFL, fcntl.fcntl(cmd.stdout.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK)
+ fcntl.fcntl(cmd.stderr.fileno(), fcntl.F_SETFL, fcntl.fcntl(cmd.stderr.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK)
+
+ if data:
++ if not binary_data:
++ data += '\n'
++ if isinstance(data, text_type):
++ data = to_bytes(data)
+ cmd.stdin.write(data)
+ cmd.stdin.close()
+
+ while True:
+- # A timeout of 1 is both a little short and a little long.
+- # With None we could deadlock, with a lower value we would
+- # waste cycles. As it is, this is a mild inconvenience if
+- # we need to exit, and likely doesn't waste too many cycles
+ events = selector.select(1)
+- stdout_changed = False
+ for key, event in events:
+- b_chunk = key.fileobj.read(32768)
+- if not b_chunk:
++ b_chunk = key.fileobj.read()
++ if b_chunk == b(''):
+ selector.unregister(key.fileobj)
+- elif key.fileobj == cmd.stdout:
++ if key.fileobj == cmd.stdout:
+ stdout += b_chunk
+- stdout_changed = True
+ elif key.fileobj == cmd.stderr:
+ stderr += b_chunk
+-
+- # if we're checking for prompts, do it now, but only if stdout
+- # actually changed since the last loop
+- if prompt_re and stdout_changed and prompt_re.search(stdout) and not data:
+- if encoding:
+- stdout = to_native(stdout, encoding=encoding, errors=errors)
+- return (257, stdout, "A prompt was encountered while running a command, but no input data was specified")
+-
+- # break out if no pipes are left to read or the pipes are completely read
+- # and the process is terminated
++ # if we're checking for prompts, do it now
++ if prompt_re:
++ if prompt_re.search(stdout) and not data:
++ if encoding:
++ stdout = to_native(stdout, encoding=encoding, errors=errors)
++ return (257, stdout, "A prompt was encountered while running a command, but no input data was specified")
++ # only break out if no pipes are left to read or
++ # the pipes are completely read and
++ # the process is terminated
+ if (not events or not selector.get_map()) and cmd.poll() is not None:
+ break
+-
+ # No pipes are left to read but process is not yet terminated
+ # Only then it is safe to wait for the process to be finished
+ # NOTE: Actually cmd.poll() is always None here if no selectors are left
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/common/_collections_compat.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/common/_collections_compat.py
+@@ -2,27 +2,45 @@
+ # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
+ """Collections ABC import shim.
+
+-Use `ansible.module_utils.six.moves.collections_abc` instead, which has been available since ansible-core 2.11.
+-This module exists only for backwards compatibility.
++This module is intended only for internal use.
++It will go away once the bundled copy of six includes equivalent functionality.
++Third parties should not use this.
+ """
+
+ from __future__ import absolute_import, division, print_function
+ __metaclass__ = type
+
+-# Although this was originally intended for internal use only, it has wide adoption in collections.
+-# This is due in part to sanity tests previously recommending its use over `collections` imports.
+-from ansible.module_utils.six.moves.collections_abc import ( # pylint: disable=unused-import
+- MappingView,
+- ItemsView,
+- KeysView,
+- ValuesView,
+- Mapping, MutableMapping,
+- Sequence, MutableSequence,
+- Set, MutableSet,
+- Container,
+- Hashable,
+- Sized,
+- Callable,
+- Iterable,
+- Iterator,
+-)
++try:
++ """Python 3.3+ branch."""
++ from collections.abc import (
++ MappingView,
++ ItemsView,
++ KeysView,
++ ValuesView,
++ Mapping, MutableMapping,
++ Sequence, MutableSequence,
++ Set, MutableSet,
++ Container,
++ Hashable,
++ Sized,
++ Callable,
++ Iterable,
++ Iterator,
++ )
++except ImportError:
++ """Use old lib location under 2.6-3.2."""
++ from collections import ( # type: ignore[no-redef,attr-defined] # pylint: disable=deprecated-class
++ MappingView,
++ ItemsView,
++ KeysView,
++ ValuesView,
++ Mapping, MutableMapping,
++ Sequence, MutableSequence,
++ Set, MutableSet,
++ Container,
++ Hashable,
++ Sized,
++ Callable,
++ Iterable,
++ Iterator,
++ )
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/common/collections.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/common/collections.py
+@@ -8,7 +8,7 @@ __metaclass__ = type
+
+
+ from ansible.module_utils.six import binary_type, text_type
+-from ansible.module_utils.six.moves.collections_abc import Hashable, Mapping, MutableMapping, Sequence # pylint: disable=unused-import
++from ansible.module_utils.common._collections_compat import Hashable, Mapping, MutableMapping, Sequence
+
+
+ class ImmutableDict(Hashable, Mapping):
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/common/dict_transformations.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/common/dict_transformations.py
+@@ -10,7 +10,7 @@ __metaclass__ = type
+ import re
+ from copy import deepcopy
+
+-from ansible.module_utils.six.moves.collections_abc import MutableMapping
++from ansible.module_utils.common._collections_compat import MutableMapping
+
+
+ def camel_dict_to_snake_dict(camel_dict, reversible=False, ignore_list=()):
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/common/file.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/common/file.py
+@@ -4,12 +4,25 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
++import errno
+ import os
+ import stat
+ import re
++import pwd
++import grp
++import time
++import shutil
++import traceback
++import fcntl
++import sys
++
++from contextlib import contextmanager
++from ansible.module_utils._text import to_bytes, to_native, to_text
++from ansible.module_utils.six import b, binary_type
++from ansible.module_utils.common.warnings import deprecate
+
+ try:
+- import selinux # pylint: disable=unused-import
++ import selinux
+ HAVE_SELINUX = True
+ except ImportError:
+ HAVE_SELINUX = False
+@@ -96,3 +109,97 @@ def get_file_arg_spec():
+ attributes=dict(aliases=['attr']),
+ )
+ return arg_spec
++
++
++class LockTimeout(Exception):
++ pass
++
++
++class FileLock:
++ '''
++ Currently FileLock is implemented via fcntl.flock on a lock file, however this
++ behaviour may change in the future. Avoid mixing lock types fcntl.flock,
++ fcntl.lockf and module_utils.common.file.FileLock as it will certainly cause
++ unwanted and/or unexpected behaviour
++ '''
++ def __init__(self):
++ deprecate("FileLock is not reliable and has never been used in core for that reason. There is no current alternative that works across POSIX targets",
++ version='2.16')
++ self.lockfd = None
++
++ @contextmanager
++ def lock_file(self, path, tmpdir, lock_timeout=None):
++ '''
++ Context for lock acquisition
++ '''
++ try:
++ self.set_lock(path, tmpdir, lock_timeout)
++ yield
++ finally:
++ self.unlock()
++
++ def set_lock(self, path, tmpdir, lock_timeout=None):
++ '''
++ Create a lock file based on path with flock to prevent other processes
++ using given path.
++ Please note that currently file locking only works when it's executed by
++ the same user, I.E single user scenarios
++
++ :kw path: Path (file) to lock
++ :kw tmpdir: Path where to place the temporary .lock file
++ :kw lock_timeout:
++ Wait n seconds for lock acquisition, fail if timeout is reached.
++ 0 = Do not wait, fail if lock cannot be acquired immediately,
++ Default is None, wait indefinitely until lock is released.
++ :returns: True
++ '''
++ lock_path = os.path.join(tmpdir, 'ansible-{0}.lock'.format(os.path.basename(path)))
++ l_wait = 0.1
++ r_exception = IOError
++ if sys.version_info[0] == 3:
++ r_exception = BlockingIOError
++
++ self.lockfd = open(lock_path, 'w')
++
++ if lock_timeout <= 0:
++ fcntl.flock(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
++ os.chmod(lock_path, stat.S_IWRITE | stat.S_IREAD)
++ return True
++
++ if lock_timeout:
++ e_secs = 0
++ while e_secs < lock_timeout:
++ try:
++ fcntl.flock(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
++ os.chmod(lock_path, stat.S_IWRITE | stat.S_IREAD)
++ return True
++ except r_exception:
++ time.sleep(l_wait)
++ e_secs += l_wait
++ continue
++
++ self.lockfd.close()
++ raise LockTimeout('{0} sec'.format(lock_timeout))
++
++ fcntl.flock(self.lockfd, fcntl.LOCK_EX)
++ os.chmod(lock_path, stat.S_IWRITE | stat.S_IREAD)
++
++ return True
++
++ def unlock(self):
++ '''
++ Make sure lock file is available for everyone and Unlock the file descriptor
++ locked by set_lock
++
++ :returns: True
++ '''
++ if not self.lockfd:
++ return True
++
++ try:
++ fcntl.flock(self.lockfd, fcntl.LOCK_UN)
++ self.lockfd.close()
++ except ValueError: # file wasn't opened, let context manager fail gracefully
++ pass
++
++ return True
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/common/json.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/common/json.py
+@@ -10,8 +10,8 @@ import json
+
+ import datetime
+
+-from ansible.module_utils.common.text.converters import to_text
+-from ansible.module_utils.six.moves.collections_abc import Mapping
++from ansible.module_utils._text import to_text
++from ansible.module_utils.common._collections_compat import Mapping
+ from ansible.module_utils.common.collections import is_sequence
+
+
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/common/locale.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/common/locale.py
+@@ -4,7 +4,7 @@
+ from __future__ import absolute_import, division, print_function
+ __metaclass__ = type
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+
+ def get_best_parsable_locale(module, preferences=None, raise_on_locale=False):
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/common/parameters.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/common/parameters.py
+@@ -13,6 +13,7 @@ from itertools import chain
+
+ from ansible.module_utils.common.collections import is_iterable
+ from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils.common.text.formatters import lenient_lowercase
+ from ansible.module_utils.common.warnings import warn
+ from ansible.module_utils.errors import (
+ AliasError,
+@@ -32,7 +33,7 @@ from ansible.module_utils.errors import
+ )
+ from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE
+
+-from ansible.module_utils.six.moves.collections_abc import (
++from ansible.module_utils.common._collections_compat import (
+ KeysView,
+ Set,
+ Sequence,
+@@ -609,7 +610,7 @@ def _validate_argument_types(argument_sp
+ continue
+
+ value = parameters[param]
+- if value is None and not spec.get('required') and spec.get('default') is None:
++ if value is None:
+ continue
+
+ wanted_type = spec.get('type')
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/common/respawn.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/common/respawn.py
+@@ -8,7 +8,7 @@ import os
+ import subprocess
+ import sys
+
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils.common.text.converters import to_bytes, to_native
+
+
+ def has_respawned():
+@@ -79,9 +79,10 @@ def _create_payload():
+ import runpy
+ import sys
+
+-module_fqn = {module_fqn!r}
+-modlib_path = {modlib_path!r}
+-smuggled_args = {smuggled_args!r}
++module_fqn = '{module_fqn}'
++modlib_path = '{modlib_path}'
++smuggled_args = b"""{smuggled_args}""".strip()
++
+
+ if __name__ == '__main__':
+ sys.path.insert(0, modlib_path)
+@@ -92,6 +93,6 @@ if __name__ == '__main__':
+ runpy.run_module(module_fqn, init_globals=dict(_respawned=True), run_name='__main__', alter_sys=True)
+ '''
+
+- respawn_code = respawn_code_template.format(module_fqn=module_fqn, modlib_path=modlib_path, smuggled_args=smuggled_args.strip())
++ respawn_code = respawn_code_template.format(module_fqn=module_fqn, modlib_path=modlib_path, smuggled_args=to_native(smuggled_args))
+
+ return respawn_code
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/common/text/converters.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/common/text/converters.py
+@@ -10,7 +10,7 @@ import codecs
+ import datetime
+ import json
+
+-from ansible.module_utils.six.moves.collections_abc import Set
++from ansible.module_utils.common._collections_compat import Set
+ from ansible.module_utils.six import (
+ PY3,
+ binary_type,
+@@ -168,7 +168,7 @@ def to_text(obj, encoding='utf-8', error
+ handler, otherwise it will use replace.
+ :surrogate_then_replace: Does the same as surrogate_or_replace but
+ `was added for symmetry with the error handlers in
+- :func:`ansible.module_utils.common.text.converters.to_bytes` (Added in Ansible 2.3)
++ :func:`ansible.module_utils._text.to_bytes` (Added in Ansible 2.3)
+
+ Because surrogateescape was added in Python3 this usually means that
+ Python3 will use `surrogateescape` and Python2 will use the fallback
+@@ -179,7 +179,7 @@ def to_text(obj, encoding='utf-8', error
+
+ The default until Ansible-2.2 was `surrogate_or_replace`
+ In Ansible-2.3 this defaults to `surrogate_then_replace` for symmetry
+- with :func:`ansible.module_utils.common.text.converters.to_bytes` .
++ with :func:`ansible.module_utils._text.to_bytes` .
+ :kwarg nonstring: The strategy to use if a nonstring is specified in
+ ``obj``. Default is 'simplerepr'. Valid values are:
+
+@@ -268,13 +268,18 @@ def _json_encode_fallback(obj):
+
+
+ def jsonify(data, **kwargs):
+- # After 2.18, we should remove this loop, and hardcode to utf-8 in alignment with requiring utf-8 module responses
+ for encoding in ("utf-8", "latin-1"):
+ try:
+- new_data = container_to_text(data, encoding=encoding)
++ return json.dumps(data, encoding=encoding, default=_json_encode_fallback, **kwargs)
++ # Old systems using old simplejson module does not support encoding keyword.
++ except TypeError:
++ try:
++ new_data = container_to_text(data, encoding=encoding)
++ except UnicodeDecodeError:
++ continue
++ return json.dumps(new_data, default=_json_encode_fallback, **kwargs)
+ except UnicodeDecodeError:
+ continue
+- return json.dumps(new_data, default=_json_encode_fallback, **kwargs)
+ raise UnicodeError('Invalid unicode encoding encountered')
+
+
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/common/text/formatters.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/common/text/formatters.py
+@@ -67,7 +67,7 @@ def human_to_bytes(number, default_unit=
+ unit = default_unit
+
+ if unit is None:
+- # No unit given, returning raw number
++ ''' No unit given, returning raw number '''
+ return int(round(num))
+ range_key = unit[0].upper()
+ try:
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/common/validation.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/common/validation.py
+@@ -9,7 +9,7 @@ import os
+ import re
+
+ from ast import literal_eval
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common._json_compat import json
+ from ansible.module_utils.common.collections import is_iterable
+ from ansible.module_utils.common.text.converters import jsonify
+@@ -381,7 +381,7 @@ def check_type_str(value, allow_conversi
+ if isinstance(value, string_types):
+ return value
+
+- if allow_conversion and value is not None:
++ if allow_conversion:
+ return to_native(value, errors='surrogate_or_strict')
+
+ msg = "'{0!r}' is not a string and conversion is not allowed".format(value)
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/common/yaml.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/common/yaml.py
+@@ -24,13 +24,13 @@ if HAS_YAML:
+ try:
+ from yaml import CSafeLoader as SafeLoader
+ from yaml import CSafeDumper as SafeDumper
+- from yaml.cyaml import CParser as Parser # type: ignore[attr-defined] # pylint: disable=unused-import
++ from yaml.cyaml import CParser as Parser
+
+ HAS_LIBYAML = True
+ except (ImportError, AttributeError):
+- from yaml import SafeLoader # type: ignore[assignment]
+- from yaml import SafeDumper # type: ignore[assignment]
+- from yaml.parser import Parser # type: ignore[assignment] # pylint: disable=unused-import
++ from yaml import SafeLoader # type: ignore[misc]
++ from yaml import SafeDumper # type: ignore[misc]
++ from yaml.parser import Parser # type: ignore[misc]
+
+ yaml_load = _partial(_yaml.load, Loader=SafeLoader)
+ yaml_load_all = _partial(_yaml.load_all, Loader=SafeLoader)
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/compat/_selectors2.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/compat/_selectors2.py
+@@ -25,7 +25,7 @@ import socket
+ import sys
+ import time
+ from collections import namedtuple
+-from ansible.module_utils.six.moves.collections_abc import Mapping
++from ansible.module_utils.common._collections_compat import Mapping
+
+ try:
+ monotonic = time.monotonic
+@@ -81,7 +81,7 @@ def _fileobj_to_fd(fileobj):
+
+ # Python 3.5 uses a more direct route to wrap system calls to increase speed.
+ if sys.version_info >= (3, 5):
+- def _syscall_wrapper(func, dummy, *args, **kwargs):
++ def _syscall_wrapper(func, _, *args, **kwargs):
+ """ This is the short-circuit version of the below logic
+ because in Python 3.5+ all selectors restart system calls. """
+ try:
+@@ -342,8 +342,8 @@ if hasattr(select, "select"):
+
+ timeout = None if timeout is None else max(timeout, 0.0)
+ ready = []
+- r, w, dummy = _syscall_wrapper(self._select, True, self._readers,
+- self._writers, timeout=timeout)
++ r, w, _ = _syscall_wrapper(self._select, True, self._readers,
++ self._writers, timeout=timeout)
+ r = set(r)
+ w = set(w)
+ for fd in r | w:
+@@ -649,7 +649,7 @@ elif 'PollSelector' in globals(): # Pla
+ elif 'SelectSelector' in globals(): # Platform-specific: Windows
+ DefaultSelector = SelectSelector
+ else: # Platform-specific: AppEngine
+- def no_selector(dummy):
++ def no_selector(_):
+ raise ValueError("Platform does not have a selector")
+ DefaultSelector = no_selector
+ HAS_SELECT = False
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/compat/importlib.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/compat/importlib.py
+@@ -8,7 +8,7 @@ __metaclass__ = type
+ import sys
+
+ try:
+- from importlib import import_module # pylint: disable=unused-import
++ from importlib import import_module
+ except ImportError:
+ # importlib.import_module returns the tail
+ # whereas __import__ returns the head
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/compat/paramiko.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/compat/paramiko.py
+@@ -5,7 +5,7 @@
+ from __future__ import absolute_import, division, print_function
+ __metaclass__ = type
+
+-import types # pylint: disable=unused-import
++import types
+ import warnings
+
+ PARAMIKO_IMPORT_ERR = None
+@@ -13,7 +13,7 @@ PARAMIKO_IMPORT_ERR = None
+ try:
+ with warnings.catch_warnings():
+ warnings.filterwarnings('ignore', message='Blowfish has been deprecated', category=UserWarning)
+- import paramiko # pylint: disable=unused-import
++ import paramiko
+ # paramiko and gssapi are incompatible and raise AttributeError not ImportError
+ # When running in FIPS mode, cryptography raises InternalError
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1778939
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/compat/selectors.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/compat/selectors.py
+@@ -35,8 +35,9 @@ _BUNDLED_METADATA = {"pypi_name": "selec
+ # Fix use of OSError exception for py3 and use the wrapper of kqueue.control so retries of
+ # interrupted syscalls work with kqueue
+
++import os.path
+ import sys
+-import types # pylint: disable=unused-import
++import types
+
+ try:
+ # Python 3.4+
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/compat/selinux.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/compat/selinux.py
+@@ -62,7 +62,7 @@ def _module_setup():
+ fn.restype = cfg.get('restype', c_int)
+
+ # just patch simple directly callable functions directly onto the module
+- if not fn.argtypes or not any(argtype for argtype in fn.argtypes if type(argtype) is base_ptr_type):
++ if not fn.argtypes or not any(argtype for argtype in fn.argtypes if type(argtype) == base_ptr_type):
+ setattr(_thismod, fname, fn)
+ continue
+
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/compat/typing.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/compat/typing.py
+@@ -13,13 +13,13 @@ except Exception: # pylint: disable=bro
+ pass
+
+ try:
+- from typing import * # type: ignore[assignment,no-redef]
++ from typing import * # type: ignore[misc]
+ except Exception: # pylint: disable=broad-except
+ pass
+
+
+ try:
+- cast # type: ignore[used-before-def]
++ cast
+ except NameError:
+ def cast(typ, val): # type: ignore[no-redef]
+ return val
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/connection.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/connection.py
+@@ -38,7 +38,7 @@ import traceback
+ import uuid
+
+ from functools import partial
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.module_utils.common.json import AnsibleJSONEncoder
+ from ansible.module_utils.six import iteritems
+ from ansible.module_utils.six.moves import cPickle
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/distro/_distro.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/distro/_distro.py
+@@ -31,8 +31,6 @@ access to OS distribution information is
+ <https://bugs.python.org/issue1322>`_ for more information.
+ """
+
+-import argparse
+-import json
+ import logging
+ import os
+ import re
+@@ -138,6 +136,56 @@ _DISTRO_RELEASE_IGNORE_BASENAMES = (
+ )
+
+
++#
++# Python 2.6 does not have subprocess.check_output so replicate it here
++#
++def _my_check_output(*popenargs, **kwargs):
++ r"""Run command with arguments and return its output as a byte string.
++
++ If the exit code was non-zero it raises a CalledProcessError. The
++ CalledProcessError object will have the return code in the returncode
++ attribute and output in the output attribute.
++
++ The arguments are the same as for the Popen constructor. Example:
++
++ >>> check_output(["ls", "-l", "/dev/null"])
++ 'crw-rw-rw- 1 root root 1, 3 Oct 18 2007 /dev/null\n'
++
++ The stdout argument is not allowed as it is used internally.
++ To capture standard error in the result, use stderr=STDOUT.
++
++ >>> check_output(["/bin/sh", "-c",
++ ... "ls -l non_existent_file ; exit 0"],
++ ... stderr=STDOUT)
++ 'ls: non_existent_file: No such file or directory\n'
++
++ This is a backport of Python-2.7's check output to Python-2.6
++ """
++ if 'stdout' in kwargs:
++ raise ValueError(
++ 'stdout argument not allowed, it will be overridden.'
++ )
++ process = subprocess.Popen(
++ stdout=subprocess.PIPE, *popenargs, **kwargs
++ )
++ output, unused_err = process.communicate()
++ retcode = process.poll()
++ if retcode:
++ cmd = kwargs.get("args")
++ if cmd is None:
++ cmd = popenargs[0]
++ # Deviation from Python-2.7: Python-2.6's CalledProcessError does not
++ # have an argument for the stdout so simply omit it.
++ raise subprocess.CalledProcessError(retcode, cmd)
++ return output
++
++
++try:
++ _check_output = subprocess.check_output
++except AttributeError:
++ _check_output = _my_check_output
++
++
+ def linux_distribution(full_distribution_name=True):
+ # type: (bool) -> Tuple[str, str, str]
+ """
+@@ -156,8 +204,7 @@ def linux_distribution(full_distribution
+
+ * ``version``: The result of :func:`distro.version`.
+
+- * ``codename``: The extra item (usually in parentheses) after the
+- os-release version number, or the result of :func:`distro.codename`.
++ * ``codename``: The result of :func:`distro.codename`.
+
+ The interface of this function is compatible with the original
+ :py:func:`platform.linux_distribution` function, supporting a subset of
+@@ -204,9 +251,8 @@ def id():
+ "fedora" Fedora
+ "sles" SUSE Linux Enterprise Server
+ "opensuse" openSUSE
+- "amzn" Amazon Linux
++ "amazon" Amazon Linux
+ "arch" Arch Linux
+- "buildroot" Buildroot
+ "cloudlinux" CloudLinux OS
+ "exherbo" Exherbo Linux
+ "gentoo" GenToo Linux
+@@ -226,8 +272,6 @@ def id():
+ "netbsd" NetBSD
+ "freebsd" FreeBSD
+ "midnightbsd" MidnightBSD
+- "rocky" Rocky Linux
+- "guix" Guix System
+ ============== =========================================
+
+ If you have a need to get distros for reliable IDs added into this set,
+@@ -322,10 +366,6 @@ def version(pretty=False, best=False):
+ sources in a fixed priority order does not always yield the most precise
+ version (e.g. for Debian 8.2, or CentOS 7.1).
+
+- Some other distributions may not provide this kind of information. In these
+- cases, an empty string would be returned. This behavior can be observed
+- with rolling releases distributions (e.g. Arch Linux).
+-
+ The *best* parameter can be used to control the approach for the returned
+ version:
+
+@@ -641,7 +681,7 @@ except ImportError:
+
+ def __get__(self, obj, owner):
+ # type: (Any, Type[Any]) -> Any
+- assert obj is not None, "call {} on an instance".format(self._fname)
++ assert obj is not None, "call {0} on an instance".format(self._fname)
+ ret = obj.__dict__[self._fname] = self._f(obj)
+ return ret
+
+@@ -736,6 +776,10 @@ class LinuxDistribution(object):
+ * :py:exc:`IOError`: Some I/O issue with an os-release file or distro
+ release file.
+
++ * :py:exc:`subprocess.CalledProcessError`: The lsb_release command had
++ some issue (other than not being available in the program execution
++ path).
++
+ * :py:exc:`UnicodeError`: A data source has unexpected characters or
+ uses an unexpected encoding.
+ """
+@@ -793,7 +837,7 @@ class LinuxDistribution(object):
+ return (
+ self.name() if full_distribution_name else self.id(),
+ self.version(),
+- self._os_release_info.get("release_codename") or self.codename(),
++ self.codename(),
+ )
+
+ def id(self):
+@@ -869,9 +913,6 @@ class LinuxDistribution(object):
+ ).get("version_id", ""),
+ self.uname_attr("release"),
+ ]
+- if self.id() == "debian" or "debian" in self.like().split():
+- # On Debian-like, add debian_version file content to candidates list.
+- versions.append(self._debian_version)
+ version = ""
+ if best:
+ # This algorithm uses the last version in priority order that has
+@@ -1114,17 +1155,12 @@ class LinuxDistribution(object):
+ # stripped, etc.), so the tokens are now either:
+ # * variable assignments: var=value
+ # * commands or their arguments (not allowed in os-release)
+- # Ignore any tokens that are not variable assignments
+ if "=" in token:
+ k, v = token.split("=", 1)
+ props[k.lower()] = v
+-
+- if "version" in props:
+- # extract release codename (if any) from version attribute
+- match = re.search(r"\((\D+)\)|,\s*(\D+)", props["version"])
+- if match:
+- release_codename = match.group(1) or match.group(2)
+- props["codename"] = props["release_codename"] = release_codename
++ else:
++ # Ignore any tokens that are not variable assignments
++ pass
+
+ if "version_codename" in props:
+ # os-release added a version_codename field. Use that in
+@@ -1135,6 +1171,16 @@ class LinuxDistribution(object):
+ elif "ubuntu_codename" in props:
+ # Same as above but a non-standard field name used on older Ubuntus
+ props["codename"] = props["ubuntu_codename"]
++ elif "version" in props:
++ # If there is no version_codename, parse it from the version
++ match = re.search(r"(\(\D+\))|,(\s+)?\D+", props["version"])
++ if match:
++ codename = match.group()
++ codename = codename.strip("()")
++ codename = codename.strip(",")
++ codename = codename.strip()
++ # codename appears within paranthese.
++ props["codename"] = codename
+
+ return props
+
+@@ -1152,7 +1198,7 @@ class LinuxDistribution(object):
+ with open(os.devnull, "wb") as devnull:
+ try:
+ cmd = ("lsb_release", "-a")
+- stdout = subprocess.check_output(cmd, stderr=devnull)
++ stdout = _check_output(cmd, stderr=devnull)
+ # Command not found or lsb_release returned error
+ except (OSError, subprocess.CalledProcessError):
+ return {}
+@@ -1187,31 +1233,18 @@ class LinuxDistribution(object):
+ @cached_property
+ def _uname_info(self):
+ # type: () -> Dict[str, str]
+- if not self.include_uname:
+- return {}
+ with open(os.devnull, "wb") as devnull:
+ try:
+ cmd = ("uname", "-rs")
+- stdout = subprocess.check_output(cmd, stderr=devnull)
++ stdout = _check_output(cmd, stderr=devnull)
+ except OSError:
+ return {}
+ content = self._to_str(stdout).splitlines()
+ return self._parse_uname_content(content)
+
+- @cached_property
+- def _debian_version(self):
+- # type: () -> str
+- try:
+- with open(os.path.join(self.etc_dir, "debian_version")) as fp:
+- return fp.readline().rstrip()
+- except (OSError, IOError):
+- return ""
+-
+ @staticmethod
+ def _parse_uname_content(lines):
+ # type: (Sequence[str]) -> Dict[str, str]
+- if not lines:
+- return {}
+ props = {}
+ match = re.search(r"^([^\s]+)\s+([\d\.]+)", lines[0].strip())
+ if match:
+@@ -1237,7 +1270,7 @@ class LinuxDistribution(object):
+ if isinstance(text, bytes):
+ return text.decode(encoding)
+ else:
+- if isinstance(text, unicode): # noqa
++ if isinstance(text, unicode): # noqa pylint: disable=undefined-variable
+ return text.encode(encoding)
+
+ return text
+@@ -1292,7 +1325,6 @@ class LinuxDistribution(object):
+ "manjaro-release",
+ "oracle-release",
+ "redhat-release",
+- "rocky-release",
+ "sl-release",
+ "slackware-version",
+ ]
+@@ -1371,36 +1403,13 @@ def main():
+ logger.setLevel(logging.DEBUG)
+ logger.addHandler(logging.StreamHandler(sys.stdout))
+
+- parser = argparse.ArgumentParser(description="OS distro info tool")
+- parser.add_argument(
+- "--json", "-j", help="Output in machine readable format", action="store_true"
+- )
+-
+- parser.add_argument(
+- "--root-dir",
+- "-r",
+- type=str,
+- dest="root_dir",
+- help="Path to the root filesystem directory (defaults to /)",
+- )
+-
+- args = parser.parse_args()
+-
+- if args.root_dir:
+- dist = LinuxDistribution(
+- include_lsb=False, include_uname=False, root_dir=args.root_dir
+- )
+- else:
+- dist = _distro
++ dist = _distro
+
+- if args.json:
+- logger.info(json.dumps(dist.info(), indent=4, sort_keys=True))
+- else:
+- logger.info("Name: %s", dist.name(pretty=True))
+- distribution_version = dist.version(pretty=True)
+- logger.info("Version: %s", distribution_version)
+- distribution_codename = dist.codename()
+- logger.info("Codename: %s", distribution_codename)
++ logger.info("Name: %s", dist.name(pretty=True))
++ distribution_version = dist.version(pretty=True)
++ logger.info("Version: %s", distribution_version)
++ distribution_codename = dist.codename()
++ logger.info("Codename: %s", distribution_codename)
+
+
+ if __name__ == "__main__":
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/hardware/linux.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/hardware/linux.py
+@@ -28,7 +28,7 @@ import time
+ from multiprocessing import cpu_count
+ from multiprocessing.pool import ThreadPool
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.common.locale import get_best_parsable_locale
+ from ansible.module_utils.common.process import get_bin_path
+ from ansible.module_utils.common.text.formatters import bytes_to_human
+@@ -170,8 +170,6 @@ class LinuxHardware(Hardware):
+ coreid = 0
+ sockets = {}
+ cores = {}
+- zp = 0
+- zmt = 0
+
+ xen = False
+ xen_paravirt = False
+@@ -211,6 +209,7 @@ class LinuxHardware(Hardware):
+
+ # model name is for Intel arch, Processor (mind the uppercase P)
+ # works for some ARM devices, like the Sheevaplug.
++ # 'ncpus active' is SPARC attribute
+ if key in ['model name', 'Processor', 'vendor_id', 'cpu', 'Vendor', 'processor']:
+ if 'processor' not in cpu_facts:
+ cpu_facts['processor'] = []
+@@ -234,12 +233,8 @@ class LinuxHardware(Hardware):
+ sockets[physid] = int(val)
+ elif key == 'siblings':
+ cores[coreid] = int(val)
+- # S390x classic cpuinfo
+ elif key == '# processors':
+- zp = int(val)
+- elif key == 'max thread id':
+- zmt = int(val) + 1
+- # SPARC
++ cpu_facts['processor_cores'] = int(val)
+ elif key == 'ncpus active':
+ i = int(val)
+
+@@ -255,20 +250,13 @@ class LinuxHardware(Hardware):
+ if collected_facts.get('ansible_architecture', '').startswith(('armv', 'aarch', 'ppc')):
+ i = processor_occurrence
+
+- if collected_facts.get('ansible_architecture') == 's390x':
+- # getting sockets would require 5.7+ with CONFIG_SCHED_TOPOLOGY
+- cpu_facts['processor_count'] = 1
+- cpu_facts['processor_cores'] = zp // zmt
+- cpu_facts['processor_threads_per_core'] = zmt
+- cpu_facts['processor_vcpus'] = zp
+- cpu_facts['processor_nproc'] = zp
+- else:
++ # FIXME
++ if collected_facts.get('ansible_architecture') != 's390x':
+ if xen_paravirt:
+ cpu_facts['processor_count'] = i
+ cpu_facts['processor_cores'] = i
+ cpu_facts['processor_threads_per_core'] = 1
+ cpu_facts['processor_vcpus'] = i
+- cpu_facts['processor_nproc'] = i
+ else:
+ if sockets:
+ cpu_facts['processor_count'] = len(sockets)
+@@ -290,25 +278,25 @@ class LinuxHardware(Hardware):
+ cpu_facts['processor_vcpus'] = (cpu_facts['processor_threads_per_core'] *
+ cpu_facts['processor_count'] * cpu_facts['processor_cores'])
+
++ # if the number of processors available to the module's
++ # thread cannot be determined, the processor count
++ # reported by /proc will be the default:
+ cpu_facts['processor_nproc'] = processor_occurrence
+
+- # if the number of processors available to the module's
+- # thread cannot be determined, the processor count
+- # reported by /proc will be the default (as previously defined)
+- try:
+- cpu_facts['processor_nproc'] = len(
+- os.sched_getaffinity(0)
+- )
+- except AttributeError:
+- # In Python < 3.3, os.sched_getaffinity() is not available
+- try:
+- cmd = get_bin_path('nproc')
+- except ValueError:
+- pass
+- else:
+- rc, out, _err = self.module.run_command(cmd)
+- if rc == 0:
+- cpu_facts['processor_nproc'] = int(out)
++ try:
++ cpu_facts['processor_nproc'] = len(
++ os.sched_getaffinity(0)
++ )
++ except AttributeError:
++ # In Python < 3.3, os.sched_getaffinity() is not available
++ try:
++ cmd = get_bin_path('nproc')
++ except ValueError:
++ pass
++ else:
++ rc, out, _err = self.module.run_command(cmd)
++ if rc == 0:
++ cpu_facts['processor_nproc'] = int(out)
+
+ return cpu_facts
+
+@@ -550,7 +538,7 @@ class LinuxHardware(Hardware):
+ # start threads to query each mount
+ results = {}
+ pool = ThreadPool(processes=min(len(mtab_entries), cpu_count()))
+- maxtime = timeout.GATHER_TIMEOUT or timeout.DEFAULT_GATHER_TIMEOUT
++ maxtime = globals().get('GATHER_TIMEOUT') or timeout.DEFAULT_GATHER_TIMEOUT
+ for fields in mtab_entries:
+ # Transform octal escape sequences
+ fields = [self._replace_octal_escapes(field) for field in fields]
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/hardware/openbsd.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/hardware/openbsd.py
+@@ -19,7 +19,7 @@ __metaclass__ = type
+ import re
+ import time
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+
+ from ansible.module_utils.facts.hardware.base import Hardware, HardwareCollector
+ from ansible.module_utils.facts import timeout
+@@ -94,7 +94,7 @@ class OpenBSDHardware(Hardware):
+ rc, out, err = self.module.run_command("/usr/bin/vmstat")
+ if rc == 0:
+ memory_facts['memfree_mb'] = int(out.splitlines()[-1].split()[4]) // 1024
+- memory_facts['memtotal_mb'] = int(self.sysctl['hw.physmem']) // 1024 // 1024
++ memory_facts['memtotal_mb'] = int(self.sysctl['hw.usermem']) // 1024 // 1024
+
+ # Get swapctl info. swapctl output looks like:
+ # total: 69268 1K-blocks allocated, 0 used, 69268 available
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/hardware/sunos.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/hardware/sunos.py
+@@ -175,7 +175,9 @@ class SunOSHardware(Hardware):
+
+ prtdiag_path = self.module.get_bin_path("prtdiag", opt_dirs=[platform_sbin])
+ rc, out, err = self.module.run_command(prtdiag_path)
+- # rc returns 1
++ """
++ rc returns 1
++ """
+ if out:
+ system_conf = out.split('\n')[0]
+
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/network/fc_wwn.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/network/fc_wwn.py
+@@ -46,14 +46,18 @@ class FcWwnInitiatorFactCollector(BaseFa
+ for line in get_file_lines(fcfile):
+ fc_facts['fibre_channel_wwn'].append(line.rstrip()[2:])
+ elif sys.platform.startswith('sunos'):
+- # on solaris 10 or solaris 11 should use `fcinfo hba-port`
+- # TBD (not implemented): on solaris 9 use `prtconf -pv`
++ """
++ on solaris 10 or solaris 11 should use `fcinfo hba-port`
++ TBD (not implemented): on solaris 9 use `prtconf -pv`
++ """
+ cmd = module.get_bin_path('fcinfo')
+ if cmd:
+ cmd = cmd + " hba-port"
+ rc, fcinfo_out, err = module.run_command(cmd)
++ """
+ # fcinfo hba-port | grep "Port WWN"
+- # HBA Port WWN: 10000090fa1658de
++ HBA Port WWN: 10000090fa1658de
++ """
+ if rc == 0 and fcinfo_out:
+ for line in fcinfo_out.splitlines():
+ if 'Port WWN' in line:
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/network/iscsi.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/network/iscsi.py
+@@ -19,6 +19,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ import sys
++import subprocess
+
+ import ansible.module_utils.compat.typing as t
+
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/network/linux.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/network/linux.py
+@@ -59,46 +59,8 @@ class LinuxNetwork(Network):
+ network_facts['default_ipv6'] = default_ipv6
+ network_facts['all_ipv4_addresses'] = ips['all_ipv4_addresses']
+ network_facts['all_ipv6_addresses'] = ips['all_ipv6_addresses']
+- network_facts['locally_reachable_ips'] = self.get_locally_reachable_ips(ip_path)
+ return network_facts
+
+- # List all `scope host` routes/addresses.
+- # They belong to routes, but it means the whole prefix is reachable
+- # locally, regardless of specific IP addresses.
+- # E.g.: 192.168.0.0/24, any IP address is reachable from this range
+- # if assigned as scope host.
+- def get_locally_reachable_ips(self, ip_path):
+- locally_reachable_ips = dict(
+- ipv4=[],
+- ipv6=[],
+- )
+-
+- def parse_locally_reachable_ips(output):
+- for line in output.splitlines():
+- if not line:
+- continue
+- words = line.split()
+- if words[0] != 'local':
+- continue
+- address = words[1]
+- if ":" in address:
+- if address not in locally_reachable_ips['ipv6']:
+- locally_reachable_ips['ipv6'].append(address)
+- else:
+- if address not in locally_reachable_ips['ipv4']:
+- locally_reachable_ips['ipv4'].append(address)
+-
+- args = [ip_path, '-4', 'route', 'show', 'table', 'local']
+- rc, routes, dummy = self.module.run_command(args)
+- if rc == 0:
+- parse_locally_reachable_ips(routes)
+- args = [ip_path, '-6', 'route', 'show', 'table', 'local']
+- rc, routes, dummy = self.module.run_command(args)
+- if rc == 0:
+- parse_locally_reachable_ips(routes)
+-
+- return locally_reachable_ips
+-
+ def get_default_interfaces(self, ip_path, collected_facts=None):
+ collected_facts = collected_facts or {}
+ # Use the commands:
+@@ -274,7 +236,7 @@ class LinuxNetwork(Network):
+ elif words[0] == 'inet6':
+ if 'peer' == words[2]:
+ address = words[1]
+- dummy, prefix = words[3].split('/')
++ _, prefix = words[3].split('/')
+ scope = words[5]
+ else:
+ address, prefix = words[1].split('/')
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/network/nvme.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/network/nvme.py
+@@ -19,6 +19,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ import sys
++import subprocess
+
+ import ansible.module_utils.compat.typing as t
+
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/other/facter.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/other/facter.py
+@@ -1,5 +1,17 @@
+-# Copyright (c) 2023 Ansible Project
+-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
++# This file is part of Ansible
++#
++# Ansible is free software: you can redistribute it and/or modify
++# it under the terms of the GNU General Public License as published by
++# the Free Software Foundation, either version 3 of the License, or
++# (at your option) any later version.
++#
++# Ansible is distributed in the hope that it will be useful,
++# but WITHOUT ANY WARRANTY; without even the implied warranty of
++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++# GNU General Public License for more details.
++#
++# You should have received a copy of the GNU General Public License
++# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+@@ -9,6 +21,7 @@ import json
+ import ansible.module_utils.compat.typing as t
+
+ from ansible.module_utils.facts.namespace import PrefixFactNamespace
++
+ from ansible.module_utils.facts.collector import BaseFactCollector
+
+
+@@ -36,12 +49,6 @@ class FacterFactCollector(BaseFactCollec
+ # if facter is installed, and we can use --json because
+ # ruby-json is ALSO installed, include facter data in the JSON
+ rc, out, err = module.run_command(facter_path + " --puppet --json")
+-
+- # for some versions of facter, --puppet returns an error if puppet is not present,
+- # try again w/o it, other errors should still appear and be sent back
+- if rc != 0:
+- rc, out, err = module.run_command(facter_path + " --json")
+-
+ return rc, out, err
+
+ def get_facter_output(self, module):
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/sysctl.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/sysctl.py
+@@ -18,7 +18,7 @@ __metaclass__ = type
+
+ import re
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+
+
+ def get_sysctl(module, prefixes):
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/system/caps.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/system/caps.py
+@@ -20,6 +20,7 @@ __metaclass__ = type
+
+ import ansible.module_utils.compat.typing as t
+
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.facts.collector import BaseFactCollector
+
+
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/system/date_time.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/system/date_time.py
+@@ -22,8 +22,8 @@ import datetime
+ import time
+
+ import ansible.module_utils.compat.typing as t
++
+ from ansible.module_utils.facts.collector import BaseFactCollector
+-from ansible.module_utils.compat.datetime import utcfromtimestamp
+
+
+ class DateTimeFactCollector(BaseFactCollector):
+@@ -37,7 +37,7 @@ class DateTimeFactCollector(BaseFactColl
+ # Store the timestamp once, then get local and UTC versions from that
+ epoch_ts = time.time()
+ now = datetime.datetime.fromtimestamp(epoch_ts)
+- utcnow = utcfromtimestamp(epoch_ts).replace(tzinfo=None)
++ utcnow = datetime.datetime.utcfromtimestamp(epoch_ts)
+
+ date_time_facts['year'] = now.strftime('%Y')
+ date_time_facts['month'] = now.strftime('%m')
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/system/distribution.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/system/distribution.py
+@@ -524,7 +524,7 @@ class Distribution(object):
+ 'Solaris': ['Solaris', 'Nexenta', 'OmniOS', 'OpenIndiana', 'SmartOS'],
+ 'Slackware': ['Slackware'],
+ 'Altlinux': ['Altlinux'],
+- 'SMGL': ['SMGL'],
++ 'SGML': ['SGML'],
+ 'Gentoo': ['Gentoo', 'Funtoo'],
+ 'Alpine': ['Alpine'],
+ 'AIX': ['AIX'],
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/system/local.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/system/local.py
+@@ -23,10 +23,9 @@ import stat
+
+ import ansible.module_utils.compat.typing as t
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.facts.utils import get_file_content
+ from ansible.module_utils.facts.collector import BaseFactCollector
+-from ansible.module_utils.six import PY3
+ from ansible.module_utils.six.moves import configparser, StringIO
+
+
+@@ -92,10 +91,7 @@ class LocalFactCollector(BaseFactCollect
+ # if that fails read it with ConfigParser
+ cp = configparser.ConfigParser()
+ try:
+- if PY3:
+- cp.read_file(StringIO(out))
+- else:
+- cp.readfp(StringIO(out))
++ cp.readfp(StringIO(out))
+ except configparser.Error:
+ fact = "error loading facts as JSON or ini - please check content: %s" % fn
+ module.warn(fact)
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/system/pkg_mgr.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/system/pkg_mgr.py
+@@ -17,13 +17,7 @@ from ansible.module_utils.facts.collecto
+ # ansible module, use that as the value for the 'name' key.
+ PKG_MGRS = [{'path': '/usr/bin/rpm-ostree', 'name': 'atomic_container'},
+ {'path': '/usr/bin/yum', 'name': 'yum'},
+-
+- # NOTE the `path` key for dnf/dnf5 is effectively discarded when matched for Red Hat OS family,
+- # special logic to infer the default `pkg_mgr` is used in `PkgMgrFactCollector._check_rh_versions()`
+- # leaving them here so a list of package modules can be constructed by iterating over `name` keys
+- {'path': '/usr/bin/dnf-3', 'name': 'dnf'},
+- {'path': '/usr/bin/dnf5', 'name': 'dnf5'},
+-
++ {'path': '/usr/bin/dnf', 'name': 'dnf'},
+ {'path': '/usr/bin/apt-get', 'name': 'apt'},
+ {'path': '/usr/bin/zypper', 'name': 'zypper'},
+ {'path': '/usr/sbin/urpmi', 'name': 'urpmi'},
+@@ -56,7 +50,10 @@ class OpenBSDPkgMgrFactCollector(BaseFac
+ _platform = 'OpenBSD'
+
+ def collect(self, module=None, collected_facts=None):
+- return {'pkg_mgr': 'openbsd_pkg'}
++ facts_dict = {}
++
++ facts_dict['pkg_mgr'] = 'openbsd_pkg'
++ return facts_dict
+
+
+ # the fact ends up being 'pkg_mgr' so stick with that naming/spelling
+@@ -66,42 +63,49 @@ class PkgMgrFactCollector(BaseFactCollec
+ _platform = 'Generic'
+ required_facts = set(['distribution'])
+
+- def __init__(self, *args, **kwargs):
+- super(PkgMgrFactCollector, self).__init__(*args, **kwargs)
+- self._default_unknown_pkg_mgr = 'unknown'
++ def _pkg_mgr_exists(self, pkg_mgr_name):
++ for cur_pkg_mgr in [pkg_mgr for pkg_mgr in PKG_MGRS if pkg_mgr['name'] == pkg_mgr_name]:
++ if os.path.exists(cur_pkg_mgr['path']):
++ return pkg_mgr_name
+
+ def _check_rh_versions(self, pkg_mgr_name, collected_facts):
+ if os.path.exists('/run/ostree-booted'):
+ return "atomic_container"
+
+- # Reset whatever was matched from PKG_MGRS, infer the default pkg_mgr below
+- pkg_mgr_name = self._default_unknown_pkg_mgr
+- # Since /usr/bin/dnf and /usr/bin/microdnf can point to different versions of dnf in different distributions
+- # the only way to infer the default package manager is to look at the binary they are pointing to.
+- # /usr/bin/microdnf is likely used only in fedora minimal container so /usr/bin/dnf takes precedence
+- for bin_path in ('/usr/bin/dnf', '/usr/bin/microdnf'):
+- if os.path.exists(bin_path):
+- pkg_mgr_name = 'dnf5' if os.path.realpath(bin_path) == '/usr/bin/dnf5' else 'dnf'
+- break
+-
+- try:
+- major_version = collected_facts['ansible_distribution_major_version']
+- if collected_facts['ansible_distribution'] == 'Kylin Linux Advanced Server':
+- major_version = major_version.lstrip('V')
+- distro_major_ver = int(major_version)
+- except ValueError:
+- # a non integer magical future version
+- return self._default_unknown_pkg_mgr
+-
+- if (
+- (collected_facts['ansible_distribution'] == 'Fedora' and distro_major_ver < 23)
+- or (collected_facts['ansible_distribution'] == 'Kylin Linux Advanced Server' and distro_major_ver < 10)
+- or (collected_facts['ansible_distribution'] == 'Amazon' and distro_major_ver < 2022)
+- or (collected_facts['ansible_distribution'] == 'TencentOS' and distro_major_ver < 3)
+- or distro_major_ver < 8 # assume RHEL or a clone
+- ) and any(pm for pm in PKG_MGRS if pm['name'] == 'yum' and os.path.exists(pm['path'])):
+- pkg_mgr_name = 'yum'
+-
++ if collected_facts['ansible_distribution'] == 'Fedora':
++ try:
++ if int(collected_facts['ansible_distribution_major_version']) < 23:
++ if self._pkg_mgr_exists('yum'):
++ pkg_mgr_name = 'yum'
++
++ else:
++ if self._pkg_mgr_exists('dnf'):
++ pkg_mgr_name = 'dnf'
++ except ValueError:
++ # If there's some new magical Fedora version in the future,
++ # just default to dnf
++ pkg_mgr_name = 'dnf'
++ elif collected_facts['ansible_distribution'] == 'Amazon':
++ try:
++ if int(collected_facts['ansible_distribution_major_version']) < 2022:
++ if self._pkg_mgr_exists('yum'):
++ pkg_mgr_name = 'yum'
++ else:
++ if self._pkg_mgr_exists('dnf'):
++ pkg_mgr_name = 'dnf'
++ except ValueError:
++ pkg_mgr_name = 'dnf'
++ else:
++ # If it's not one of the above and it's Red Hat family of distros, assume
++ # RHEL or a clone. For versions of RHEL < 8 that Ansible supports, the
++ # vendor supported official package manager is 'yum' and in RHEL 8+
++ # (as far as we know at the time of this writing) it is 'dnf'.
++ # If anyone wants to force a non-official package manager then they
++ # can define a provider to either the package or yum action plugins.
++ if int(collected_facts['ansible_distribution_major_version']) < 8:
++ pkg_mgr_name = 'yum'
++ else:
++ pkg_mgr_name = 'dnf'
+ return pkg_mgr_name
+
+ def _check_apt_flavor(self, pkg_mgr_name):
+@@ -132,9 +136,10 @@ class PkgMgrFactCollector(BaseFactCollec
+ return PKG_MGRS
+
+ def collect(self, module=None, collected_facts=None):
++ facts_dict = {}
+ collected_facts = collected_facts or {}
+
+- pkg_mgr_name = self._default_unknown_pkg_mgr
++ pkg_mgr_name = 'unknown'
+ for pkg in self.pkg_mgrs(collected_facts):
+ if os.path.exists(pkg['path']):
+ pkg_mgr_name = pkg['name']
+@@ -156,4 +161,5 @@ class PkgMgrFactCollector(BaseFactCollec
+ if pkg_mgr_name == 'apt':
+ pkg_mgr_name = self._check_apt_flavor(pkg_mgr_name)
+
+- return {'pkg_mgr': pkg_mgr_name}
++ facts_dict['pkg_mgr'] = pkg_mgr_name
++ return facts_dict
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/facts/system/service_mgr.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/facts/system/service_mgr.py
+@@ -24,7 +24,7 @@ import re
+
+ import ansible.module_utils.compat.typing as t
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+ from ansible.module_utils.facts.utils import get_file_content
+ from ansible.module_utils.facts.collector import BaseFactCollector
+@@ -47,7 +47,7 @@ class ServiceMgrFactCollector(BaseFactCo
+ # tools must be installed
+ if module.get_bin_path('systemctl'):
+
+- # this should show if systemd is the boot init system, if checking init failed to mark as systemd
++ # this should show if systemd is the boot init system, if checking init faild to mark as systemd
+ # these mirror systemd's own sd_boot test http://www.freedesktop.org/software/systemd/man/sd_booted.html
+ for canary in ["/run/systemd/system/", "/dev/.run/systemd/", "/dev/.systemd/"]:
+ if os.path.exists(canary):
+@@ -131,8 +131,6 @@ class ServiceMgrFactCollector(BaseFactCo
+ service_mgr_name = 'smf'
+ elif collected_facts.get('ansible_distribution') == 'OpenWrt':
+ service_mgr_name = 'openwrt_init'
+- elif collected_facts.get('ansible_distribution') == 'SMGL':
+- service_mgr_name = 'simpleinit_msb'
+ elif collected_facts.get('ansible_system') == 'Linux':
+ # FIXME: mv is_systemd_managed
+ if self.is_systemd_managed(module=module):
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/json_utils.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/json_utils.py
+@@ -27,7 +27,7 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-import json # pylint: disable=unused-import
++import json
+
+
+ # NB: a copy of this function exists in ../../modules/core/async_wrapper.py. Ensure any
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/parsing/convert_bool.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/parsing/convert_bool.py
+@@ -5,7 +5,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ from ansible.module_utils.six import binary_type, text_type
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+
+
+ BOOLEANS_TRUE = frozenset(('y', 'yes', 'on', '1', 'true', 't', 1, 1.0, True))
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1
++++ ansible-core-2.16.5/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1
+@@ -65,10 +65,6 @@ Function Add-CSharpType {
+ * Create automatic type accelerators to simplify long namespace names (Ansible 2.9+)
+
+ //TypeAccelerator -Name <AcceleratorName> -TypeName <Name of compiled type>
+-
+- * Compile with unsafe support (Ansible 2.15+)
+-
+- //AllowUnsafe
+ #>
+ param(
+ [Parameter(Mandatory = $true)][AllowEmptyCollection()][String[]]$References,
+@@ -121,7 +117,6 @@ Function Add-CSharpType {
+ $assembly_pattern = [Regex]"//\s*AssemblyReference\s+-(?<Parameter>(Name)|(Type))\s+(?<Name>[\w.]*)(\s+-CLR\s+(?<CLR>Core|Framework))?"
+ $no_warn_pattern = [Regex]"//\s*NoWarn\s+-Name\s+(?<Name>[\w\d]*)(\s+-CLR\s+(?<CLR>Core|Framework))?"
+ $type_pattern = [Regex]"//\s*TypeAccelerator\s+-Name\s+(?<Name>[\w.]*)\s+-TypeName\s+(?<TypeName>[\w.]*)"
+- $allow_unsafe_pattern = [Regex]"//\s*AllowUnsafe?"
+
+ # PSCore vs PSDesktop use different methods to compile the code,
+ # PSCore uses Roslyn and can compile the code purely in memory
+@@ -147,13 +142,11 @@ Function Add-CSharpType {
+ $ignore_warnings = New-Object -TypeName 'System.Collections.Generic.Dictionary`2[[String], [Microsoft.CodeAnalysis.ReportDiagnostic]]'
+ $parse_options = ([Microsoft.CodeAnalysis.CSharp.CSharpParseOptions]::Default).WithPreprocessorSymbols($defined_symbols)
+ $syntax_trees = [System.Collections.Generic.List`1[Microsoft.CodeAnalysis.SyntaxTree]]@()
+- $allow_unsafe = $false
+ foreach ($reference in $References) {
+ # scan through code and add any assemblies that match
+ # //AssemblyReference -Name ... [-CLR Core]
+ # //NoWarn -Name ... [-CLR Core]
+ # //TypeAccelerator -Name ... -TypeName ...
+- # //AllowUnsafe
+ $assembly_matches = $assembly_pattern.Matches($reference)
+ foreach ($match in $assembly_matches) {
+ $clr = $match.Groups["CLR"].Value
+@@ -187,10 +180,6 @@ Function Add-CSharpType {
+ foreach ($match in $type_matches) {
+ $type_accelerators.Add(@{Name = $match.Groups["Name"].Value; TypeName = $match.Groups["TypeName"].Value })
+ }
+-
+- if ($allow_unsafe_pattern.Matches($reference).Count) {
+- $allow_unsafe = $true
+- }
+ }
+
+ # Release seems to contain the correct line numbers compared to
+@@ -205,10 +194,6 @@ Function Add-CSharpType {
+ $compiler_options = $compiler_options.WithSpecificDiagnosticOptions($ignore_warnings)
+ }
+
+- if ($allow_unsafe) {
+- $compiler_options = $compiler_options.WithAllowUnsafe($true)
+- }
+-
+ # create compilation object
+ $compilation = [Microsoft.CodeAnalysis.CSharp.CSharpCompilation]::Create(
+ [System.Guid]::NewGuid().ToString(),
+@@ -312,7 +297,6 @@ Function Add-CSharpType {
+ # //AssemblyReference -Name ... [-CLR Framework]
+ # //NoWarn -Name ... [-CLR Framework]
+ # //TypeAccelerator -Name ... -TypeName ...
+- # //AllowUnsafe
+ $assembly_matches = $assembly_pattern.Matches($reference)
+ foreach ($match in $assembly_matches) {
+ $clr = $match.Groups["CLR"].Value
+@@ -346,10 +330,6 @@ Function Add-CSharpType {
+ foreach ($match in $type_matches) {
+ $type_accelerators.Add(@{Name = $match.Groups["Name"].Value; TypeName = $match.Groups["TypeName"].Value })
+ }
+-
+- if ($allow_unsafe_pattern.Matches($reference).Count) {
+- $compiler_options.Add("/unsafe") > $null
+- }
+ }
+ if ($ignore_warnings.Count -gt 0) {
+ $compiler_options.Add("/nowarn:" + ([String]::Join(",", $ignore_warnings.ToArray()))) > $null
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Backup.psm1
++++ ansible-core-2.16.5/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Backup.psm1
+@@ -18,7 +18,7 @@ Function Backup-File {
+ Process {
+ $backup_path = $null
+ if (Test-Path -LiteralPath $path -PathType Leaf) {
+- $backup_path = "$path.$pid." + [DateTime]::Now.ToString("yyyyMMdd-HHmmss") + ".bak"
++ $backup_path = "$path.$pid." + [DateTime]::Now.ToString("yyyyMMdd-HHmmss") + ".bak";
+ Try {
+ Copy-Item -LiteralPath $path -Destination $backup_path
+ }
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1
++++ ansible-core-2.16.5/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1
+@@ -354,16 +354,16 @@ Function Get-FileChecksum($path, $algori
+ $hash = $raw_hash.Hash.ToLower()
+ }
+ Else {
+- $fp = [System.IO.File]::Open($path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
+- $hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower()
+- $fp.Dispose()
++ $fp = [System.IO.File]::Open($path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite);
++ $hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower();
++ $fp.Dispose();
+ }
+ }
+ ElseIf (Test-Path -LiteralPath $path -PathType Container) {
+- $hash = "3"
++ $hash = "3";
+ }
+ Else {
+- $hash = "1"
++ $hash = "1";
+ }
+ return $hash
+ }
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/pycompat24.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/pycompat24.py
+@@ -47,7 +47,45 @@ def get_exception():
+ return sys.exc_info()[1]
+
+
+-from ast import literal_eval
++try:
++ # Python 2.6+
++ from ast import literal_eval
++except ImportError:
++ # a replacement for literal_eval that works with python 2.4. from:
++ # https://mail.python.org/pipermail/python-list/2009-September/551880.html
++ # which is essentially a cut/paste from an earlier (2.6) version of python's
++ # ast.py
++ from compiler import ast, parse
++ from ansible.module_utils.six import binary_type, integer_types, string_types, text_type
+
++ def literal_eval(node_or_string): # type: ignore[misc]
++ """
++ Safely evaluate an expression node or a string containing a Python
++ expression. The string or node provided may only consist of the following
++ Python literal structures: strings, numbers, tuples, lists, dicts, booleans,
++ and None.
++ """
++ _safe_names = {'None': None, 'True': True, 'False': False}
++ if isinstance(node_or_string, string_types):
++ node_or_string = parse(node_or_string, mode='eval')
++ if isinstance(node_or_string, ast.Expression):
++ node_or_string = node_or_string.node
++
++ def _convert(node):
++ if isinstance(node, ast.Const) and isinstance(node.value, (text_type, binary_type, float, complex) + integer_types):
++ return node.value
++ elif isinstance(node, ast.Tuple):
++ return tuple(map(_convert, node.nodes))
++ elif isinstance(node, ast.List):
++ return list(map(_convert, node.nodes))
++ elif isinstance(node, ast.Dict):
++ return dict((_convert(k), _convert(v)) for k, v in node.items())
++ elif isinstance(node, ast.Name):
++ if node.name in _safe_names:
++ return _safe_names[node.name]
++ elif isinstance(node, ast.UnarySub):
++ return -_convert(node.expr) # pylint: disable=invalid-unary-operand-type
++ raise ValueError('malformed string')
++ return _convert(node_or_string)
+
+ __all__ = ('get_exception', 'literal_eval')
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/service.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/service.py
+@@ -39,7 +39,7 @@ import subprocess
+ import traceback
+
+ from ansible.module_utils.six import PY2, b
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+
+
+ def sysv_is_enabled(name, runlevel=None):
+@@ -207,20 +207,17 @@ def daemonize(module, cmd):
+ p = subprocess.Popen(run_cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=lambda: os.close(pipe[1]))
+ fds = [p.stdout, p.stderr]
+
+- # loop reading output till it is done
++ # loop reading output till its done
+ output = {p.stdout: b(""), p.stderr: b("")}
+ while fds:
+ rfd, wfd, efd = select.select(fds, [], fds, 1)
+- if (rfd + wfd + efd) or p.poll() is None:
++ if (rfd + wfd + efd) or p.poll():
+ for out in list(fds):
+ if out in rfd:
+ data = os.read(out.fileno(), chunk)
+- if data:
+- output[out] += to_bytes(data, errors=errors)
+- else:
++ if not data:
+ fds.remove(out)
+- else:
+- break
++ output[out] += b(data)
+
+ # even after fds close, we might want to wait for pid to die
+ p.wait()
+@@ -249,7 +246,7 @@ def daemonize(module, cmd):
+ data = os.read(pipe[0], chunk)
+ if not data:
+ break
+- return_data += to_bytes(data, errors=errors)
++ return_data += b(data)
+
+ # Note: no need to specify encoding on py3 as this module sends the
+ # pickle to itself (thus same python interpreter so we aren't mixing
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/urls.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/urls.py
+@@ -53,7 +53,7 @@ import socket
+ import sys
+ import tempfile
+ import traceback
+-import types # pylint: disable=unused-import
++import types
+
+ from contextlib import contextmanager
+
+@@ -88,7 +88,7 @@ from ansible.module_utils.common.collect
+ from ansible.module_utils.six import PY2, PY3, string_types
+ from ansible.module_utils.six.moves import cStringIO
+ from ansible.module_utils.basic import get_distribution, missing_required_lib
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+
+ try:
+ # python3
+@@ -99,7 +99,7 @@ except ImportError:
+ import urllib2 as urllib_request # type: ignore[no-redef]
+ from urllib2 import AbstractHTTPHandler, BaseHandler # type: ignore[no-redef]
+
+-urllib_request.HTTPRedirectHandler.http_error_308 = urllib_request.HTTPRedirectHandler.http_error_307 # type: ignore[attr-defined,assignment]
++urllib_request.HTTPRedirectHandler.http_error_308 = urllib_request.HTTPRedirectHandler.http_error_307 # type: ignore[attr-defined]
+
+ try:
+ from ansible.module_utils.six.moves.urllib.parse import urlparse, urlunparse, unquote
+@@ -115,7 +115,7 @@ except Exception:
+
+ try:
+ # SNI Handling needs python2.7.9's SSLContext
+- from ssl import create_default_context, SSLContext # pylint: disable=unused-import
++ from ssl import create_default_context, SSLContext
+ HAS_SSLCONTEXT = True
+ except ImportError:
+ HAS_SSLCONTEXT = False
+@@ -129,13 +129,13 @@ if not HAS_SSLCONTEXT:
+ try:
+ from urllib3.contrib.pyopenssl import PyOpenSSLContext
+ except Exception:
+- from requests.packages.urllib3.contrib.pyopenssl import PyOpenSSLContext # type: ignore[no-redef]
++ from requests.packages.urllib3.contrib.pyopenssl import PyOpenSSLContext
+ HAS_URLLIB3_PYOPENSSLCONTEXT = True
+ except Exception:
+ # urllib3<1.15,>=1.6
+ try:
+ try:
+- from urllib3.contrib.pyopenssl import ssl_wrap_socket # type: ignore[attr-defined]
++ from urllib3.contrib.pyopenssl import ssl_wrap_socket
+ except Exception:
+ from requests.packages.urllib3.contrib.pyopenssl import ssl_wrap_socket
+ HAS_URLLIB3_SSL_WRAP_SOCKET = True
+@@ -160,7 +160,7 @@ if not HAS_SSLCONTEXT and HAS_SSL:
+ libssl = ctypes.CDLL(libssl_name)
+ for method in ('TLSv1_1_method', 'TLSv1_2_method'):
+ try:
+- libssl[method] # pylint: disable=pointless-statement
++ libssl[method]
+ # Found something - we'll let openssl autonegotiate and hope
+ # the server has disabled sslv2 and 3. best we can do.
+ PROTOCOL = ssl.PROTOCOL_SSLv23
+@@ -181,7 +181,7 @@ try:
+ from ssl import match_hostname, CertificateError
+ except ImportError:
+ try:
+- from backports.ssl_match_hostname import match_hostname, CertificateError # type: ignore[assignment]
++ from backports.ssl_match_hostname import match_hostname, CertificateError # type: ignore[misc]
+ except ImportError:
+ HAS_MATCH_HOSTNAME = False
+
+@@ -196,7 +196,7 @@ except ImportError:
+
+ # Old import for GSSAPI authentication, this is not used in urls.py but kept for backwards compatibility.
+ try:
+- import urllib_gssapi # pylint: disable=unused-import
++ import urllib_gssapi
+ HAS_GSSAPI = True
+ except ImportError:
+ HAS_GSSAPI = False
+@@ -288,7 +288,7 @@ if not HAS_MATCH_HOSTNAME:
+ # The following block of code is under the terms and conditions of the
+ # Python Software Foundation License
+
+- # The match_hostname() function from Python 3.4, essential when using SSL.
++ """The match_hostname() function from Python 3.4, essential when using SSL."""
+
+ try:
+ # Divergence: Python-3.7+'s _ssl has this exception type but older Pythons do not
+@@ -535,18 +535,15 @@ HTTPSClientAuthHandler = None
+ UnixHTTPSConnection = None
+ if hasattr(httplib, 'HTTPSConnection') and hasattr(urllib_request, 'HTTPSHandler'):
+ class CustomHTTPSConnection(httplib.HTTPSConnection): # type: ignore[no-redef]
+- def __init__(self, client_cert=None, client_key=None, *args, **kwargs):
++ def __init__(self, *args, **kwargs):
+ httplib.HTTPSConnection.__init__(self, *args, **kwargs)
+ self.context = None
+ if HAS_SSLCONTEXT:
+ self.context = self._context
+ elif HAS_URLLIB3_PYOPENSSLCONTEXT:
+ self.context = self._context = PyOpenSSLContext(PROTOCOL)
+-
+- self._client_cert = client_cert
+- self._client_key = client_key
+- if self.context and self._client_cert:
+- self.context.load_cert_chain(self._client_cert, self._client_key)
++ if self.context and self.cert_file:
++ self.context.load_cert_chain(self.cert_file, self.key_file)
+
+ def connect(self):
+ "Connect to a host on a given (SSL) port."
+@@ -567,10 +564,10 @@ if hasattr(httplib, 'HTTPSConnection') a
+ if HAS_SSLCONTEXT or HAS_URLLIB3_PYOPENSSLCONTEXT:
+ self.sock = self.context.wrap_socket(sock, server_hostname=server_hostname)
+ elif HAS_URLLIB3_SSL_WRAP_SOCKET:
+- self.sock = ssl_wrap_socket(sock, keyfile=self._client_key, cert_reqs=ssl.CERT_NONE, # pylint: disable=used-before-assignment
+- certfile=self._client_cert, ssl_version=PROTOCOL, server_hostname=server_hostname)
++ self.sock = ssl_wrap_socket(sock, keyfile=self.key_file, cert_reqs=ssl.CERT_NONE, # pylint: disable=used-before-assignment
++ certfile=self.cert_file, ssl_version=PROTOCOL, server_hostname=server_hostname)
+ else:
+- self.sock = ssl.wrap_socket(sock, keyfile=self._client_key, certfile=self._client_cert, ssl_version=PROTOCOL)
++ self.sock = ssl.wrap_socket(sock, keyfile=self.key_file, certfile=self.cert_file, ssl_version=PROTOCOL)
+
+ class CustomHTTPSHandler(urllib_request.HTTPSHandler): # type: ignore[no-redef]
+
+@@ -605,6 +602,10 @@ if hasattr(httplib, 'HTTPSConnection') a
+ return self.do_open(self._build_https_connection, req)
+
+ def _build_https_connection(self, host, **kwargs):
++ kwargs.update({
++ 'cert_file': self.client_cert,
++ 'key_file': self.client_key,
++ })
+ try:
+ kwargs['context'] = self._context
+ except AttributeError:
+@@ -612,7 +613,7 @@ if hasattr(httplib, 'HTTPSConnection') a
+ if self._unix_socket:
+ return UnixHTTPSConnection(self._unix_socket)(host, **kwargs)
+ if not HAS_SSLCONTEXT:
+- return CustomHTTPSConnection(host, client_cert=self.client_cert, client_key=self.client_key, **kwargs)
++ return CustomHTTPSConnection(host, **kwargs)
+ return httplib.HTTPSConnection(host, **kwargs)
+
+ @contextmanager
+@@ -771,18 +772,6 @@ def extract_pem_certs(b_data):
+ yield match.group(0)
+
+
+-def _py2_get_param(headers, param, header='content-type'):
+- m = httplib.HTTPMessage(io.StringIO())
+- cd = headers.getheader(header) or ''
+- try:
+- m.plisttext = cd[cd.index(';'):]
+- m.parseplist()
+- except ValueError:
+- return None
+-
+- return m.getparam(param)
+-
+-
+ def get_response_filename(response):
+ url = response.geturl()
+ path = urlparse(url)[2]
+@@ -790,12 +779,7 @@ def get_response_filename(response):
+ if filename:
+ filename = unquote(filename)
+
+- if PY2:
+- get_param = functools.partial(_py2_get_param, response.headers)
+- else:
+- get_param = response.headers.get_param
+-
+- return get_param('filename', header='content-disposition') or filename
++ return response.headers.get_param('filename', header='content-disposition') or filename
+
+
+ def parse_content_type(response):
+@@ -882,7 +866,7 @@ def RedirectHandlerFactory(follow_redire
+ to determine how redirects should be handled in urllib2.
+ """
+
+- def redirect_request(self, req, fp, code, msg, headers, newurl):
++ def redirect_request(self, req, fp, code, msg, hdrs, newurl):
+ if not any((HAS_SSLCONTEXT, HAS_URLLIB3_PYOPENSSLCONTEXT)):
+ handler = maybe_add_ssl_handler(newurl, validate_certs, ca_path=ca_path, ciphers=ciphers)
+ if handler:
+@@ -890,23 +874,23 @@ def RedirectHandlerFactory(follow_redire
+
+ # Preserve urllib2 compatibility
+ if follow_redirects == 'urllib2':
+- return urllib_request.HTTPRedirectHandler.redirect_request(self, req, fp, code, msg, headers, newurl)
++ return urllib_request.HTTPRedirectHandler.redirect_request(self, req, fp, code, msg, hdrs, newurl)
+
+ # Handle disabled redirects
+ elif follow_redirects in ['no', 'none', False]:
+- raise urllib_error.HTTPError(newurl, code, msg, headers, fp)
++ raise urllib_error.HTTPError(newurl, code, msg, hdrs, fp)
+
+ method = req.get_method()
+
+ # Handle non-redirect HTTP status or invalid follow_redirects
+ if follow_redirects in ['all', 'yes', True]:
+ if code < 300 or code >= 400:
+- raise urllib_error.HTTPError(req.get_full_url(), code, msg, headers, fp)
++ raise urllib_error.HTTPError(req.get_full_url(), code, msg, hdrs, fp)
+ elif follow_redirects == 'safe':
+ if code < 300 or code >= 400 or method not in ('GET', 'HEAD'):
+- raise urllib_error.HTTPError(req.get_full_url(), code, msg, headers, fp)
++ raise urllib_error.HTTPError(req.get_full_url(), code, msg, hdrs, fp)
+ else:
+- raise urllib_error.HTTPError(req.get_full_url(), code, msg, headers, fp)
++ raise urllib_error.HTTPError(req.get_full_url(), code, msg, hdrs, fp)
+
+ try:
+ # Python 2-3.3
+@@ -923,12 +907,12 @@ def RedirectHandlerFactory(follow_redire
+ # Support redirect with payload and original headers
+ if code in (307, 308):
+ # Preserve payload and headers
+- req_headers = req.headers
++ headers = req.headers
+ else:
+ # Do not preserve payload and filter headers
+ data = None
+- req_headers = dict((k, v) for k, v in req.headers.items()
+- if k.lower() not in ("content-length", "content-type", "transfer-encoding"))
++ headers = dict((k, v) for k, v in req.headers.items()
++ if k.lower() not in ("content-length", "content-type", "transfer-encoding"))
+
+ # http://tools.ietf.org/html/rfc7231#section-6.4.4
+ if code == 303 and method != 'HEAD':
+@@ -945,7 +929,7 @@ def RedirectHandlerFactory(follow_redire
+
+ return RequestWithMethod(newurl,
+ method=method,
+- headers=req_headers,
++ headers=headers,
+ data=data,
+ origin_req_host=origin_req_host,
+ unverifiable=True,
+@@ -995,7 +979,7 @@ def atexit_remove_file(filename):
+ pass
+
+
+-def make_context(cafile=None, cadata=None, ciphers=None, validate_certs=True, client_cert=None, client_key=None):
++def make_context(cafile=None, cadata=None, ciphers=None, validate_certs=True):
+ if ciphers is None:
+ ciphers = []
+
+@@ -1022,9 +1006,6 @@ def make_context(cafile=None, cadata=Non
+ if ciphers:
+ context.set_ciphers(':'.join(map(to_native, ciphers)))
+
+- if client_cert:
+- context.load_cert_chain(client_cert, keyfile=client_key)
+-
+ return context
+
+
+@@ -1328,7 +1309,7 @@ class Request:
+ follow_redirects='urllib2', client_cert=None, client_key=None, cookies=None, unix_socket=None,
+ ca_path=None, unredirected_headers=None, decompress=True, ciphers=None, use_netrc=True):
+ """This class works somewhat similarly to the ``Session`` class of from requests
+- by defining a cookiejar that can be used across requests as well as cascaded defaults that
++ by defining a cookiejar that an be used across requests as well as cascaded defaults that
+ can apply to repeated requests
+
+ For documentation of params, see ``Request.open``
+@@ -1480,7 +1461,7 @@ class Request:
+ url = urlunparse(parsed_list)
+
+ if use_gssapi:
+- if HTTPGSSAPIAuthHandler: # type: ignore[truthy-function]
++ if HTTPGSSAPIAuthHandler:
+ handlers.append(HTTPGSSAPIAuthHandler(username, password))
+ else:
+ imp_err_msg = missing_required_lib('gssapi', reason='for use_gssapi=True',
+@@ -1514,7 +1495,7 @@ class Request:
+ login = None
+
+ if login:
+- username, dummy, password = login
++ username, _, password = login
+ if username and password:
+ headers["Authorization"] = basic_auth_header(username, password)
+
+@@ -1533,8 +1514,6 @@ class Request:
+ cadata=cadata,
+ ciphers=ciphers,
+ validate_certs=validate_certs,
+- client_cert=client_cert,
+- client_key=client_key,
+ )
+ handlers.append(HTTPSClientAuthHandler(client_cert=client_cert,
+ client_key=client_key,
+@@ -1886,8 +1865,12 @@ def fetch_url(module, url, data=None, he
+ if not HAS_URLPARSE:
+ module.fail_json(msg='urlparse is not installed')
+
+- if not HAS_GZIP:
+- module.fail_json(msg=GzipDecodedReader.missing_gzip_error())
++ if not HAS_GZIP and decompress is True:
++ decompress = False
++ module.deprecate(
++ '%s. "decompress" has been automatically disabled to prevent a failure' % GzipDecodedReader.missing_gzip_error(),
++ version='2.16'
++ )
+
+ # ensure we use proper tempdir
+ old_tempdir = tempfile.tempdir
+@@ -1901,7 +1884,7 @@ def fetch_url(module, url, data=None, he
+
+ username = module.params.get('url_username', '')
+ password = module.params.get('url_password', '')
+- http_agent = module.params.get('http_agent', get_user_agent())
++ http_agent = module.params.get('http_agent', 'ansible-httpget')
+ force_basic_auth = module.params.get('force_basic_auth', '')
+
+ follow_redirects = module.params.get('follow_redirects', 'urllib2')
+@@ -2085,8 +2068,3 @@ def fetch_file(module, url, data=None, h
+ except Exception as e:
+ module.fail_json(msg="Failure downloading %s, %s" % (url, to_native(e)))
+ return fetch_temp_file.name
+-
+-
+-def get_user_agent():
+- """Returns a user agent used by open_url"""
+- return u"ansible-httpget"
+--- ansible-core-2.16.5.orig/lib/ansible/module_utils/yumdnf.py
++++ ansible-core-2.16.5/lib/ansible/module_utils/yumdnf.py
+@@ -15,8 +15,10 @@ __metaclass__ = type
+ import os
+ import time
+ import glob
++import tempfile
+ from abc import ABCMeta, abstractmethod
+
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.six import with_metaclass
+
+ yumdnf_argument_spec = dict(
+--- ansible-core-2.16.5.orig/lib/ansible/modules/add_host.py
++++ ansible-core-2.16.5/lib/ansible/modules/add_host.py
+@@ -59,8 +59,8 @@ attributes:
+ platform:
+ platforms: all
+ notes:
+-- The alias O(host) of the parameter O(name) is only available on Ansible 2.4 and newer.
+-- Since Ansible 2.4, the C(inventory_dir) variable is now set to V(None) instead of the 'global inventory source',
++- The alias C(host) of the parameter C(name) is only available on Ansible 2.4 and newer.
++- Since Ansible 2.4, the C(inventory_dir) variable is now set to C(None) instead of the 'global inventory source',
+ because you can now have multiple sources. An example was added that shows how to partially restore the previous behaviour.
+ - Though this module does not change the remote host, we do provide 'changed' status as it can be useful for those trying to track inventory changes.
+ - The hosts added will not bypass the C(--limit) from the command line, so both of those need to be in agreement to make them available as play targets.
+--- ansible-core-2.16.5.orig/lib/ansible/modules/apt.py
++++ ansible-core-2.16.5/lib/ansible/modules/apt.py
+@@ -20,15 +20,15 @@ version_added: "0.0.2"
+ options:
+ name:
+ description:
+- - A list of package names, like V(foo), or package specifier with version, like V(foo=1.0) or V(foo>=1.0).
+- Name wildcards (fnmatch) like V(apt*) and version wildcards like V(foo=1.0*) are also supported.
++ - A list of package names, like C(foo), or package specifier with version, like C(foo=1.0) or C(foo>=1.0).
++ Name wildcards (fnmatch) like C(apt*) and version wildcards like C(foo=1.0*) are also supported.
+ aliases: [ package, pkg ]
+ type: list
+ elements: str
+ state:
+ description:
+- - Indicates the desired package state. V(latest) ensures that the latest version is installed. V(build-dep) ensures the package build dependencies
+- are installed. V(fixed) attempt to correct a system with broken dependencies in place.
++ - Indicates the desired package state. C(latest) ensures that the latest version is installed. C(build-dep) ensures the package build dependencies
++ are installed. C(fixed) attempt to correct a system with broken dependencies in place.
+ type: str
+ default: present
+ choices: [ absent, build-dep, latest, present, fixed ]
+@@ -40,25 +40,25 @@ options:
+ type: bool
+ update_cache_retries:
+ description:
+- - Amount of retries if the cache update fails. Also see O(update_cache_retry_max_delay).
++ - Amount of retries if the cache update fails. Also see I(update_cache_retry_max_delay).
+ type: int
+ default: 5
+ version_added: '2.10'
+ update_cache_retry_max_delay:
+ description:
+- - Use an exponential backoff delay for each retry (see O(update_cache_retries)) up to this max delay in seconds.
++ - Use an exponential backoff delay for each retry (see I(update_cache_retries)) up to this max delay in seconds.
+ type: int
+ default: 12
+ version_added: '2.10'
+ cache_valid_time:
+ description:
+- - Update the apt cache if it is older than the O(cache_valid_time). This option is set in seconds.
+- - As of Ansible 2.4, if explicitly set, this sets O(update_cache=yes).
++ - Update the apt cache if it is older than the I(cache_valid_time). This option is set in seconds.
++ - As of Ansible 2.4, if explicitly set, this sets I(update_cache=yes).
+ type: int
+ default: 0
+ purge:
+ description:
+- - Will force purging of configuration files if O(state=absent) or O(autoremove=yes).
++ - Will force purging of configuration files if the module state is set to I(absent).
+ type: bool
+ default: 'no'
+ default_release:
+@@ -68,13 +68,13 @@ options:
+ type: str
+ install_recommends:
+ description:
+- - Corresponds to the C(--no-install-recommends) option for I(apt). V(true) installs recommended packages. V(false) does not install
++ - Corresponds to the C(--no-install-recommends) option for I(apt). C(true) installs recommended packages. C(false) does not install
+ recommended packages. By default, Ansible will use the same defaults as the operating system. Suggested packages are never installed.
+ aliases: [ install-recommends ]
+ type: bool
+ force:
+ description:
+- - 'Corresponds to the C(--force-yes) to I(apt-get) and implies O(allow_unauthenticated=yes) and O(allow_downgrade=yes)'
++ - 'Corresponds to the C(--force-yes) to I(apt-get) and implies C(allow_unauthenticated: yes) and C(allow_downgrade: yes)'
+ - "This option will disable checking both the packages' signatures and the certificates of the
+ web servers they are downloaded from."
+ - 'This option *is not* the equivalent of passing the C(-f) flag to I(apt-get) on the command line'
+@@ -93,7 +93,7 @@ options:
+ allow_unauthenticated:
+ description:
+ - Ignore if packages cannot be authenticated. This is useful for bootstrapping environments that manage their own apt-key setup.
+- - 'O(allow_unauthenticated) is only supported with O(state): V(install)/V(present)'
++ - 'C(allow_unauthenticated) is only supported with state: I(install)/I(present)'
+ aliases: [ allow-unauthenticated ]
+ type: bool
+ default: 'no'
+@@ -102,9 +102,8 @@ options:
+ description:
+ - Corresponds to the C(--allow-downgrades) option for I(apt).
+ - This option enables the named package and version to replace an already installed higher version of that package.
+- - Note that setting O(allow_downgrade=true) can make this module behave in a non-idempotent way.
++ - Note that setting I(allow_downgrade=true) can make this module behave in a non-idempotent way.
+ - (The task could end up with a set of packages that does not match the complete list of specified packages to install).
+- - 'O(allow_downgrade) is only supported by C(apt) and will be ignored if C(aptitude) is detected or specified.'
+ aliases: [ allow-downgrade, allow_downgrades, allow-downgrades ]
+ type: bool
+ default: 'no'
+@@ -142,14 +141,14 @@ options:
+ version_added: "1.6"
+ autoremove:
+ description:
+- - If V(true), remove unused dependency packages for all module states except V(build-dep). It can also be used as the only option.
++ - If C(true), remove unused dependency packages for all module states except I(build-dep). It can also be used as the only option.
+ - Previous to version 2.4, autoclean was also an alias for autoremove, now it is its own separate command. See documentation for further information.
+ type: bool
+ default: 'no'
+ version_added: "2.1"
+ autoclean:
+ description:
+- - If V(true), cleans the local repository of retrieved package files that can no longer be downloaded.
++ - If C(true), cleans the local repository of retrieved package files that can no longer be downloaded.
+ type: bool
+ default: 'no'
+ version_added: "2.4"
+@@ -158,7 +157,7 @@ options:
+ - Force the exit code of /usr/sbin/policy-rc.d.
+ - For example, if I(policy_rc_d=101) the installed package will not trigger a service start.
+ - If /usr/sbin/policy-rc.d already exists, it is backed up and restored after the package installation.
+- - If V(null), the /usr/sbin/policy-rc.d isn't created/changed.
++ - If C(null), the /usr/sbin/policy-rc.d isn't created/changed.
+ type: int
+ default: null
+ version_added: "2.8"
+@@ -171,9 +170,8 @@ options:
+ fail_on_autoremove:
+ description:
+ - 'Corresponds to the C(--no-remove) option for C(apt).'
+- - 'If V(true), it is ensured that no packages will be removed or the task will fail.'
+- - 'O(fail_on_autoremove) is only supported with O(state) except V(absent).'
+- - 'O(fail_on_autoremove) is only supported by C(apt) and will be ignored if C(aptitude) is detected or specified.'
++ - 'If C(true), it is ensured that no packages will be removed or the task will fail.'
++ - 'C(fail_on_autoremove) is only supported with state except C(absent)'
+ type: bool
+ default: 'no'
+ version_added: "2.11"
+@@ -204,15 +202,15 @@ attributes:
+ platform:
+ platforms: debian
+ notes:
+- - Three of the upgrade modes (V(full), V(safe) and its alias V(true)) required C(aptitude) up to 2.3, since 2.4 C(apt-get) is used as a fall-back.
++ - Three of the upgrade modes (C(full), C(safe) and its alias C(true)) required C(aptitude) up to 2.3, since 2.4 C(apt-get) is used as a fall-back.
+ - In most cases, packages installed with apt will start newly installed services by default. Most distributions have mechanisms to avoid this.
+ For example when installing Postgresql-9.5 in Debian 9, creating an excutable shell script (/usr/sbin/policy-rc.d) that throws
+ a return code of 101 will stop Postgresql 9.5 starting up after install. Remove the file or remove its execute permission afterwards.
+ - The apt-get commandline supports implicit regex matches here but we do not because it can let typos through easier
+ (If you typo C(foo) as C(fo) apt-get would install packages that have "fo" in their name with a warning and a prompt for the user.
+ Since we don't have warnings and prompts before installing we disallow this.Use an explicit fnmatch pattern if you want wildcarding)
+- - When used with a C(loop:) each package will be processed individually, it is much more efficient to pass the list directly to the O(name) option.
+- - When O(default_release) is used, an implicit priority of 990 is used. This is the same behavior as C(apt-get -t).
++ - When used with a C(loop:) each package will be processed individually, it is much more efficient to pass the list directly to the I(name) option.
++ - When C(default_release) is used, an implicit priority of 990 is used. This is the same behavior as C(apt-get -t).
+ - When an exact version is specified, an implicit priority of 1001 is used.
+ '''
+
+@@ -316,11 +314,6 @@ EXAMPLES = '''
+ ansible.builtin.apt:
+ autoremove: yes
+
+-- name: Remove dependencies that are no longer required and purge their configuration files
+- ansible.builtin.apt:
+- autoremove: yes
+- purge: true
+-
+ - name: Run the equivalent of "apt-get clean" as a separate step
+ apt:
+ clean: yes
+@@ -360,7 +353,7 @@ warnings.filterwarnings('ignore', "apt A
+
+ import datetime
+ import fnmatch
+-import locale as locale_module
++import itertools
+ import os
+ import random
+ import re
+@@ -372,7 +365,7 @@ import time
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.common.locale import get_best_parsable_locale
+ from ansible.module_utils.common.respawn import has_respawned, probe_interpreters_for_module, respawn_module
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.six import PY3, string_types
+ from ansible.module_utils.urls import fetch_file
+
+@@ -452,7 +445,7 @@ class PolicyRcD(object):
+
+ def __exit__(self, type, value, traceback):
+ """
+- This method will be called when we exit the context, after `apt-get …` is done
++ This method will be called when we enter the context, before we call `apt-get …`
+ """
+
+ # if policy_rc_d is null then we don't need to modify policy-rc.d
+@@ -936,8 +929,7 @@ def install_deb(
+
+
+ def remove(m, pkgspec, cache, purge=False, force=False,
+- dpkg_options=expand_dpkg_options(DPKG_OPTIONS), autoremove=False,
+- allow_change_held_packages=False):
++ dpkg_options=expand_dpkg_options(DPKG_OPTIONS), autoremove=False):
+ pkg_list = []
+ pkgspec = expand_pkgspec_from_fnmatches(m, pkgspec, cache)
+ for package in pkgspec:
+@@ -970,21 +962,7 @@ def remove(m, pkgspec, cache, purge=Fals
+ else:
+ check_arg = ''
+
+- if allow_change_held_packages:
+- allow_change_held_packages = '--allow-change-held-packages'
+- else:
+- allow_change_held_packages = ''
+-
+- cmd = "%s -q -y %s %s %s %s %s %s remove %s" % (
+- APT_GET_CMD,
+- dpkg_options,
+- purge,
+- force_yes,
+- autoremove,
+- check_arg,
+- allow_change_held_packages,
+- packages
+- )
++ cmd = "%s -q -y %s %s %s %s %s remove %s" % (APT_GET_CMD, dpkg_options, purge, force_yes, autoremove, check_arg, packages)
+
+ with PolicyRcD(m):
+ rc, out, err = m.run_command(cmd)
+@@ -1038,13 +1016,15 @@ def cleanup(m, purge=False, force=False,
+
+ def aptclean(m):
+ clean_rc, clean_out, clean_err = m.run_command(['apt-get', 'clean'])
+- clean_diff = parse_diff(clean_out) if m._diff else {}
+-
++ if m._diff:
++ clean_diff = parse_diff(clean_out)
++ else:
++ clean_diff = {}
+ if clean_rc:
+ m.fail_json(msg="apt-get clean failed", stdout=clean_out, rc=clean_rc)
+ if clean_err:
+ m.fail_json(msg="apt-get clean failed: %s" % clean_err, stdout=clean_out, rc=clean_rc)
+- return (clean_out, clean_err, clean_diff)
++ return clean_out, clean_err
+
+
+ def upgrade(m, mode="yes", force=False, default_release=None,
+@@ -1093,24 +1073,13 @@ def upgrade(m, mode="yes", force=False,
+ force_yes = ''
+
+ if fail_on_autoremove:
+- if apt_cmd == APT_GET_CMD:
+- fail_on_autoremove = '--no-remove'
+- else:
+- m.warn("APTITUDE does not support '--no-remove', ignoring the 'fail_on_autoremove' parameter.")
+- fail_on_autoremove = ''
++ fail_on_autoremove = '--no-remove'
+ else:
+ fail_on_autoremove = ''
+
+ allow_unauthenticated = '--allow-unauthenticated' if allow_unauthenticated else ''
+
+- if allow_downgrade:
+- if apt_cmd == APT_GET_CMD:
+- allow_downgrade = '--allow-downgrades'
+- else:
+- m.warn("APTITUDE does not support '--allow-downgrades', ignoring the 'allow_downgrade' parameter.")
+- allow_downgrade = ''
+- else:
+- allow_downgrade = ''
++ allow_downgrade = '--allow-downgrades' if allow_downgrade else ''
+
+ if apt_cmd is None:
+ if use_apt_get:
+@@ -1234,7 +1203,6 @@ def main():
+ # to make sure we use the best parsable locale when running commands
+ # also set apt specific vars for desired behaviour
+ locale = get_best_parsable_locale(module)
+- locale_module.setlocale(locale_module.LC_ALL, locale)
+ # APT related constants
+ APT_ENV_VARS = dict(
+ DEBIAN_FRONTEND='noninteractive',
+@@ -1309,7 +1277,7 @@ def main():
+ p = module.params
+
+ if p['clean'] is True:
+- aptclean_stdout, aptclean_stderr, aptclean_diff = aptclean(module)
++ aptclean_stdout, aptclean_stderr = aptclean(module)
+ # If there is nothing else to do exit. This will set state as
+ # changed based on if the cache was updated.
+ if not p['package'] and not p['upgrade'] and not p['deb']:
+@@ -1317,8 +1285,7 @@ def main():
+ changed=True,
+ msg=aptclean_stdout,
+ stdout=aptclean_stdout,
+- stderr=aptclean_stderr,
+- diff=aptclean_diff
++ stderr=aptclean_stderr
+ )
+
+ if p['upgrade'] == 'no':
+@@ -1503,16 +1470,7 @@ def main():
+ else:
+ module.fail_json(**retvals)
+ elif p['state'] == 'absent':
+- remove(
+- module,
+- packages,
+- cache,
+- p['purge'],
+- force=force_yes,
+- dpkg_options=dpkg_options,
+- autoremove=autoremove,
+- allow_change_held_packages=allow_change_held_packages
+- )
++ remove(module, packages, cache, p['purge'], force=force_yes, dpkg_options=dpkg_options, autoremove=autoremove)
+
+ except apt.cache.LockFailedException as lockFailedException:
+ if time.time() < deadline:
+--- ansible-core-2.16.5.orig/lib/ansible/modules/apt_key.py
++++ ansible-core-2.16.5/lib/ansible/modules/apt_key.py
+@@ -27,24 +27,22 @@ attributes:
+ platform:
+ platforms: debian
+ notes:
+- - The apt-key command used by this module has been deprecated. See the L(Debian wiki,https://wiki.debian.org/DebianRepository/UseThirdParty) for details.
++ - The apt-key command has been deprecated and suggests to 'manage keyring files in trusted.gpg.d instead'. See the Debian wiki for details.
+ This module is kept for backwards compatibility for systems that still use apt-key as the main way to manage apt repository keys.
+ - As a sanity check, downloaded key id must match the one specified.
+ - "Use full fingerprint (40 characters) key ids to avoid key collisions.
+ To generate a full-fingerprint imported key: C(apt-key adv --list-public-keys --with-fingerprint --with-colons)."
+- - If you specify both the key id and the URL with O(state=present), the task can verify or add the key as needed.
++ - If you specify both the key id and the URL with C(state=present), the task can verify or add the key as needed.
+ - Adding a new key requires an apt cache update (e.g. using the M(ansible.builtin.apt) module's update_cache option).
+ requirements:
+ - gpg
+-seealso:
+- - module: ansible.builtin.deb822_repository
+ options:
+ id:
+ description:
+ - The identifier of the key.
+ - Including this allows check mode to correctly report the changed state.
+ - If specifying a subkey's id be aware that apt-key does not understand how to remove keys via a subkey id. Specify the primary key's id instead.
+- - This parameter is required when O(state) is set to V(absent).
++ - This parameter is required when C(state) is set to C(absent).
+ type: str
+ data:
+ description:
+@@ -76,24 +74,23 @@ options:
+ default: present
+ validate_certs:
+ description:
+- - If V(false), SSL certificates for the target url will not be validated. This should only be used
++ - If C(false), SSL certificates for the target url will not be validated. This should only be used
+ on personally controlled sites using self-signed certificates.
+ type: bool
+ default: 'yes'
+ '''
+
+ EXAMPLES = '''
+-- name: One way to avoid apt_key once it is removed from your distro, armored keys should use .asc extension, binary should use .gpg
++- name: One way to avoid apt_key once it is removed from your distro
+ block:
+- - name: somerepo | no apt key
++ - name: somerepo |no apt key
+ ansible.builtin.get_url:
+- url: https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x36a1d7869245c8950f966e92d8576a8ba88d21e9
+- dest: /etc/apt/keyrings/myrepo.asc
+- checksum: sha256:bb42f0db45d46bab5f9ec619e1a47360b94c27142e57aa71f7050d08672309e0
++ url: https://download.example.com/linux/ubuntu/gpg
++ dest: /etc/apt/trusted.gpg.d/somerepo.asc
+
+ - name: somerepo | apt source
+ ansible.builtin.apt_repository:
+- repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/myrepo.asc] https://download.example.com/linux/ubuntu {{ ansible_distribution_release }} stable"
++ repo: "deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/myrepo.asc] https://download.example.com/linux/ubuntu {{ ansible_distribution_release }} stable"
+ state: present
+
+ - name: Add an apt key by id from a keyserver
+@@ -174,7 +171,7 @@ import os
+ # FIXME: standardize into module_common
+ from traceback import format_exc
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.common.locale import get_best_parsable_locale
+ from ansible.module_utils.urls import fetch_url
+--- ansible-core-2.16.5.orig/lib/ansible/modules/apt_repository.py
++++ ansible-core-2.16.5/lib/ansible/modules/apt_repository.py
+@@ -26,8 +26,6 @@ attributes:
+ platforms: debian
+ notes:
+ - This module supports Debian Squeeze (version 6) as well as its successors and derivatives.
+-seealso:
+- - module: ansible.builtin.deb822_repository
+ options:
+ repo:
+ description:
+@@ -54,19 +52,19 @@ options:
+ aliases: [ update-cache ]
+ update_cache_retries:
+ description:
+- - Amount of retries if the cache update fails. Also see O(update_cache_retry_max_delay).
++ - Amount of retries if the cache update fails. Also see I(update_cache_retry_max_delay).
+ type: int
+ default: 5
+ version_added: '2.10'
+ update_cache_retry_max_delay:
+ description:
+- - Use an exponential backoff delay for each retry (see O(update_cache_retries)) up to this max delay in seconds.
++ - Use an exponential backoff delay for each retry (see I(update_cache_retries)) up to this max delay in seconds.
+ type: int
+ default: 12
+ version_added: '2.10'
+ validate_certs:
+ description:
+- - If V(false), SSL certificates for the target repo will not be validated. This should only be used
++ - If C(false), SSL certificates for the target repo will not be validated. This should only be used
+ on personally controlled sites using self-signed certificates.
+ type: bool
+ default: 'yes'
+@@ -91,7 +89,7 @@ options:
+ Without this library, the module does not work.
+ - Runs C(apt-get install python-apt) for Python 2, and C(apt-get install python3-apt) for Python 3.
+ - Only works with the system Python 2 or Python 3. If you are using a Python on the remote that is not
+- the system Python, set O(install_python_apt=false) and ensure that the Python apt library
++ the system Python, set I(install_python_apt=false) and ensure that the Python apt library
+ for your Python version is installed some other way.
+ type: bool
+ default: true
+@@ -140,35 +138,15 @@ EXAMPLES = '''
+ - name: somerepo |no apt key
+ ansible.builtin.get_url:
+ url: https://download.example.com/linux/ubuntu/gpg
+- dest: /etc/apt/keyrings/somerepo.asc
++ dest: /etc/apt/trusted.gpg.d/somerepo.asc
+
+ - name: somerepo | apt source
+ ansible.builtin.apt_repository:
+- repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/myrepo.asc] https://download.example.com/linux/ubuntu {{ ansible_distribution_release }} stable"
++ repo: "deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/myrepo.asc] https://download.example.com/linux/ubuntu {{ ansible_distribution_release }} stable"
+ state: present
+ '''
+
+-RETURN = '''
+-repo:
+- description: A source string for the repository
+- returned: always
+- type: str
+- sample: "deb https://artifacts.elastic.co/packages/6.x/apt stable main"
+-
+-sources_added:
+- description: List of sources added
+- returned: success, sources were added
+- type: list
+- sample: ["/etc/apt/sources.list.d/artifacts_elastic_co_packages_6_x_apt.list"]
+- version_added: "2.15"
+-
+-sources_removed:
+- description: List of sources removed
+- returned: success, sources were removed
+- type: list
+- sample: ["/etc/apt/sources.list.d/artifacts_elastic_co_packages_6_x_apt.list"]
+- version_added: "2.15"
+-'''
++RETURN = '''#'''
+
+ import copy
+ import glob
+@@ -182,12 +160,10 @@ import time
+
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.common.respawn import has_respawned, probe_interpreters_for_module, respawn_module
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.six import PY3
+ from ansible.module_utils.urls import fetch_url
+
+-from ansible.module_utils.common.locale import get_best_parsable_locale
+-
+ try:
+ import apt
+ import apt_pkg
+@@ -495,11 +471,8 @@ class UbuntuSourcesList(SourcesList):
+ def _key_already_exists(self, key_fingerprint):
+
+ if self.apt_key_bin:
+- locale = get_best_parsable_locale(self.module)
+- APT_ENV = dict(LANG=locale, LC_ALL=locale, LC_MESSAGES=locale, LC_CTYPE=locale)
+- self.module.run_command_environ_update = APT_ENV
+ rc, out, err = self.module.run_command([self.apt_key_bin, 'export', key_fingerprint], check_rc=True)
+- found = bool(not err or 'nothing exported' not in err)
++ found = len(err) == 0
+ else:
+ found = self._gpg_key_exists(key_fingerprint)
+
+@@ -715,18 +688,15 @@ def main():
+ sources_after = sourceslist.dump()
+ changed = sources_before != sources_after
+
+- diff = []
+- sources_added = set()
+- sources_removed = set()
+- if changed:
+- sources_added = set(sources_after.keys()).difference(sources_before.keys())
+- sources_removed = set(sources_before.keys()).difference(sources_after.keys())
+- if module._diff:
+- for filename in set(sources_added.union(sources_removed)):
+- diff.append({'before': sources_before.get(filename, ''),
+- 'after': sources_after.get(filename, ''),
+- 'before_header': (filename, '/dev/null')[filename not in sources_before],
+- 'after_header': (filename, '/dev/null')[filename not in sources_after]})
++ if changed and module._diff:
++ diff = []
++ for filename in set(sources_before.keys()).union(sources_after.keys()):
++ diff.append({'before': sources_before.get(filename, ''),
++ 'after': sources_after.get(filename, ''),
++ 'before_header': (filename, '/dev/null')[filename not in sources_before],
++ 'after_header': (filename, '/dev/null')[filename not in sources_after]})
++ else:
++ diff = {}
+
+ if changed and not module.check_mode:
+ try:
+@@ -758,7 +728,7 @@ def main():
+ revert_sources_list(sources_before, sources_after, sourceslist_before)
+ module.fail_json(msg=to_native(ex))
+
+- module.exit_json(changed=changed, repo=repo, sources_added=sources_added, sources_removed=sources_removed, state=state, diff=diff)
++ module.exit_json(changed=changed, repo=repo, state=state, diff=diff)
+
+
+ if __name__ == '__main__':
+--- ansible-core-2.16.5.orig/lib/ansible/modules/assemble.py
++++ ansible-core-2.16.5/lib/ansible/modules/assemble.py
+@@ -17,7 +17,7 @@ description:
+ - Assembles a configuration file from fragments.
+ - Often a particular program will take a single configuration file and does not support a
+ C(conf.d) style structure where it is easy to build up the configuration
+- from multiple sources. M(ansible.builtin.assemble) will take a directory of files that can be
++ from multiple sources. C(assemble) will take a directory of files that can be
+ local or have already been transferred to the system, and concatenate them
+ together to produce a destination file.
+ - Files are assembled in string sorting order.
+@@ -36,7 +36,7 @@ options:
+ required: true
+ backup:
+ description:
+- - Create a backup file (if V(true)), including the timestamp information so
++ - Create a backup file (if C(true)), including the timestamp information so
+ you can get the original file back if you somehow clobbered it
+ incorrectly.
+ type: bool
+@@ -48,16 +48,16 @@ options:
+ version_added: '1.4'
+ remote_src:
+ description:
+- - If V(false), it will search for src at originating/master machine.
+- - If V(true), it will go to the remote/target machine for the src.
++ - If C(false), it will search for src at originating/master machine.
++ - If C(true), it will go to the remote/target machine for the src.
+ type: bool
+ default: yes
+ version_added: '1.4'
+ regexp:
+ description:
+- - Assemble files only if the given regular expression matches the filename.
++ - Assemble files only if C(regex) matches the filename.
+ - If not set, all files are assembled.
+- - Every V(\\) (backslash) must be escaped as V(\\\\) to comply to YAML syntax.
++ - Every C(\) (backslash) must be escaped as C(\\) to comply to YAML syntax.
+ - Uses L(Python regular expressions,https://docs.python.org/3/library/re.html).
+ type: str
+ ignore_hidden:
+@@ -133,7 +133,7 @@ import tempfile
+
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.six import b, indexbytes
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+
+ def assemble_from_fragments(src_path, delimiter=None, compiled_regexp=None, ignore_hidden=False, tmpdir=None):
+--- ansible-core-2.16.5.orig/lib/ansible/modules/assert.py
++++ ansible-core-2.16.5/lib/ansible/modules/assert.py
+@@ -36,7 +36,7 @@ options:
+ version_added: "2.7"
+ quiet:
+ description:
+- - Set this to V(true) to avoid verbose output.
++ - Set this to C(true) to avoid verbose output.
+ type: bool
+ default: no
+ version_added: "2.8"
+--- ansible-core-2.16.5.orig/lib/ansible/modules/async_status.py
++++ ansible-core-2.16.5/lib/ansible/modules/async_status.py
+@@ -23,8 +23,8 @@ options:
+ required: true
+ mode:
+ description:
+- - If V(status), obtain the status.
+- - If V(cleanup), clean up the async job cache (by default in C(~/.ansible_async/)) for the specified job O(jid), without waiting for it to finish.
++ - If C(status), obtain the status.
++ - If C(cleanup), clean up the async job cache (by default in C(~/.ansible_async/)) for the specified job I(jid).
+ type: str
+ choices: [ cleanup, status ]
+ default: status
+@@ -70,11 +70,6 @@ EXAMPLES = r'''
+ until: job_result.finished
+ retries: 100
+ delay: 10
+-
+-- name: Clean up async file
+- ansible.builtin.async_status:
+- jid: '{{ yum_sleeper.ansible_job_id }}'
+- mode: cleanup
+ '''
+
+ RETURN = r'''
+@@ -84,12 +79,12 @@ ansible_job_id:
+ type: str
+ sample: '360874038559.4169'
+ finished:
+- description: Whether the asynchronous job has finished (V(1)) or not (V(0))
++ description: Whether the asynchronous job has finished (C(1)) or not (C(0))
+ returned: always
+ type: int
+ sample: 1
+ started:
+- description: Whether the asynchronous job has started (V(1)) or not (V(0))
++ description: Whether the asynchronous job has started (C(1)) or not (C(0))
+ returned: always
+ type: int
+ sample: 1
+@@ -112,7 +107,7 @@ import os
+
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.six import iteritems
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+
+ def main():
+@@ -129,7 +124,8 @@ def main():
+ async_dir = module.params['_async_dir']
+
+ # setup logging directory
+- log_path = os.path.join(async_dir, jid)
++ logdir = os.path.expanduser(async_dir)
++ log_path = os.path.join(logdir, jid)
+
+ if not os.path.exists(log_path):
+ module.fail_json(msg="could not find job", ansible_job_id=jid, started=1, finished=1)
+--- ansible-core-2.16.5.orig/lib/ansible/modules/async_wrapper.py
++++ ansible-core-2.16.5/lib/ansible/modules/async_wrapper.py
+@@ -20,7 +20,7 @@ import time
+ import syslog
+ import multiprocessing
+
+-from ansible.module_utils.common.text.converters import to_text, to_bytes
++from ansible.module_utils._text import to_text, to_bytes
+
+ PY3 = sys.version_info[0] == 3
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/blockinfile.py
++++ ansible-core-2.16.5/lib/ansible/modules/blockinfile.py
+@@ -21,7 +21,7 @@ options:
+ path:
+ description:
+ - The file to modify.
+- - Before Ansible 2.3 this option was only usable as O(dest), O(destfile) and O(name).
++ - Before Ansible 2.3 this option was only usable as I(dest), I(destfile) and I(name).
+ type: path
+ required: yes
+ aliases: [ dest, destfile, name ]
+@@ -34,24 +34,24 @@ options:
+ marker:
+ description:
+ - The marker line template.
+- - C({mark}) will be replaced with the values in O(marker_begin) (default="BEGIN") and O(marker_end) (default="END").
++ - C({mark}) will be replaced with the values in C(marker_begin) (default="BEGIN") and C(marker_end) (default="END").
+ - Using a custom marker without the C({mark}) variable may result in the block being repeatedly inserted on subsequent playbook runs.
+ - Multi-line markers are not supported and will result in the block being repeatedly inserted on subsequent playbook runs.
+- - A newline is automatically appended by the module to O(marker_begin) and O(marker_end).
++ - A newline is automatically appended by the module to C(marker_begin) and C(marker_end).
+ type: str
+ default: '# {mark} ANSIBLE MANAGED BLOCK'
+ block:
+ description:
+ - The text to insert inside the marker lines.
+- - If it is missing or an empty string, the block will be removed as if O(state) were specified to V(absent).
++ - If it is missing or an empty string, the block will be removed as if C(state) were specified to C(absent).
+ type: str
+ default: ''
+ aliases: [ content ]
+ insertafter:
+ description:
+- - If specified and no begin/ending O(marker) lines are found, the block will be inserted after the last match of specified regular expression.
+- - A special value is available; V(EOF) for inserting the block at the end of the file.
+- - If specified regular expression has no matches, V(EOF) will be used instead.
++ - If specified and no begin/ending C(marker) lines are found, the block will be inserted after the last match of specified regular expression.
++ - A special value is available; C(EOF) for inserting the block at the end of the file.
++ - If specified regular expression has no matches, C(EOF) will be used instead.
+ - The presence of the multiline flag (?m) in the regular expression controls whether the match is done line by line or with multiple lines.
+ This behaviour was added in ansible-core 2.14.
+ type: str
+@@ -59,8 +59,8 @@ options:
+ default: EOF
+ insertbefore:
+ description:
+- - If specified and no begin/ending O(marker) lines are found, the block will be inserted before the last match of specified regular expression.
+- - A special value is available; V(BOF) for inserting the block at the beginning of the file.
++ - If specified and no begin/ending C(marker) lines are found, the block will be inserted before the last match of specified regular expression.
++ - A special value is available; C(BOF) for inserting the block at the beginning of the file.
+ - If specified regular expression has no matches, the block will be inserted at the end of the file.
+ - The presence of the multiline flag (?m) in the regular expression controls whether the match is done line by line or with multiple lines.
+ This behaviour was added in ansible-core 2.14.
+@@ -79,39 +79,22 @@ options:
+ default: no
+ marker_begin:
+ description:
+- - This will be inserted at C({mark}) in the opening ansible block O(marker).
++ - This will be inserted at C({mark}) in the opening ansible block marker.
+ type: str
+ default: BEGIN
+ version_added: '2.5'
+ marker_end:
+ required: false
+ description:
+- - This will be inserted at C({mark}) in the closing ansible block O(marker).
++ - This will be inserted at C({mark}) in the closing ansible block marker.
+ type: str
+ default: END
+ version_added: '2.5'
+- append_newline:
+- required: false
+- description:
+- - Append a blank line to the inserted block, if this does not appear at the end of the file.
+- - Note that this attribute is not considered when C(state) is set to C(absent)
+- type: bool
+- default: no
+- version_added: '2.16'
+- prepend_newline:
+- required: false
+- description:
+- - Prepend a blank line to the inserted block, if this does not appear at the beginning of the file.
+- - Note that this attribute is not considered when C(state) is set to C(absent)
+- type: bool
+- default: no
+- version_added: '2.16'
+ notes:
+ - When using 'with_*' loops be aware that if you do not set a unique mark the block will be overwritten on each iteration.
+- - As of Ansible 2.3, the O(dest) option has been changed to O(path) as default, but O(dest) still works as well.
+- - Option O(ignore:follow) has been removed in Ansible 2.5, because this module modifies the contents of the file
+- so O(ignore:follow=no) does not make sense.
+- - When more then one block should be handled in one file you must change the O(marker) per task.
++ - As of Ansible 2.3, the I(dest) option has been changed to I(path) as default, but I(dest) still works as well.
++ - Option I(follow) has been removed in Ansible 2.5, because this module modifies the contents of the file so I(follow=no) doesn't make sense.
++ - When more then one block should be handled in one file you must change the I(marker) per task.
+ extends_documentation_fragment:
+ - action_common_attributes
+ - action_common_attributes.files
+@@ -133,11 +116,9 @@ attributes:
+
+ EXAMPLES = r'''
+ # Before Ansible 2.3, option 'dest' or 'name' was used instead of 'path'
+-- name: Insert/Update "Match User" configuration block in /etc/ssh/sshd_config prepending and appending a new line
++- name: Insert/Update "Match User" configuration block in /etc/ssh/sshd_config
+ ansible.builtin.blockinfile:
+ path: /etc/ssh/sshd_config
+- append_newline: true
+- prepend_newline: true
+ block: |
+ Match User ansible-agent
+ PasswordAuthentication no
+@@ -198,7 +179,7 @@ import os
+ import tempfile
+ from ansible.module_utils.six import b
+ from ansible.module_utils.basic import AnsibleModule
+-from ansible.module_utils.common.text.converters import to_bytes, to_native
++from ansible.module_utils._text import to_bytes, to_native
+
+
+ def write_changes(module, contents, path):
+@@ -249,8 +230,6 @@ def main():
+ validate=dict(type='str'),
+ marker_begin=dict(type='str', default='BEGIN'),
+ marker_end=dict(type='str', default='END'),
+- append_newline=dict(type='bool', default=False),
+- prepend_newline=dict(type='bool', default=False),
+ ),
+ mutually_exclusive=[['insertbefore', 'insertafter']],
+ add_file_common_args=True,
+@@ -272,10 +251,8 @@ def main():
+ if not os.path.exists(destpath) and not module.check_mode:
+ try:
+ os.makedirs(destpath)
+- except OSError as e:
+- module.fail_json(msg='Error creating %s Error code: %s Error description: %s' % (destpath, e.errno, e.strerror))
+ except Exception as e:
+- module.fail_json(msg='Error creating %s Error: %s' % (destpath, to_native(e)))
++ module.fail_json(msg='Error creating %s Error code: %s Error description: %s' % (destpath, e[0], e[1]))
+ original = None
+ lines = []
+ else:
+@@ -296,7 +273,6 @@ def main():
+ block = to_bytes(params['block'])
+ marker = to_bytes(params['marker'])
+ present = params['state'] == 'present'
+- blank_line = [b(os.linesep)]
+
+ if not present and not path_exists:
+ module.exit_json(changed=False, msg="File %s not present" % path)
+@@ -360,26 +336,7 @@ def main():
+ if not lines[n0 - 1].endswith(b(os.linesep)):
+ lines[n0 - 1] += b(os.linesep)
+
+- # Before the block: check if we need to prepend a blank line
+- # If yes, we need to add the blank line if we are not at the beginning of the file
+- # and the previous line is not a blank line
+- # In both cases, we need to shift by one on the right the inserting position of the block
+- if params['prepend_newline'] and present:
+- if n0 != 0 and lines[n0 - 1] != b(os.linesep):
+- lines[n0:n0] = blank_line
+- n0 += 1
+-
+- # Insert the block
+ lines[n0:n0] = blocklines
+-
+- # After the block: check if we need to append a blank line
+- # If yes, we need to add the blank line if we are not at the end of the file
+- # and the line right after is not a blank line
+- if params['append_newline'] and present:
+- line_after_block = n0 + len(blocklines)
+- if line_after_block < len(lines) and lines[line_after_block] != b(os.linesep):
+- lines[line_after_block:line_after_block] = blank_line
+-
+ if lines:
+ result = b''.join(lines)
+ else:
+--- ansible-core-2.16.5.orig/lib/ansible/modules/command.py
++++ ansible-core-2.16.5/lib/ansible/modules/command.py
+@@ -14,7 +14,7 @@ module: command
+ short_description: Execute commands on targets
+ version_added: historical
+ description:
+- - The M(ansible.builtin.command) module takes the command name followed by a list of space-delimited arguments.
++ - The C(command) module takes the command name followed by a list of space-delimited arguments.
+ - The given command will be executed on all selected nodes.
+ - The command(s) will not be
+ processed through the shell, so variables like C($HOSTNAME) and operations
+@@ -22,15 +22,15 @@ description:
+ Use the M(ansible.builtin.shell) module if you need these features.
+ - To create C(command) tasks that are easier to read than the ones using space-delimited
+ arguments, pass parameters using the C(args) L(task keyword,https://docs.ansible.com/ansible/latest/reference_appendices/playbooks_keywords.html#task)
+- or use O(cmd) parameter.
+- - Either a free form command or O(cmd) parameter is required, see the examples.
++ or use C(cmd) parameter.
++ - Either a free form command or C(cmd) parameter is required, see the examples.
+ - For Windows targets, use the M(ansible.windows.win_command) module instead.
+ extends_documentation_fragment:
+ - action_common_attributes
+ - action_common_attributes.raw
+ attributes:
+ check_mode:
+- details: while the command itself is arbitrary and cannot be subject to the check mode semantics it adds O(creates)/O(removes) options as a workaround
++ details: while the command itself is arbitrary and cannot be subject to the check mode semantics it adds C(creates)/C(removes) options as a workaround
+ support: partial
+ diff_mode:
+ support: none
+@@ -40,14 +40,6 @@ attributes:
+ raw:
+ support: full
+ options:
+- expand_argument_vars:
+- description:
+- - Expands the arguments that are variables, for example C($HOME) will be expanded before being passed to the
+- command to run.
+- - Set to V(false) to disable expansion and treat the value as a literal argument.
+- type: bool
+- default: true
+- version_added: "2.16"
+ free_form:
+ description:
+ - The command module takes a free form string as a command to run.
+@@ -61,19 +53,19 @@ options:
+ elements: str
+ description:
+ - Passes the command as a list rather than a string.
+- - Use O(argv) to avoid quoting values that would otherwise be interpreted incorrectly (for example "user name").
++ - Use C(argv) to avoid quoting values that would otherwise be interpreted incorrectly (for example "user name").
+ - Only the string (free form) or the list (argv) form can be provided, not both. One or the other must be provided.
+ version_added: "2.6"
+ creates:
+ type: path
+ description:
+ - A filename or (since 2.0) glob pattern. If a matching file already exists, this step B(will not) be run.
+- - This is checked before O(removes) is checked.
++ - This is checked before I(removes) is checked.
+ removes:
+ type: path
+ description:
+ - A filename or (since 2.0) glob pattern. If a matching file exists, this step B(will) be run.
+- - This is checked after O(creates) is checked.
++ - This is checked after I(creates) is checked.
+ version_added: "0.8"
+ chdir:
+ type: path
+@@ -89,7 +81,7 @@ options:
+ type: bool
+ default: yes
+ description:
+- - If set to V(true), append a newline to stdin data.
++ - If set to C(true), append a newline to stdin data.
+ version_added: "2.8"
+ strip_empty_ends:
+ description:
+@@ -101,16 +93,14 @@ notes:
+ - If you want to run a command through the shell (say you are using C(<), C(>), C(|), and so on),
+ you actually want the M(ansible.builtin.shell) module instead.
+ Parsing shell metacharacters can lead to unexpected commands being executed if quoting is not done correctly so it is more secure to
+- use the M(ansible.builtin.command) module when possible.
+- - O(creates), O(removes), and O(chdir) can be specified after the command.
++ use the C(command) module when possible.
++ - C(creates), C(removes), and C(chdir) can be specified after the command.
+ For instance, if you only want to run a command if a certain file does not exist, use this.
+- - Check mode is supported when passing O(creates) or O(removes). If running in check mode and either of these are specified, the module will
++ - Check mode is supported when passing C(creates) or C(removes). If running in check mode and either of these are specified, the module will
+ check for the existence of the file and report the correct changed status. If these are not supplied, the task will be skipped.
+- - The O(ignore:executable) parameter is removed since version 2.4. If you have a need for this parameter, use the M(ansible.builtin.shell) module instead.
++ - The C(executable) parameter is removed since version 2.4. If you have a need for this parameter, use the M(ansible.builtin.shell) module instead.
+ - For Windows targets, use the M(ansible.windows.win_command) module instead.
+ - For rebooting systems, use the M(ansible.builtin.reboot) or M(ansible.windows.win_reboot) module.
+- - If the command returns non UTF-8 data, it must be encoded to avoid issues. This may necessitate using M(ansible.builtin.shell) so the output
+- can be piped through C(base64).
+ seealso:
+ - module: ansible.builtin.raw
+ - module: ansible.builtin.script
+@@ -161,17 +151,6 @@ EXAMPLES = r'''
+ - dbname with whitespace
+ creates: /path/to/database
+
+-- name: Run command using argv with mixed argument formats
+- ansible.builtin.command:
+- argv:
+- - /path/to/binary
+- - -v
+- - --debug
+- - --longopt
+- - value for longopt
+- - --other-longopt=value for other longopt
+- - positional
+-
+ - name: Safely use templated variable to run command. Always use the quote filter to avoid injection issues
+ ansible.builtin.command: cat {{ myfile|quote }}
+ register: myoutput
+@@ -238,7 +217,7 @@ import os
+ import shlex
+
+ from ansible.module_utils.basic import AnsibleModule
+-from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text
++from ansible.module_utils._text import to_native, to_bytes, to_text
+ from ansible.module_utils.common.collections import is_iterable
+
+
+@@ -254,7 +233,6 @@ def main():
+ argv=dict(type='list', elements='str'),
+ chdir=dict(type='path'),
+ executable=dict(),
+- expand_argument_vars=dict(type='bool', default=True),
+ creates=dict(type='path'),
+ removes=dict(type='path'),
+ # The default for this really comes from the action plugin
+@@ -274,9 +252,8 @@ def main():
+ stdin = module.params['stdin']
+ stdin_add_newline = module.params['stdin_add_newline']
+ strip = module.params['strip_empty_ends']
+- expand_argument_vars = module.params['expand_argument_vars']
+
+- # we promised these in 'always' ( _lines get auto-added on action plugin)
++ # we promissed these in 'always' ( _lines get autoaded on action plugin)
+ r = {'changed': False, 'stdout': '', 'stderr': '', 'rc': None, 'cmd': None, 'start': None, 'end': None, 'delta': None, 'msg': ''}
+
+ if not shell and executable:
+@@ -342,8 +319,7 @@ def main():
+ if not module.check_mode:
+ r['start'] = datetime.datetime.now()
+ r['rc'], r['stdout'], r['stderr'] = module.run_command(args, executable=executable, use_unsafe_shell=shell, encoding=None,
+- data=stdin, binary_data=(not stdin_add_newline),
+- expand_user_and_vars=expand_argument_vars)
++ data=stdin, binary_data=(not stdin_add_newline))
+ r['end'] = datetime.datetime.now()
+ else:
+ # this is partial check_mode support, since we end up skipping if we get here
+--- ansible-core-2.16.5.orig/lib/ansible/modules/copy.py
++++ ansible-core-2.16.5/lib/ansible/modules/copy.py
+@@ -14,14 +14,10 @@ module: copy
+ version_added: historical
+ short_description: Copy files to remote locations
+ description:
+- - The M(ansible.builtin.copy) module copies a file or a directory structure from the local or remote machine to a location on the remote machine.
+- File system meta-information (permissions, ownership, etc.) may be set, even when the file or directory already exists on the target system.
+- Some meta-information may be copied on request.
+- - Get meta-information with the M(ansible.builtin.stat) module.
+- - Set meta-information with the M(ansible.builtin.file) module.
++ - The C(copy) module copies a file from the local or remote machine to a location on the remote machine.
+ - Use the M(ansible.builtin.fetch) module to copy files from remote locations to the local box.
+ - If you need variable interpolation in copied files, use the M(ansible.builtin.template) module.
+- Using a variable with the O(content) parameter produces unpredictable results.
++ Using a variable in the C(content) field will result in unpredictable output.
+ - For Windows targets, use the M(ansible.windows.win_copy) module instead.
+ options:
+ src:
+@@ -35,19 +31,19 @@ options:
+ type: path
+ content:
+ description:
+- - When used instead of O(src), sets the contents of a file directly to the specified value.
+- - Works only when O(dest) is a file. Creates the file if it does not exist.
+- - For advanced formatting or if O(content) contains a variable, use the
++ - When used instead of C(src), sets the contents of a file directly to the specified value.
++ - Works only when C(dest) is a file. Creates the file if it does not exist.
++ - For advanced formatting or if C(content) contains a variable, use the
+ M(ansible.builtin.template) module.
+ type: str
+ version_added: '1.1'
+ dest:
+ description:
+ - Remote absolute path where the file should be copied to.
+- - If O(src) is a directory, this must be a directory too.
+- - If O(dest) is a non-existent path and if either O(dest) ends with "/" or O(src) is a directory, O(dest) is created.
+- - If O(dest) is a relative path, the starting directory is determined by the remote host.
+- - If O(src) and O(dest) are files, the parent directory of O(dest) is not created and the task fails if it does not already exist.
++ - If C(src) is a directory, this must be a directory too.
++ - If C(dest) is a non-existent path and if either C(dest) ends with "/" or C(src) is a directory, C(dest) is created.
++ - If I(dest) is a relative path, the starting directory is determined by the remote host.
++ - If C(src) and C(dest) are files, the parent directory of C(dest) is not created and the task fails if it does not already exist.
+ type: path
+ required: yes
+ backup:
+@@ -59,8 +55,8 @@ options:
+ force:
+ description:
+ - Influence whether the remote file must always be replaced.
+- - If V(true), the remote file will be replaced when contents are different than the source.
+- - If V(false), the file will only be transferred if the destination does not exist.
++ - If C(true), the remote file will be replaced when contents are different than the source.
++ - If C(false), the file will only be transferred if the destination does not exist.
+ type: bool
+ default: yes
+ version_added: '1.1'
+@@ -69,34 +65,33 @@ options:
+ - The permissions of the destination file or directory.
+ - For those used to C(/usr/bin/chmod) remember that modes are actually octal numbers.
+ You must either add a leading zero so that Ansible's YAML parser knows it is an octal number
+- (like V(0644) or V(01777)) or quote it (like V('644') or V('1777')) so Ansible receives a string
++ (like C(0644) or C(01777)) or quote it (like C('644') or C('1777')) so Ansible receives a string
+ and can do its own conversion from string into number. Giving Ansible a number without following
+ one of these rules will end up with a decimal number which will have unexpected results.
+- - As of Ansible 1.8, the mode may be specified as a symbolic mode (for example, V(u+rwx) or V(u=rw,g=r,o=r)).
+- - As of Ansible 2.3, the mode may also be the special string V(preserve).
+- - V(preserve) means that the file will be given the same permissions as the source file.
+- - When doing a recursive copy, see also O(directory_mode).
+- - If O(mode) is not specified and the destination file B(does not) exist, the default C(umask) on the system will be used
++ - As of Ansible 1.8, the mode may be specified as a symbolic mode (for example, C(u+rwx) or C(u=rw,g=r,o=r)).
++ - As of Ansible 2.3, the mode may also be the special string C(preserve).
++ - C(preserve) means that the file will be given the same permissions as the source file.
++ - When doing a recursive copy, see also C(directory_mode).
++ - If C(mode) is not specified and the destination file B(does not) exist, the default C(umask) on the system will be used
+ when setting the mode for the newly created file.
+- - If O(mode) is not specified and the destination file B(does) exist, the mode of the existing file will be used.
+- - Specifying O(mode) is the best way to ensure files are created with the correct permissions.
++ - If C(mode) is not specified and the destination file B(does) exist, the mode of the existing file will be used.
++ - Specifying C(mode) is the best way to ensure files are created with the correct permissions.
+ See CVE-2020-1736 for further details.
+ directory_mode:
+ description:
+- - Set the access permissions of newly created directories to the given mode.
+- Permissions on existing directories do not change.
+- - See O(mode) for the syntax of accepted values.
+- - The target system's defaults determine permissions when this parameter is not set.
++ - When doing a recursive copy set the mode for the directories.
++ - If this is not set we will use the system defaults.
++ - The mode is only set on directories which are newly created, and will not affect those that already existed.
+ type: raw
+ version_added: '1.5'
+ remote_src:
+ description:
+- - Influence whether O(src) needs to be transferred or already is present remotely.
+- - If V(false), it will search for O(src) on the controller node.
+- - If V(true) it will search for O(src) on the managed (remote) node.
+- - O(remote_src) supports recursive copying as of version 2.8.
+- - O(remote_src) only works with O(mode=preserve) as of version 2.6.
+- - Autodecryption of files does not work when O(remote_src=yes).
++ - Influence whether C(src) needs to be transferred or already is present remotely.
++ - If C(false), it will search for C(src) on the controller node.
++ - If C(true) it will search for C(src) on the managed (remote) node.
++ - C(remote_src) supports recursive copying as of version 2.8.
++ - C(remote_src) only works with C(mode=preserve) as of version 2.6.
++ - Autodecryption of files does not work when C(remote_src=yes).
+ type: bool
+ default: no
+ version_added: '2.0'
+@@ -298,7 +293,7 @@ import stat
+ import tempfile
+ import traceback
+
+-from ansible.module_utils.common.text.converters import to_bytes, to_native
++from ansible.module_utils._text import to_bytes, to_native
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.common.process import get_bin_path
+ from ansible.module_utils.common.locale import get_best_parsable_locale
+@@ -523,7 +518,7 @@ def copy_common_dirs(src, dest, module):
+ changed = True
+
+ # recurse into subdirectory
+- changed = copy_common_dirs(os.path.join(src, item), os.path.join(dest, item), module) or changed
++ changed = changed or copy_common_dirs(os.path.join(src, item), os.path.join(dest, item), module)
+ return changed
+
+
+@@ -624,7 +619,6 @@ def main():
+ if module.check_mode:
+ module.exit_json(msg='dest directory %s would be created' % dirname, changed=True, src=src)
+ os.makedirs(b_dirname)
+- changed = True
+ directory_args = module.load_file_common_arguments(module.params)
+ directory_mode = module.params["directory_mode"]
+ if directory_mode is not None:
+@@ -694,7 +688,7 @@ def main():
+ b_mysrc = b_src
+ if remote_src and os.path.isfile(b_src):
+
+- dummy, b_mysrc = tempfile.mkstemp(dir=os.path.dirname(b_dest))
++ _, b_mysrc = tempfile.mkstemp(dir=os.path.dirname(b_dest))
+
+ shutil.copyfile(b_src, b_mysrc)
+ try:
+@@ -757,6 +751,8 @@ def main():
+ except (IOError, OSError):
+ module.fail_json(msg="failed to copy: %s to %s" % (src, dest), traceback=traceback.format_exc())
+ changed = True
++ else:
++ changed = False
+
+ # If neither have checksums, both src and dest are directories.
+ if checksum_src is None and checksum_dest is None:
+@@ -804,12 +800,13 @@ def main():
+ b_dest = to_bytes(os.path.join(b_dest, b_basename), errors='surrogate_or_strict')
+ if not module.check_mode and not os.path.exists(b_dest):
+ os.makedirs(b_dest)
+- changed = True
+ b_src = to_bytes(os.path.join(module.params['src'], ""), errors='surrogate_or_strict')
+ diff_files_changed = copy_diff_files(b_src, b_dest, module)
+ left_only_changed = copy_left_only(b_src, b_dest, module)
+ common_dirs_changed = copy_common_dirs(b_src, b_dest, module)
+ owner_group_changed = chown_recursive(b_dest, module)
++ if diff_files_changed or left_only_changed or common_dirs_changed or owner_group_changed:
++ changed = True
+ if module.check_mode and not os.path.exists(b_dest):
+ changed = True
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/cron.py
++++ ansible-core-2.16.5/lib/ansible/modules/cron.py
+@@ -44,7 +44,7 @@ options:
+ description:
+ - The command to execute or, if env is set, the value of environment variable.
+ - The command should not contain line breaks.
+- - Required if O(state=present).
++ - Required if I(state=present).
+ type: str
+ aliases: [ value ]
+ state:
+@@ -58,42 +58,42 @@ options:
+ - If specified, uses this file instead of an individual user's crontab.
+ The assumption is that this file is exclusively managed by the module,
+ do not use if the file contains multiple entries, NEVER use for /etc/crontab.
+- - If this is a relative path, it is interpreted with respect to C(/etc/cron.d).
++ - If this is a relative path, it is interpreted with respect to I(/etc/cron.d).
+ - Many linux distros expect (and some require) the filename portion to consist solely
+ of upper- and lower-case letters, digits, underscores, and hyphens.
+- - Using this parameter requires you to specify the O(user) as well, unless O(state) is not V(present).
+- - Either this parameter or O(name) is required
++ - Using this parameter requires you to specify the I(user) as well, unless I(state) is not I(present).
++ - Either this parameter or I(name) is required
+ type: path
+ backup:
+ description:
+ - If set, create a backup of the crontab before it is modified.
+- The location of the backup is returned in the RV(ignore:backup_file) variable by this module.
++ The location of the backup is returned in the C(backup_file) variable by this module.
+ type: bool
+ default: no
+ minute:
+ description:
+- - Minute when the job should run (V(0-59), V(*), V(*/2), and so on).
++ - Minute when the job should run (C(0-59), C(*), C(*/2), and so on).
+ type: str
+ default: "*"
+ hour:
+ description:
+- - Hour when the job should run (V(0-23), V(*), V(*/2), and so on).
++ - Hour when the job should run (C(0-23), C(*), C(*/2), and so on).
+ type: str
+ default: "*"
+ day:
+ description:
+- - Day of the month the job should run (V(1-31), V(*), V(*/2), and so on).
++ - Day of the month the job should run (C(1-31), C(*), C(*/2), and so on).
+ type: str
+ default: "*"
+ aliases: [ dom ]
+ month:
+ description:
+- - Month of the year the job should run (V(1-12), V(*), V(*/2), and so on).
++ - Month of the year the job should run (C(1-12), C(*), C(*/2), and so on).
+ type: str
+ default: "*"
+ weekday:
+ description:
+- - Day of the week that the job should run (V(0-6) for Sunday-Saturday, V(*), and so on).
++ - Day of the week that the job should run (C(0-6) for Sunday-Saturday, C(*), and so on).
+ type: str
+ default: "*"
+ aliases: [ dow ]
+@@ -106,7 +106,7 @@ options:
+ disabled:
+ description:
+ - If the job should be disabled (commented out) in the crontab.
+- - Only has effect if O(state=present).
++ - Only has effect if I(state=present).
+ type: bool
+ default: no
+ version_added: "2.0"
+@@ -114,19 +114,19 @@ options:
+ description:
+ - If set, manages a crontab's environment variable.
+ - New variables are added on top of crontab.
+- - O(name) and O(value) parameters are the name and the value of environment variable.
++ - I(name) and I(value) parameters are the name and the value of environment variable.
+ type: bool
+ default: false
+ version_added: "2.1"
+ insertafter:
+ description:
+- - Used with O(state=present) and O(env).
++ - Used with I(state=present) and I(env).
+ - If specified, the environment variable will be inserted after the declaration of specified environment variable.
+ type: str
+ version_added: "2.1"
+ insertbefore:
+ description:
+- - Used with O(state=present) and O(env).
++ - Used with I(state=present) and I(env).
+ - If specified, the environment variable will be inserted before the declaration of specified environment variable.
+ type: str
+ version_added: "2.1"
+--- ansible-core-2.16.5.orig/lib/ansible/modules/debconf.py
++++ ansible-core-2.16.5/lib/ansible/modules/debconf.py
+@@ -27,13 +27,13 @@ attributes:
+ platforms: debian
+ notes:
+ - This module requires the command line debconf tools.
+- - Several questions have to be answered (depending on the package).
++ - A number of questions have to be answered (depending on the package).
+ Use 'debconf-show <package>' on any Debian or derivative with the package
+ installed to see questions/settings available.
+ - Some distros will always record tasks involving the setting of passwords as changed. This is due to debconf-get-selections masking passwords.
+- - It is highly recommended to add C(no_log=True) to the task while handling sensitive information using this module.
++ - It is highly recommended to add I(no_log=True) to task while handling sensitive information using this module.
+ - The debconf module does not reconfigure packages, it just updates the debconf database.
+- An additional step is needed (typically with C(notify) if debconf makes a change)
++ An additional step is needed (typically with I(notify) if debconf makes a change)
+ to reconfigure the package and apply the changes.
+ debconf is extensively used for pre-seeding configuration prior to installation
+ rather than modifying configurations.
+@@ -46,7 +46,7 @@ notes:
+ - The main issue is that the C(<package>.config reconfigure) step for many packages
+ will first reset the debconf database (overriding changes made by this module) by
+ checking the on-disk configuration. If this is the case for your package then
+- dpkg-reconfigure will effectively ignore changes made by debconf.
++ dpkg-reconfigure will effectively ignore changes made by debconf.
+ - However as dpkg-reconfigure only executes the C(<package>.config) step if the file
+ exists, it is possible to rename it to C(/var/lib/dpkg/info/<package>.config.ignore)
+ before executing C(dpkg-reconfigure -f noninteractive <package>) and then restore it.
+@@ -69,8 +69,8 @@ options:
+ vtype:
+ description:
+ - The type of the value supplied.
+- - It is highly recommended to add C(no_log=True) to task while specifying O(vtype=password).
+- - V(seen) was added in Ansible 2.2.
++ - It is highly recommended to add I(no_log=True) to task while specifying I(vtype=password).
++ - C(seen) was added in Ansible 2.2.
+ type: str
+ choices: [ boolean, error, multiselect, note, password, seen, select, string, text, title ]
+ value:
+@@ -124,32 +124,10 @@ EXAMPLES = r'''
+
+ RETURN = r'''#'''
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.basic import AnsibleModule
+
+
+-def get_password_value(module, pkg, question, vtype):
+- getsel = module.get_bin_path('debconf-get-selections', True)
+- cmd = [getsel]
+- rc, out, err = module.run_command(cmd)
+- if rc != 0:
+- module.fail_json(msg="Failed to get the value '%s' from '%s'" % (question, pkg))
+-
+- desired_line = None
+- for line in out.split("\n"):
+- if line.startswith(pkg):
+- desired_line = line
+- break
+-
+- if not desired_line:
+- module.fail_json(msg="Failed to find the value '%s' from '%s'" % (question, pkg))
+-
+- (dpkg, dquestion, dvtype, dvalue) = desired_line.split()
+- if dquestion == question and dvtype == vtype:
+- return dvalue
+- return ''
+-
+-
+ def get_selections(module, pkg):
+ cmd = [module.get_bin_path('debconf-show', True), pkg]
+ rc, out, err = module.run_command(' '.join(cmd))
+@@ -173,7 +151,10 @@ def set_selection(module, pkg, question,
+ cmd.append('-u')
+
+ if vtype == 'boolean':
+- value = value.lower()
++ if value == 'True':
++ value = 'true'
++ elif value == 'False':
++ value = 'false'
+ data = ' '.join([pkg, question, vtype, value])
+
+ return module.run_command(cmd, data=data)
+@@ -212,6 +193,7 @@ def main():
+ if question not in prev:
+ changed = True
+ else:
++
+ existing = prev[question]
+
+ # ensure we compare booleans supplied to the way debconf sees them (true/false strings)
+@@ -219,9 +201,6 @@ def main():
+ value = to_text(value).lower()
+ existing = to_text(prev[question]).lower()
+
+- if vtype == 'password':
+- existing = get_password_value(module, pkg, question, vtype)
+-
+ if value != existing:
+ changed = True
+
+@@ -236,12 +215,12 @@ def main():
+ prev = {question: prev[question]}
+ else:
+ prev[question] = ''
+-
+- diff_dict = {}
+ if module._diff:
+ after = prev.copy()
+ after.update(curr)
+ diff_dict = {'before': prev, 'after': after}
++ else:
++ diff_dict = {}
+
+ module.exit_json(changed=changed, msg=msg, current=curr, previous=prev, diff=diff_dict)
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/debug.py
++++ ansible-core-2.16.5/lib/ansible/modules/debug.py
+@@ -27,7 +27,7 @@ options:
+ var:
+ description:
+ - A variable name to debug.
+- - Mutually exclusive with the O(msg) option.
++ - Mutually exclusive with the C(msg) option.
+ - Be aware that this option already runs in Jinja2 context and has an implicit C({{ }}) wrapping,
+ so you should not be using Jinja2 delimiters unless you are looking for double interpolation.
+ type: str
+--- ansible-core-2.16.5.orig/lib/ansible/modules/dnf.py
++++ ansible-core-2.16.5/lib/ansible/modules/dnf.py
+@@ -18,40 +18,33 @@ short_description: Manages packages with
+ description:
+ - Installs, upgrade, removes, and lists packages and groups with the I(dnf) package manager.
+ options:
+- use_backend:
+- description:
+- - By default, this module will select the backend based on the C(ansible_pkg_mgr) fact.
+- default: "auto"
+- choices: [ auto, dnf4, dnf5 ]
+- type: str
+- version_added: 2.15
+ name:
+ description:
+ - "A package name or package specifier with version, like C(name-1.0).
+ When using state=latest, this can be '*' which means run: dnf -y update.
+- You can also pass a url or a local path to an rpm file.
++ You can also pass a url or a local path to a rpm file.
+ To operate on several packages this can accept a comma separated string of packages or a list of packages."
+ - Comparison operators for package version are valid here C(>), C(<), C(>=), C(<=). Example - C(name >= 1.0).
+ Spaces around the operator are required.
+ - You can also pass an absolute path for a binary which is provided by the package to install.
+ See examples for more information.
++ required: true
+ aliases:
+ - pkg
+ type: list
+ elements: str
+- default: []
+
+ list:
+ description:
+ - Various (non-idempotent) commands for usage with C(/usr/bin/ansible) and I(not) playbooks.
+- Use M(ansible.builtin.package_facts) instead of the O(list) argument as a best practice.
++ Use M(ansible.builtin.package_facts) instead of the C(list) argument as a best practice.
+ type: str
+
+ state:
+ description:
+- - Whether to install (V(present), V(latest)), or remove (V(absent)) a package.
+- - Default is V(None), however in effect the default action is V(present) unless the O(autoremove) option is
+- enabled for this module, then V(absent) is inferred.
++ - Whether to install (C(present), C(latest)), or remove (C(absent)) a package.
++ - Default is C(None), however in effect the default action is C(present) unless the C(autoremove) option is
++ enabled for this module, then C(absent) is inferred.
+ choices: ['absent', 'present', 'installed', 'removed', 'latest']
+ type: str
+
+@@ -62,7 +55,6 @@ options:
+ When specifying multiple repos, separate them with a ",".
+ type: list
+ elements: str
+- default: []
+
+ disablerepo:
+ description:
+@@ -71,7 +63,6 @@ options:
+ When specifying multiple repos, separate them with a ",".
+ type: list
+ elements: str
+- default: []
+
+ conf_file:
+ description:
+@@ -81,7 +72,7 @@ options:
+ disable_gpg_check:
+ description:
+ - Whether to disable the GPG checking of signatures of packages being
+- installed. Has an effect only if O(state) is V(present) or V(latest).
++ installed. Has an effect only if state is I(present) or I(latest).
+ - This setting affects packages installed from a repository as well as
+ "local" packages installed from the filesystem or a URL.
+ type: bool
+@@ -104,9 +95,9 @@ options:
+
+ autoremove:
+ description:
+- - If V(true), removes all "leaf" packages from the system that were originally
++ - If C(true), removes all "leaf" packages from the system that were originally
+ installed as dependencies of user-installed packages but which are no longer
+- required by any such package. Should be used alone or when O(state) is V(absent)
++ required by any such package. Should be used alone or when state is I(absent)
+ type: bool
+ default: "no"
+ version_added: "2.4"
+@@ -117,7 +108,6 @@ options:
+ version_added: "2.7"
+ type: list
+ elements: str
+- default: []
+ skip_broken:
+ description:
+ - Skip all unavailable packages or packages with broken dependencies
+@@ -128,7 +118,7 @@ options:
+ update_cache:
+ description:
+ - Force dnf to check if cache is out of date and redownload if needed.
+- Has an effect only if O(state) is V(present) or V(latest).
++ Has an effect only if state is I(present) or I(latest).
+ type: bool
+ default: "no"
+ aliases: [ expire-cache ]
+@@ -136,20 +126,20 @@ options:
+ update_only:
+ description:
+ - When using latest, only update installed packages. Do not install packages.
+- - Has an effect only if O(state) is V(latest)
++ - Has an effect only if state is I(latest)
+ default: "no"
+ type: bool
+ version_added: "2.7"
+ security:
+ description:
+- - If set to V(true), and O(state=latest) then only installs updates that have been marked security related.
++ - If set to C(true), and C(state=latest) then only installs updates that have been marked security related.
+ - Note that, similar to C(dnf upgrade-minimal), this filter applies to dependencies as well.
+ type: bool
+ default: "no"
+ version_added: "2.7"
+ bugfix:
+ description:
+- - If set to V(true), and O(state=latest) then only installs updates that have been marked bugfix related.
++ - If set to C(true), and C(state=latest) then only installs updates that have been marked bugfix related.
+ - Note that, similar to C(dnf upgrade-minimal), this filter applies to dependencies as well.
+ default: "no"
+ type: bool
+@@ -161,34 +151,32 @@ options:
+ version_added: "2.7"
+ type: list
+ elements: str
+- default: []
+ disable_plugin:
+ description:
+ - I(Plugin) name to disable for the install/update operation.
+ The disabled plugins will not persist beyond the transaction.
+ version_added: "2.7"
+ type: list
+- default: []
+ elements: str
+ disable_excludes:
+ description:
+ - Disable the excludes defined in DNF config files.
+- - If set to V(all), disables all excludes.
+- - If set to V(main), disable excludes defined in [main] in dnf.conf.
+- - If set to V(repoid), disable excludes defined for given repo id.
++ - If set to C(all), disables all excludes.
++ - If set to C(main), disable excludes defined in [main] in dnf.conf.
++ - If set to C(repoid), disable excludes defined for given repo id.
+ version_added: "2.7"
+ type: str
+ validate_certs:
+ description:
+- - This only applies if using a https url as the source of the rpm. e.g. for localinstall. If set to V(false), the SSL certificates will not be validated.
+- - This should only set to V(false) used on personally controlled sites using self-signed certificates as it avoids verifying the source site.
++ - This only applies if using a https url as the source of the rpm. e.g. for localinstall. If set to C(false), the SSL certificates will not be validated.
++ - This should only set to C(false) used on personally controlled sites using self-signed certificates as it avoids verifying the source site.
+ type: bool
+ default: "yes"
+ version_added: "2.7"
+ sslverify:
+ description:
+ - Disables SSL validation of the repository server for this transaction.
+- - This should be set to V(false) if one of the configured repositories is using an untrusted or self-signed certificate.
++ - This should be set to C(false) if one of the configured repositories is using an untrusted or self-signed certificate.
+ type: bool
+ default: "yes"
+ version_added: "2.13"
+@@ -208,7 +196,7 @@ options:
+ install_repoquery:
+ description:
+ - This is effectively a no-op in DNF as it is not needed with DNF, but is an accepted parameter for feature
+- parity/compatibility with the M(ansible.builtin.yum) module.
++ parity/compatibility with the I(yum) module.
+ type: bool
+ default: "yes"
+ version_added: "2.7"
+@@ -234,12 +222,12 @@ options:
+ download_dir:
+ description:
+ - Specifies an alternate directory to store packages.
+- - Has an effect only if O(download_only) is specified.
++ - Has an effect only if I(download_only) is specified.
+ type: str
+ version_added: "2.8"
+ allowerasing:
+ description:
+- - If V(true) it allows erasing of installed packages to resolve dependencies.
++ - If C(true) it allows erasing of installed packages to resolve dependencies.
+ required: false
+ type: bool
+ default: "no"
+@@ -383,8 +371,9 @@ import os
+ import re
+ import sys
+
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.urls import fetch_file
++from ansible.module_utils.six import PY2, text_type
+ from ansible.module_utils.compat.version import LooseVersion
+
+ from ansible.module_utils.basic import AnsibleModule
+@@ -581,7 +570,6 @@ class DnfModule(YumDnf):
+ import dnf.cli
+ import dnf.const
+ import dnf.exceptions
+- import dnf.package
+ import dnf.subject
+ import dnf.util
+ HAS_DNF = True
+@@ -966,14 +954,12 @@ class DnfModule(YumDnf):
+ def _update_only(self, pkgs):
+ not_installed = []
+ for pkg in pkgs:
+- if self._is_installed(
+- self._package_dict(pkg)["nevra"] if isinstance(pkg, dnf.package.Package) else pkg
+- ):
++ if self._is_installed(pkg):
+ try:
+- if isinstance(pkg, dnf.package.Package):
+- self.base.package_upgrade(pkg)
+- else:
++ if isinstance(to_text(pkg), text_type):
+ self.base.upgrade(pkg)
++ else:
++ self.base.package_upgrade(pkg)
+ except Exception as e:
+ self.module.fail_json(
+ msg="Error occurred attempting update_only operation: {0}".format(to_native(e)),
+@@ -1461,7 +1447,6 @@ def main():
+ # backported to yum because yum is now in "maintenance mode" upstream
+ yumdnf_argument_spec['argument_spec']['allowerasing'] = dict(default=False, type='bool')
+ yumdnf_argument_spec['argument_spec']['nobest'] = dict(default=False, type='bool')
+- yumdnf_argument_spec['argument_spec']['use_backend'] = dict(default='auto', choices=['auto', 'dnf4', 'dnf5'])
+
+ module = AnsibleModule(
+ **yumdnf_argument_spec
+--- ansible-core-2.16.5.orig/lib/ansible/modules/dpkg_selections.py
++++ ansible-core-2.16.5/lib/ansible/modules/dpkg_selections.py
+@@ -39,7 +39,7 @@ attributes:
+ support: full
+ platforms: debian
+ notes:
+- - This module will not cause any packages to be installed/removed/purged, use the M(ansible.builtin.apt) module for that.
++ - This module won't cause any packages to be installed/removed/purged, use the C(apt) module for that.
+ '''
+ EXAMPLES = '''
+ - name: Prevent python from being upgraded
+@@ -54,7 +54,6 @@ EXAMPLES = '''
+ '''
+
+ from ansible.module_utils.basic import AnsibleModule
+-from ansible.module_utils.common.locale import get_best_parsable_locale
+
+
+ def main():
+@@ -68,18 +67,12 @@ def main():
+
+ dpkg = module.get_bin_path('dpkg', True)
+
+- locale = get_best_parsable_locale(module)
+- DPKG_ENV = dict(LANG=locale, LC_ALL=locale, LC_MESSAGES=locale, LC_CTYPE=locale)
+- module.run_command_environ_update = DPKG_ENV
+-
+ name = module.params['name']
+ selection = module.params['selection']
+
+ # Get current settings.
+ rc, out, err = module.run_command([dpkg, '--get-selections', name], check_rc=True)
+- if 'no packages found matching' in err:
+- module.fail_json(msg="Failed to find package '%s' to perform selection '%s'." % (name, selection))
+- elif not out:
++ if not out:
+ current = 'not present'
+ else:
+ current = out.split()[1]
+--- ansible-core-2.16.5.orig/lib/ansible/modules/expect.py
++++ ansible-core-2.16.5/lib/ansible/modules/expect.py
+@@ -13,7 +13,7 @@ module: expect
+ version_added: '2.0'
+ short_description: Executes a command and responds to prompts
+ description:
+- - The M(ansible.builtin.expect) module executes a command and responds to prompts.
++ - The C(expect) module executes a command and responds to prompts.
+ - The given command will be executed on all selected nodes. It will not be
+ processed through the shell, so variables like C($HOME) and operations
+ like C("<"), C(">"), C("|"), and C("&") will not work.
+@@ -43,10 +43,10 @@ options:
+ responses. List functionality is new in 2.1.
+ required: true
+ timeout:
+- type: raw
++ type: int
+ description:
+ - Amount of time in seconds to wait for the expected strings. Use
+- V(null) to disable timeout.
++ C(null) to disable timeout.
+ default: 30
+ echo:
+ description:
+@@ -69,7 +69,7 @@ notes:
+ - If you want to run a command through the shell (say you are using C(<),
+ C(>), C(|), and so on), you must specify a shell in the command such as
+ C(/bin/bash -c "/path/to/something | grep else").
+- - The question, or key, under O(responses) is a python regex match. Case
++ - The question, or key, under I(responses) is a python regex match. Case
+ insensitive searches are indicated with a prefix of C(?i).
+ - The C(pexpect) library used by this module operates with a search window
+ of 2000 bytes, and does not use a multiline regex match. To perform a
+@@ -81,8 +81,6 @@ notes:
+ - The M(ansible.builtin.expect) module is designed for simple scenarios.
+ For more complex needs, consider the use of expect code with the M(ansible.builtin.shell)
+ or M(ansible.builtin.script) modules. (An example is part of the M(ansible.builtin.shell) module documentation).
+- - If the command returns non UTF-8 data, it must be encoded to avoid issues. One option is to pipe
+- the output through C(base64).
+ seealso:
+ - module: ansible.builtin.script
+ - module: ansible.builtin.shell
+@@ -121,8 +119,7 @@ except ImportError:
+ HAS_PEXPECT = False
+
+ from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+-from ansible.module_utils.common.text.converters import to_bytes, to_native
+-from ansible.module_utils.common.validation import check_type_int
++from ansible.module_utils._text import to_bytes, to_native, to_text
+
+
+ def response_closure(module, question, responses):
+@@ -148,7 +145,7 @@ def main():
+ creates=dict(type='path'),
+ removes=dict(type='path'),
+ responses=dict(type='dict', required=True),
+- timeout=dict(type='raw', default=30),
++ timeout=dict(type='int', default=30),
+ echo=dict(type='bool', default=False),
+ )
+ )
+@@ -163,13 +160,6 @@ def main():
+ removes = module.params['removes']
+ responses = module.params['responses']
+ timeout = module.params['timeout']
+- if timeout is not None:
+- try:
+- timeout = check_type_int(timeout)
+- except TypeError as te:
+- module.fail_json(
+- msg="argument 'timeout' is of type {timeout_type} and we were unable to convert to int: {te}".format(timeout_type=type(timeout), te=te)
+- )
+ echo = module.params['echo']
+
+ events = dict()
+--- ansible-core-2.16.5.orig/lib/ansible/modules/fetch.py
++++ ansible-core-2.16.5/lib/ansible/modules/fetch.py
+@@ -16,7 +16,7 @@ short_description: Fetch files from remo
+ description:
+ - This module works like M(ansible.builtin.copy), but in reverse.
+ - It is used for fetching files from remote machines and storing them locally in a file tree, organized by hostname.
+-- Files that already exist at O(dest) will be overwritten if they are different than the O(src).
++- Files that already exist at I(dest) will be overwritten if they are different than the I(src).
+ - This module is also supported for Windows targets.
+ version_added: '0.2'
+ options:
+@@ -29,16 +29,16 @@ options:
+ dest:
+ description:
+ - A directory to save the file into.
+- - For example, if the O(dest) directory is C(/backup) a O(src) file named C(/etc/profile) on host
++ - For example, if the I(dest) directory is C(/backup) a I(src) file named C(/etc/profile) on host
+ C(host.example.com), would be saved into C(/backup/host.example.com/etc/profile).
+ The host name is based on the inventory name.
+ required: yes
+ fail_on_missing:
+ version_added: '1.1'
+ description:
+- - When set to V(true), the task will fail if the remote file cannot be read for any reason.
++ - When set to C(true), the task will fail if the remote file cannot be read for any reason.
+ - Prior to Ansible 2.5, setting this would only fail if the source file was missing.
+- - The default was changed to V(true) in Ansible 2.5.
++ - The default was changed to C(true) in Ansible 2.5.
+ type: bool
+ default: yes
+ validate_checksum:
+@@ -51,7 +51,7 @@ options:
+ version_added: '1.2'
+ description:
+ - Allows you to override the default behavior of appending hostname/path/to/file to the destination.
+- - If O(dest) ends with '/', it will use the basename of the source file, similar to the copy module.
++ - If C(dest) ends with '/', it will use the basename of the source file, similar to the copy module.
+ - This can be useful if working with a single host, or if retrieving files that are uniquely named per host.
+ - If using multiple hosts with the same filename, the file will be overwritten for each host.
+ type: bool
+@@ -85,10 +85,10 @@ notes:
+ remote or local hosts causing a C(MemoryError). Due to this it is
+ advisable to run this module without C(become) whenever possible.
+ - Prior to Ansible 2.5 this module would not fail if reading the remote
+- file was impossible unless O(fail_on_missing) was set.
++ file was impossible unless C(fail_on_missing) was set.
+ - In Ansible 2.5 or later, playbook authors are encouraged to use
+ C(fail_when) or C(ignore_errors) to get this ability. They may
+- also explicitly set O(fail_on_missing) to V(false) to get the
++ also explicitly set C(fail_on_missing) to C(false) to get the
+ non-failing behaviour.
+ seealso:
+ - module: ansible.builtin.copy
+--- ansible-core-2.16.5.orig/lib/ansible/modules/file.py
++++ ansible-core-2.16.5/lib/ansible/modules/file.py
+@@ -17,7 +17,7 @@ extends_documentation_fragment: [files,
+ description:
+ - Set attributes of files, directories, or symlinks and their targets.
+ - Alternatively, remove files, symlinks or directories.
+-- Many other modules support the same options as the M(ansible.builtin.file) module - including M(ansible.builtin.copy),
++- Many other modules support the same options as the C(file) module - including M(ansible.builtin.copy),
+ M(ansible.builtin.template), and M(ansible.builtin.assemble).
+ - For Windows targets, use the M(ansible.windows.win_file) module instead.
+ options:
+@@ -29,35 +29,35 @@ options:
+ aliases: [ dest, name ]
+ state:
+ description:
+- - If V(absent), directories will be recursively deleted, and files or symlinks will
++ - If C(absent), directories will be recursively deleted, and files or symlinks will
+ be unlinked. In the case of a directory, if C(diff) is declared, you will see the files and folders deleted listed
+- under C(path_contents). Note that V(absent) will not cause M(ansible.builtin.file) to fail if the O(path) does
++ under C(path_contents). Note that C(absent) will not cause C(file) to fail if the C(path) does
+ not exist as the state did not change.
+- - If V(directory), all intermediate subdirectories will be created if they
++ - If C(directory), all intermediate subdirectories will be created if they
+ do not exist. Since Ansible 1.7 they will be created with the supplied permissions.
+- - If V(file), with no other options, returns the current state of C(path).
+- - If V(file), even with other options (such as O(mode)), the file will be modified if it exists but will NOT be created if it does not exist.
+- Set to V(touch) or use the M(ansible.builtin.copy) or M(ansible.builtin.template) module if you want to create the file if it does not exist.
+- - If V(hard), the hard link will be created or changed.
+- - If V(link), the symbolic link will be created or changed.
+- - If V(touch) (new in 1.4), an empty file will be created if the file does not
++ - If C(file), with no other options, returns the current state of C(path).
++ - If C(file), even with other options (such as C(mode)), the file will be modified if it exists but will NOT be created if it does not exist.
++ Set to C(touch) or use the M(ansible.builtin.copy) or M(ansible.builtin.template) module if you want to create the file if it does not exist.
++ - If C(hard), the hard link will be created or changed.
++ - If C(link), the symbolic link will be created or changed.
++ - If C(touch) (new in 1.4), an empty file will be created if the file does not
+ exist, while an existing file or directory will receive updated file access and
+- modification times (similar to the way V(touch) works from the command line).
+- - Default is the current state of the file if it exists, V(directory) if O(recurse=yes), or V(file) otherwise.
++ modification times (similar to the way C(touch) works from the command line).
++ - Default is the current state of the file if it exists, C(directory) if C(recurse=yes), or C(file) otherwise.
+ type: str
+ choices: [ absent, directory, file, hard, link, touch ]
+ src:
+ description:
+ - Path of the file to link to.
+- - This applies only to O(state=link) and O(state=hard).
+- - For O(state=link), this will also accept a non-existing path.
+- - Relative paths are relative to the file being created (O(path)) which is how
++ - This applies only to C(state=link) and C(state=hard).
++ - For C(state=link), this will also accept a non-existing path.
++ - Relative paths are relative to the file being created (C(path)) which is how
+ the Unix command C(ln -s SRC DEST) treats relative paths.
+ type: path
+ recurse:
+ description:
+ - Recursively set the specified file attributes on directory contents.
+- - This applies only when O(state) is set to V(directory).
++ - This applies only when C(state) is set to C(directory).
+ type: bool
+ default: no
+ version_added: '1.1'
+@@ -66,27 +66,27 @@ options:
+ - >
+ Force the creation of the symlinks in two cases: the source file does
+ not exist (but will appear later); the destination exists and is a file (so, we need to unlink the
+- O(path) file and create symlink to the O(src) file in place of it).
++ C(path) file and create symlink to the C(src) file in place of it).
+ type: bool
+ default: no
+ follow:
+ description:
+ - This flag indicates that filesystem links, if they exist, should be followed.
+- - O(follow=yes) and O(state=link) can modify O(src) when combined with parameters such as O(mode).
+- - Previous to Ansible 2.5, this was V(false) by default.
++ - I(follow=yes) and I(state=link) can modify I(src) when combined with parameters such as I(mode).
++ - Previous to Ansible 2.5, this was C(false) by default.
+ type: bool
+ default: yes
+ version_added: '1.8'
+ modification_time:
+ description:
+ - This parameter indicates the time the file's modification time should be set to.
+- - Should be V(preserve) when no modification is required, C(YYYYMMDDHHMM.SS) when using default time format, or V(now).
+- - Default is None meaning that V(preserve) is the default for O(state=[file,directory,link,hard]) and V(now) is default for O(state=touch).
++ - Should be C(preserve) when no modification is required, C(YYYYMMDDHHMM.SS) when using default time format, or C(now).
++ - Default is None meaning that C(preserve) is the default for C(state=[file,directory,link,hard]) and C(now) is default for C(state=touch).
+ type: str
+ version_added: "2.7"
+ modification_time_format:
+ description:
+- - When used with O(modification_time), indicates the time format that must be used.
++ - When used with C(modification_time), indicates the time format that must be used.
+ - Based on default Python format (see time.strftime doc).
+ type: str
+ default: "%Y%m%d%H%M.%S"
+@@ -94,13 +94,13 @@ options:
+ access_time:
+ description:
+ - This parameter indicates the time the file's access time should be set to.
+- - Should be V(preserve) when no modification is required, C(YYYYMMDDHHMM.SS) when using default time format, or V(now).
+- - Default is V(None) meaning that V(preserve) is the default for O(state=[file,directory,link,hard]) and V(now) is default for O(state=touch).
++ - Should be C(preserve) when no modification is required, C(YYYYMMDDHHMM.SS) when using default time format, or C(now).
++ - Default is C(None) meaning that C(preserve) is the default for C(state=[file,directory,link,hard]) and C(now) is default for C(state=touch).
+ type: str
+ version_added: '2.7'
+ access_time_format:
+ description:
+- - When used with O(access_time), indicates the time format that must be used.
++ - When used with C(access_time), indicates the time format that must be used.
+ - Based on default Python format (see time.strftime doc).
+ type: str
+ default: "%Y%m%d%H%M.%S"
+@@ -216,13 +216,13 @@ EXAMPLES = r'''
+ '''
+ RETURN = r'''
+ dest:
+- description: Destination file/path, equal to the value passed to O(path).
+- returned: O(state=touch), O(state=hard), O(state=link)
++ description: Destination file/path, equal to the value passed to I(path).
++ returned: state=touch, state=hard, state=link
+ type: str
+ sample: /path/to/file.txt
+ path:
+- description: Destination file/path, equal to the value passed to O(path).
+- returned: O(state=absent), O(state=directory), O(state=file)
++ description: Destination file/path, equal to the value passed to I(path).
++ returned: state=absent, state=directory, state=file
+ type: str
+ sample: /path/to/file.txt
+ '''
+@@ -237,7 +237,7 @@ from pwd import getpwnam, getpwuid
+ from grp import getgrnam, getgrgid
+
+ from ansible.module_utils.basic import AnsibleModule
+-from ansible.module_utils.common.text.converters import to_bytes, to_native
++from ansible.module_utils._text import to_bytes, to_native
+
+
+ # There will only be a single AnsibleModule object per module
+--- ansible-core-2.16.5.orig/lib/ansible/modules/find.py
++++ ansible-core-2.16.5/lib/ansible/modules/find.py
+@@ -19,9 +19,6 @@ short_description: Return a list of file
+ description:
+ - Return a list of files based on specific criteria. Multiple criteria are AND'd together.
+ - For Windows targets, use the M(ansible.windows.win_find) module instead.
+- - This module does not use the C(find) command, it is a much simpler and slower Python implementation.
+- It is intended for small and simple uses. Those that need the extra power or speed and have expertise
+- with the UNIX command, should use it directly.
+ options:
+ age:
+ description:
+@@ -33,7 +30,7 @@ options:
+ patterns:
+ default: []
+ description:
+- - One or more (shell or regex) patterns, which type is controlled by O(use_regex) option.
++ - One or more (shell or regex) patterns, which type is controlled by C(use_regex) option.
+ - The patterns restrict the list of files to be returned to those whose basenames match at
+ least one of the patterns specified. Multiple patterns can be specified using a list.
+ - The pattern is matched against the file base name, excluding the directory.
+@@ -43,14 +40,14 @@ options:
+ - This parameter expects a list, which can be either comma separated or YAML. If any of the
+ patterns contain a comma, make sure to put them in a list to avoid splitting the patterns
+ in undesirable ways.
+- - Defaults to V(*) when O(use_regex=False), or V(.*) when O(use_regex=True).
++ - Defaults to C(*) when I(use_regex=False), or C(.*) when I(use_regex=True).
+ type: list
+ aliases: [ pattern ]
+ elements: str
+ excludes:
+ description:
+- - One or more (shell or regex) patterns, which type is controlled by O(use_regex) option.
+- - Items whose basenames match an O(excludes) pattern are culled from O(patterns) matches.
++ - One or more (shell or regex) patterns, which type is controlled by I(use_regex) option.
++ - Items whose basenames match an I(excludes) pattern are culled from I(patterns) matches.
+ Multiple patterns can be specified using a list.
+ type: list
+ aliases: [ exclude ]
+@@ -59,17 +56,14 @@ options:
+ contains:
+ description:
+ - A regular expression or pattern which should be matched against the file content.
+- - If O(read_whole_file) is V(false) it matches against the beginning of the line (uses
+- V(re.match(\))). If O(read_whole_file) is V(true), it searches anywhere for that pattern
+- (uses V(re.search(\))).
+- - Works only when O(file_type) is V(file).
++ - Works only when I(file_type) is C(file).
+ type: str
+ read_whole_file:
+ description:
+ - When doing a C(contains) search, determines whether the whole file should be read into
+ memory or if the regex should be applied to the file line-by-line.
+ - Setting this to C(true) can have performance and memory implications for large files.
+- - This uses V(re.search(\)) instead of V(re.match(\)).
++ - This uses C(re.search()) instead of C(re.match()).
+ type: bool
+ default: false
+ version_added: "2.11"
+@@ -108,45 +102,29 @@ options:
+ default: mtime
+ hidden:
+ description:
+- - Set this to V(true) to include hidden files, otherwise they will be ignored.
++ - Set this to C(true) to include hidden files, otherwise they will be ignored.
+ type: bool
+ default: no
+- mode:
+- description:
+- - Choose objects matching a specified permission. This value is
+- restricted to modes that can be applied using the python
+- C(os.chmod) function.
+- - The mode can be provided as an octal such as V("0644") or
+- as symbolic such as V(u=rw,g=r,o=r)
+- type: raw
+- version_added: '2.16'
+- exact_mode:
+- description:
+- - Restrict mode matching to exact matches only, and not as a
+- minimum set of permissions to match.
+- type: bool
+- default: true
+- version_added: '2.16'
+ follow:
+ description:
+- - Set this to V(true) to follow symlinks in path for systems with python 2.6+.
++ - Set this to C(true) to follow symlinks in path for systems with python 2.6+.
+ type: bool
+ default: no
+ get_checksum:
+ description:
+- - Set this to V(true) to retrieve a file's SHA1 checksum.
++ - Set this to C(true) to retrieve a file's SHA1 checksum.
+ type: bool
+ default: no
+ use_regex:
+ description:
+- - If V(false), the patterns are file globs (shell).
+- - If V(true), they are python regexes.
++ - If C(false), the patterns are file globs (shell).
++ - If C(true), they are python regexes.
+ type: bool
+ default: no
+ depth:
+ description:
+ - Set the maximum number of levels to descend into.
+- - Setting recurse to V(false) will override this value, which is effectively depth 1.
++ - Setting recurse to C(false) will override this value, which is effectively depth 1.
+ - Default is unlimited depth.
+ type: int
+ version_added: "2.6"
+@@ -266,15 +244,8 @@ import re
+ import stat
+ import time
+
+-from ansible.module_utils.common.text.converters import to_text, to_native
++from ansible.module_utils._text import to_text, to_native
+ from ansible.module_utils.basic import AnsibleModule
+-from ansible.module_utils.six import string_types
+-
+-
+-class _Object:
+- def __init__(self, **kwargs):
+- for k, v in kwargs.items():
+- setattr(self, k, v)
+
+
+ def pfilter(f, patterns=None, excludes=None, use_regex=False):
+@@ -367,25 +338,6 @@ def contentfilter(fsname, pattern, read_
+ return False
+
+
+-def mode_filter(st, mode, exact, module):
+- if not mode:
+- return True
+-
+- st_mode = stat.S_IMODE(st.st_mode)
+-
+- try:
+- mode = int(mode, 8)
+- except ValueError:
+- mode = module._symbolic_mode_to_octal(_Object(st_mode=0), mode)
+-
+- mode = stat.S_IMODE(mode)
+-
+- if exact:
+- return st_mode == mode
+-
+- return bool(st_mode & mode)
+-
+-
+ def statinfo(st):
+ pw_name = ""
+ gr_name = ""
+@@ -456,19 +408,12 @@ def main():
+ get_checksum=dict(type='bool', default=False),
+ use_regex=dict(type='bool', default=False),
+ depth=dict(type='int'),
+- mode=dict(type='raw'),
+- exact_mode=dict(type='bool', default=True),
+ ),
+ supports_check_mode=True,
+ )
+
+ params = module.params
+
+- if params['mode'] and not isinstance(params['mode'], string_types):
+- module.fail_json(
+- msg="argument 'mode' is not a string and conversion is not allowed, value is of type %s" % params['mode'].__class__.__name__
+- )
+-
+ # Set the default match pattern to either a match-all glob or
+ # regex depending on use_regex being set. This makes sure if you
+ # set excludes: without a pattern pfilter gets something it can
+@@ -538,9 +483,7 @@ def main():
+
+ r = {'path': fsname}
+ if params['file_type'] == 'any':
+- if (pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and
+- agefilter(st, now, age, params['age_stamp']) and
+- mode_filter(st, params['mode'], params['exact_mode'], module)):
++ if pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and agefilter(st, now, age, params['age_stamp']):
+
+ r.update(statinfo(st))
+ if stat.S_ISREG(st.st_mode) and params['get_checksum']:
+@@ -553,19 +496,15 @@ def main():
+ filelist.append(r)
+
+ elif stat.S_ISDIR(st.st_mode) and params['file_type'] == 'directory':
+- if (pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and
+- agefilter(st, now, age, params['age_stamp']) and
+- mode_filter(st, params['mode'], params['exact_mode'], module)):
++ if pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and agefilter(st, now, age, params['age_stamp']):
+
+ r.update(statinfo(st))
+ filelist.append(r)
+
+ elif stat.S_ISREG(st.st_mode) and params['file_type'] == 'file':
+- if (pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and
+- agefilter(st, now, age, params['age_stamp']) and
+- sizefilter(st, size) and
+- contentfilter(fsname, params['contains'], params['read_whole_file']) and
+- mode_filter(st, params['mode'], params['exact_mode'], module)):
++ if pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and \
++ agefilter(st, now, age, params['age_stamp']) and \
++ sizefilter(st, size) and contentfilter(fsname, params['contains'], params['read_whole_file']):
+
+ r.update(statinfo(st))
+ if params['get_checksum']:
+@@ -573,9 +512,7 @@ def main():
+ filelist.append(r)
+
+ elif stat.S_ISLNK(st.st_mode) and params['file_type'] == 'link':
+- if (pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and
+- agefilter(st, now, age, params['age_stamp']) and
+- mode_filter(st, params['mode'], params['exact_mode'], module)):
++ if pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and agefilter(st, now, age, params['age_stamp']):
+
+ r.update(statinfo(st))
+ filelist.append(r)
+--- ansible-core-2.16.5.orig/lib/ansible/modules/gather_facts.py
++++ ansible-core-2.16.5/lib/ansible/modules/gather_facts.py
+@@ -26,15 +26,13 @@ options:
+ - A toggle that controls if the fact modules are executed in parallel or serially and in order.
+ This can guarantee the merge order of module facts at the expense of performance.
+ - By default it will be true if more than one fact module is used.
+- - For low cost/delay fact modules parallelism overhead might end up meaning the whole process takes longer.
+- Test your specific case to see if it is a speed improvement or not.
+ type: bool
+ attributes:
+ action:
+ support: full
+ async:
+- details: while this action does not support the task 'async' keywords it can do its own parallel processing using the O(parallel) option.
+- support: none
++ details: multiple modules can be executed in parallel or serially, but the action itself will not be async
++ support: partial
+ bypass_host_loop:
+ support: none
+ check_mode:
+@@ -50,8 +48,6 @@ attributes:
+ notes:
+ - This is mostly a wrapper around other fact gathering modules.
+ - Options passed into this action must be supported by all the underlying fact modules configured.
+- - If using C(gather_timeout) and parallel execution, it will limit the total execution time of
+- modules that do not accept C(gather_timeout) themselves.
+ - Facts returned by each module will be merged, conflicts will favor 'last merged'.
+ Order is not guaranteed, when doing parallel gathering on multiple modules.
+ author:
+--- ansible-core-2.16.5.orig/lib/ansible/modules/get_url.py
++++ ansible-core-2.16.5/lib/ansible/modules/get_url.py
+@@ -29,7 +29,7 @@ options:
+ ciphers:
+ description:
+ - SSL/TLS Ciphers to use for the request
+- - 'When a list is provided, all ciphers are joined in order with V(:)'
++ - 'When a list is provided, all ciphers are joined in order with C(:)'
+ - See the L(OpenSSL Cipher List Format,https://www.openssl.org/docs/manmaster/man1/openssl-ciphers.html#CIPHER-LIST-FORMAT)
+ for more details.
+ - The available ciphers is dependent on the Python and OpenSSL/LibreSSL versions
+@@ -50,11 +50,11 @@ options:
+ dest:
+ description:
+ - Absolute path of where to download the file to.
+- - If O(dest) is a directory, either the server provided filename or, if
++ - If C(dest) is a directory, either the server provided filename or, if
+ none provided, the base name of the URL on the remote server will be
+- used. If a directory, O(force) has no effect.
+- - If O(dest) is a directory, the file will always be downloaded
+- (regardless of the O(force) and O(checksum) option), but
++ used. If a directory, C(force) has no effect.
++ - If C(dest) is a directory, the file will always be downloaded
++ (regardless of the C(force) and C(checksum) option), but
+ replaced only if the contents changed.
+ type: path
+ required: true
+@@ -62,17 +62,17 @@ options:
+ description:
+ - Absolute path of where temporary file is downloaded to.
+ - When run on Ansible 2.5 or greater, path defaults to ansible's remote_tmp setting
+- - When run on Ansible prior to 2.5, it defaults to E(TMPDIR), E(TEMP) or E(TMP) env variables or a platform specific value.
++ - When run on Ansible prior to 2.5, it defaults to C(TMPDIR), C(TEMP) or C(TMP) env variables or a platform specific value.
+ - U(https://docs.python.org/3/library/tempfile.html#tempfile.tempdir)
+ type: path
+ version_added: '2.1'
+ force:
+ description:
+- - If V(true) and O(dest) is not a directory, will download the file every
+- time and replace the file if the contents change. If V(false), the file
++ - If C(true) and C(dest) is not a directory, will download the file every
++ time and replace the file if the contents change. If C(false), the file
+ will only be downloaded if the destination does not exist. Generally
+- should be V(true) only for small local files.
+- - Prior to 0.6, this module behaved as if V(true) was the default.
++ should be C(true) only for small local files.
++ - Prior to 0.6, this module behaved as if C(true) was the default.
+ type: bool
+ default: no
+ version_added: '0.7'
+@@ -92,26 +92,24 @@ options:
+ checksum="sha256:http://example.com/path/sha256sum.txt"'
+ - If you worry about portability, only the sha1 algorithm is available
+ on all platforms and python versions.
+- - The Python ``hashlib`` module is responsible for providing the available algorithms.
+- The choices vary based on Python version and OpenSSL version.
+- - On systems running in FIPS compliant mode, the ``md5`` algorithm may be unavailable.
++ - The third party hashlib library can be installed for access to additional algorithms.
+ - Additionally, if a checksum is passed to this parameter, and the file exist under
+- the O(dest) location, the C(destination_checksum) would be calculated, and if
+- checksum equals C(destination_checksum), the file download would be skipped
+- (unless O(force) is V(true)). If the checksum does not equal C(destination_checksum),
++ the C(dest) location, the I(destination_checksum) would be calculated, and if
++ checksum equals I(destination_checksum), the file download would be skipped
++ (unless C(force) is true). If the checksum does not equal I(destination_checksum),
+ the destination file is deleted.
+ type: str
+ default: ''
+ version_added: "2.0"
+ use_proxy:
+ description:
+- - if V(false), it will not use a proxy, even if one is defined in
++ - if C(false), it will not use a proxy, even if one is defined in
+ an environment variable on the target hosts.
+ type: bool
+ default: yes
+ validate_certs:
+ description:
+- - If V(false), SSL certificates will not be validated.
++ - If C(false), SSL certificates will not be validated.
+ - This should only be used on personally controlled sites using self-signed certificates.
+ type: bool
+ default: yes
+@@ -132,16 +130,16 @@ options:
+ url_username:
+ description:
+ - The username for use in HTTP basic authentication.
+- - This parameter can be used without O(url_password) for sites that allow empty passwords.
+- - Since version 2.8 you can also use the O(username) alias for this option.
++ - This parameter can be used without C(url_password) for sites that allow empty passwords.
++ - Since version 2.8 you can also use the C(username) alias for this option.
+ type: str
+ aliases: ['username']
+ version_added: '1.6'
+ url_password:
+ description:
+ - The password for use in HTTP basic authentication.
+- - If the O(url_username) parameter is not specified, the O(url_password) parameter will not be used.
+- - Since version 2.8 you can also use the O(password) alias for this option.
++ - If the C(url_username) parameter is not specified, the C(url_password) parameter will not be used.
++ - Since version 2.8 you can also use the 'password' alias for this option.
+ type: str
+ aliases: ['password']
+ version_added: '1.6'
+@@ -157,13 +155,13 @@ options:
+ client_cert:
+ description:
+ - PEM formatted certificate chain file to be used for SSL client authentication.
+- - This file can also include the key as well, and if the key is included, O(client_key) is not required.
++ - This file can also include the key as well, and if the key is included, C(client_key) is not required.
+ type: path
+ version_added: '2.4'
+ client_key:
+ description:
+ - PEM formatted file that contains your private key to be used for SSL client authentication.
+- - If O(client_cert) contains both the certificate and key, this option is not required.
++ - If C(client_cert) contains both the certificate and key, this option is not required.
+ type: path
+ version_added: '2.4'
+ http_agent:
+@@ -185,7 +183,7 @@ options:
+ - Use GSSAPI to perform the authentication, typically this is for Kerberos or Kerberos through Negotiate
+ authentication.
+ - Requires the Python library L(gssapi,https://github.com/pythongssapi/python-gssapi) to be installed.
+- - Credentials for GSSAPI can be specified with O(url_username)/O(url_password) or with the GSSAPI env var
++ - Credentials for GSSAPI can be specified with I(url_username)/I(url_password) or with the GSSAPI env var
+ C(KRB5CCNAME) that specified a custom Kerberos credential cache.
+ - NTLM authentication is I(not) supported even if the GSSAPI mech for NTLM has been installed.
+ type: bool
+@@ -366,6 +364,7 @@ url:
+ sample: https://www.ansible.com/
+ '''
+
++import datetime
+ import os
+ import re
+ import shutil
+@@ -374,8 +373,7 @@ import traceback
+
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.six.moves.urllib.parse import urlsplit
+-from ansible.module_utils.compat.datetime import utcnow, utcfromtimestamp
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.urls import fetch_url, url_argument_spec
+
+ # ==============================================================
+@@ -397,10 +395,10 @@ def url_get(module, url, dest, use_proxy
+ Return (tempfile, info about the request)
+ """
+
+- start = utcnow()
++ start = datetime.datetime.utcnow()
+ rsp, info = fetch_url(module, url, use_proxy=use_proxy, force=force, last_mod_time=last_mod_time, timeout=timeout, headers=headers, method=method,
+ unredirected_headers=unredirected_headers, decompress=decompress, ciphers=ciphers, use_netrc=use_netrc)
+- elapsed = (utcnow() - start).seconds
++ elapsed = (datetime.datetime.utcnow() - start).seconds
+
+ if info['status'] == 304:
+ module.exit_json(url=url, dest=dest, changed=False, msg=info.get('msg', ''), status_code=info['status'], elapsed=elapsed)
+@@ -600,7 +598,7 @@ def main():
+ # If the file already exists, prepare the last modified time for the
+ # request.
+ mtime = os.path.getmtime(dest)
+- last_mod_time = utcfromtimestamp(mtime)
++ last_mod_time = datetime.datetime.utcfromtimestamp(mtime)
+
+ # If the checksum does not match we have to force the download
+ # because last_mod_time may be newer than on remote
+@@ -608,11 +606,11 @@ def main():
+ force = True
+
+ # download to tmpsrc
+- start = utcnow()
++ start = datetime.datetime.utcnow()
+ method = 'HEAD' if module.check_mode else 'GET'
+ tmpsrc, info = url_get(module, url, dest, use_proxy, last_mod_time, force, timeout, headers, tmp_dest, method,
+ unredirected_headers=unredirected_headers, decompress=decompress, ciphers=ciphers, use_netrc=use_netrc)
+- result['elapsed'] = (utcnow() - start).seconds
++ result['elapsed'] = (datetime.datetime.utcnow() - start).seconds
+ result['src'] = tmpsrc
+
+ # Now the request has completed, we can finally generate the final
+--- ansible-core-2.16.5.orig/lib/ansible/modules/getent.py
++++ ansible-core-2.16.5/lib/ansible/modules/getent.py
+@@ -13,7 +13,7 @@ module: getent
+ short_description: A wrapper to the unix getent utility
+ description:
+ - Runs getent against one of its various databases and returns information into
+- the host's facts, in a C(getent_<database>) prefixed variable.
++ the host's facts, in a getent_<database> prefixed variable.
+ version_added: "1.8"
+ options:
+ database:
+@@ -27,6 +27,7 @@ options:
+ - Key from which to return values from the specified database, otherwise the
+ full contents are returned.
+ type: str
++ default: ''
+ service:
+ description:
+ - Override all databases with the specified service
+@@ -35,12 +36,12 @@ options:
+ version_added: "2.9"
+ split:
+ description:
+- - Character used to split the database values into lists/arrays such as V(:) or V(\\t),
++ - Character used to split the database values into lists/arrays such as C(:) or C(\t),
+ otherwise it will try to pick one depending on the database.
+ type: str
+ fail_key:
+ description:
+- - If a supplied key is missing this will make the task fail if V(true).
++ - If a supplied key is missing this will make the task fail if C(true).
+ type: bool
+ default: 'yes'
+ extends_documentation_fragment:
+@@ -118,7 +119,7 @@ ansible_facts:
+ import traceback
+
+ from ansible.module_utils.basic import AnsibleModule
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+
+ def main():
+--- ansible-core-2.16.5.orig/lib/ansible/modules/git.py
++++ ansible-core-2.16.5/lib/ansible/modules/git.py
+@@ -29,15 +29,15 @@ options:
+ description:
+ - The path of where the repository should be checked out. This
+ is equivalent to C(git clone [repo_url] [directory]). The repository
+- named in O(repo) is not appended to this path and the destination directory must be empty. This
+- parameter is required, unless O(clone) is set to V(false).
++ named in I(repo) is not appended to this path and the destination directory must be empty. This
++ parameter is required, unless I(clone) is set to C(false).
+ type: path
+ required: true
+ version:
+ description:
+ - What version of the repository to check out. This can be
+- the literal string V(HEAD), a branch name, a tag name.
+- It can also be a I(SHA-1) hash, in which case O(refspec) needs
++ the literal string C(HEAD), a branch name, a tag name.
++ It can also be a I(SHA-1) hash, in which case I(refspec) needs
+ to be specified if the given revision is not already available.
+ type: str
+ default: "HEAD"
+@@ -45,7 +45,7 @@ options:
+ description:
+ - Will ensure or not that "-o StrictHostKeyChecking=no" is present as an ssh option.
+ - Be aware that this disables a protection against MITM attacks.
+- - Those using OpenSSH >= 7.5 might want to set O(ssh_opts) to V(StrictHostKeyChecking=accept-new)
++ - Those using OpenSSH >= 7.5 might want to set I(ssh_opts) to 'StrictHostKeyChecking=accept-new'
+ instead, it does not remove the MITM issue but it does restrict it to the first attempt.
+ type: bool
+ default: 'no'
+@@ -54,7 +54,7 @@ options:
+ description:
+ - As of OpenSSH 7.5, "-o StrictHostKeyChecking=accept-new" can be
+ used which is safer and will only accepts host keys which are
+- not present or are the same. if V(true), ensure that
++ not present or are the same. if C(true), ensure that
+ "-o StrictHostKeyChecking=accept-new" is present as an ssh option.
+ type: bool
+ default: 'no'
+@@ -62,12 +62,12 @@ options:
+ ssh_opts:
+ description:
+ - Options git will pass to ssh when used as protocol, it works via C(git)'s
+- E(GIT_SSH)/E(GIT_SSH_COMMAND) environment variables.
+- - For older versions it appends E(GIT_SSH_OPTS) (specific to this module) to the
++ GIT_SSH/GIT_SSH_COMMAND environment variables.
++ - For older versions it appends GIT_SSH_OPTS (specific to this module) to the
+ variables above or via a wrapper script.
+- - Other options can add to this list, like O(key_file) and O(accept_hostkey).
++ - Other options can add to this list, like I(key_file) and I(accept_hostkey).
+ - An example value could be "-o StrictHostKeyChecking=no" (although this particular
+- option is better set by O(accept_hostkey)).
++ option is better set by I(accept_hostkey)).
+ - The module ensures that 'BatchMode=yes' is always present to avoid prompts.
+ type: str
+ version_added: "1.5"
+@@ -75,13 +75,12 @@ options:
+ key_file:
+ description:
+ - Specify an optional private key file path, on the target host, to use for the checkout.
+- - This ensures 'IdentitiesOnly=yes' is present in O(ssh_opts).
++ - This ensures 'IdentitiesOnly=yes' is present in ssh_opts.
+ type: path
+ version_added: "1.5"
+ reference:
+ description:
+ - Reference repository (see "git clone --reference ...").
+- type: str
+ version_added: "1.4"
+ remote:
+ description:
+@@ -100,29 +99,29 @@ options:
+ version_added: "1.9"
+ force:
+ description:
+- - If V(true), any modified files in the working
++ - If C(true), any modified files in the working
+ repository will be discarded. Prior to 0.7, this was always
+- V(true) and could not be disabled. Prior to 1.9, the default was
+- V(true).
++ C(true) and could not be disabled. Prior to 1.9, the default was
++ C(true).
+ type: bool
+ default: 'no'
+ version_added: "0.7"
+ depth:
+ description:
+ - Create a shallow clone with a history truncated to the specified
+- number or revisions. The minimum possible value is V(1), otherwise
++ number or revisions. The minimum possible value is C(1), otherwise
+ ignored. Needs I(git>=1.9.1) to work correctly.
+ type: int
+ version_added: "1.2"
+ clone:
+ description:
+- - If V(false), do not clone the repository even if it does not exist locally.
++ - If C(false), do not clone the repository even if it does not exist locally.
+ type: bool
+ default: 'yes'
+ version_added: "1.9"
+ update:
+ description:
+- - If V(false), do not retrieve new revisions from the origin repository.
++ - If C(false), do not retrieve new revisions from the origin repository.
+ - Operations like archive will work on the existing (old) repository and might
+ not respond to changes to the options version or remote.
+ type: bool
+@@ -136,7 +135,7 @@ options:
+ version_added: "1.4"
+ bare:
+ description:
+- - If V(true), repository will be created as a bare repo, otherwise
++ - If C(true), repository will be created as a bare repo, otherwise
+ it will be a standard repo with a workspace.
+ type: bool
+ default: 'no'
+@@ -150,7 +149,7 @@ options:
+
+ recursive:
+ description:
+- - If V(false), repository will be cloned without the C(--recursive)
++ - If C(false), repository will be cloned without the --recursive
+ option, skipping sub-modules.
+ type: bool
+ default: 'yes'
+@@ -165,10 +164,10 @@ options:
+
+ track_submodules:
+ description:
+- - If V(true), submodules will track the latest commit on their
++ - If C(true), submodules will track the latest commit on their
+ master branch (or other branch specified in .gitmodules). If
+- V(false), submodules will be kept at the revision specified by the
+- main project. This is equivalent to specifying the C(--remote) flag
++ C(false), submodules will be kept at the revision specified by the
++ main project. This is equivalent to specifying the --remote flag
+ to git submodule update.
+ type: bool
+ default: 'no'
+@@ -176,7 +175,7 @@ options:
+
+ verify_commit:
+ description:
+- - If V(true), when cloning or checking out a O(version) verify the
++ - If C(true), when cloning or checking out a I(version) verify the
+ signature of a GPG signed commit. This requires git version>=2.1.0
+ to be installed. The commit MUST be signed and the public key MUST
+ be present in the GPG keyring.
+@@ -197,7 +196,7 @@ options:
+
+ archive_prefix:
+ description:
+- - Specify a prefix to add to each file path in archive. Requires O(archive) to be specified.
++ - Specify a prefix to add to each file path in archive. Requires I(archive) to be specified.
+ version_added: "2.10"
+ type: str
+
+@@ -212,7 +211,7 @@ options:
+ description:
+ - A list of trusted GPG fingerprints to compare to the fingerprint of the
+ GPG-signed commit.
+- - Only used when O(verify_commit=yes).
++ - Only used when I(verify_commit=yes).
+ - Use of this feature requires Git 2.6+ due to its reliance on git's C(--raw) flag to C(verify-commit) and C(verify-tag).
+ type: list
+ elements: str
+@@ -338,7 +337,7 @@ import shutil
+ import tempfile
+ from ansible.module_utils.compat.version import LooseVersion
+
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.common.locale import get_best_parsable_locale
+ from ansible.module_utils.common.process import get_bin_path
+@@ -826,7 +825,7 @@ def get_head_branch(git_path, module, de
+ repo_path = get_repo_path(dest, bare)
+ except (IOError, ValueError) as err:
+ # No repo path found
+- # ``.git`` file does not have a valid format for detached Git dir.
++ """``.git`` file does not have a valid format for detached Git dir."""
+ module.fail_json(
+ msg='Current repo does not have a valid reference to a '
+ 'separate Git dir or it refers to the invalid path',
+@@ -1124,7 +1123,7 @@ def create_archive(git_path, module, des
+ """ Helper function for creating archive using git_archive """
+ all_archive_fmt = {'.zip': 'zip', '.gz': 'tar.gz', '.tar': 'tar',
+ '.tgz': 'tgz'}
+- dummy, archive_ext = os.path.splitext(archive)
++ _, archive_ext = os.path.splitext(archive)
+ archive_fmt = all_archive_fmt.get(archive_ext, None)
+ if archive_fmt is None:
+ module.fail_json(msg="Unable to get file extension from "
+@@ -1283,7 +1282,7 @@ def main():
+ repo_path = separate_git_dir
+ except (IOError, ValueError) as err:
+ # No repo path found
+- # ``.git`` file does not have a valid format for detached Git dir.
++ """``.git`` file does not have a valid format for detached Git dir."""
+ module.fail_json(
+ msg='Current repo does not have a valid reference to a '
+ 'separate Git dir or it refers to the invalid path',
+--- ansible-core-2.16.5.orig/lib/ansible/modules/group.py
++++ ansible-core-2.16.5/lib/ansible/modules/group.py
+@@ -35,16 +35,9 @@ options:
+ type: str
+ choices: [ absent, present ]
+ default: present
+- force:
+- description:
+- - Whether to delete a group even if it is the primary group of a user.
+- - Only applicable on platforms which implement a --force flag on the group deletion command.
+- type: bool
+- default: false
+- version_added: "2.15"
+ system:
+ description:
+- - If V(yes), indicates that the group created is a system group.
++ - If I(yes), indicates that the group created is a system group.
+ type: bool
+ default: no
+ local:
+@@ -58,7 +51,7 @@ options:
+ version_added: "2.6"
+ non_unique:
+ description:
+- - This option allows to change the group ID to a non-unique value. Requires O(gid).
++ - This option allows to change the group ID to a non-unique value. Requires C(gid).
+ - Not supported on macOS or BusyBox distributions.
+ type: bool
+ default: no
+@@ -94,7 +87,7 @@ EXAMPLES = '''
+ RETURN = r'''
+ gid:
+ description: Group ID of the group.
+- returned: When O(state) is C(present)
++ returned: When C(state) is 'present'
+ type: int
+ sample: 1001
+ name:
+@@ -109,7 +102,7 @@ state:
+ sample: 'absent'
+ system:
+ description: Whether the group is a system group or not.
+- returned: When O(state) is C(present)
++ returned: When C(state) is 'present'
+ type: bool
+ sample: False
+ '''
+@@ -117,7 +110,7 @@ system:
+ import grp
+ import os
+
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.common.sys_info import get_platform_subclass
+
+@@ -147,7 +140,6 @@ class Group(object):
+ self.module = module
+ self.state = module.params['state']
+ self.name = module.params['name']
+- self.force = module.params['force']
+ self.gid = module.params['gid']
+ self.system = module.params['system']
+ self.local = module.params['local']
+@@ -227,7 +219,14 @@ class Group(object):
+ if line.startswith(to_bytes(name_test)):
+ exists = True
+ break
++
++ if not exists:
++ self.module.warn(
++ "'local: true' specified and group was not found in {file}. "
++ "The local group may already exist if the local group database exists somewhere other than {file}.".format(file=self.GROUPFILE))
++
+ return exists
++
+ else:
+ try:
+ if grp.getgrnam(self.name):
+@@ -247,31 +246,6 @@ class Group(object):
+
+ # ===========================================
+
+-class Linux(Group):
+- """
+- This is a Linux Group manipulation class. This is to apply the '-f' parameter to the groupdel command
+-
+- This overrides the following methods from the generic class:-
+- - group_del()
+- """
+-
+- platform = 'Linux'
+- distribution = None
+-
+- def group_del(self):
+- if self.local:
+- command_name = 'lgroupdel'
+- else:
+- command_name = 'groupdel'
+- cmd = [self.module.get_bin_path(command_name, True)]
+- if self.force:
+- cmd.append('-f')
+- cmd.append(self.name)
+- return self.execute_command(cmd)
+-
+-
+-# ===========================================
+-
+ class SunOS(Group):
+ """
+ This is a SunOS Group manipulation class. Solaris doesn't have
+@@ -622,7 +596,6 @@ def main():
+ argument_spec=dict(
+ state=dict(type='str', default='present', choices=['absent', 'present']),
+ name=dict(type='str', required=True),
+- force=dict(type='bool', default=False),
+ gid=dict(type='int'),
+ system=dict(type='bool', default=False),
+ local=dict(type='bool', default=False),
+@@ -634,9 +607,6 @@ def main():
+ ],
+ )
+
+- if module.params['force'] and module.params['local']:
+- module.fail_json(msg='force is not a valid option for local, force=True and local=True are mutually exclusive')
+-
+ group = Group(module)
+
+ module.debug('Group instantiated - platform %s' % group.platform)
+--- ansible-core-2.16.5.orig/lib/ansible/modules/group_by.py
++++ ansible-core-2.16.5/lib/ansible/modules/group_by.py
+@@ -40,7 +40,7 @@ attributes:
+ become:
+ support: none
+ bypass_host_loop:
+- support: none
++ support: full
+ bypass_task_loop:
+ support: none
+ check_mode:
+--- ansible-core-2.16.5.orig/lib/ansible/modules/hostname.py
++++ ansible-core-2.16.5/lib/ansible/modules/hostname.py
+@@ -81,7 +81,7 @@ from ansible.module_utils.basic import (
+ from ansible.module_utils.common.sys_info import get_platform_subclass
+ from ansible.module_utils.facts.system.service_mgr import ServiceMgrFactCollector
+ from ansible.module_utils.facts.utils import get_file_lines, get_file_content
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.six import PY3, text_type
+
+ STRATS = {
+@@ -387,29 +387,10 @@ class OpenRCStrategy(BaseStrategy):
+ class OpenBSDStrategy(FileStrategy):
+ """
+ This is a OpenBSD family Hostname manipulation strategy class - it edits
+- the /etc/myname file for the permanent hostname and executes hostname
+- command for the current hostname.
++ the /etc/myname file.
+ """
+
+ FILE = '/etc/myname'
+- COMMAND = "hostname"
+-
+- def __init__(self, module):
+- super(OpenBSDStrategy, self).__init__(module)
+- self.hostname_cmd = self.module.get_bin_path(self.COMMAND, True)
+-
+- def get_current_hostname(self):
+- cmd = [self.hostname_cmd]
+- rc, out, err = self.module.run_command(cmd)
+- if rc != 0:
+- self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % (rc, out, err))
+- return to_native(out).strip()
+-
+- def set_current_hostname(self, name):
+- cmd = [self.hostname_cmd, name]
+- rc, out, err = self.module.run_command(cmd)
+- if rc != 0:
+- self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % (rc, out, err))
+
+
+ class SolarisStrategy(BaseStrategy):
+--- ansible-core-2.16.5.orig/lib/ansible/modules/import_playbook.py
++++ ansible-core-2.16.5/lib/ansible/modules/import_playbook.py
+@@ -41,7 +41,7 @@ seealso:
+ - module: ansible.builtin.import_tasks
+ - module: ansible.builtin.include_role
+ - module: ansible.builtin.include_tasks
+-- ref: playbooks_reuse
++- ref: playbooks_reuse_includes
+ description: More information related to including and importing playbooks, roles and tasks.
+ '''
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/import_role.py
++++ ansible-core-2.16.5/lib/ansible/modules/import_role.py
+@@ -78,7 +78,7 @@ seealso:
+ - module: ansible.builtin.import_tasks
+ - module: ansible.builtin.include_role
+ - module: ansible.builtin.include_tasks
+-- ref: playbooks_reuse
++- ref: playbooks_reuse_includes
+ description: More information related to including and importing playbooks, roles and tasks.
+ '''
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/import_tasks.py
++++ ansible-core-2.16.5/lib/ansible/modules/import_tasks.py
+@@ -45,7 +45,7 @@ seealso:
+ - module: ansible.builtin.import_role
+ - module: ansible.builtin.include_role
+ - module: ansible.builtin.include_tasks
+-- ref: playbooks_reuse
++- ref: playbooks_reuse_includes
+ description: More information related to including and importing playbooks, roles and tasks.
+ '''
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/include_role.py
++++ ansible-core-2.16.5/lib/ansible/modules/include_role.py
+@@ -16,7 +16,7 @@ description:
+ - Dynamically loads and executes a specified role as a task.
+ - May be used only where Ansible tasks are allowed - inside C(pre_tasks), C(tasks), or C(post_tasks) play objects, or as a task inside a role.
+ - Task-level keywords, loops, and conditionals apply only to the C(include_role) statement itself.
+- - To apply keywords to the tasks within the role, pass them using the O(apply) option or use M(ansible.builtin.import_role) instead.
++ - To apply keywords to the tasks within the role, pass them using the C(apply) option or use M(ansible.builtin.import_role) instead.
+ - Ignores some keywords, like C(until) and C(retries).
+ - This module is also supported for Windows targets.
+ - Does not work in handlers.
+@@ -24,7 +24,7 @@ version_added: "2.2"
+ options:
+ apply:
+ description:
+- - Accepts a hash of task keywords (for example C(tags), C(become)) that will be applied to all tasks within the included role.
++ - Accepts a hash of task keywords (e.g. C(tags), C(become)) that will be applied to all tasks within the included role.
+ version_added: '2.7'
+ name:
+ description:
+@@ -53,9 +53,9 @@ options:
+ default: yes
+ public:
+ description:
+- - This option dictates whether the role's C(vars) and C(defaults) are exposed to the play. If set to V(true)
++ - This option dictates whether the role's C(vars) and C(defaults) are exposed to the play. If set to C(true)
+ the variables will be available to tasks following the C(include_role) task. This functionality differs from
+- standard variable exposure for roles listed under the C(roles) header or M(ansible.builtin.import_role) as they are exposed
++ standard variable exposure for roles listed under the C(roles) header or C(import_role) as they are exposed
+ to the play at playbook parsing time, and available to earlier roles and tasks as well.
+ type: bool
+ default: no
+@@ -85,13 +85,13 @@ attributes:
+ support: none
+ notes:
+ - Handlers and are made available to the whole play.
+- - After Ansible 2.4, you can use M(ansible.builtin.import_role) for B(static) behaviour and this action for B(dynamic) one.
++ - After Ansible 2.4, you can use M(ansible.builtin.import_role) for C(static) behaviour and this action for C(dynamic) one.
+ seealso:
+ - module: ansible.builtin.import_playbook
+ - module: ansible.builtin.import_role
+ - module: ansible.builtin.import_tasks
+ - module: ansible.builtin.include_tasks
+-- ref: playbooks_reuse
++- ref: playbooks_reuse_includes
+ description: More information related to including and importing playbooks, roles and tasks.
+ '''
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/include_tasks.py
++++ ansible-core-2.16.5/lib/ansible/modules/include_tasks.py
+@@ -23,14 +23,14 @@ options:
+ version_added: '2.7'
+ apply:
+ description:
+- - Accepts a hash of task keywords (for example C(tags), C(become)) that will be applied to the tasks within the include.
++ - Accepts a hash of task keywords (e.g. C(tags), C(become)) that will be applied to the tasks within the include.
+ type: str
+ version_added: '2.7'
+ free-form:
+ description:
+ - |
+ Specifies the name of the imported file directly without any other option C(- include_tasks: file.yml).
+- - Is the equivalent of specifying an argument for the O(file) parameter.
++ - Is the equivalent of specifying an argument for the I(file) parameter.
+ - Most keywords, including loop, with_items, and conditionals, apply to this statement unlike M(ansible.builtin.import_tasks).
+ - The do-until loop is not supported.
+ extends_documentation_fragment:
+@@ -49,7 +49,7 @@ seealso:
+ - module: ansible.builtin.import_role
+ - module: ansible.builtin.import_tasks
+ - module: ansible.builtin.include_role
+-- ref: playbooks_reuse
++- ref: playbooks_reuse_includes
+ description: More information related to including and importing playbooks, roles and tasks.
+ '''
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/include_vars.py
++++ ansible-core-2.16.5/lib/ansible/modules/include_vars.py
+@@ -40,7 +40,7 @@ options:
+ version_added: "2.2"
+ depth:
+ description:
+- - When using O(dir), this module will, by default, recursively go through each sub directory and load up the
++ - When using C(dir), this module will, by default, recursively go through each sub directory and load up the
+ variables. By explicitly setting the depth, this module will only go as deep as the depth.
+ type: int
+ default: 0
+@@ -58,7 +58,7 @@ options:
+ version_added: "2.2"
+ extensions:
+ description:
+- - List of file extensions to read when using O(dir).
++ - List of file extensions to read when using C(dir).
+ type: list
+ elements: str
+ default: [ json, yaml, yml ]
+@@ -73,9 +73,8 @@ options:
+ version_added: "2.7"
+ hash_behaviour:
+ description:
+- - If set to V(merge), merges existing hash variables instead of overwriting them.
+- - If omitted (V(null)), the behavior falls back to the global C(hash_behaviour) configuration.
+- - This option is self-contained and does not apply to individual files in O(dir). You can use a loop to apply O(hash_behaviour) per file.
++ - If set to C(merge), merges existing hash variables instead of overwriting them.
++ - If omitted C(null), the behavior falls back to the global I(hash_behaviour) configuration.
+ default: null
+ type: str
+ choices: ["replace", "merge"]
+--- ansible-core-2.16.5.orig/lib/ansible/modules/iptables.py
++++ ansible-core-2.16.5/lib/ansible/modules/iptables.py
+@@ -17,7 +17,7 @@ author:
+ - Linus Unnebäck (@LinusU) <linus@folkdatorn.se>
+ - Sébastien DA ROCHA (@sebastiendarocha)
+ description:
+- - M(ansible.builtin.iptables) is used to set up, maintain, and inspect the tables of IP packet
++ - C(iptables) is used to set up, maintain, and inspect the tables of IP packet
+ filter rules in the Linux kernel.
+ - This module does not handle the saving and/or loading of rules, but rather
+ only manipulates the current rules that are present in memory. This is the
+@@ -61,7 +61,7 @@ options:
+ rule_num:
+ description:
+ - Insert the rule as the given rule number.
+- - This works only with O(action=insert).
++ - This works only with C(action=insert).
+ type: str
+ version_added: "2.5"
+ ip_version:
+@@ -74,18 +74,18 @@ options:
+ description:
+ - Specify the iptables chain to modify.
+ - This could be a user-defined chain or one of the standard iptables chains, like
+- V(INPUT), V(FORWARD), V(OUTPUT), V(PREROUTING), V(POSTROUTING), V(SECMARK) or V(CONNSECMARK).
++ C(INPUT), C(FORWARD), C(OUTPUT), C(PREROUTING), C(POSTROUTING), C(SECMARK) or C(CONNSECMARK).
+ type: str
+ protocol:
+ description:
+ - The protocol of the rule or of the packet to check.
+- - The specified protocol can be one of V(tcp), V(udp), V(udplite), V(icmp), V(ipv6-icmp) or V(icmpv6),
+- V(esp), V(ah), V(sctp) or the special keyword V(all), or it can be a numeric value,
++ - The specified protocol can be one of C(tcp), C(udp), C(udplite), C(icmp), C(ipv6-icmp) or C(icmpv6),
++ C(esp), C(ah), C(sctp) or the special keyword C(all), or it can be a numeric value,
+ representing one of these protocols or a different one.
+- - A protocol name from C(/etc/protocols) is also allowed.
+- - A V(!) argument before the protocol inverts the test.
++ - A protocol name from I(/etc/protocols) is also allowed.
++ - A C(!) argument before the protocol inverts the test.
+ - The number zero is equivalent to all.
+- - V(all) will match with all protocols and is taken as default when this option is omitted.
++ - C(all) will match with all protocols and is taken as default when this option is omitted.
+ type: str
+ source:
+ description:
+@@ -97,7 +97,7 @@ options:
+ a remote query such as DNS is a really bad idea.
+ - The mask can be either a network mask or a plain number, specifying
+ the number of 1's at the left side of the network mask. Thus, a mask
+- of 24 is equivalent to 255.255.255.0. A V(!) argument before the
++ of 24 is equivalent to 255.255.255.0. A C(!) argument before the
+ address specification inverts the sense of the address.
+ type: str
+ destination:
+@@ -110,14 +110,15 @@ options:
+ a remote query such as DNS is a really bad idea.
+ - The mask can be either a network mask or a plain number, specifying
+ the number of 1's at the left side of the network mask. Thus, a mask
+- of 24 is equivalent to 255.255.255.0. A V(!) argument before the
++ of 24 is equivalent to 255.255.255.0. A C(!) argument before the
+ address specification inverts the sense of the address.
+ type: str
+ tcp_flags:
+ description:
+ - TCP flags specification.
+- - O(tcp_flags) expects a dict with the two keys C(flags) and C(flags_set).
++ - C(tcp_flags) expects a dict with the two keys C(flags) and C(flags_set).
+ type: dict
++ default: {}
+ version_added: "2.4"
+ suboptions:
+ flags:
+@@ -154,7 +155,7 @@ options:
+ gateway:
+ description:
+ - This specifies the IP address of host to send the cloned packets.
+- - This option is only valid when O(jump) is set to V(TEE).
++ - This option is only valid when C(jump) is set to C(TEE).
+ type: str
+ version_added: "2.8"
+ log_prefix:
+@@ -166,7 +167,7 @@ options:
+ description:
+ - Logging level according to the syslogd-defined priorities.
+ - The value can be strings or numbers from 1-8.
+- - This parameter is only applicable if O(jump) is set to V(LOG).
++ - This parameter is only applicable if C(jump) is set to C(LOG).
+ type: str
+ version_added: "2.8"
+ choices: [ '0', '1', '2', '3', '4', '5', '6', '7', 'emerg', 'alert', 'crit', 'error', 'warning', 'notice', 'info', 'debug' ]
+@@ -179,18 +180,18 @@ options:
+ in_interface:
+ description:
+ - Name of an interface via which a packet was received (only for packets
+- entering the V(INPUT), V(FORWARD) and V(PREROUTING) chains).
+- - When the V(!) argument is used before the interface name, the sense is inverted.
+- - If the interface name ends in a V(+), then any interface which begins with
++ entering the C(INPUT), C(FORWARD) and C(PREROUTING) chains).
++ - When the C(!) argument is used before the interface name, the sense is inverted.
++ - If the interface name ends in a C(+), then any interface which begins with
+ this name will match.
+ - If this option is omitted, any interface name will match.
+ type: str
+ out_interface:
+ description:
+ - Name of an interface via which a packet is going to be sent (for
+- packets entering the V(FORWARD), V(OUTPUT) and V(POSTROUTING) chains).
+- - When the V(!) argument is used before the interface name, the sense is inverted.
+- - If the interface name ends in a V(+), then any interface which begins
++ packets entering the C(FORWARD), C(OUTPUT) and C(POSTROUTING) chains).
++ - When the C(!) argument is used before the interface name, the sense is inverted.
++ - If the interface name ends in a C(+), then any interface which begins
+ with this name will match.
+ - If this option is omitted, any interface name will match.
+ type: str
+@@ -206,14 +207,14 @@ options:
+ set_counters:
+ description:
+ - This enables the administrator to initialize the packet and byte
+- counters of a rule (during V(INSERT), V(APPEND), V(REPLACE) operations).
++ counters of a rule (during C(INSERT), C(APPEND), C(REPLACE) operations).
+ type: str
+ source_port:
+ description:
+ - Source port or port range specification.
+ - This can either be a service name or a port number.
+ - An inclusive range can also be specified, using the format C(first:last).
+- - If the first port is omitted, V(0) is assumed; if the last is omitted, V(65535) is assumed.
++ - If the first port is omitted, C(0) is assumed; if the last is omitted, C(65535) is assumed.
+ - If the first port is greater than the second one they will be swapped.
+ type: str
+ destination_port:
+@@ -232,14 +233,13 @@ options:
+ - It can only be used in conjunction with the protocols tcp, udp, udplite, dccp and sctp.
+ type: list
+ elements: str
+- default: []
+ version_added: "2.11"
+ to_ports:
+ description:
+ - This specifies a destination port or range of ports to use, without
+ this, the destination port is never altered.
+ - This is only valid if the rule also specifies one of the protocol
+- V(tcp), V(udp), V(dccp) or V(sctp).
++ C(tcp), C(udp), C(dccp) or C(sctp).
+ type: str
+ to_destination:
+ description:
+@@ -266,14 +266,14 @@ options:
+ description:
+ - This allows specifying a DSCP mark to be added to packets.
+ It takes either an integer or hex value.
+- - Mutually exclusive with O(set_dscp_mark_class).
++ - Mutually exclusive with C(set_dscp_mark_class).
+ type: str
+ version_added: "2.1"
+ set_dscp_mark_class:
+ description:
+ - This allows specifying a predefined DiffServ class which will be
+ translated to the corresponding DSCP mark.
+- - Mutually exclusive with O(set_dscp_mark).
++ - Mutually exclusive with C(set_dscp_mark).
+ type: str
+ version_added: "2.1"
+ comment:
+@@ -283,7 +283,7 @@ options:
+ ctstate:
+ description:
+ - A list of the connection states to match in the conntrack module.
+- - Possible values are V(INVALID), V(NEW), V(ESTABLISHED), V(RELATED), V(UNTRACKED), V(SNAT), V(DNAT).
++ - Possible values are C(INVALID), C(NEW), C(ESTABLISHED), C(RELATED), C(UNTRACKED), C(SNAT), C(DNAT).
+ type: list
+ elements: str
+ default: []
+@@ -301,7 +301,7 @@ options:
+ description:
+ - Specifies a set name which can be defined by ipset.
+ - Must be used together with the match_set_flags parameter.
+- - When the V(!) argument is prepended then it inverts the rule.
++ - When the C(!) argument is prepended then it inverts the rule.
+ - Uses the iptables set extension.
+ type: str
+ version_added: "2.11"
+@@ -317,8 +317,8 @@ options:
+ description:
+ - Specifies the maximum average number of matches to allow per second.
+ - The number can specify units explicitly, using C(/second), C(/minute),
+- C(/hour) or C(/day), or parts of them (so V(5/second) is the same as
+- V(5/s)).
++ C(/hour) or C(/day), or parts of them (so C(5/second) is the same as
++ C(5/s)).
+ type: str
+ limit_burst:
+ description:
+@@ -362,10 +362,10 @@ options:
+ description:
+ - Set the policy for the chain to the given target.
+ - Only built-in chains can have policies.
+- - This parameter requires the O(chain) parameter.
++ - This parameter requires the C(chain) parameter.
+ - If you specify this parameter, all other parameters will be ignored.
+- - This parameter is used to set default policy for the given O(chain).
+- Do not confuse this with O(jump) parameter.
++ - This parameter is used to set default policy for the given C(chain).
++ Do not confuse this with C(jump) parameter.
+ type: str
+ choices: [ ACCEPT, DROP, QUEUE, RETURN ]
+ version_added: "2.2"
+@@ -377,21 +377,12 @@ options:
+ version_added: "2.10"
+ chain_management:
+ description:
+- - If V(true) and O(state) is V(present), the chain will be created if needed.
+- - If V(true) and O(state) is V(absent), the chain will be deleted if the only
+- other parameter passed are O(chain) and optionally O(table).
++ - If C(true) and C(state) is C(present), the chain will be created if needed.
++ - If C(true) and C(state) is C(absent), the chain will be deleted if the only
++ other parameter passed are C(chain) and optionally C(table).
+ type: bool
+ default: false
+ version_added: "2.13"
+- numeric:
+- description:
+- - This parameter controls the running of the list -action of iptables, which is used internally by the module
+- - Does not affect the actual functionality. Use this if iptables hangs when creating chain or altering policy
+- - If V(true), then iptables skips the DNS-lookup of the IP addresses in a chain when it uses the list -action
+- - Listing is used internally for example when setting a policy or creting of a chain
+- type: bool
+- default: false
+- version_added: "2.15"
+ '''
+
+ EXAMPLES = r'''
+@@ -698,7 +689,7 @@ def push_arguments(iptables_path, action
+
+ def check_rule_present(iptables_path, module, params):
+ cmd = push_arguments(iptables_path, '-C', params)
+- rc, stdout, stderr = module.run_command(cmd, check_rc=False)
++ rc, _, __ = module.run_command(cmd, check_rc=False)
+ return (rc == 0)
+
+
+@@ -730,9 +721,7 @@ def set_chain_policy(iptables_path, modu
+
+ def get_chain_policy(iptables_path, module, params):
+ cmd = push_arguments(iptables_path, '-L', params, make_rule=False)
+- if module.params['numeric']:
+- cmd.append('--numeric')
+- rc, out, err = module.run_command(cmd, check_rc=True)
++ rc, out, _ = module.run_command(cmd, check_rc=True)
+ chain_header = out.split("\n")[0]
+ result = re.search(r'\(policy ([A-Z]+)\)', chain_header)
+ if result:
+@@ -742,7 +731,7 @@ def get_chain_policy(iptables_path, modu
+
+ def get_iptables_version(iptables_path, module):
+ cmd = [iptables_path, '--version']
+- rc, out, err = module.run_command(cmd, check_rc=True)
++ rc, out, _ = module.run_command(cmd, check_rc=True)
+ return out.split('v')[1].rstrip('\n')
+
+
+@@ -753,9 +742,7 @@ def create_chain(iptables_path, module,
+
+ def check_chain_present(iptables_path, module, params):
+ cmd = push_arguments(iptables_path, '-L', params, make_rule=False)
+- if module.params['numeric']:
+- cmd.append('--numeric')
+- rc, out, err = module.run_command(cmd, check_rc=False)
++ rc, _, __ = module.run_command(cmd, check_rc=False)
+ return (rc == 0)
+
+
+@@ -822,7 +809,6 @@ def main():
+ flush=dict(type='bool', default=False),
+ policy=dict(type='str', choices=['ACCEPT', 'DROP', 'QUEUE', 'RETURN']),
+ chain_management=dict(type='bool', default=False),
+- numeric=dict(type='bool', default=False),
+ ),
+ mutually_exclusive=(
+ ['set_dscp_mark', 'set_dscp_mark_class'],
+@@ -895,38 +881,33 @@ def main():
+ delete_chain(iptables_path, module, module.params)
+
+ else:
+- # Create the chain if there are no rule arguments
+- if (args['state'] == 'present') and not args['rule']:
+- chain_is_present = check_chain_present(
+- iptables_path, module, module.params
+- )
+- args['changed'] = not chain_is_present
+-
+- if (not chain_is_present and args['chain_management'] and not module.check_mode):
+- create_chain(iptables_path, module, module.params)
+-
+- else:
+- insert = (module.params['action'] == 'insert')
+- rule_is_present = check_rule_present(
+- iptables_path, module, module.params
+- )
+-
+- should_be_present = (args['state'] == 'present')
+- # Check if target is up to date
+- args['changed'] = (rule_is_present != should_be_present)
+- if args['changed'] is False:
+- # Target is already up to date
+- module.exit_json(**args)
+-
+- # Modify if not check_mode
+- if not module.check_mode:
+- if should_be_present:
+- if insert:
+- insert_rule(iptables_path, module, module.params)
+- else:
+- append_rule(iptables_path, module, module.params)
++ insert = (module.params['action'] == 'insert')
++ rule_is_present = check_rule_present(
++ iptables_path, module, module.params
++ )
++ chain_is_present = rule_is_present or check_chain_present(
++ iptables_path, module, module.params
++ )
++ should_be_present = (args['state'] == 'present')
++
++ # Check if target is up to date
++ args['changed'] = (rule_is_present != should_be_present)
++ if args['changed'] is False:
++ # Target is already up to date
++ module.exit_json(**args)
++
++ # Check only; don't modify
++ if not module.check_mode:
++ if should_be_present:
++ if not chain_is_present and args['chain_management']:
++ create_chain(iptables_path, module, module.params)
++
++ if insert:
++ insert_rule(iptables_path, module, module.params)
+ else:
+- remove_rule(iptables_path, module, module.params)
++ append_rule(iptables_path, module, module.params)
++ else:
++ remove_rule(iptables_path, module, module.params)
+
+ module.exit_json(**args)
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/known_hosts.py
++++ ansible-core-2.16.5/lib/ansible/modules/known_hosts.py
+@@ -11,7 +11,7 @@ DOCUMENTATION = r'''
+ module: known_hosts
+ short_description: Add or remove a host from the C(known_hosts) file
+ description:
+- - The M(ansible.builtin.known_hosts) module lets you add or remove a host keys from the C(known_hosts) file.
++ - The C(known_hosts) module lets you add or remove a host keys from the C(known_hosts) file.
+ - Starting at Ansible 2.2, multiple entries per host are allowed, but only one for each key type supported by ssh.
+ This is useful if you're going to want to use the M(ansible.builtin.git) module over ssh, for example.
+ - If you have a very large number of host keys to manage, you will find the M(ansible.builtin.template) module more useful.
+@@ -22,19 +22,19 @@ options:
+ description:
+ - The host to add or remove (must match a host specified in key). It will be converted to lowercase so that ssh-keygen can find it.
+ - Must match with <hostname> or <ip> present in key attribute.
+- - For custom SSH port, O(name) needs to specify port as well. See example section.
++ - For custom SSH port, C(name) needs to specify port as well. See example section.
+ type: str
+ required: true
+ key:
+ description:
+ - The SSH public host key, as a string.
+- - Required if O(state=present), optional when O(state=absent), in which case all keys for the host are removed.
++ - Required if C(state=present), optional when C(state=absent), in which case all keys for the host are removed.
+ - The key must be in the right format for SSH (see sshd(8), section "SSH_KNOWN_HOSTS FILE FORMAT").
+ - Specifically, the key should not match the format that is found in an SSH pubkey file, but should rather have the hostname prepended to a
+ line that includes the pubkey, the same way that it would appear in the known_hosts file. The value prepended to the line must also match
+ the value of the name parameter.
+ - Should be of format C(<hostname[,IP]> ssh-rsa <pubkey>).
+- - For custom SSH port, O(key) needs to specify port as well. See example section.
++ - For custom SSH port, C(key) needs to specify port as well. See example section.
+ type: str
+ path:
+ description:
+@@ -50,8 +50,8 @@ options:
+ version_added: "2.3"
+ state:
+ description:
+- - V(present) to add the host key.
+- - V(absent) to remove it.
++ - I(present) to add the host key.
++ - I(absent) to remove it.
+ choices: [ "absent", "present" ]
+ default: "present"
+ type: str
+@@ -111,7 +111,7 @@ import re
+ import tempfile
+
+ from ansible.module_utils.basic import AnsibleModule
+-from ansible.module_utils.common.text.converters import to_bytes, to_native
++from ansible.module_utils._text import to_bytes, to_native
+
+
+ def enforce_state(module, params):
+--- ansible-core-2.16.5.orig/lib/ansible/modules/lineinfile.py
++++ ansible-core-2.16.5/lib/ansible/modules/lineinfile.py
+@@ -25,20 +25,20 @@ options:
+ path:
+ description:
+ - The file to modify.
+- - Before Ansible 2.3 this option was only usable as O(dest), O(destfile) and O(name).
++ - Before Ansible 2.3 this option was only usable as I(dest), I(destfile) and I(name).
+ type: path
+ required: true
+ aliases: [ dest, destfile, name ]
+ regexp:
+ description:
+ - The regular expression to look for in every line of the file.
+- - For O(state=present), the pattern to replace if found. Only the last line found will be replaced.
+- - For O(state=absent), the pattern of the line(s) to remove.
++ - For C(state=present), the pattern to replace if found. Only the last line found will be replaced.
++ - For C(state=absent), the pattern of the line(s) to remove.
+ - If the regular expression is not matched, the line will be
+- added to the file in keeping with O(insertbefore) or O(insertafter)
++ added to the file in keeping with C(insertbefore) or C(insertafter)
+ settings.
+ - When modifying a line the regexp should typically match both the initial state of
+- the line as well as its state after replacement by O(line) to ensure idempotence.
++ the line as well as its state after replacement by C(line) to ensure idempotence.
+ - Uses Python regular expressions. See U(https://docs.python.org/3/library/re.html).
+ type: str
+ aliases: [ regex ]
+@@ -46,12 +46,12 @@ options:
+ search_string:
+ description:
+ - The literal string to look for in every line of the file. This does not have to match the entire line.
+- - For O(state=present), the line to replace if the string is found in the file. Only the last line found will be replaced.
+- - For O(state=absent), the line(s) to remove if the string is in the line.
++ - For C(state=present), the line to replace if the string is found in the file. Only the last line found will be replaced.
++ - For C(state=absent), the line(s) to remove if the string is in the line.
+ - If the literal expression is not matched, the line will be
+- added to the file in keeping with O(insertbefore) or O(insertafter)
++ added to the file in keeping with C(insertbefore) or C(insertafter)
+ settings.
+- - Mutually exclusive with O(backrefs) and O(regexp).
++ - Mutually exclusive with C(backrefs) and C(regexp).
+ type: str
+ version_added: '2.11'
+ state:
+@@ -63,53 +63,53 @@ options:
+ line:
+ description:
+ - The line to insert/replace into the file.
+- - Required for O(state=present).
+- - If O(backrefs) is set, may contain backreferences that will get
+- expanded with the O(regexp) capture groups if the regexp matches.
++ - Required for C(state=present).
++ - If C(backrefs) is set, may contain backreferences that will get
++ expanded with the C(regexp) capture groups if the regexp matches.
+ type: str
+ aliases: [ value ]
+ backrefs:
+ description:
+- - Used with O(state=present).
+- - If set, O(line) can contain backreferences (both positional and named)
+- that will get populated if the O(regexp) matches.
++ - Used with C(state=present).
++ - If set, C(line) can contain backreferences (both positional and named)
++ that will get populated if the C(regexp) matches.
+ - This parameter changes the operation of the module slightly;
+- O(insertbefore) and O(insertafter) will be ignored, and if the O(regexp)
++ C(insertbefore) and C(insertafter) will be ignored, and if the C(regexp)
+ does not match anywhere in the file, the file will be left unchanged.
+- - If the O(regexp) does match, the last matching line will be replaced by
++ - If the C(regexp) does match, the last matching line will be replaced by
+ the expanded line parameter.
+- - Mutually exclusive with O(search_string).
++ - Mutually exclusive with C(search_string).
+ type: bool
+ default: no
+ version_added: "1.1"
+ insertafter:
+ description:
+- - Used with O(state=present).
++ - Used with C(state=present).
+ - If specified, the line will be inserted after the last match of specified regular expression.
+ - If the first match is required, use(firstmatch=yes).
+- - A special value is available; V(EOF) for inserting the line at the end of the file.
++ - A special value is available; C(EOF) for inserting the line at the end of the file.
+ - If specified regular expression has no matches, EOF will be used instead.
+- - If O(insertbefore) is set, default value V(EOF) will be ignored.
+- - If regular expressions are passed to both O(regexp) and O(insertafter), O(insertafter) is only honored if no match for O(regexp) is found.
+- - May not be used with O(backrefs) or O(insertbefore).
++ - If C(insertbefore) is set, default value C(EOF) will be ignored.
++ - If regular expressions are passed to both C(regexp) and C(insertafter), C(insertafter) is only honored if no match for C(regexp) is found.
++ - May not be used with C(backrefs) or C(insertbefore).
+ type: str
+ choices: [ EOF, '*regex*' ]
+ default: EOF
+ insertbefore:
+ description:
+- - Used with O(state=present).
++ - Used with C(state=present).
+ - If specified, the line will be inserted before the last match of specified regular expression.
+- - If the first match is required, use O(firstmatch=yes).
+- - A value is available; V(BOF) for inserting the line at the beginning of the file.
++ - If the first match is required, use C(firstmatch=yes).
++ - A value is available; C(BOF) for inserting the line at the beginning of the file.
+ - If specified regular expression has no matches, the line will be inserted at the end of the file.
+- - If regular expressions are passed to both O(regexp) and O(insertbefore), O(insertbefore) is only honored if no match for O(regexp) is found.
+- - May not be used with O(backrefs) or O(insertafter).
++ - If regular expressions are passed to both C(regexp) and C(insertbefore), C(insertbefore) is only honored if no match for C(regexp) is found.
++ - May not be used with C(backrefs) or C(insertafter).
+ type: str
+ choices: [ BOF, '*regex*' ]
+ version_added: "1.1"
+ create:
+ description:
+- - Used with O(state=present).
++ - Used with C(state=present).
+ - If specified, the file will be created if it does not already exist.
+ - By default it will fail if the file is missing.
+ type: bool
+@@ -122,8 +122,8 @@ options:
+ default: no
+ firstmatch:
+ description:
+- - Used with O(insertafter) or O(insertbefore).
+- - If set, O(insertafter) and O(insertbefore) will work with the first line that matches the given regular expression.
++ - Used with C(insertafter) or C(insertbefore).
++ - If set, C(insertafter) and C(insertbefore) will work with the first line that matches the given regular expression.
+ type: bool
+ default: no
+ version_added: "2.5"
+@@ -148,7 +148,7 @@ attributes:
+ vault:
+ support: none
+ notes:
+- - As of Ansible 2.3, the O(dest) option has been changed to O(path) as default, but O(dest) still works as well.
++ - As of Ansible 2.3, the I(dest) option has been changed to I(path) as default, but I(dest) still works as well.
+ seealso:
+ - module: ansible.builtin.blockinfile
+ - module: ansible.builtin.copy
+@@ -255,7 +255,7 @@ import tempfile
+
+ # import module snippets
+ from ansible.module_utils.basic import AnsibleModule
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+
+
+ def write_changes(module, b_lines, dest):
+--- ansible-core-2.16.5.orig/lib/ansible/modules/meta.py
++++ ansible-core-2.16.5/lib/ansible/modules/meta.py
+@@ -19,21 +19,21 @@ options:
+ free_form:
+ description:
+ - This module takes a free form command, as a string. There is not an actual option named "free form". See the examples!
+- - V(flush_handlers) makes Ansible run any handler tasks which have thus far been notified. Ansible inserts these tasks internally at certain
++ - C(flush_handlers) makes Ansible run any handler tasks which have thus far been notified. Ansible inserts these tasks internally at certain
+ points to implicitly trigger handler runs (after pre/post tasks, the final role execution, and the main tasks section of your plays).
+- - V(refresh_inventory) (added in Ansible 2.0) forces the reload of the inventory, which in the case of dynamic inventory scripts means they will be
++ - C(refresh_inventory) (added in Ansible 2.0) forces the reload of the inventory, which in the case of dynamic inventory scripts means they will be
+ re-executed. If the dynamic inventory script is using a cache, Ansible cannot know this and has no way of refreshing it (you can disable the cache
+ or, if available for your specific inventory datasource (e.g. aws), you can use the an inventory plugin instead of an inventory script).
+ This is mainly useful when additional hosts are created and users wish to use them instead of using the M(ansible.builtin.add_host) module.
+- - V(noop) (added in Ansible 2.0) This literally does 'nothing'. It is mainly used internally and not recommended for general use.
+- - V(clear_facts) (added in Ansible 2.1) causes the gathered facts for the hosts specified in the play's list of hosts to be cleared,
++ - C(noop) (added in Ansible 2.0) This literally does 'nothing'. It is mainly used internally and not recommended for general use.
++ - C(clear_facts) (added in Ansible 2.1) causes the gathered facts for the hosts specified in the play's list of hosts to be cleared,
+ including the fact cache.
+- - V(clear_host_errors) (added in Ansible 2.1) clears the failed state (if any) from hosts specified in the play's list of hosts.
+- - V(end_play) (added in Ansible 2.2) causes the play to end without failing the host(s). Note that this affects all hosts.
+- - V(reset_connection) (added in Ansible 2.3) interrupts a persistent connection (i.e. ssh + control persist)
+- - V(end_host) (added in Ansible 2.8) is a per-host variation of V(end_play). Causes the play to end for the current host without failing it.
+- - V(end_batch) (added in Ansible 2.12) causes the current batch (see C(serial)) to end without failing the host(s).
+- Note that with C(serial=0) or undefined this behaves the same as V(end_play).
++ - C(clear_host_errors) (added in Ansible 2.1) clears the failed state (if any) from hosts specified in the play's list of hosts.
++ - C(end_play) (added in Ansible 2.2) causes the play to end without failing the host(s). Note that this affects all hosts.
++ - C(reset_connection) (added in Ansible 2.3) interrupts a persistent connection (i.e. ssh + control persist)
++ - C(end_host) (added in Ansible 2.8) is a per-host variation of C(end_play). Causes the play to end for the current host without failing it.
++ - C(end_batch) (added in Ansible 2.12) causes the current batch (see C(serial)) to end without failing the host(s).
++ Note that with C(serial=0) or undefined this behaves the same as C(end_play).
+ choices: [ clear_facts, clear_host_errors, end_host, end_play, flush_handlers, noop, refresh_inventory, reset_connection, end_batch ]
+ required: true
+ extends_documentation_fragment:
+@@ -61,12 +61,12 @@ attributes:
+ details: Only some options support conditionals and when they do they act 'bypassing the host loop', taking the values from first available host
+ support: partial
+ connection:
+- details: Most options in this action do not use a connection, except V(reset_connection) which still does not connect to the remote
++ details: Most options in this action do not use a connection, except C(reset_connection) which still does not connect to the remote
+ support: partial
+ notes:
+- - V(clear_facts) will remove the persistent facts from M(ansible.builtin.set_fact) using O(ansible.builtin.set_fact#module:cacheable=True),
++ - C(clear_facts) will remove the persistent facts from M(ansible.builtin.set_fact) using C(cacheable=True),
+ but not the current host variable it creates for the current run.
+- - Skipping M(ansible.builtin.meta) tasks with tags is not supported before Ansible 2.11.
++ - Skipping C(meta) tasks with tags is not supported before Ansible 2.11.
+ seealso:
+ - module: ansible.builtin.assert
+ - module: ansible.builtin.fail
+--- ansible-core-2.16.5.orig/lib/ansible/modules/package.py
++++ ansible-core-2.16.5/lib/ansible/modules/package.py
+@@ -18,8 +18,8 @@ short_description: Generic OS package ma
+ description:
+ - This modules manages packages on a target without specifying a package manager module (like M(ansible.builtin.yum), M(ansible.builtin.apt), ...).
+ It is convenient to use in an heterogeneous environment of machines without having to create a specific task for
+- each package manager. M(ansible.builtin.package) calls behind the module for the package manager used by the operating system
+- discovered by the module M(ansible.builtin.setup). If M(ansible.builtin.setup) was not yet run, M(ansible.builtin.package) will run it.
++ each package manager. C(package) calls behind the module for the package manager used by the operating system
++ discovered by the module M(ansible.builtin.setup). If C(setup) was not yet run, C(package) will run it.
+ - This module acts as a proxy to the underlying package manager module. While all arguments will be passed to the
+ underlying module, not all modules support the same arguments. This documentation only covers the minimum intersection
+ of module arguments that all packaging modules support.
+@@ -28,17 +28,17 @@ options:
+ name:
+ description:
+ - Package name, or package specifier with version.
+- - Syntax varies with package manager. For example V(name-1.0) or V(name=1.0).
+- - Package names also vary with package manager; this module will not "translate" them per distro. For example V(libyaml-dev), V(libyaml-devel).
++ - Syntax varies with package manager. For example C(name-1.0) or C(name=1.0).
++ - Package names also vary with package manager; this module will not "translate" them per distro. For example C(libyaml-dev), C(libyaml-devel).
+ required: true
+ state:
+ description:
+- - Whether to install (V(present)), or remove (V(absent)) a package.
+- - You can use other states like V(latest) ONLY if they are supported by the underlying package module(s) executed.
++ - Whether to install (C(present)), or remove (C(absent)) a package.
++ - You can use other states like C(latest) ONLY if they are supported by the underlying package module(s) executed.
+ required: true
+ use:
+ description:
+- - The required package manager module to use (V(yum), V(apt), and so on). The default V(auto) will use existing facts or try to autodetect it.
++ - The required package manager module to use (C(yum), C(apt), and so on). The default 'auto' will use existing facts or try to autodetect it.
+ - You should only use this field if the automatic selection is not working for some reason.
+ default: auto
+ requirements:
+@@ -63,7 +63,7 @@ attributes:
+ details: The support depends on the availability for the specific plugin for each platform and if fact gathering is able to detect it
+ platforms: all
+ notes:
+- - While M(ansible.builtin.package) abstracts package managers to ease dealing with multiple distributions, package name often differs for the same software.
++ - While C(package) abstracts package managers to ease dealing with multiple distributions, package name often differs for the same software.
+
+ '''
+ EXAMPLES = '''
+--- ansible-core-2.16.5.orig/lib/ansible/modules/package_facts.py
++++ ansible-core-2.16.5/lib/ansible/modules/package_facts.py
+@@ -27,8 +27,8 @@ options:
+ strategy:
+ description:
+ - This option controls how the module queries the package managers on the system.
+- V(first) means it will return only information for the first supported package manager available.
+- V(all) will return information for all supported and available package managers on the system.
++ C(first) means it will return only information for the first supported package manager available.
++ C(all) will return information for all supported and available package managers on the system.
+ choices: ['first', 'all']
+ default: 'first'
+ type: str
+@@ -240,7 +240,7 @@ ansible_facts:
+
+ import re
+
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+ from ansible.module_utils.common.locale import get_best_parsable_locale
+ from ansible.module_utils.common.process import get_bin_path
+--- ansible-core-2.16.5.orig/lib/ansible/modules/pause.py
++++ ansible-core-2.16.5/lib/ansible/modules/pause.py
+@@ -15,7 +15,6 @@ description:
+ - To pause/wait/sleep per host, use the M(ansible.builtin.wait_for) module.
+ - You can use C(ctrl+c) if you wish to advance a pause earlier than it is set to expire or if you need to abort a playbook run entirely.
+ To continue early press C(ctrl+c) and then C(c). To abort a playbook press C(ctrl+c) and then C(a).
+- - Prompting for a set amount of time is not supported. Pausing playbook execution is interruptable but does not return user input.
+ - The pause module integrates into async/parallelized playbooks without any special considerations (see Rolling Updates).
+ When using pauses with the C(serial) playbook parameter (as in rolling updates) you are only prompted once for the current group of hosts.
+ - This module is also supported for Windows targets.
+@@ -30,11 +29,10 @@ options:
+ prompt:
+ description:
+ - Optional text to use for the prompt message.
+- - User input is only returned if O(seconds=None) and O(minutes=None), otherwise this is just a custom message before playbook execution is paused.
+ echo:
+ description:
+ - Controls whether or not keyboard input is shown when typing.
+- - Only has effect if O(seconds=None) and O(minutes=None).
++ - Has no effect if 'seconds' or 'minutes' is set.
+ type: bool
+ default: 'yes'
+ version_added: 2.5
+--- ansible-core-2.16.5.orig/lib/ansible/modules/ping.py
++++ ansible-core-2.16.5/lib/ansible/modules/ping.py
+@@ -12,9 +12,9 @@ DOCUMENTATION = '''
+ ---
+ module: ping
+ version_added: historical
+-short_description: Try to connect to host, verify a usable python and return V(pong) on success
++short_description: Try to connect to host, verify a usable python and return C(pong) on success
+ description:
+- - A trivial test module, this module always returns V(pong) on successful
++ - A trivial test module, this module always returns C(pong) on successful
+ contact. It does not make sense in playbooks, but it is useful from
+ C(/usr/bin/ansible) to verify the ability to login and that a usable Python is configured.
+ - This is NOT ICMP ping, this is just a trivial test module that requires Python on the remote-node.
+@@ -23,8 +23,8 @@ description:
+ options:
+ data:
+ description:
+- - Data to return for the RV(ping) return value.
+- - If this parameter is set to V(crash), the module will cause an exception.
++ - Data to return for the C(ping) return value.
++ - If this parameter is set to C(crash), the module will cause an exception.
+ type: str
+ default: pong
+ extends_documentation_fragment:
+@@ -58,7 +58,7 @@ EXAMPLES = '''
+
+ RETURN = '''
+ ping:
+- description: Value provided with the O(data) parameter.
++ description: Value provided with the data parameter.
+ returned: success
+ type: str
+ sample: pong
+--- ansible-core-2.16.5.orig/lib/ansible/modules/pip.py
++++ ansible-core-2.16.5/lib/ansible/modules/pip.py
+@@ -12,8 +12,8 @@ DOCUMENTATION = '''
+ module: pip
+ short_description: Manages Python library dependencies
+ description:
+- - "Manage Python library dependencies. To use this module, one of the following keys is required: O(name)
+- or O(requirements)."
++ - "Manage Python library dependencies. To use this module, one of the following keys is required: C(name)
++ or C(requirements)."
+ version_added: "0.7"
+ options:
+ name:
+@@ -24,7 +24,7 @@ options:
+ elements: str
+ version:
+ description:
+- - The version number to install of the Python library specified in the O(name) parameter.
++ - The version number to install of the Python library specified in the I(name) parameter.
+ type: str
+ requirements:
+ description:
+@@ -53,17 +53,17 @@ options:
+ virtualenv_command:
+ description:
+ - The command or a pathname to the command to create the virtual
+- environment with. For example V(pyvenv), V(virtualenv),
+- V(virtualenv2), V(~/bin/virtualenv), V(/usr/local/bin/virtualenv).
++ environment with. For example C(pyvenv), C(virtualenv),
++ C(virtualenv2), C(~/bin/virtualenv), C(/usr/local/bin/virtualenv).
+ type: path
+ default: virtualenv
+ version_added: "1.1"
+ virtualenv_python:
+ description:
+ - The Python executable used for creating the virtual environment.
+- For example V(python3.12), V(python2.7). When not specified, the
++ For example C(python3.5), C(python2.7). When not specified, the
+ Python version used to run the ansible module is used. This parameter
+- should not be used when O(virtualenv_command) is using V(pyvenv) or
++ should not be used when C(virtualenv_command) is using C(pyvenv) or
+ the C(-m venv) module.
+ type: str
+ version_added: "2.0"
+@@ -94,9 +94,9 @@ options:
+ description:
+ - The explicit executable or pathname for the pip executable,
+ if different from the Ansible Python interpreter. For
+- example V(pip3.3), if there are both Python 2.7 and 3.3 installations
++ example C(pip3.3), if there are both Python 2.7 and 3.3 installations
+ in the system and you want to run pip for the Python 3.3 installation.
+- - Mutually exclusive with O(virtualenv) (added in 2.1).
++ - Mutually exclusive with I(virtualenv) (added in 2.1).
+ - Does not affect the Ansible Python interpreter.
+ - The setuptools package must be installed for both the Ansible Python interpreter
+ and for the version of Python specified by this option.
+@@ -127,16 +127,16 @@ notes:
+ installed on the remote host if the virtualenv parameter is specified and
+ the virtualenv needs to be created.
+ - Although it executes using the Ansible Python interpreter, the pip module shells out to
+- run the actual pip command, so it can use any pip version you specify with O(executable).
++ run the actual pip command, so it can use any pip version you specify with I(executable).
+ By default, it uses the pip version for the Ansible Python interpreter. For example, pip3 on python 3, and pip2 or pip on python 2.
+ - The interpreter used by Ansible
+ (see R(ansible_python_interpreter, ansible_python_interpreter))
+ requires the setuptools package, regardless of the version of pip set with
+- the O(executable) option.
++ the I(executable) option.
+ requirements:
+ - pip
+ - virtualenv
+-- setuptools or packaging
++- setuptools
+ author:
+ - Matt Wright (@mattupstate)
+ '''
+@@ -266,7 +266,6 @@ virtualenv:
+ sample: "/tmp/virtualenv"
+ '''
+
+-import argparse
+ import os
+ import re
+ import sys
+@@ -274,28 +273,20 @@ import tempfile
+ import operator
+ import shlex
+ import traceback
++import types
+
+ from ansible.module_utils.compat.version import LooseVersion
+
+-PACKAGING_IMP_ERR = None
+-HAS_PACKAGING = False
+-HAS_SETUPTOOLS = False
++SETUPTOOLS_IMP_ERR = None
+ try:
+- from packaging.requirements import Requirement as parse_requirement
+- HAS_PACKAGING = True
+-except Exception:
+- # This is catching a generic Exception, due to packaging on EL7 raising a TypeError on import
+- HAS_PACKAGING = False
+- PACKAGING_IMP_ERR = traceback.format_exc()
+- try:
+- from pkg_resources import Requirement
+- parse_requirement = Requirement.parse # type: ignore[misc,assignment]
+- del Requirement
+- HAS_SETUPTOOLS = True
+- except ImportError:
+- pass
++ from pkg_resources import Requirement
+
+-from ansible.module_utils.common.text.converters import to_native
++ HAS_SETUPTOOLS = True
++except ImportError:
++ HAS_SETUPTOOLS = False
++ SETUPTOOLS_IMP_ERR = traceback.format_exc()
++
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.basic import AnsibleModule, is_executable, missing_required_lib
+ from ansible.module_utils.common.locale import get_best_parsable_locale
+ from ansible.module_utils.six import PY3
+@@ -304,16 +295,8 @@ from ansible.module_utils.six import PY3
+ #: Python one-liners to be run at the command line that will determine the
+ # installed version for these special libraries. These are libraries that
+ # don't end up in the output of pip freeze.
+-_SPECIAL_PACKAGE_CHECKERS = {
+- 'importlib': {
+- 'setuptools': 'from importlib.metadata import version; print(version("setuptools"))',
+- 'pip': 'from importlib.metadata import version; print(version("pip"))',
+- },
+- 'pkg_resources': {
+- 'setuptools': 'import setuptools; print(setuptools.__version__)',
+- 'pip': 'import pkg_resources; print(pkg_resources.get_distribution("pip").version)',
+- }
+-}
++_SPECIAL_PACKAGE_CHECKERS = {'setuptools': 'import setuptools; print(setuptools.__version__)',
++ 'pip': 'import pkg_resources; print(pkg_resources.get_distribution("pip").version)'}
+
+ _VCS_RE = re.compile(r'(svn|git|hg|bzr)\+')
+
+@@ -326,18 +309,6 @@ def _is_vcs_url(name):
+ return re.match(_VCS_RE, name)
+
+
+-def _is_venv_command(command):
+- venv_parser = argparse.ArgumentParser()
+- venv_parser.add_argument('-m', type=str)
+- argv = shlex.split(command)
+- if argv[0] == 'pyvenv':
+- return True
+- args, dummy = venv_parser.parse_known_args(argv[1:])
+- if args.m == 'venv':
+- return True
+- return False
+-
+-
+ def _is_package_name(name):
+ """Test whether the name is a package name or a version specifier."""
+ return not name.lstrip().startswith(tuple(op_dict.keys()))
+@@ -490,7 +461,7 @@ def _have_pip_module(): # type: () -> b
+ except ImportError:
+ find_spec = None # type: ignore[assignment] # type: ignore[no-redef]
+
+- if find_spec: # type: ignore[truthy-function]
++ if find_spec:
+ # noinspection PyBroadException
+ try:
+ # noinspection PyUnresolvedReferences
+@@ -522,7 +493,7 @@ def _fail(module, cmd, out, err):
+ module.fail_json(cmd=cmd, msg=msg)
+
+
+-def _get_package_info(module, package, python_bin=None):
++def _get_package_info(module, package, env=None):
+ """This is only needed for special packages which do not show up in pip freeze
+
+ pip and setuptools fall into this category.
+@@ -530,19 +501,20 @@ def _get_package_info(module, package, p
+ :returns: a string containing the version number if the package is
+ installed. None if the package is not installed.
+ """
+- if python_bin is None:
+- return
+-
+- discovery_mechanism = 'pkg_resources'
+- importlib_rc = module.run_command([python_bin, '-c', 'import importlib.metadata'])[0]
+- if importlib_rc == 0:
+- discovery_mechanism = 'importlib'
++ if env:
++ opt_dirs = ['%s/bin' % env]
++ else:
++ opt_dirs = []
++ python_bin = module.get_bin_path('python', False, opt_dirs)
+
+- rc, out, err = module.run_command([python_bin, '-c', _SPECIAL_PACKAGE_CHECKERS[discovery_mechanism][package]])
+- if rc:
++ if python_bin is None:
+ formatted_dep = None
+ else:
+- formatted_dep = '%s==%s' % (package, out.strip())
++ rc, out, err = module.run_command([python_bin, '-c', _SPECIAL_PACKAGE_CHECKERS[package]])
++ if rc:
++ formatted_dep = None
++ else:
++ formatted_dep = '%s==%s' % (package, out.strip())
+ return formatted_dep
+
+
+@@ -571,7 +543,7 @@ def setup_virtualenv(module, env, chdir,
+ virtualenv_python = module.params['virtualenv_python']
+ # -p is a virtualenv option, not compatible with pyenv or venv
+ # this conditional validates if the command being used is not any of them
+- if not _is_venv_command(module.params['virtualenv_command']):
++ if not any(ex in module.params['virtualenv_command'] for ex in ('pyvenv', '-m venv')):
+ if virtualenv_python:
+ cmd.append('-p%s' % virtualenv_python)
+ elif PY3:
+@@ -620,15 +592,13 @@ class Package:
+ separator = '==' if version_string[0].isdigit() else ' '
+ name_string = separator.join((name_string, version_string))
+ try:
+- self._requirement = parse_requirement(name_string)
++ self._requirement = Requirement.parse(name_string)
+ # old pkg_resource will replace 'setuptools' with 'distribute' when it's already installed
+- project_name = Package.canonicalize_name(
+- getattr(self._requirement, 'name', None) or getattr(self._requirement, 'project_name', None)
+- )
+- if project_name == "distribute" and "setuptools" in name_string:
++ if self._requirement.project_name == "distribute" and "setuptools" in name_string:
+ self.package_name = "setuptools"
++ self._requirement.project_name = "setuptools"
+ else:
+- self.package_name = project_name
++ self.package_name = Package.canonicalize_name(self._requirement.project_name)
+ self._plain_package = True
+ except ValueError as e:
+ pass
+@@ -636,7 +606,7 @@ class Package:
+ @property
+ def has_version_specifier(self):
+ if self._plain_package:
+- return bool(getattr(self._requirement, 'specifier', None) or getattr(self._requirement, 'specs', None))
++ return bool(self._requirement.specs)
+ return False
+
+ def is_satisfied_by(self, version_to_test):
+@@ -692,9 +662,9 @@ def main():
+ supports_check_mode=True,
+ )
+
+- if not HAS_SETUPTOOLS and not HAS_PACKAGING:
+- module.fail_json(msg=missing_required_lib("packaging"),
+- exception=PACKAGING_IMP_ERR)
++ if not HAS_SETUPTOOLS:
++ module.fail_json(msg=missing_required_lib("setuptools"),
++ exception=SETUPTOOLS_IMP_ERR)
+
+ state = module.params['state']
+ name = module.params['name']
+@@ -734,9 +704,6 @@ def main():
+ if not os.path.exists(os.path.join(env, 'bin', 'activate')):
+ venv_created = True
+ out, err = setup_virtualenv(module, env, chdir, out, err)
+- py_bin = os.path.join(env, 'bin', 'python')
+- else:
+- py_bin = module.params['executable'] or sys.executable
+
+ pip = _get_pip(module, env, module.params['executable'])
+
+@@ -819,7 +786,7 @@ def main():
+ # So we need to get those via a specialcase
+ for pkg in ('setuptools', 'pip'):
+ if pkg in name:
+- formatted_dep = _get_package_info(module, pkg, py_bin)
++ formatted_dep = _get_package_info(module, pkg, env)
+ if formatted_dep is not None:
+ pkg_list.append(formatted_dep)
+ out += '%s\n' % formatted_dep
+@@ -833,7 +800,7 @@ def main():
+
+ out_freeze_before = None
+ if requirements or has_vcs:
+- dummy, out_freeze_before, dummy = _get_packages(module, pip, chdir)
++ _, out_freeze_before, _ = _get_packages(module, pip, chdir)
+
+ rc, out_pip, err_pip = module.run_command(cmd, path_prefix=path_prefix, cwd=chdir)
+ out += out_pip
+@@ -850,7 +817,7 @@ def main():
+ if out_freeze_before is None:
+ changed = 'Successfully installed' in out_pip
+ else:
+- dummy, out_freeze_after, dummy = _get_packages(module, pip, chdir)
++ _, out_freeze_after, _ = _get_packages(module, pip, chdir)
+ changed = out_freeze_before != out_freeze_after
+
+ changed = changed or venv_created
+--- ansible-core-2.16.5.orig/lib/ansible/modules/raw.py
++++ ansible-core-2.16.5/lib/ansible/modules/raw.py
+@@ -39,8 +39,6 @@ description:
+ - This module does not require python on the remote system, much like
+ the M(ansible.builtin.script) module.
+ - This module is also supported for Windows targets.
+- - If the command returns non UTF-8 data, it must be encoded to avoid issues. One option is to pipe
+- the output through C(base64).
+ extends_documentation_fragment:
+ - action_common_attributes
+ - action_common_attributes.raw
+--- ansible-core-2.16.5.orig/lib/ansible/modules/reboot.py
++++ ansible-core-2.16.5/lib/ansible/modules/reboot.py
+@@ -10,7 +10,7 @@ DOCUMENTATION = r'''
+ module: reboot
+ short_description: Reboot a machine
+ notes:
+- - E(PATH) is ignored on the remote node when searching for the C(shutdown) command. Use O(search_paths)
++ - C(PATH) is ignored on the remote node when searching for the C(shutdown) command. Use C(search_paths)
+ to specify locations to search if the default paths do not work.
+ description:
+ - Reboot a machine, wait for it to go down, come back up, and respond to commands.
+@@ -57,7 +57,7 @@ options:
+ search_paths:
+ description:
+ - Paths to search on the remote machine for the C(shutdown) command.
+- - I(Only) these paths will be searched for the C(shutdown) command. E(PATH) is ignored in the remote node when searching for the C(shutdown) command.
++ - I(Only) these paths will be searched for the C(shutdown) command. C(PATH) is ignored in the remote node when searching for the C(shutdown) command.
+ type: list
+ elements: str
+ default: ['/sbin', '/bin', '/usr/sbin', '/usr/bin', '/usr/local/sbin']
+@@ -75,8 +75,8 @@ options:
+ description:
+ - Command to run that reboots the system, including any parameters passed to the command.
+ - Can be an absolute path to the command or just the command name. If an absolute path to the
+- command is not given, O(search_paths) on the target system will be searched to find the absolute path.
+- - This will cause O(pre_reboot_delay), O(post_reboot_delay), and O(msg) to be ignored.
++ command is not given, C(search_paths) on the target system will be searched to find the absolute path.
++ - This will cause C(pre_reboot_delay), C(post_reboot_delay), and C(msg) to be ignored.
+ type: str
+ default: '[determined based on target OS]'
+ version_added: '2.11'
+@@ -121,10 +121,6 @@ EXAMPLES = r'''
+ reboot_command: launchctl reboot userspace
+ boot_time_command: uptime | cut -d ' ' -f 5
+
+-- name: Reboot machine and send a message
+- ansible.builtin.reboot:
+- msg: "Rebooting machine in 5 seconds"
+-
+ '''
+
+ RETURN = r'''
+--- ansible-core-2.16.5.orig/lib/ansible/modules/replace.py
++++ ansible-core-2.16.5/lib/ansible/modules/replace.py
+@@ -39,7 +39,7 @@ options:
+ path:
+ description:
+ - The file to modify.
+- - Before Ansible 2.3 this option was only usable as O(dest), O(destfile) and O(name).
++ - Before Ansible 2.3 this option was only usable as I(dest), I(destfile) and I(name).
+ type: path
+ required: true
+ aliases: [ dest, destfile, name ]
+@@ -48,13 +48,13 @@ options:
+ - The regular expression to look for in the contents of the file.
+ - Uses Python regular expressions; see
+ U(https://docs.python.org/3/library/re.html).
+- - Uses MULTILINE mode, which means V(^) and V($) match the beginning
++ - Uses MULTILINE mode, which means C(^) and C($) match the beginning
+ and end of the file, as well as the beginning and end respectively
+ of I(each line) of the file.
+- - Does not use DOTALL, which means the V(.) special character matches
++ - Does not use DOTALL, which means the C(.) special character matches
+ any character I(except newlines). A common mistake is to assume that
+- a negated character set like V([^#]) will also not match newlines.
+- - In order to exclude newlines, they must be added to the set like V([^#\\n]).
++ a negated character set like C([^#]) will also not match newlines.
++ - In order to exclude newlines, they must be added to the set like C([^#\n]).
+ - Note that, as of Ansible 2.0, short form tasks should have any escape
+ sequences backslash-escaped in order to prevent them being parsed
+ as string literal escapes. See the examples.
+@@ -65,25 +65,24 @@ options:
+ - The string to replace regexp matches.
+ - May contain backreferences that will get expanded with the regexp capture groups if the regexp matches.
+ - If not set, matches are removed entirely.
+- - Backreferences can be used ambiguously like V(\\1), or explicitly like V(\\g<1>).
++ - Backreferences can be used ambiguously like C(\1), or explicitly like C(\g<1>).
+ type: str
+- default: ''
+ after:
+ description:
+ - If specified, only content after this match will be replaced/removed.
+- - Can be used in combination with O(before).
++ - Can be used in combination with C(before).
+ - Uses Python regular expressions; see
+ U(https://docs.python.org/3/library/re.html).
+- - Uses DOTALL, which means the V(.) special character I(can match newlines).
++ - Uses DOTALL, which means the C(.) special character I(can match newlines).
+ type: str
+ version_added: "2.4"
+ before:
+ description:
+ - If specified, only content before this match will be replaced/removed.
+- - Can be used in combination with O(after).
++ - Can be used in combination with C(after).
+ - Uses Python regular expressions; see
+ U(https://docs.python.org/3/library/re.html).
+- - Uses DOTALL, which means the V(.) special character I(can match newlines).
++ - Uses DOTALL, which means the C(.) special character I(can match newlines).
+ type: str
+ version_added: "2.4"
+ backup:
+@@ -103,12 +102,11 @@ options:
+ default: utf-8
+ version_added: "2.4"
+ notes:
+- - As of Ansible 2.3, the O(dest) option has been changed to O(path) as default, but O(dest) still works as well.
+- - As of Ansible 2.7.10, the combined use of O(before) and O(after) works properly. If you were relying on the
++ - As of Ansible 2.3, the I(dest) option has been changed to I(path) as default, but I(dest) still works as well.
++ - As of Ansible 2.7.10, the combined use of I(before) and I(after) works properly. If you were relying on the
+ previous incorrect behavior, you may be need to adjust your tasks.
+ See U(https://github.com/ansible/ansible/issues/31354) for details.
+- - Option O(ignore:follow) has been removed in Ansible 2.5, because this module modifies the contents of the file
+- so O(ignore:follow=no) does not make sense.
++ - Option I(follow) has been removed in Ansible 2.5, because this module modifies the contents of the file so I(follow=no) doesn't make sense.
+ '''
+
+ EXAMPLES = r'''
+@@ -186,7 +184,7 @@ import re
+ import tempfile
+ from traceback import format_exc
+
+-from ansible.module_utils.common.text.converters import to_text, to_bytes
++from ansible.module_utils._text import to_text, to_bytes
+ from ansible.module_utils.basic import AnsibleModule
+
+
+@@ -285,11 +283,7 @@ def main():
+ section = contents
+
+ mre = re.compile(params['regexp'], re.MULTILINE)
+- try:
+- result = re.subn(mre, params['replace'], section, 0)
+- except re.error as e:
+- module.fail_json(msg="Unable to process replace due to error: %s" % to_text(e),
+- exception=format_exc())
++ result = re.subn(mre, params['replace'], section, 0)
+
+ if result[1] > 0 and section != result[0]:
+ if pattern:
+--- ansible-core-2.16.5.orig/lib/ansible/modules/rpm_key.py
++++ ansible-core-2.16.5/lib/ansible/modules/rpm_key.py
+@@ -33,7 +33,7 @@ options:
+ choices: [ absent, present ]
+ validate_certs:
+ description:
+- - If V(false) and the O(key) is a url starting with V(https), SSL certificates will not be validated.
++ - If C(false) and the C(key) is a url starting with https, SSL certificates will not be validated.
+ - This should only be used on personally controlled sites using self-signed certificates.
+ type: bool
+ default: 'yes'
+@@ -85,7 +85,7 @@ import tempfile
+ # import module snippets
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.urls import fetch_url
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+
+ def is_pubkey(string):
+--- ansible-core-2.16.5.orig/lib/ansible/modules/script.py
++++ ansible-core-2.16.5/lib/ansible/modules/script.py
+@@ -11,17 +11,16 @@ module: script
+ version_added: "0.9"
+ short_description: Runs a local script on a remote node after transferring it
+ description:
+- - The M(ansible.builtin.script) module takes the script name followed by a list of space-delimited arguments.
+- - Either a free-form command or O(cmd) parameter is required, see the examples.
+- - The local script at the path will be transferred to the remote node and then executed.
++ - The C(script) module takes the script name followed by a list of space-delimited arguments.
++ - Either a free form command or C(cmd) parameter is required, see the examples.
++ - The local script at path will be transferred to the remote node and then executed.
+ - The given script will be processed through the shell environment on the remote node.
+- - This module does not require Python on the remote system, much like the M(ansible.builtin.raw) module.
++ - This module does not require python on the remote system, much like the M(ansible.builtin.raw) module.
+ - This module is also supported for Windows targets.
+ options:
+ free_form:
+ description:
+ - Path to the local script file followed by optional arguments.
+- type: str
+ cmd:
+ type: str
+ description:
+@@ -30,31 +29,24 @@ options:
+ description:
+ - A filename on the remote node, when it already exists, this step will B(not) be run.
+ version_added: "1.5"
+- type: str
+ removes:
+ description:
+ - A filename on the remote node, when it does not exist, this step will B(not) be run.
+ version_added: "1.5"
+- type: str
+ chdir:
+ description:
+ - Change into this directory on the remote node before running the script.
+ version_added: "2.4"
+- type: str
+ executable:
+ description:
+- - Name or path of an executable to invoke the script with.
++ - Name or path of a executable to invoke the script with.
+ version_added: "2.6"
+- type: str
+ notes:
+ - It is usually preferable to write Ansible modules rather than pushing scripts. Convert your script to an Ansible module for bonus points!
+- - The P(ansible.builtin.ssh#connection) connection plugin will force pseudo-tty allocation via C(-tt) when scripts are executed.
+- Pseudo-ttys do not have a stderr channel and all stderr is sent to stdout. If you depend on separated stdout and stderr result keys,
+- please switch to a set of tasks that comprises M(ansible.builtin.copy) with M(ansible.builtin.command) instead of using M(ansible.builtin.script).
++ - The C(ssh) connection plugin will force pseudo-tty allocation via C(-tt) when scripts are executed. Pseudo-ttys do not have a stderr channel and all
++ stderr is sent to stdout. If you depend on separated stdout and stderr result keys, please switch to a copy+command set of tasks instead of using script.
+ - If the path to the local script contains spaces, it needs to be quoted.
+ - This module is also supported for Windows targets.
+- - If the script returns non-UTF-8 data, it must be encoded to avoid issues. One option is to pipe
+- the output through C(base64).
+ seealso:
+ - module: ansible.builtin.shell
+ - module: ansible.windows.win_shell
+@@ -69,7 +61,7 @@ extends_documentation_fragment:
+ attributes:
+ check_mode:
+ support: partial
+- details: while the script itself is arbitrary and cannot be subject to the check mode semantics it adds O(creates)/O(removes) options as a workaround
++ details: while the script itself is arbitrary and cannot be subject to the check mode semantics it adds C(creates)/C(removes) options as a workaround
+ diff_mode:
+ support: none
+ platform:
+@@ -111,6 +103,6 @@ EXAMPLES = r'''
+ args:
+ executable: python3
+
+-- name: Run a Powershell script on a Windows host
++- name: Run a Powershell script on a windows host
+ script: subdirectories/under/path/with/your/playbook/script.ps1
+ '''
+--- ansible-core-2.16.5.orig/lib/ansible/modules/service.py
++++ ansible-core-2.16.5/lib/ansible/modules/service.py
+@@ -21,8 +21,8 @@ description:
+ - This module is a proxy for multiple more specific service manager modules
+ (such as M(ansible.builtin.systemd) and M(ansible.builtin.sysvinit)).
+ This allows management of a heterogeneous environment of machines without creating a specific task for
+- each service manager. The module to be executed is determined by the O(use) option, which defaults to the
+- service manager discovered by M(ansible.builtin.setup). If M(ansible.builtin.setup) was not yet run, this module may run it.
++ each service manager. The module to be executed is determined by the I(use) option, which defaults to the
++ service manager discovered by M(ansible.builtin.setup). If C(setup) was not yet run, this module may run it.
+ - For Windows targets, use the M(ansible.windows.win_service) module instead.
+ options:
+ name:
+@@ -32,10 +32,10 @@ options:
+ required: true
+ state:
+ description:
+- - V(started)/V(stopped) are idempotent actions that will not run
++ - C(started)/C(stopped) are idempotent actions that will not run
+ commands unless necessary.
+- - V(restarted) will always bounce the service.
+- - V(reloaded) will always reload.
++ - C(restarted) will always bounce the service.
++ - C(reloaded) will always reload.
+ - B(At least one of state and enabled are required.)
+ - Note that reloaded will start the service if it is not already started,
+ even if your chosen init system wouldn't normally.
+@@ -43,7 +43,7 @@ options:
+ choices: [ reloaded, restarted, started, stopped ]
+ sleep:
+ description:
+- - If the service is being V(restarted) then sleep this many seconds
++ - If the service is being C(restarted) then sleep this many seconds
+ between the stop and start command.
+ - This helps to work around badly-behaving init scripts that exit immediately
+ after signaling a process to stop.
+@@ -76,13 +76,11 @@ options:
+ - Additional arguments provided on the command line.
+ - While using remote hosts with systemd this setting will be ignored.
+ type: str
+- default: ''
+ aliases: [ args ]
+ use:
+ description:
+ - The service module actually uses system specific modules, normally through auto detection, this setting can force a specific module.
+ - Normally it uses the value of the 'ansible_service_mgr' fact and falls back to the old 'service' module when none matching is found.
+- - The 'old service module' still uses autodetection and in no way does it correspond to the C(service) command.
+ type: str
+ default: auto
+ version_added: 2.2
+@@ -107,9 +105,6 @@ attributes:
+ platforms: all
+ notes:
+ - For AIX, group subsystem names can be used.
+- - The C(service) command line utility is not part of any service manager system but a convenience.
+- It does not have a standard implementation across systems, and this action cannot use it directly.
+- Though it might be used if found in certain circumstances, the detected system service manager is normally preferred.
+ seealso:
+ - module: ansible.windows.win_service
+ author:
+@@ -176,7 +171,7 @@ import time
+ if platform.system() != 'SunOS':
+ from ansible.module_utils.compat.version import LooseVersion
+
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.common.locale import get_best_parsable_locale
+ from ansible.module_utils.common.sys_info import get_platform_subclass
+@@ -1195,31 +1190,107 @@ class OpenBsdService(Service):
+ return self.execute_command("%s -f %s" % (self.svc_cmd, self.action))
+
+ def service_enable(self):
+-
+ if not self.enable_cmd:
+ return super(OpenBsdService, self).service_enable()
+
++ rc, stdout, stderr = self.execute_command("%s %s %s %s" % (self.enable_cmd, 'getdef', self.name, 'flags'))
++
++ if stderr:
++ self.module.fail_json(msg=stderr)
++
++ getdef_string = stdout.rstrip()
++
++ # Depending on the service the string returned from 'getdef' may be
++ # either a set of flags or the boolean YES/NO
++ if getdef_string == "YES" or getdef_string == "NO":
++ default_flags = ''
++ else:
++ default_flags = getdef_string
++
++ rc, stdout, stderr = self.execute_command("%s %s %s %s" % (self.enable_cmd, 'get', self.name, 'flags'))
++
++ if stderr:
++ self.module.fail_json(msg=stderr)
++
++ get_string = stdout.rstrip()
++
++ # Depending on the service the string returned from 'get' may be
++ # either a set of flags or the boolean YES/NO
++ if get_string == "YES" or get_string == "NO":
++ current_flags = ''
++ else:
++ current_flags = get_string
++
++ # If there are arguments from the user we use these as flags unless
++ # they are already set.
++ if self.arguments and self.arguments != current_flags:
++ changed_flags = self.arguments
++ # If the user has not supplied any arguments and the current flags
++ # differ from the default we reset them.
++ elif not self.arguments and current_flags != default_flags:
++ changed_flags = ' '
++ # Otherwise there is no need to modify flags.
++ else:
++ changed_flags = ''
++
+ rc, stdout, stderr = self.execute_command("%s %s %s %s" % (self.enable_cmd, 'get', self.name, 'status'))
+
+- status_action = None
+ if self.enable:
++ if rc == 0 and not changed_flags:
++ return
++
+ if rc != 0:
+- status_action = "on"
+- elif self.enable is not None:
+- # should be explicit False at this point
+- if rc != 1:
+- status_action = "off"
++ status_action = "set %s status on" % (self.name)
++ else:
++ status_action = ''
++ if changed_flags:
++ flags_action = "set %s flags %s" % (self.name, changed_flags)
++ else:
++ flags_action = ''
++ else:
++ if rc == 1:
++ return
+
+- if status_action is not None:
+- self.changed = True
+- if not self.module.check_mode:
+- rc, stdout, stderr = self.execute_command("%s set %s status %s" % (self.enable_cmd, self.name, status_action))
++ status_action = "set %s status off" % self.name
++ flags_action = ''
+
+- if rc != 0:
+- if stderr:
+- self.module.fail_json(msg=stderr)
++ # Verify state assumption
++ if not status_action and not flags_action:
++ self.module.fail_json(msg="neither status_action or status_flags is set, this should never happen")
++
++ if self.module.check_mode:
++ self.module.exit_json(changed=True, msg="changing service enablement")
++
++ status_modified = 0
++ if status_action:
++ rc, stdout, stderr = self.execute_command("%s %s" % (self.enable_cmd, status_action))
++
++ if rc != 0:
++ if stderr:
++ self.module.fail_json(msg=stderr)
++ else:
++ self.module.fail_json(msg="rcctl failed to modify service status")
++
++ status_modified = 1
++
++ if flags_action:
++ rc, stdout, stderr = self.execute_command("%s %s" % (self.enable_cmd, flags_action))
++
++ if rc != 0:
++ if stderr:
++ if status_modified:
++ error_message = "rcctl modified service status but failed to set flags: " + stderr
+ else:
+- self.module.fail_json(msg="rcctl failed to modify service status")
++ error_message = stderr
++ else:
++ if status_modified:
++ error_message = "rcctl modified service status but failed to set flags"
++ else:
++ error_message = "rcctl failed to modify service flags"
++
++ self.module.fail_json(msg=error_message)
++
++ self.changed = True
+
+
+ class NetBsdService(Service):
+--- ansible-core-2.16.5.orig/lib/ansible/modules/service_facts.py
++++ ansible-core-2.16.5/lib/ansible/modules/service_facts.py
+@@ -28,7 +28,7 @@ attributes:
+ platform:
+ platforms: posix
+ notes:
+- - When accessing the RV(ansible_facts.services) facts collected by this module,
++ - When accessing the C(ansible_facts.services) facts collected by this module,
+ it is recommended to not use "dot notation" because services can have a C(-)
+ character in their name which would result in invalid "dot notation", such as
+ C(ansible_facts.services.zuul-gateway). It is instead recommended to
+@@ -57,20 +57,19 @@ ansible_facts:
+ services:
+ description: States of the services with service name as key.
+ returned: always
+- type: list
+- elements: dict
++ type: complex
+ contains:
+ source:
+ description:
+ - Init system of the service.
+- - One of V(rcctl), V(systemd), V(sysv), V(upstart), V(src).
++ - One of C(rcctl), C(systemd), C(sysv), C(upstart), C(src).
+ returned: always
+ type: str
+ sample: sysv
+ state:
+ description:
+ - State of the service.
+- - 'This commonly includes (but is not limited to) the following: V(failed), V(running), V(stopped) or V(unknown).'
++ - 'This commonly includes (but is not limited to) the following: C(failed), C(running), C(stopped) or C(unknown).'
+ - Depending on the used init system additional states might be returned.
+ returned: always
+ type: str
+@@ -78,7 +77,7 @@ ansible_facts:
+ status:
+ description:
+ - State of the service.
+- - Either V(enabled), V(disabled), V(static), V(indirect) or V(unknown).
++ - Either C(enabled), C(disabled), C(static), C(indirect) or C(unknown).
+ returned: systemd systems or RedHat/SUSE flavored sysvinit/upstart or OpenBSD
+ type: str
+ sample: enabled
+@@ -362,31 +361,14 @@ class OpenBSDScanService(BaseService):
+ svcs.append(svc)
+ return svcs
+
+- def get_info(self, name):
+- info = {}
+- rc, stdout, stderr = self.module.run_command("%s get %s" % (self.rcctl_path, name))
+- if 'needs root privileges' in stderr.lower():
+- self.module.warn('rcctl requires root privileges')
+- else:
+- undy = '%s_' % name
+- for variable in stdout.split('\n'):
+- if variable == '' or '=' not in variable:
+- continue
+- else:
+- k, v = variable.replace(undy, '', 1).split('=')
+- info[k] = v
+- return info
+-
+ def gather_services(self):
+
+ services = {}
+ self.rcctl_path = self.module.get_bin_path("rcctl")
+ if self.rcctl_path:
+
+- # populate services will all possible
+ for svc in self.query_rcctl('all'):
+- services[svc] = {'name': svc, 'source': 'rcctl', 'rogue': False}
+- services[svc].update(self.get_info(svc))
++ services[svc] = {'name': svc, 'source': 'rcctl'}
+
+ for svc in self.query_rcctl('on'):
+ services[svc].update({'status': 'enabled'})
+@@ -394,22 +376,16 @@ class OpenBSDScanService(BaseService):
+ for svc in self.query_rcctl('started'):
+ services[svc].update({'state': 'running'})
+
++ # Based on the list of services that are enabled, determine which are disabled
++ [services[svc].update({'status': 'disabled'}) for svc in services if services[svc].get('status') is None]
++
++ # and do the same for those are aren't running
++ [services[svc].update({'state': 'stopped'}) for svc in services if services[svc].get('state') is None]
++
+ # Override the state for services which are marked as 'failed'
+ for svc in self.query_rcctl('failed'):
+ services[svc].update({'state': 'failed'})
+
+- for svc in services.keys():
+- # Based on the list of services that are enabled/failed, determine which are disabled
+- if services[svc].get('status') is None:
+- services[svc].update({'status': 'disabled'})
+-
+- # and do the same for those are aren't running
+- if services[svc].get('state') is None:
+- services[svc].update({'state': 'stopped'})
+-
+- for svc in self.query_rcctl('rogue'):
+- services[svc]['rogue'] = True
+-
+ return services
+
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/set_fact.py
++++ ansible-core-2.16.5/lib/ansible/modules/set_fact.py
+@@ -15,13 +15,13 @@ version_added: "1.2"
+ description:
+ - This action allows setting variables associated to the current host.
+ - These variables will be available to subsequent plays during an ansible-playbook run via the host they were set on.
+- - Set O(cacheable) to V(true) to save variables across executions using a fact cache.
++ - Set C(cacheable) to C(true) to save variables across executions using a fact cache.
+ Variables will keep the set_fact precedence for the current run, but will used 'cached fact' precedence for subsequent ones.
+ - Per the standard Ansible variable precedence rules, other types of variables have a higher priority, so this value may be overridden.
+ options:
+ key_value:
+ description:
+- - "The M(ansible.builtin.set_fact) module takes C(key=value) pairs or C(key: value) (YAML notation) as variables to set in the playbook scope.
++ - "The C(set_fact) module takes C(key=value) pairs or C(key: value) (YAML notation) as variables to set in the playbook scope.
+ The 'key' is the resulting variable name and the value is, of course, the value of said variable."
+ - You can create multiple variables at once, by supplying multiple pairs, but do NOT mix notations.
+ required: true
+@@ -45,7 +45,7 @@ extends_documentation_fragment:
+ - action_core
+ attributes:
+ action:
+- details: While the action plugin does do some of the work it relies on the core engine to actually create the variables, that part cannot be overridden
++ details: While the action plugin does do some of the work it relies on the core engine to actually create the variables, that part cannot be overriden
+ support: partial
+ bypass_host_loop:
+ support: none
+--- ansible-core-2.16.5.orig/lib/ansible/modules/set_stats.py
++++ ansible-core-2.16.5/lib/ansible/modules/set_stats.py
+@@ -28,7 +28,7 @@ options:
+ default: no
+ aggregate:
+ description:
+- - Whether the provided value is aggregated to the existing stat V(true) or will replace it V(false).
++ - Whether the provided value is aggregated to the existing stat C(true) or will replace it C(false).
+ type: bool
+ default: yes
+ extends_documentation_fragment:
+@@ -55,7 +55,7 @@ attributes:
+ support: none
+ notes:
+ - In order for custom stats to be displayed, you must set C(show_custom_stats) in section C([defaults]) in C(ansible.cfg)
+- or by defining environment variable C(ANSIBLE_SHOW_CUSTOM_STATS) to V(true). See the P(ansible.builtin.default#callback) callback plugin for details.
++ or by defining environment variable C(ANSIBLE_SHOW_CUSTOM_STATS) to C(true). See the C(default) callback plugin for details.
+ version_added: "2.3"
+ '''
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/setup.py
++++ ansible-core-2.16.5/lib/ansible/modules/setup.py
+@@ -17,24 +17,24 @@ options:
+ version_added: "2.1"
+ description:
+ - "If supplied, restrict the additional facts collected to the given subset.
+- Possible values: V(all), V(all_ipv4_addresses), V(all_ipv6_addresses), V(apparmor), V(architecture),
+- V(caps), V(chroot),V(cmdline), V(date_time), V(default_ipv4), V(default_ipv6), V(devices),
+- V(distribution), V(distribution_major_version), V(distribution_release), V(distribution_version),
+- V(dns), V(effective_group_ids), V(effective_user_id), V(env), V(facter), V(fips), V(hardware),
+- V(interfaces), V(is_chroot), V(iscsi), V(kernel), V(local), V(lsb), V(machine), V(machine_id),
+- V(mounts), V(network), V(ohai), V(os_family), V(pkg_mgr), V(platform), V(processor), V(processor_cores),
+- V(processor_count), V(python), V(python_version), V(real_user_id), V(selinux), V(service_mgr),
+- V(ssh_host_key_dsa_public), V(ssh_host_key_ecdsa_public), V(ssh_host_key_ed25519_public),
+- V(ssh_host_key_rsa_public), V(ssh_host_pub_keys), V(ssh_pub_keys), V(system), V(system_capabilities),
+- V(system_capabilities_enforced), V(user), V(user_dir), V(user_gecos), V(user_gid), V(user_id),
+- V(user_shell), V(user_uid), V(virtual), V(virtualization_role), V(virtualization_type).
++ Possible values: C(all), C(all_ipv4_addresses), C(all_ipv6_addresses), C(apparmor), C(architecture),
++ C(caps), C(chroot),C(cmdline), C(date_time), C(default_ipv4), C(default_ipv6), C(devices),
++ C(distribution), C(distribution_major_version), C(distribution_release), C(distribution_version),
++ C(dns), C(effective_group_ids), C(effective_user_id), C(env), C(facter), C(fips), C(hardware),
++ C(interfaces), C(is_chroot), C(iscsi), C(kernel), C(local), C(lsb), C(machine), C(machine_id),
++ C(mounts), C(network), C(ohai), C(os_family), C(pkg_mgr), C(platform), C(processor), C(processor_cores),
++ C(processor_count), C(python), C(python_version), C(real_user_id), C(selinux), C(service_mgr),
++ C(ssh_host_key_dsa_public), C(ssh_host_key_ecdsa_public), C(ssh_host_key_ed25519_public),
++ C(ssh_host_key_rsa_public), C(ssh_host_pub_keys), C(ssh_pub_keys), C(system), C(system_capabilities),
++ C(system_capabilities_enforced), C(user), C(user_dir), C(user_gecos), C(user_gid), C(user_id),
++ C(user_shell), C(user_uid), C(virtual), C(virtualization_role), C(virtualization_type).
+ Can specify a list of values to specify a larger subset.
+ Values can also be used with an initial C(!) to specify that
+ that specific subset should not be collected. For instance:
+- V(!hardware,!network,!virtual,!ohai,!facter). If V(!all) is specified
++ C(!hardware,!network,!virtual,!ohai,!facter). If C(!all) is specified
+ then only the min subset is collected. To avoid collecting even the
+- min subset, specify V(!all,!min). To collect only specific facts,
+- use V(!all,!min), and specify the particular fact subsets.
++ min subset, specify C(!all,!min). To collect only specific facts,
++ use C(!all,!min), and specify the particular fact subsets.
+ Use the filter parameter if you do not want to display some collected
+ facts."
+ type: list
+@@ -64,12 +64,12 @@ options:
+ - Path used for local ansible facts (C(*.fact)) - files in this dir
+ will be run (if executable) and their results be added to C(ansible_local) facts.
+ If a file is not executable it is read instead.
+- File/results format can be JSON or INI-format. The default O(fact_path) can be
++ File/results format can be JSON or INI-format. The default C(fact_path) can be
+ specified in C(ansible.cfg) for when setup is automatically called as part of
+ C(gather_facts).
+ NOTE - For windows clients, the results will be added to a variable named after the
+ local file (without extension suffix), rather than C(ansible_local).
+- - Since Ansible 2.1, Windows hosts can use O(fact_path). Make sure that this path
++ - Since Ansible 2.1, Windows hosts can use C(fact_path). Make sure that this path
+ exists on the target host. Files in this path MUST be PowerShell scripts C(.ps1)
+ which outputs an object. This object will be formatted by Ansible as json so the
+ script should be outputting a raw hashtable, array, or other primitive object.
+@@ -104,7 +104,7 @@ notes:
+ remote systems. (See also M(community.general.facter) and M(community.general.ohai).)
+ - The filter option filters only the first level subkey below ansible_facts.
+ - If the target host is Windows, you will not currently have the ability to use
+- O(filter) as this is provided by a simpler implementation of the module.
++ C(filter) as this is provided by a simpler implementation of the module.
+ - This module should be run with elevated privileges on BSD systems to gather facts like ansible_product_version.
+ - For more information about delegated facts,
+ please check U(https://docs.ansible.com/ansible/latest/user_guide/playbooks_delegation.html#delegating-facts).
+@@ -174,7 +174,7 @@ EXAMPLES = r"""
+ # import module snippets
+ from ..module_utils.basic import AnsibleModule
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.facts import ansible_collector, default_collectors
+ from ansible.module_utils.facts.collector import CollectorNotFoundError, CycleFoundInFactDeps, UnresolvedFactDep
+ from ansible.module_utils.facts.namespace import PrefixFactNamespace
+--- ansible-core-2.16.5.orig/lib/ansible/modules/shell.py
++++ ansible-core-2.16.5/lib/ansible/modules/shell.py
+@@ -16,8 +16,8 @@ DOCUMENTATION = r'''
+ module: shell
+ short_description: Execute shell commands on targets
+ description:
+- - The M(ansible.builtin.shell) module takes the command name followed by a list of space-delimited arguments.
+- - Either a free form command or O(cmd) parameter is required, see the examples.
++ - The C(shell) module takes the command name followed by a list of space-delimited arguments.
++ - Either a free form command or C(cmd) parameter is required, see the examples.
+ - It is almost exactly like the M(ansible.builtin.command) module but runs
+ the command through a shell (C(/bin/sh)) on the remote node.
+ - For Windows targets, use the M(ansible.windows.win_shell) module instead.
+@@ -69,7 +69,7 @@ extends_documentation_fragment:
+ - action_common_attributes.raw
+ attributes:
+ check_mode:
+- details: while the command itself is arbitrary and cannot be subject to the check mode semantics it adds O(creates)/O(removes) options as a workaround
++ details: while the command itself is arbitrary and cannot be subject to the check mode semantics it adds C(creates)/C(removes) options as a workaround
+ support: partial
+ diff_mode:
+ support: none
+@@ -90,8 +90,6 @@ notes:
+ - An alternative to using inline shell scripts with this module is to use
+ the M(ansible.builtin.script) module possibly together with the M(ansible.builtin.template) module.
+ - For rebooting systems, use the M(ansible.builtin.reboot) or M(ansible.windows.win_reboot) module.
+- - If the command returns non UTF-8 data, it must be encoded to avoid issues. One option is to pipe
+- the output through C(base64).
+ seealso:
+ - module: ansible.builtin.command
+ - module: ansible.builtin.raw
+--- ansible-core-2.16.5.orig/lib/ansible/modules/slurp.py
++++ ansible-core-2.16.5/lib/ansible/modules/slurp.py
+@@ -84,6 +84,7 @@ source:
+
+ import base64
+ import errno
++import os
+
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.common.text.converters import to_native
+--- ansible-core-2.16.5.orig/lib/ansible/modules/stat.py
++++ ansible-core-2.16.5/lib/ansible/modules/stat.py
+@@ -36,7 +36,7 @@ options:
+ description:
+ - Algorithm to determine checksum of file.
+ - Will throw an error if the host is unable to use specified algorithm.
+- - The remote host has to support the hashing method specified, V(md5)
++ - The remote host has to support the hashing method specified, C(md5)
+ can be unavailable if the host is FIPS-140 compliant.
+ type: str
+ choices: [ md5, sha1, sha224, sha256, sha384, sha512 ]
+@@ -47,8 +47,8 @@ options:
+ description:
+ - Use file magic and return data about the nature of the file. this uses
+ the 'file' utility found on most Linux/Unix systems.
+- - This will add both RV(stat.mimetype) and RV(stat.charset) fields to the return, if possible.
+- - In Ansible 2.3 this option changed from O(mime) to O(get_mime) and the default changed to V(true).
++ - This will add both C(mimetype) and C(charset) fields to the return, if possible.
++ - In Ansible 2.3 this option changed from I(mime) to I(get_mime) and the default changed to C(true).
+ type: bool
+ default: yes
+ aliases: [ mime, mime_type, mime-type ]
+@@ -144,7 +144,7 @@ RETURN = r'''
+ stat:
+ description: Dictionary containing all the stat data, some platforms might add additional fields.
+ returned: success
+- type: dict
++ type: complex
+ contains:
+ exists:
+ description: If the destination path actually exists or not
+@@ -307,6 +307,13 @@ stat:
+ type: str
+ sample: ../foobar/21102015-1445431274-908472971
+ version_added: 2.4
++ md5:
++ description: md5 hash of the file; this will be removed in Ansible 2.9 in
++ favor of the checksum return value
++ returned: success, path exists and user can read stats and path
++ supports hashing and md5 is supported
++ type: str
++ sample: f88fa92d8cf2eeecf4c0a50ccc96d0c0
+ checksum:
+ description: hash of the file
+ returned: success, path exists, user can read stats, path supports
+@@ -326,15 +333,15 @@ stat:
+ mimetype:
+ description: file magic data or mime-type
+ returned: success, path exists and user can read stats and
+- installed python supports it and the O(get_mime) option was V(true), will
+- return V(unknown) on error.
++ installed python supports it and the I(get_mime) option was true, will
++ return C(unknown) on error.
+ type: str
+ sample: application/pdf; charset=binary
+ charset:
+ description: file character set or encoding
+ returned: success, path exists and user can read stats and
+- installed python supports it and the O(get_mime) option was V(true), will
+- return V(unknown) on error.
++ installed python supports it and the I(get_mime) option was true, will
++ return C(unknown) on error.
+ type: str
+ sample: us-ascii
+ readable:
+@@ -377,7 +384,7 @@ import stat
+
+ # import module snippets
+ from ansible.module_utils.basic import AnsibleModule
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+
+
+ def format_output(module, path, st):
+@@ -447,6 +454,7 @@ def main():
+ argument_spec=dict(
+ path=dict(type='path', required=True, aliases=['dest', 'name']),
+ follow=dict(type='bool', default=False),
++ get_md5=dict(type='bool', default=False),
+ get_checksum=dict(type='bool', default=True),
+ get_mime=dict(type='bool', default=True, aliases=['mime', 'mime_type', 'mime-type']),
+ get_attributes=dict(type='bool', default=True, aliases=['attr', 'attributes']),
+@@ -465,6 +473,10 @@ def main():
+ get_checksum = module.params.get('get_checksum')
+ checksum_algorithm = module.params.get('checksum_algorithm')
+
++ # NOTE: undocumented option since 2.9 to be removed at a later date if possible (3.0+)
++ # no real reason for keeping other than fear we may break older content.
++ get_md5 = module.params.get('get_md5')
++
+ # main stat data
+ try:
+ if follow:
+@@ -504,6 +516,15 @@ def main():
+
+ # checksums
+ if output.get('isreg') and output.get('readable'):
++
++ # NOTE: see above about get_md5
++ if get_md5:
++ # Will fail on FIPS-140 compliant systems
++ try:
++ output['md5'] = module.md5(b_path)
++ except ValueError:
++ output['md5'] = None
++
+ if get_checksum:
+ output['checksum'] = module.digest_from_file(b_path, checksum_algorithm)
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/subversion.py
++++ ansible-core-2.16.5/lib/ansible/modules/subversion.py
+@@ -26,7 +26,7 @@ options:
+ dest:
+ description:
+ - Absolute path where the repository should be deployed.
+- - The destination directory must be specified unless O(checkout=no), O(update=no), and O(export=no).
++ - The destination directory must be specified unless I(checkout=no), I(update=no), and I(export=no).
+ type: path
+ revision:
+ description:
+@@ -36,8 +36,8 @@ options:
+ aliases: [ rev, version ]
+ force:
+ description:
+- - If V(true), modified files will be discarded. If V(false), module will fail if it encounters modified files.
+- Prior to 1.9 the default was V(true).
++ - If C(true), modified files will be discarded. If C(false), module will fail if it encounters modified files.
++ Prior to 1.9 the default was C(true).
+ type: bool
+ default: "no"
+ in_place:
+@@ -65,32 +65,32 @@ options:
+ version_added: "1.4"
+ checkout:
+ description:
+- - If V(false), do not check out the repository if it does not exist locally.
++ - If C(false), do not check out the repository if it does not exist locally.
+ type: bool
+ default: "yes"
+ version_added: "2.3"
+ update:
+ description:
+- - If V(false), do not retrieve new revisions from the origin repository.
++ - If C(false), do not retrieve new revisions from the origin repository.
+ type: bool
+ default: "yes"
+ version_added: "2.3"
+ export:
+ description:
+- - If V(true), do export instead of checkout/update.
++ - If C(true), do export instead of checkout/update.
+ type: bool
+ default: "no"
+ version_added: "1.6"
+ switch:
+ description:
+- - If V(false), do not call svn switch before update.
++ - If C(false), do not call svn switch before update.
+ default: "yes"
+ version_added: "2.0"
+ type: bool
+ validate_certs:
+ description:
+- - If V(false), passes the C(--trust-server-cert) flag to svn.
+- - If V(true), does not pass the flag.
++ - If C(false), passes the C(--trust-server-cert) flag to svn.
++ - If C(true), does not pass the flag.
+ default: "no"
+ version_added: "2.11"
+ type: bool
+--- ansible-core-2.16.5.orig/lib/ansible/modules/systemd.py
++++ ansible-core-2.16.5/lib/ansible/modules/systemd.py
+@@ -25,9 +25,8 @@ options:
+ aliases: [ service, unit ]
+ state:
+ description:
+- - V(started)/V(stopped) are idempotent actions that will not run commands unless necessary.
+- V(restarted) will always bounce the unit.
+- V(reloaded) will always reload and if the service is not running at the moment of the reload, it is started.
++ - C(started)/C(stopped) are idempotent actions that will not run commands unless necessary.
++ C(restarted) will always bounce the unit. C(reloaded) will always reload.
+ type: str
+ choices: [ reloaded, restarted, started, stopped ]
+ enabled:
+@@ -46,7 +45,7 @@ options:
+ daemon_reload:
+ description:
+ - Run daemon-reload before doing any other operations, to make sure systemd has read any changes.
+- - When set to V(true), runs daemon-reload even if the module does not start or stop anything.
++ - When set to C(true), runs daemon-reload even if the module does not start or stop anything.
+ type: bool
+ default: no
+ aliases: [ daemon-reload ]
+@@ -59,8 +58,8 @@ options:
+ version_added: "2.8"
+ scope:
+ description:
+- - Run systemctl within a given service manager scope, either as the default system scope V(system),
+- the current user's scope V(user), or the scope of all users V(global).
++ - Run systemctl within a given service manager scope, either as the default system scope C(system),
++ the current user's scope C(user), or the scope of all users C(global).
+ - "For systemd to work with 'user', the executing user must have its own instance of dbus started and accessible (systemd requirement)."
+ - "The user dbus process is normally started during normal login, but not during the run of Ansible tasks.
+ Otherwise you will probably get a 'Failed to connect to bus: no such file or directory' error."
+@@ -86,61 +85,59 @@ attributes:
+ platform:
+ platforms: posix
+ notes:
+- - Since 2.4, one of the following options is required O(state), O(enabled), O(masked), O(daemon_reload), (O(daemon_reexec) since 2.8),
+- and all except O(daemon_reload) and (O(daemon_reexec) since 2.8) also require O(name).
+- - Before 2.4 you always required O(name).
++ - Since 2.4, one of the following options is required C(state), C(enabled), C(masked), C(daemon_reload), (C(daemon_reexec) since 2.8),
++ and all except C(daemon_reload) and (C(daemon_reexec) since 2.8) also require C(name).
++ - Before 2.4 you always required C(name).
+ - Globs are not supported in name, i.e C(postgres*.service).
+ - The service names might vary by specific OS/distribution
+- - The order of execution when having multiple properties is to first enable/disable, then mask/unmask and then deal with service state.
+- It has been reported that systemctl can behave differently depending on the order of operations if you do the same manually.
+ requirements:
+ - A system managed by systemd.
+ '''
+
+ EXAMPLES = '''
+ - name: Make sure a service unit is running
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ state: started
+ name: httpd
+
+ - name: Stop service cron on debian, if running
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ name: cron
+ state: stopped
+
+ - name: Restart service cron on centos, in all cases, also issue daemon-reload to pick up config changes
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ state: restarted
+ daemon_reload: true
+ name: crond
+
+ - name: Reload service httpd, in all cases
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ name: httpd.service
+ state: reloaded
+
+ - name: Enable service httpd and ensure it is not masked
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ name: httpd
+ enabled: true
+ masked: no
+
+ - name: Enable a timer unit for dnf-automatic
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ name: dnf-automatic.timer
+ state: started
+ enabled: true
+
+ - name: Just force systemd to reread configs (2.4 and above)
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ daemon_reload: true
+
+ - name: Just force systemd to re-execute itself (2.8 and above)
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ daemon_reexec: true
+
+ - name: Run a user service when XDG_RUNTIME_DIR is not set on remote login
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ name: myservice
+ state: started
+ scope: user
+@@ -152,7 +149,7 @@ RETURN = '''
+ status:
+ description: A dictionary with the key=value pairs returned from C(systemctl show).
+ returned: success
+- type: dict
++ type: complex
+ sample: {
+ "ActiveEnterTimestamp": "Sun 2016-05-15 18:28:49 EDT",
+ "ActiveEnterTimestampMonotonic": "8135942",
+@@ -283,7 +280,7 @@ import os
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.facts.system.chroot import is_chroot
+ from ansible.module_utils.service import sysv_exists, sysv_is_enabled, fail_if_missing
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+
+ def is_running_service(service_status):
+@@ -370,7 +367,7 @@ def main():
+ if os.getenv('XDG_RUNTIME_DIR') is None:
+ os.environ['XDG_RUNTIME_DIR'] = '/run/user/%s' % os.geteuid()
+
+- # Set CLI options depending on params
++ ''' Set CLI options depending on params '''
+ # if scope is 'system' or None, we can ignore as there is no extra switch.
+ # The other choices match the corresponding switch
+ if module.params['scope'] != 'system':
+@@ -394,19 +391,13 @@ def main():
+ if module.params['daemon_reload'] and not module.check_mode:
+ (rc, out, err) = module.run_command("%s daemon-reload" % (systemctl))
+ if rc != 0:
+- if is_chroot(module) or os.environ.get('SYSTEMD_OFFLINE') == '1':
+- module.warn('daemon-reload failed, but target is a chroot or systemd is offline. Continuing. Error was: %d / %s' % (rc, err))
+- else:
+- module.fail_json(msg='failure %d during daemon-reload: %s' % (rc, err))
++ module.fail_json(msg='failure %d during daemon-reload: %s' % (rc, err))
+
+ # Run daemon-reexec
+ if module.params['daemon_reexec'] and not module.check_mode:
+ (rc, out, err) = module.run_command("%s daemon-reexec" % (systemctl))
+ if rc != 0:
+- if is_chroot(module) or os.environ.get('SYSTEMD_OFFLINE') == '1':
+- module.warn('daemon-reexec failed, but target is a chroot or systemd is offline. Continuing. Error was: %d / %s' % (rc, err))
+- else:
+- module.fail_json(msg='failure %d during daemon-reexec: %s' % (rc, err))
++ module.fail_json(msg='failure %d during daemon-reexec: %s' % (rc, err))
+
+ if unit:
+ found = False
+--- ansible-core-2.16.5.orig/lib/ansible/modules/systemd_service.py
++++ ansible-core-2.16.5/lib/ansible/modules/systemd_service.py
+@@ -25,9 +25,8 @@ options:
+ aliases: [ service, unit ]
+ state:
+ description:
+- - V(started)/V(stopped) are idempotent actions that will not run commands unless necessary.
+- V(restarted) will always bounce the unit.
+- V(reloaded) will always reload and if the service is not running at the moment of the reload, it is started.
++ - C(started)/C(stopped) are idempotent actions that will not run commands unless necessary.
++ C(restarted) will always bounce the unit. C(reloaded) will always reload.
+ type: str
+ choices: [ reloaded, restarted, started, stopped ]
+ enabled:
+@@ -46,7 +45,7 @@ options:
+ daemon_reload:
+ description:
+ - Run daemon-reload before doing any other operations, to make sure systemd has read any changes.
+- - When set to V(true), runs daemon-reload even if the module does not start or stop anything.
++ - When set to C(true), runs daemon-reload even if the module does not start or stop anything.
+ type: bool
+ default: no
+ aliases: [ daemon-reload ]
+@@ -59,8 +58,8 @@ options:
+ version_added: "2.8"
+ scope:
+ description:
+- - Run systemctl within a given service manager scope, either as the default system scope V(system),
+- the current user's scope V(user), or the scope of all users V(global).
++ - Run systemctl within a given service manager scope, either as the default system scope C(system),
++ the current user's scope C(user), or the scope of all users C(global).
+ - "For systemd to work with 'user', the executing user must have its own instance of dbus started and accessible (systemd requirement)."
+ - "The user dbus process is normally started during normal login, but not during the run of Ansible tasks.
+ Otherwise you will probably get a 'Failed to connect to bus: no such file or directory' error."
+@@ -86,61 +85,59 @@ attributes:
+ platform:
+ platforms: posix
+ notes:
+- - Since 2.4, one of the following options is required O(state), O(enabled), O(masked), O(daemon_reload), (O(daemon_reexec) since 2.8),
+- and all except O(daemon_reload) and (O(daemon_reexec) since 2.8) also require O(name).
+- - Before 2.4 you always required O(name).
++ - Since 2.4, one of the following options is required C(state), C(enabled), C(masked), C(daemon_reload), (C(daemon_reexec) since 2.8),
++ and all except C(daemon_reload) and (C(daemon_reexec) since 2.8) also require C(name).
++ - Before 2.4 you always required C(name).
+ - Globs are not supported in name, i.e C(postgres*.service).
+ - The service names might vary by specific OS/distribution
+- - The order of execution when having multiple properties is to first enable/disable, then mask/unmask and then deal with service state.
+- It has been reported that systemctl can behave differently depending on the order of operations if you do the same manually.
+ requirements:
+ - A system managed by systemd.
+ '''
+
+ EXAMPLES = '''
+ - name: Make sure a service unit is running
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ state: started
+ name: httpd
+
+ - name: Stop service cron on debian, if running
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ name: cron
+ state: stopped
+
+ - name: Restart service cron on centos, in all cases, also issue daemon-reload to pick up config changes
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ state: restarted
+ daemon_reload: true
+ name: crond
+
+ - name: Reload service httpd, in all cases
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ name: httpd.service
+ state: reloaded
+
+ - name: Enable service httpd and ensure it is not masked
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ name: httpd
+ enabled: true
+ masked: no
+
+ - name: Enable a timer unit for dnf-automatic
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ name: dnf-automatic.timer
+ state: started
+ enabled: true
+
+ - name: Just force systemd to reread configs (2.4 and above)
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ daemon_reload: true
+
+ - name: Just force systemd to re-execute itself (2.8 and above)
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ daemon_reexec: true
+
+ - name: Run a user service when XDG_RUNTIME_DIR is not set on remote login
+- ansible.builtin.systemd_service:
++ ansible.builtin.systemd:
+ name: myservice
+ state: started
+ scope: user
+@@ -152,7 +149,7 @@ RETURN = '''
+ status:
+ description: A dictionary with the key=value pairs returned from C(systemctl show).
+ returned: success
+- type: dict
++ type: complex
+ sample: {
+ "ActiveEnterTimestamp": "Sun 2016-05-15 18:28:49 EDT",
+ "ActiveEnterTimestampMonotonic": "8135942",
+@@ -283,7 +280,7 @@ import os
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.facts.system.chroot import is_chroot
+ from ansible.module_utils.service import sysv_exists, sysv_is_enabled, fail_if_missing
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+
+ def is_running_service(service_status):
+@@ -370,7 +367,7 @@ def main():
+ if os.getenv('XDG_RUNTIME_DIR') is None:
+ os.environ['XDG_RUNTIME_DIR'] = '/run/user/%s' % os.geteuid()
+
+- # Set CLI options depending on params
++ ''' Set CLI options depending on params '''
+ # if scope is 'system' or None, we can ignore as there is no extra switch.
+ # The other choices match the corresponding switch
+ if module.params['scope'] != 'system':
+@@ -394,19 +391,13 @@ def main():
+ if module.params['daemon_reload'] and not module.check_mode:
+ (rc, out, err) = module.run_command("%s daemon-reload" % (systemctl))
+ if rc != 0:
+- if is_chroot(module) or os.environ.get('SYSTEMD_OFFLINE') == '1':
+- module.warn('daemon-reload failed, but target is a chroot or systemd is offline. Continuing. Error was: %d / %s' % (rc, err))
+- else:
+- module.fail_json(msg='failure %d during daemon-reload: %s' % (rc, err))
++ module.fail_json(msg='failure %d during daemon-reload: %s' % (rc, err))
+
+ # Run daemon-reexec
+ if module.params['daemon_reexec'] and not module.check_mode:
+ (rc, out, err) = module.run_command("%s daemon-reexec" % (systemctl))
+ if rc != 0:
+- if is_chroot(module) or os.environ.get('SYSTEMD_OFFLINE') == '1':
+- module.warn('daemon-reexec failed, but target is a chroot or systemd is offline. Continuing. Error was: %d / %s' % (rc, err))
+- else:
+- module.fail_json(msg='failure %d during daemon-reexec: %s' % (rc, err))
++ module.fail_json(msg='failure %d during daemon-reexec: %s' % (rc, err))
+
+ if unit:
+ found = False
+--- ansible-core-2.16.5.orig/lib/ansible/modules/sysvinit.py
++++ ansible-core-2.16.5/lib/ansible/modules/sysvinit.py
+@@ -26,8 +26,8 @@ options:
+ state:
+ choices: [ 'started', 'stopped', 'restarted', 'reloaded' ]
+ description:
+- - V(started)/V(stopped) are idempotent actions that will not run commands unless necessary.
+- Not all init scripts support V(restarted) nor V(reloaded) natively, so these will both trigger a stop and start as needed.
++ - C(started)/C(stopped) are idempotent actions that will not run commands unless necessary.
++ Not all init scripts support C(restarted) nor C(reloaded) natively, so these will both trigger a stop and start as needed.
+ type: str
+ enabled:
+ type: bool
+@@ -36,7 +36,7 @@ options:
+ sleep:
+ default: 1
+ description:
+- - If the service is being V(restarted) or V(reloaded) then sleep this many seconds between the stop and start command.
++ - If the service is being C(restarted) or C(reloaded) then sleep this many seconds between the stop and start command.
+ This helps to workaround badly behaving services.
+ type: int
+ pattern:
+@@ -102,29 +102,24 @@ results:
+ description: results from actions taken
+ returned: always
+ type: complex
+- contains:
+- name:
+- description: Name of the service
+- type: str
+- returned: always
+- sample: "apache2"
+- status:
+- description: Status of the service
+- type: dict
+- returned: changed
+- sample: {
+- "enabled": {
+- "changed": true,
+- "rc": 0,
+- "stderr": "",
+- "stdout": ""
+- },
+- "stopped": {
+- "changed": true,
+- "rc": 0,
+- "stderr": "",
+- "stdout": "Stopping web server: apache2.\n"
+- }
++ sample: {
++ "attempts": 1,
++ "changed": true,
++ "name": "apache2",
++ "status": {
++ "enabled": {
++ "changed": true,
++ "rc": 0,
++ "stderr": "",
++ "stdout": ""
++ },
++ "stopped": {
++ "changed": true,
++ "rc": 0,
++ "stderr": "",
++ "stdout": "Stopping web server: apache2.\n"
++ }
++ }
+ }
+ '''
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/tempfile.py
++++ ansible-core-2.16.5/lib/ansible/modules/tempfile.py
+@@ -14,10 +14,9 @@ module: tempfile
+ version_added: "2.3"
+ short_description: Creates temporary files and directories
+ description:
+- - The M(ansible.builtin.tempfile) module creates temporary files and directories. C(mktemp) command
+- takes different parameters on various systems, this module helps to avoid troubles related to that.
+- Files/directories created by module are accessible only by creator. In case you need to make them
+- world-accessible you need to use M(ansible.builtin.file) module.
++ - The C(tempfile) module creates temporary files and directories. C(mktemp) command takes different parameters on various systems, this module helps
++ to avoid troubles related to that. Files/directories created by module are accessible only by creator. In case you need to make them world-accessible
++ you need to use M(ansible.builtin.file) module.
+ - For Windows targets, use the M(ansible.windows.win_tempfile) module instead.
+ options:
+ state:
+@@ -88,7 +87,7 @@ from tempfile import mkstemp, mkdtemp
+ from traceback import format_exc
+
+ from ansible.module_utils.basic import AnsibleModule
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+
+ def main():
+--- ansible-core-2.16.5.orig/lib/ansible/modules/template.py
++++ ansible-core-2.16.5/lib/ansible/modules/template.py
+@@ -18,17 +18,16 @@ options:
+ follow:
+ description:
+ - Determine whether symbolic links should be followed.
+- - When set to V(true) symbolic links will be followed, if they exist.
+- - When set to V(false) symbolic links will not be followed.
+- - Previous to Ansible 2.4, this was hardcoded as V(true).
++ - When set to C(true) symbolic links will be followed, if they exist.
++ - When set to C(false) symbolic links will not be followed.
++ - Previous to Ansible 2.4, this was hardcoded as C(true).
+ type: bool
+ default: no
+ version_added: '2.4'
+ notes:
+-- For Windows you can use M(ansible.windows.win_template) which uses V(\\r\\n) as O(newline_sequence) by default.
+-- The C(jinja2_native) setting has no effect. Native types are never used in the M(ansible.builtin.template) module
+- which is by design used for generating text files. For working with templates and utilizing Jinja2 native types see
+- the O(ansible.builtin.template#lookup:jinja2_native) parameter of the P(ansible.builtin.template#lookup) lookup.
++- For Windows you can use M(ansible.windows.win_template) which uses C(\r\n) as C(newline_sequence) by default.
++- The C(jinja2_native) setting has no effect. Native types are never used in the C(template) module which is by design used for generating text files.
++ For working with templates and utilizing Jinja2 native types see the C(jinja2_native) parameter of the C(template lookup).
+ seealso:
+ - module: ansible.builtin.copy
+ - module: ansible.windows.win_copy
+@@ -110,56 +109,3 @@ EXAMPLES = r'''
+ validate: /usr/sbin/sshd -t -f %s
+ backup: yes
+ '''
+-
+-RETURN = r'''
+-dest:
+- description: Destination file/path, equal to the value passed to I(dest).
+- returned: success
+- type: str
+- sample: /path/to/file.txt
+-checksum:
+- description: SHA1 checksum of the rendered file
+- returned: always
+- type: str
+- sample: 373296322247ab85d26d5d1257772757e7afd172
+-uid:
+- description: Numeric id representing the file owner
+- returned: success
+- type: int
+- sample: 1003
+-gid:
+- description: Numeric id representing the group of the owner
+- returned: success
+- type: int
+- sample: 1003
+-owner:
+- description: User name of owner
+- returned: success
+- type: str
+- sample: httpd
+-group:
+- description: Group name of owner
+- returned: success
+- type: str
+- sample: www-data
+-md5sum:
+- description: MD5 checksum of the rendered file
+- returned: changed
+- type: str
+- sample: d41d8cd98f00b204e9800998ecf8427e
+-mode:
+- description: Unix permissions of the file in octal representation as a string
+- returned: success
+- type: str
+- sample: 1755
+-size:
+- description: Size of the rendered file in bytes
+- returned: success
+- type: int
+- sample: 42
+-src:
+- description: Source file used for the copy on the target machine.
+- returned: changed
+- type: str
+- sample: /home/httpd/.ansible/tmp/ansible-tmp-1423796390.97-147729857856000/source
+-'''
+--- ansible-core-2.16.5.orig/lib/ansible/modules/unarchive.py
++++ ansible-core-2.16.5/lib/ansible/modules/unarchive.py
+@@ -17,17 +17,17 @@ module: unarchive
+ version_added: '1.4'
+ short_description: Unpacks an archive after (optionally) copying it from the local machine
+ description:
+- - The M(ansible.builtin.unarchive) module unpacks an archive. It will not unpack a compressed file that does not contain an archive.
++ - The C(unarchive) module unpacks an archive. It will not unpack a compressed file that does not contain an archive.
+ - By default, it will copy the source file from the local system to the target before unpacking.
+- - Set O(remote_src=yes) to unpack an archive which already exists on the target.
+- - If checksum validation is desired, use M(ansible.builtin.get_url) or M(ansible.builtin.uri) instead to fetch the file and set O(remote_src=yes).
++ - Set C(remote_src=yes) to unpack an archive which already exists on the target.
++ - If checksum validation is desired, use M(ansible.builtin.get_url) or M(ansible.builtin.uri) instead to fetch the file and set C(remote_src=yes).
+ - For Windows targets, use the M(community.windows.win_unzip) module instead.
+ options:
+ src:
+ description:
+- - If O(remote_src=no) (default), local path to archive file to copy to the target server; can be absolute or relative. If O(remote_src=yes), path on the
++ - If C(remote_src=no) (default), local path to archive file to copy to the target server; can be absolute or relative. If C(remote_src=yes), path on the
+ target server to existing archive file to unpack.
+- - If O(remote_src=yes) and O(src) contains V(://), the remote machine will download the file from the URL first. (version_added 2.0). This is only for
++ - If C(remote_src=yes) and C(src) contains C(://), the remote machine will download the file from the URL first. (version_added 2.0). This is only for
+ simple cases, for full download support use the M(ansible.builtin.get_url) module.
+ type: path
+ required: true
+@@ -40,14 +40,14 @@ options:
+ copy:
+ description:
+ - If true, the file is copied from local controller to the managed (remote) node, otherwise, the plugin will look for src archive on the managed machine.
+- - This option has been deprecated in favor of O(remote_src).
+- - This option is mutually exclusive with O(remote_src).
++ - This option has been deprecated in favor of C(remote_src).
++ - This option is mutually exclusive with C(remote_src).
+ type: bool
+ default: yes
+ creates:
+ description:
+ - If the specified absolute path (file or directory) already exists, this step will B(not) be run.
+- - The specified absolute path (file or directory) must be below the base path given with O(dest).
++ - The specified absolute path (file or directory) must be below the base path given with C(dest:).
+ type: path
+ version_added: "1.6"
+ io_buffer_size:
+@@ -65,16 +65,16 @@ options:
+ exclude:
+ description:
+ - List the directory and file entries that you would like to exclude from the unarchive action.
+- - Mutually exclusive with O(include).
++ - Mutually exclusive with C(include).
+ type: list
+ default: []
+ elements: str
+ version_added: "2.1"
+ include:
+ description:
+- - List of directory and file entries that you would like to extract from the archive. If O(include)
++ - List of directory and file entries that you would like to extract from the archive. If C(include)
+ is not empty, only files listed here will be extracted.
+- - Mutually exclusive with O(exclude).
++ - Mutually exclusive with C(exclude).
+ type: list
+ default: []
+ elements: str
+@@ -92,20 +92,20 @@ options:
+ - Command-line options with multiple elements must use multiple lines in the array, one for each element.
+ type: list
+ elements: str
+- default: []
++ default: ""
+ version_added: "2.1"
+ remote_src:
+ description:
+- - Set to V(true) to indicate the archived file is already on the remote system and not local to the Ansible controller.
+- - This option is mutually exclusive with O(copy).
++ - Set to C(true) to indicate the archived file is already on the remote system and not local to the Ansible controller.
++ - This option is mutually exclusive with C(copy).
+ type: bool
+ default: no
+ version_added: "2.2"
+ validate_certs:
+ description:
+ - This only applies if using a https URL as the source of the file.
+- - This should only set to V(false) used on personally controlled sites using self-signed certificate.
+- - Prior to 2.2 the code worked as if this was set to V(true).
++ - This should only set to C(false) used on personally controlled sites using self-signed certificate.
++ - Prior to 2.2 the code worked as if this was set to C(true).
+ type: bool
+ default: yes
+ version_added: "2.2"
+@@ -188,7 +188,7 @@ dest:
+ sample: /opt/software
+ files:
+ description: List of all the files in the archive.
+- returned: When O(list_files) is V(True)
++ returned: When I(list_files) is True
+ type: list
+ sample: '["file1", "file2"]'
+ gid:
+@@ -224,7 +224,7 @@ size:
+ src:
+ description:
+ - The source archive's path.
+- - If O(src) was a remote web URL, or from the local ansible controller, this shows the temporary location where the download was stored.
++ - If I(src) was a remote web URL, or from the local ansible controller, this shows the temporary location where the download was stored.
+ returned: always
+ type: str
+ sample: "/home/paul/test.tar.gz"
+@@ -253,9 +253,9 @@ import stat
+ import time
+ import traceback
+ from functools import partial
+-from zipfile import ZipFile
++from zipfile import ZipFile, BadZipfile
+
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.common.process import get_bin_path
+ from ansible.module_utils.common.locale import get_best_parsable_locale
+@@ -266,11 +266,6 @@ try: # python 3.3+
+ except ImportError: # older python
+ from pipes import quote
+
+-try: # python 3.2+
+- from zipfile import BadZipFile # type: ignore[attr-defined]
+-except ImportError: # older python
+- from zipfile import BadZipfile as BadZipFile
+-
+ # String from tar that shows the tar contents are different from the
+ # filesystem
+ OWNER_DIFF_RE = re.compile(r': Uid differs$')
+@@ -342,7 +337,6 @@ class ZipArchive(object):
+ def _legacy_file_list(self):
+ rc, out, err = self.module.run_command([self.cmd_path, '-v', self.src])
+ if rc:
+- self.module.debug(err)
+ raise UnarchiveError('Neither python zipfile nor unzip can read %s' % self.src)
+
+ for line in out.splitlines()[3:-2]:
+@@ -356,7 +350,7 @@ class ZipArchive(object):
+
+ try:
+ archive = ZipFile(self.src)
+- except BadZipFile as e:
++ except BadZipfile as e:
+ if e.args[0].lower().startswith('bad magic number'):
+ # Python2.4 can't handle zipfiles with > 64K files. Try using
+ # /usr/bin/unzip instead
+@@ -381,7 +375,7 @@ class ZipArchive(object):
+ self._files_in_archive = []
+ try:
+ archive = ZipFile(self.src)
+- except BadZipFile as e:
++ except BadZipfile as e:
+ if e.args[0].lower().startswith('bad magic number'):
+ # Python2.4 can't handle zipfiles with > 64K files. Try using
+ # /usr/bin/unzip instead
+@@ -423,7 +417,6 @@ class ZipArchive(object):
+ if self.include_files:
+ cmd.extend(self.include_files)
+ rc, out, err = self.module.run_command(cmd)
+- self.module.debug(err)
+
+ old_out = out
+ diff = ''
+@@ -752,9 +745,6 @@ class ZipArchive(object):
+ rc, out, err = self.module.run_command(cmd)
+ if rc == 0:
+ return True, None
+-
+- self.module.debug(err)
+-
+ return False, 'Command "%s" could not handle archive: %s' % (self.cmd_path, err)
+
+
+@@ -804,7 +794,6 @@ class TgzArchive(object):
+ locale = get_best_parsable_locale(self.module)
+ rc, out, err = self.module.run_command(cmd, cwd=self.b_dest, environ_update=dict(LANG=locale, LC_ALL=locale, LC_MESSAGES=locale, LANGUAGE=locale))
+ if rc != 0:
+- self.module.debug(err)
+ raise UnarchiveError('Unable to list files in the archive: %s' % err)
+
+ for filename in out.splitlines():
+@@ -1033,12 +1022,7 @@ def main():
+
+ src = module.params['src']
+ dest = module.params['dest']
+- abs_dest = os.path.abspath(dest)
+- b_dest = to_bytes(abs_dest, errors='surrogate_or_strict')
+-
+- if not os.path.isabs(dest):
+- module.warn("Relative destination path '{dest}' was resolved to absolute path '{abs_dest}'.".format(dest=dest, abs_dest=abs_dest))
+-
++ b_dest = to_bytes(dest, errors='surrogate_or_strict')
+ remote_src = module.params['remote_src']
+ file_args = module.load_file_common_arguments(module.params)
+
+@@ -1054,9 +1038,6 @@ def main():
+ if not os.access(src, os.R_OK):
+ module.fail_json(msg="Source '%s' not readable" % src)
+
+- # ensure src is an absolute path before picking handlers
+- src = os.path.abspath(src)
+-
+ # skip working with 0 size archives
+ try:
+ if os.path.getsize(src) == 0:
+--- ansible-core-2.16.5.orig/lib/ansible/modules/uri.py
++++ ansible-core-2.16.5/lib/ansible/modules/uri.py
+@@ -20,7 +20,7 @@ options:
+ ciphers:
+ description:
+ - SSL/TLS Ciphers to use for the request.
+- - 'When a list is provided, all ciphers are joined in order with V(:)'
++ - 'When a list is provided, all ciphers are joined in order with C(:)'
+ - See the L(OpenSSL Cipher List Format,https://www.openssl.org/docs/manmaster/man1/openssl-ciphers.html#CIPHER-LIST-FORMAT)
+ for more details.
+ - The available ciphers is dependent on the Python and OpenSSL/LibreSSL versions
+@@ -40,7 +40,7 @@ options:
+ required: true
+ dest:
+ description:
+- - A path of where to download the file to (if desired). If O(dest) is a
++ - A path of where to download the file to (if desired). If I(dest) is a
+ directory, the basename of the file on the remote server will be used.
+ type: path
+ url_username:
+@@ -55,23 +55,23 @@ options:
+ aliases: [ password ]
+ body:
+ description:
+- - The body of the http request/response to the web service. If O(body_format) is set
+- to V(json) it will take an already formatted JSON string or convert a data structure
++ - The body of the http request/response to the web service. If C(body_format) is set
++ to 'json' it will take an already formatted JSON string or convert a data structure
+ into JSON.
+- - If O(body_format) is set to V(form-urlencoded) it will convert a dictionary
++ - If C(body_format) is set to 'form-urlencoded' it will convert a dictionary
+ or list of tuples into an 'application/x-www-form-urlencoded' string. (Added in v2.7)
+- - If O(body_format) is set to V(form-multipart) it will convert a dictionary
++ - If C(body_format) is set to 'form-multipart' it will convert a dictionary
+ into 'multipart/form-multipart' body. (Added in v2.10)
+ type: raw
+ body_format:
+ description:
+- - The serialization format of the body. When set to V(json), V(form-multipart), or V(form-urlencoded), encodes
++ - The serialization format of the body. When set to C(json), C(form-multipart), or C(form-urlencoded), encodes
+ the body argument, if needed, and automatically sets the Content-Type header accordingly.
+ - As of v2.3 it is possible to override the C(Content-Type) header, when
+- set to V(json) or V(form-urlencoded) via the O(headers) option.
+- - The 'Content-Type' header cannot be overridden when using V(form-multipart)
+- - V(form-urlencoded) was added in v2.7.
+- - V(form-multipart) was added in v2.10.
++ set to C(json) or C(form-urlencoded) via the I(headers) option.
++ - The 'Content-Type' header cannot be overridden when using C(form-multipart)
++ - C(form-urlencoded) was added in v2.7.
++ - C(form-multipart) was added in v2.10.
+ type: str
+ choices: [ form-urlencoded, json, raw, form-multipart ]
+ default: raw
+@@ -88,15 +88,15 @@ options:
+ - Whether or not to return the body of the response as a "content" key in
+ the dictionary result no matter it succeeded or failed.
+ - Independently of this option, if the reported Content-type is "application/json", then the JSON is
+- always loaded into a key called RV(ignore:json) in the dictionary results.
++ always loaded into a key called C(json) in the dictionary results.
+ type: bool
+ default: no
+ force_basic_auth:
+ description:
+ - Force the sending of the Basic authentication header upon initial request.
+- - When this setting is V(false), this module will first try an unauthenticated request, and when the server replies
++ - When this setting is C(false), this module will first try an unauthenticated request, and when the server replies
+ with an C(HTTP 401) error, it will submit the Basic authentication header.
+- - When this setting is V(true), this module will immediately send a Basic authentication header on the first
++ - When this setting is C(true), this module will immediately send a Basic authentication header on the first
+ request.
+ - "Use this setting in any of the following scenarios:"
+ - You know the webservice endpoint always requires HTTP Basic authentication, and you want to speed up your
+@@ -108,11 +108,11 @@ options:
+ default: no
+ follow_redirects:
+ description:
+- - Whether or not the URI module should follow redirects. V(all) will follow all redirects.
+- V(safe) will follow only "safe" redirects, where "safe" means that the client is only
+- doing a GET or HEAD on the URI to which it is being redirected. V(none) will not follow
+- any redirects. Note that V(true) and V(false) choices are accepted for backwards compatibility,
+- where V(true) is the equivalent of V(all) and V(false) is the equivalent of V(safe). V(true) and V(false)
++ - Whether or not the URI module should follow redirects. C(all) will follow all redirects.
++ C(safe) will follow only "safe" redirects, where "safe" means that the client is only
++ doing a GET or HEAD on the URI to which it is being redirected. C(none) will not follow
++ any redirects. Note that C(true) and C(false) choices are accepted for backwards compatibility,
++ where C(true) is the equivalent of C(all) and C(false) is the equivalent of C(safe). C(true) and C(false)
+ are deprecated and will be removed in some future version of Ansible.
+ type: str
+ choices: ['all', 'no', 'none', 'safe', 'urllib2', 'yes']
+@@ -139,29 +139,28 @@ options:
+ headers:
+ description:
+ - Add custom HTTP headers to a request in the format of a YAML hash. As
+- of Ansible 2.3 supplying C(Content-Type) here will override the header
+- generated by supplying V(json) or V(form-urlencoded) for O(body_format).
++ of C(2.3) supplying C(Content-Type) here will override the header
++ generated by supplying C(json) or C(form-urlencoded) for I(body_format).
+ type: dict
+- default: {}
+ version_added: '2.1'
+ validate_certs:
+ description:
+- - If V(false), SSL certificates will not be validated.
+- - This should only set to V(false) used on personally controlled sites using self-signed certificates.
+- - Prior to 1.9.2 the code defaulted to V(false).
++ - If C(false), SSL certificates will not be validated.
++ - This should only set to C(false) used on personally controlled sites using self-signed certificates.
++ - Prior to 1.9.2 the code defaulted to C(false).
+ type: bool
+ default: true
+ version_added: '1.9.2'
+ client_cert:
+ description:
+ - PEM formatted certificate chain file to be used for SSL client authentication.
+- - This file can also include the key as well, and if the key is included, O(client_key) is not required
++ - This file can also include the key as well, and if the key is included, I(client_key) is not required
+ type: path
+ version_added: '2.4'
+ client_key:
+ description:
+ - PEM formatted file that contains your private key to be used for SSL client authentication.
+- - If O(client_cert) contains both the certificate and key, this option is not required.
++ - If I(client_cert) contains both the certificate and key, this option is not required.
+ type: path
+ version_added: '2.4'
+ ca_path:
+@@ -172,25 +171,25 @@ options:
+ src:
+ description:
+ - Path to file to be submitted to the remote server.
+- - Cannot be used with O(body).
+- - Should be used with O(force_basic_auth) to ensure success when the remote end sends a 401.
++ - Cannot be used with I(body).
++ - Should be used with I(force_basic_auth) to ensure success when the remote end sends a 401.
+ type: path
+ version_added: '2.7'
+ remote_src:
+ description:
+- - If V(false), the module will search for the O(src) on the controller node.
+- - If V(true), the module will search for the O(src) on the managed (remote) node.
++ - If C(false), the module will search for the C(src) on the controller node.
++ - If C(true), the module will search for the C(src) on the managed (remote) node.
+ type: bool
+ default: no
+ version_added: '2.7'
+ force:
+ description:
+- - If V(true) do not get a cached copy.
++ - If C(true) do not get a cached copy.
+ type: bool
+ default: no
+ use_proxy:
+ description:
+- - If V(false), it will not use a proxy, even if one is defined in an environment variable on the target hosts.
++ - If C(false), it will not use a proxy, even if one is defined in an environment variable on the target hosts.
+ type: bool
+ default: true
+ unix_socket:
+@@ -217,9 +216,9 @@ options:
+ - Use GSSAPI to perform the authentication, typically this is for Kerberos or Kerberos through Negotiate
+ authentication.
+ - Requires the Python library L(gssapi,https://github.com/pythongssapi/python-gssapi) to be installed.
+- - Credentials for GSSAPI can be specified with O(url_username)/O(url_password) or with the GSSAPI env var
++ - Credentials for GSSAPI can be specified with I(url_username)/I(url_password) or with the GSSAPI env var
+ C(KRB5CCNAME) that specified a custom Kerberos credential cache.
+- - NTLM authentication is B(not) supported even if the GSSAPI mech for NTLM has been installed.
++ - NTLM authentication is C(not) supported even if the GSSAPI mech for NTLM has been installed.
+ type: bool
+ default: no
+ version_added: '2.11'
+@@ -257,12 +256,12 @@ EXAMPLES = r'''
+ ansible.builtin.uri:
+ url: http://www.example.com
+
+-- name: Check that a page returns successfully but fail if the word AWESOME is not in the page contents
++- name: Check that a page returns a status 200 and fail if the word AWESOME is not in the page contents
+ ansible.builtin.uri:
+ url: http://www.example.com
+ return_content: true
+ register: this
+- failed_when: this is failed or "'AWESOME' not in this.content"
++ failed_when: "'AWESOME' not in this.content"
+
+ - name: Create a JIRA issue
+ ansible.builtin.uri:
+@@ -440,6 +439,7 @@ url:
+ sample: https://www.ansible.com/
+ '''
+
++import datetime
+ import json
+ import os
+ import re
+@@ -450,9 +450,8 @@ import tempfile
+ from ansible.module_utils.basic import AnsibleModule, sanitize_keys
+ from ansible.module_utils.six import PY2, PY3, binary_type, iteritems, string_types
+ from ansible.module_utils.six.moves.urllib.parse import urlencode, urlsplit
+-from ansible.module_utils.common.text.converters import to_native, to_text
+-from ansible.module_utils.compat.datetime import utcnow, utcfromtimestamp
+-from ansible.module_utils.six.moves.collections_abc import Mapping, Sequence
++from ansible.module_utils._text import to_native, to_text
++from ansible.module_utils.common._collections_compat import Mapping, Sequence
+ from ansible.module_utils.urls import fetch_url, get_response_filename, parse_content_type, prepare_multipart, url_argument_spec
+
+ JSON_CANDIDATES = {'json', 'javascript'}
+@@ -580,7 +579,7 @@ def uri(module, url, dest, body, body_fo
+ kwargs = {}
+ if dest is not None and os.path.isfile(dest):
+ # if destination file already exist, only download if file newer
+- kwargs['last_mod_time'] = utcfromtimestamp(os.path.getmtime(dest))
++ kwargs['last_mod_time'] = datetime.datetime.utcfromtimestamp(os.path.getmtime(dest))
+
+ resp, info = fetch_url(module, url, data=data, headers=headers,
+ method=method, timeout=socket_timeout, unix_socket=module.params['unix_socket'],
+@@ -686,12 +685,12 @@ def main():
+ module.exit_json(stdout="skipped, since '%s' does not exist" % removes, changed=False)
+
+ # Make the request
+- start = utcnow()
++ start = datetime.datetime.utcnow()
+ r, info = uri(module, url, dest, body, body_format, method,
+ dict_headers, socket_timeout, ca_path, unredirected_headers,
+ decompress, ciphers, use_netrc)
+
+- elapsed = (utcnow() - start).seconds
++ elapsed = (datetime.datetime.utcnow() - start).seconds
+
+ if r and dest is not None and os.path.isdir(dest):
+ filename = get_response_filename(r) or 'index.html'
+--- ansible-core-2.16.5.orig/lib/ansible/modules/user.py
++++ ansible-core-2.16.5/lib/ansible/modules/user.py
+@@ -28,12 +28,11 @@ options:
+ comment:
+ description:
+ - Optionally sets the description (aka I(GECOS)) of user account.
+- - On macOS, this defaults to the O(name) option.
+ type: str
+ hidden:
+ description:
+ - macOS only, optionally hide the user from the login window and system preferences.
+- - The default will be V(true) if the O(system) option is used.
++ - The default will be C(true) if the I(system) option is used.
+ type: bool
+ version_added: "2.6"
+ non_unique:
+@@ -50,29 +49,28 @@ options:
+ group:
+ description:
+ - Optionally sets the user's primary group (takes a group name).
+- - On macOS, this defaults to V('staff')
+ type: str
+ groups:
+ description:
+- - A list of supplementary groups which the user is also a member of.
+- - By default, the user is removed from all other groups. Configure O(append) to modify this.
+- - When set to an empty string V(''),
++ - List of groups user will be added to.
++ - By default, the user is removed from all other groups. Configure C(append) to modify this.
++ - When set to an empty string C(''),
+ the user is removed from all groups except the primary group.
+ - Before Ansible 2.3, the only input format allowed was a comma separated string.
+ type: list
+ elements: str
+ append:
+ description:
+- - If V(true), add the user to the groups specified in O(groups).
+- - If V(false), user will only be added to the groups specified in O(groups),
++ - If C(true), add the user to the groups specified in C(groups).
++ - If C(false), user will only be added to the groups specified in C(groups),
+ removing them from all other groups.
+ type: bool
+ default: no
+ shell:
+ description:
+ - Optionally set the user's shell.
+- - On macOS, before Ansible 2.5, the default shell for non-system users was V(/usr/bin/false).
+- Since Ansible 2.5, the default shell for non-system users on macOS is V(/bin/bash).
++ - On macOS, before Ansible 2.5, the default shell for non-system users was C(/usr/bin/false).
++ Since Ansible 2.5, the default shell for non-system users on macOS is C(/bin/bash).
+ - See notes for details on how other operating systems determine the default shell by
+ the underlying tool.
+ type: str
+@@ -83,7 +81,7 @@ options:
+ skeleton:
+ description:
+ - Optionally set a home skeleton directory.
+- - Requires O(create_home) option!
++ - Requires C(create_home) option!
+ type: str
+ version_added: "2.0"
+ password:
+@@ -92,51 +90,46 @@ options:
+ - B(Linux/Unix/POSIX:) Enter the hashed password as the value.
+ - See L(FAQ entry,https://docs.ansible.com/ansible/latest/reference_appendices/faq.html#how-do-i-generate-encrypted-passwords-for-the-user-module)
+ for details on various ways to generate the hash of a password.
+- - To create an account with a locked/disabled password on Linux systems, set this to V('!') or V('*').
+- - To create an account with a locked/disabled password on OpenBSD, set this to V('*************').
++ - To create an account with a locked/disabled password on Linux systems, set this to C('!') or C('*').
++ - To create an account with a locked/disabled password on OpenBSD, set this to C('*************').
+ - B(OS X/macOS:) Enter the cleartext password as the value. Be sure to take relevant security precautions.
+- - On macOS, the password specified in the C(password) option will always be set, regardless of whether the user account already exists or not.
+- - When the password is passed as an argument, the C(user) module will always return changed to C(true) for macOS systems.
+- Since macOS no longer provides access to the hashed passwords directly.
+ type: str
+ state:
+ description:
+ - Whether the account should exist or not, taking action if the state is different from what is stated.
+- - See this L(FAQ entry,https://docs.ansible.com/ansible/latest/reference_appendices/faq.html#running-on-macos-as-a-target)
+- for additional requirements when removing users on macOS systems.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ create_home:
+ description:
+- - Unless set to V(false), a home directory will be made for the user
++ - Unless set to C(false), a home directory will be made for the user
+ when the account is created or if the home directory does not exist.
+- - Changed from O(createhome) to O(create_home) in Ansible 2.5.
++ - Changed from C(createhome) to C(create_home) in Ansible 2.5.
+ type: bool
+ default: yes
+ aliases: [ createhome ]
+ move_home:
+ description:
+- - "If set to V(true) when used with O(home), attempt to move the user's old home
++ - "If set to C(true) when used with C(home: ), attempt to move the user's old home
+ directory to the specified directory if it isn't there already and the old home exists."
+ type: bool
+ default: no
+ system:
+ description:
+- - When creating an account O(state=present), setting this to V(true) makes the user a system account.
++ - When creating an account C(state=present), setting this to C(true) makes the user a system account.
+ - This setting cannot be changed on existing users.
+ type: bool
+ default: no
+ force:
+ description:
+- - This only affects O(state=absent), it forces removal of the user and associated directories on supported platforms.
++ - This only affects C(state=absent), it forces removal of the user and associated directories on supported platforms.
+ - The behavior is the same as C(userdel --force), check the man page for C(userdel) on your system for details and support.
+- - When used with O(generate_ssh_key=yes) this forces an existing key to be overwritten.
++ - When used with C(generate_ssh_key=yes) this forces an existing key to be overwritten.
+ type: bool
+ default: no
+ remove:
+ description:
+- - This only affects O(state=absent), it attempts to remove directories associated with the user.
++ - This only affects C(state=absent), it attempts to remove directories associated with the user.
+ - The behavior is the same as C(userdel --remove), check the man page for details and support.
+ type: bool
+ default: no
+@@ -147,7 +140,7 @@ options:
+ generate_ssh_key:
+ description:
+ - Whether to generate a SSH key for the user in question.
+- - This will B(not) overwrite an existing SSH key unless used with O(force=yes).
++ - This will B(not) overwrite an existing SSH key unless used with C(force=yes).
+ type: bool
+ default: no
+ version_added: "0.9"
+@@ -169,7 +162,7 @@ options:
+ description:
+ - Optionally specify the SSH key filename.
+ - If this is a relative filename then it will be relative to the user's home directory.
+- - This parameter defaults to V(.ssh/id_rsa).
++ - This parameter defaults to I(.ssh/id_rsa).
+ type: path
+ version_added: "0.9"
+ ssh_key_comment:
+@@ -186,8 +179,8 @@ options:
+ version_added: "0.9"
+ update_password:
+ description:
+- - V(always) will update passwords if they differ.
+- - V(on_create) will only set the password for newly created users.
++ - C(always) will update passwords if they differ.
++ - C(on_create) will only set the password for newly created users.
+ type: str
+ choices: [ always, on_create ]
+ default: always
+@@ -205,7 +198,7 @@ options:
+ - Lock the password (C(usermod -L), C(usermod -U), C(pw lock)).
+ - Implementation differs by platform. This option does not always mean the user cannot login using other methods.
+ - This option does not disable the user, only lock the password.
+- - This must be set to V(False) in order to unlock a currently locked password. The absence of this parameter will not unlock a password.
++ - This must be set to C(False) in order to unlock a currently locked password. The absence of this parameter will not unlock a password.
+ - Currently supported on Linux, FreeBSD, DragonFlyBSD, NetBSD, OpenBSD.
+ type: bool
+ version_added: "2.6"
+@@ -223,25 +216,28 @@ options:
+ profile:
+ description:
+ - Sets the profile of the user.
++ - Does nothing when used with other platforms.
+ - Can set multiple profiles using comma separation.
+- - To delete all the profiles, use O(profile='').
+- - Currently supported on Illumos/Solaris. Does nothing when used with other platforms.
++ - To delete all the profiles, use C(profile='').
++ - Currently supported on Illumos/Solaris.
+ type: str
+ version_added: "2.8"
+ authorization:
+ description:
+ - Sets the authorization of the user.
++ - Does nothing when used with other platforms.
+ - Can set multiple authorizations using comma separation.
+- - To delete all authorizations, use O(authorization='').
+- - Currently supported on Illumos/Solaris. Does nothing when used with other platforms.
++ - To delete all authorizations, use C(authorization='').
++ - Currently supported on Illumos/Solaris.
+ type: str
+ version_added: "2.8"
+ role:
+ description:
+ - Sets the role of the user.
++ - Does nothing when used with other platforms.
+ - Can set multiple roles using comma separation.
+- - To delete all roles, use O(role='').
+- - Currently supported on Illumos/Solaris. Does nothing when used with other platforms.
++ - To delete all roles, use C(role='').
++ - Currently supported on Illumos/Solaris.
+ type: str
+ version_added: "2.8"
+ password_expire_max:
+@@ -256,17 +252,12 @@ options:
+ - Supported on Linux only.
+ type: int
+ version_added: "2.11"
+- password_expire_warn:
+- description:
+- - Number of days of warning before password expires.
+- - Supported on Linux only.
+- type: int
+- version_added: "2.16"
+ umask:
+ description:
+ - Sets the umask of the user.
+- - Currently supported on Linux. Does nothing when used with other platforms.
+- - Requires O(local) is omitted or V(False).
++ - Does nothing when used with other platforms.
++ - Currently supported on Linux.
++ - Requires C(local) is omitted or False.
+ type: str
+ version_added: "2.12"
+ extends_documentation_fragment: action_common_attributes
+@@ -347,17 +338,12 @@ EXAMPLES = r'''
+ ansible.builtin.user:
+ name: pushkar15
+ password_expire_min: 5
+-
+-- name: Set number of warning days for password expiration
+- ansible.builtin.user:
+- name: jane157
+- password_expire_warn: 30
+ '''
+
+ RETURN = r'''
+ append:
+ description: Whether or not to append the user to groups.
+- returned: When O(state) is V(present) and the user exists
++ returned: When state is C(present) and the user exists
+ type: bool
+ sample: True
+ comment:
+@@ -372,7 +358,7 @@ create_home:
+ sample: True
+ force:
+ description: Whether or not a user account was forcibly deleted.
+- returned: When O(state) is V(absent) and user exists
++ returned: When I(state) is C(absent) and user exists
+ type: bool
+ sample: False
+ group:
+@@ -382,17 +368,17 @@ group:
+ sample: 1001
+ groups:
+ description: List of groups of which the user is a member.
+- returned: When O(groups) is not empty and O(state) is V(present)
++ returned: When I(groups) is not empty and I(state) is C(present)
+ type: str
+ sample: 'chrony,apache'
+ home:
+ description: "Path to user's home directory."
+- returned: When O(state) is V(present)
++ returned: When I(state) is C(present)
+ type: str
+ sample: '/home/asmith'
+ move_home:
+ description: Whether or not to move an existing home directory.
+- returned: When O(state) is V(present) and user exists
++ returned: When I(state) is C(present) and user exists
+ type: bool
+ sample: False
+ name:
+@@ -402,32 +388,32 @@ name:
+ sample: asmith
+ password:
+ description: Masked value of the password.
+- returned: When O(state) is V(present) and O(password) is not empty
++ returned: When I(state) is C(present) and I(password) is not empty
+ type: str
+ sample: 'NOT_LOGGING_PASSWORD'
+ remove:
+ description: Whether or not to remove the user account.
+- returned: When O(state) is V(absent) and user exists
++ returned: When I(state) is C(absent) and user exists
+ type: bool
+ sample: True
+ shell:
+ description: User login shell.
+- returned: When O(state) is V(present)
++ returned: When I(state) is C(present)
+ type: str
+ sample: '/bin/bash'
+ ssh_fingerprint:
+ description: Fingerprint of generated SSH key.
+- returned: When O(generate_ssh_key) is V(True)
++ returned: When I(generate_ssh_key) is C(True)
+ type: str
+ sample: '2048 SHA256:aYNHYcyVm87Igh0IMEDMbvW0QDlRQfE0aJugp684ko8 ansible-generated on host (RSA)'
+ ssh_key_file:
+ description: Path to generated SSH private key file.
+- returned: When O(generate_ssh_key) is V(True)
++ returned: When I(generate_ssh_key) is C(True)
+ type: str
+ sample: /home/asmith/.ssh/id_rsa
+ ssh_public_key:
+ description: Generated SSH public key file.
+- returned: When O(generate_ssh_key) is V(True)
++ returned: When I(generate_ssh_key) is C(True)
+ type: str
+ sample: >
+ 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC95opt4SPEC06tOYsJQJIuN23BbLMGmYo8ysVZQc4h2DZE9ugbjWWGS1/pweUGjVstgzMkBEeBCByaEf/RJKNecKRPeGd2Bw9DCj/bn5Z6rGfNENKBmo
+@@ -445,18 +431,30 @@ stdout:
+ sample:
+ system:
+ description: Whether or not the account is a system account.
+- returned: When O(system) is passed to the module and the account does not exist
++ returned: When I(system) is passed to the module and the account does not exist
+ type: bool
+ sample: True
+ uid:
+ description: User ID of the user account.
+- returned: When O(uid) is passed to the module
++ returned: When I(uid) is passed to the module
+ type: int
+ sample: 1044
++password_expire_max:
++ description: Maximum number of days during which a password is valid.
++ returned: When user exists
++ type: int
++ sample: 20
++password_expire_min:
++ description: Minimum number of days between password change
++ returned: When user exists
++ type: int
++ sample: 20
+ '''
+
+
++import ctypes
+ import ctypes.util
++import errno
+ import grp
+ import calendar
+ import os
+@@ -471,7 +469,7 @@ import time
+ import math
+
+ from ansible.module_utils import distro
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.common.locale import get_best_parsable_locale
+ from ansible.module_utils.common.sys_info import get_platform_subclass
+@@ -576,7 +574,6 @@ class User(object):
+ self.role = module.params['role']
+ self.password_expire_max = module.params['password_expire_max']
+ self.password_expire_min = module.params['password_expire_min']
+- self.password_expire_warn = module.params['password_expire_warn']
+ self.umask = module.params['umask']
+
+ if self.umask is not None and self.local:
+@@ -870,7 +867,7 @@ class User(object):
+ if current_groups and not self.append:
+ groups_need_mod = True
+ else:
+- groups = self.get_groups_set(remove_existing=False, names_only=True)
++ groups = self.get_groups_set(remove_existing=False)
+ group_diff = set(current_groups).symmetric_difference(groups)
+
+ if group_diff:
+@@ -916,8 +913,7 @@ class User(object):
+
+ if self.expires is not None:
+
+- current_expires = self.user_password()[1] or '0'
+- current_expires = int(current_expires)
++ current_expires = int(self.user_password()[1])
+
+ if self.expires < time.gmtime(0):
+ if current_expires >= 0:
+@@ -1012,22 +1008,16 @@ class User(object):
+ except (ValueError, KeyError):
+ return list(grp.getgrnam(group))
+
+- def get_groups_set(self, remove_existing=True, names_only=False):
++ def get_groups_set(self, remove_existing=True):
+ if self.groups is None:
+ return None
+ info = self.user_info()
+ groups = set(x.strip() for x in self.groups.split(',') if x)
+- group_names = set()
+ for g in groups.copy():
+ if not self.group_exists(g):
+ self.module.fail_json(msg="Group %s does not exist" % (g))
+- group_info = self.group_info(g)
+- if info and remove_existing and group_info[2] == info[3]:
++ if info and remove_existing and self.group_info(g)[2] == info[3]:
+ groups.remove(g)
+- elif names_only:
+- group_names.add(group_info[0])
+- if names_only:
+- return group_names
+ return groups
+
+ def user_group_membership(self, exclude_primary=True):
+@@ -1094,7 +1084,6 @@ class User(object):
+ def set_password_expire(self):
+ min_needs_change = self.password_expire_min is not None
+ max_needs_change = self.password_expire_max is not None
+- warn_needs_change = self.password_expire_warn is not None
+
+ if HAVE_SPWD:
+ try:
+@@ -1104,9 +1093,8 @@ class User(object):
+
+ min_needs_change &= self.password_expire_min != shadow_info.sp_min
+ max_needs_change &= self.password_expire_max != shadow_info.sp_max
+- warn_needs_change &= self.password_expire_warn != shadow_info.sp_warn
+
+- if not (min_needs_change or max_needs_change or warn_needs_change):
++ if not (min_needs_change or max_needs_change):
+ return (None, '', '') # target state already reached
+
+ command_name = 'chage'
+@@ -1115,8 +1103,6 @@ class User(object):
+ cmd.extend(["-m", self.password_expire_min])
+ if max_needs_change:
+ cmd.extend(["-M", self.password_expire_max])
+- if warn_needs_change:
+- cmd.extend(["-W", self.password_expire_warn])
+ cmd.append(self.name)
+
+ return self.execute_command(cmd)
+@@ -1291,7 +1277,7 @@ class User(object):
+ else:
+ skeleton = '/etc/skel'
+
+- if os.path.exists(skeleton) and skeleton != os.devnull:
++ if os.path.exists(skeleton):
+ try:
+ shutil.copytree(skeleton, path, symlinks=True)
+ except OSError as e:
+@@ -1537,7 +1523,7 @@ class FreeBsdUser(User):
+
+ if self.groups is not None:
+ current_groups = self.user_group_membership()
+- groups = self.get_groups_set(names_only=True)
++ groups = self.get_groups_set()
+
+ group_diff = set(current_groups).symmetric_difference(groups)
+ groups_need_mod = False
+@@ -1560,8 +1546,7 @@ class FreeBsdUser(User):
+
+ if self.expires is not None:
+
+- current_expires = self.user_password()[1] or '0'
+- current_expires = int(current_expires)
++ current_expires = int(self.user_password()[1])
+
+ # If expiration is negative or zero and the current expiration is greater than zero, disable expiration.
+ # In OpenBSD, setting expiration to zero disables expiration. It does not expire the account.
+@@ -1732,7 +1717,7 @@ class OpenBSDUser(User):
+ if current_groups and not self.append:
+ groups_need_mod = True
+ else:
+- groups = self.get_groups_set(names_only=True)
++ groups = self.get_groups_set()
+ group_diff = set(current_groups).symmetric_difference(groups)
+
+ if group_diff:
+@@ -1908,7 +1893,7 @@ class NetBSDUser(User):
+ if current_groups and not self.append:
+ groups_need_mod = True
+ else:
+- groups = self.get_groups_set(names_only=True)
++ groups = self.get_groups_set()
+ group_diff = set(current_groups).symmetric_difference(groups)
+
+ if group_diff:
+@@ -2142,7 +2127,7 @@ class SunOS(User):
+
+ if self.groups is not None:
+ current_groups = self.user_group_membership()
+- groups = self.get_groups_set(names_only=True)
++ groups = self.get_groups_set()
+ group_diff = set(current_groups).symmetric_difference(groups)
+ groups_need_mod = False
+
+@@ -2419,7 +2404,7 @@ class DarwinUser(User):
+
+ current = set(self._list_user_groups())
+ if self.groups is not None:
+- target = self.get_groups_set(names_only=True)
++ target = set(self.groups.split(','))
+ else:
+ target = set([])
+
+@@ -2513,14 +2498,6 @@ class DarwinUser(User):
+ if rc != 0:
+ self.module.fail_json(msg='Cannot create user "%s".' % self.name, err=err, out=out, rc=rc)
+
+- # Make the Gecos (alias display name) default to username
+- if self.comment is None:
+- self.comment = self.name
+-
+- # Make user group default to 'staff'
+- if self.group is None:
+- self.group = 'staff'
+-
+ self._make_group_numerical()
+ if self.uid is None:
+ self.uid = str(self._get_next_uid(self.system))
+@@ -2711,7 +2688,7 @@ class AIX(User):
+ if current_groups and not self.append:
+ groups_need_mod = True
+ else:
+- groups = self.get_groups_set(names_only=True)
++ groups = self.get_groups_set()
+ group_diff = set(current_groups).symmetric_difference(groups)
+
+ if group_diff:
+@@ -2909,7 +2886,7 @@ class HPUX(User):
+ if current_groups and not self.append:
+ groups_need_mod = True
+ else:
+- groups = self.get_groups_set(remove_existing=False, names_only=True)
++ groups = self.get_groups_set(remove_existing=False)
+ group_diff = set(current_groups).symmetric_difference(groups)
+
+ if group_diff:
+@@ -3119,7 +3096,6 @@ def main():
+ login_class=dict(type='str'),
+ password_expire_max=dict(type='int', no_log=False),
+ password_expire_min=dict(type='int', no_log=False),
+- password_expire_warn=dict(type='int', no_log=False),
+ # following options are specific to macOS
+ hidden=dict(type='bool'),
+ # following options are specific to selinux
+--- ansible-core-2.16.5.orig/lib/ansible/modules/validate_argument_spec.py
++++ ansible-core-2.16.5/lib/ansible/modules/validate_argument_spec.py
+@@ -17,7 +17,7 @@ version_added: "2.11"
+ options:
+ argument_spec:
+ description:
+- - A dictionary like AnsibleModule argument_spec. See R(argument spec definition,argument_spec)
++ - A dictionary like AnsibleModule argument_spec
+ required: true
+ provided_arguments:
+ description:
+@@ -69,7 +69,7 @@ EXAMPLES = r'''
+
+ - name: verify vars needed for this task file are present when included, with spec from a spec file
+ ansible.builtin.validate_argument_spec:
+- argument_spec: "{{(lookup('ansible.builtin.file', 'myargspec.yml') | from_yaml )['specname']['options']}}"
++ argument_spec: "{{lookup('ansible.builtin.file', 'myargspec.yml')['specname']['options']}}"
+
+
+ - name: verify vars needed for next include and not from inside it, also with params i'll only define there
+--- ansible-core-2.16.5.orig/lib/ansible/modules/wait_for.py
++++ ansible-core-2.16.5/lib/ansible/modules/wait_for.py
+@@ -12,7 +12,7 @@ DOCUMENTATION = r'''
+ module: wait_for
+ short_description: Waits for a condition before continuing
+ description:
+- - You can wait for a set amount of time O(timeout), this is the default if nothing is specified or just O(timeout) is specified.
++ - You can wait for a set amount of time C(timeout), this is the default if nothing is specified or just C(timeout) is specified.
+ This does not produce an error.
+ - Waiting for a port to become available is useful for when services are not immediately available after their init scripts return
+ which is true of certain Java application servers.
+@@ -49,7 +49,7 @@ options:
+ port:
+ description:
+ - Port number to poll.
+- - O(path) and O(port) are mutually exclusive parameters.
++ - C(path) and C(port) are mutually exclusive parameters.
+ type: int
+ active_connection_states:
+ description:
+@@ -60,17 +60,17 @@ options:
+ version_added: "2.3"
+ state:
+ description:
+- - Either V(present), V(started), or V(stopped), V(absent), or V(drained).
+- - When checking a port V(started) will ensure the port is open, V(stopped) will check that it is closed, V(drained) will check for active connections.
+- - When checking for a file or a search string V(present) or V(started) will ensure that the file or string is present before continuing,
+- V(absent) will check that file is absent or removed.
++ - Either C(present), C(started), or C(stopped), C(absent), or C(drained).
++ - When checking a port C(started) will ensure the port is open, C(stopped) will check that it is closed, C(drained) will check for active connections.
++ - When checking for a file or a search string C(present) or C(started) will ensure that the file or string is present before continuing,
++ C(absent) will check that file is absent or removed.
+ type: str
+ choices: [ absent, drained, present, started, stopped ]
+ default: started
+ path:
+ description:
+ - Path to a file on the filesystem that must exist before continuing.
+- - O(path) and O(port) are mutually exclusive parameters.
++ - C(path) and C(port) are mutually exclusive parameters.
+ type: path
+ version_added: "1.4"
+ search_regex:
+@@ -81,7 +81,7 @@ options:
+ version_added: "1.4"
+ exclude_hosts:
+ description:
+- - List of hosts or IPs to ignore when looking for active TCP connections for V(drained) state.
++ - List of hosts or IPs to ignore when looking for active TCP connections for C(drained) state.
+ type: list
+ elements: str
+ version_added: "1.8"
+@@ -100,7 +100,7 @@ options:
+ extends_documentation_fragment: action_common_attributes
+ attributes:
+ check_mode:
+- support: none
++ support: full
+ diff_mode:
+ support: none
+ platform:
+@@ -238,8 +238,7 @@ import traceback
+
+ from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+ from ansible.module_utils.common.sys_info import get_platform_subclass
+-from ansible.module_utils.common.text.converters import to_bytes, to_native
+-from ansible.module_utils.compat.datetime import utcnow
++from ansible.module_utils._text import to_bytes
+
+
+ HAS_PSUTIL = False
+@@ -533,7 +532,7 @@ def main():
+ except Exception:
+ module.fail_json(msg="unknown active_connection_state (%s) defined" % _connection_state, elapsed=0)
+
+- start = utcnow()
++ start = datetime.datetime.utcnow()
+
+ if delay:
+ time.sleep(delay)
+@@ -544,7 +543,7 @@ def main():
+ # first wait for the stop condition
+ end = start + datetime.timedelta(seconds=timeout)
+
+- while utcnow() < end:
++ while datetime.datetime.utcnow() < end:
+ if path:
+ try:
+ if not os.access(b_path, os.F_OK):
+@@ -561,7 +560,7 @@ def main():
+ # Conditions not yet met, wait and try again
+ time.sleep(module.params['sleep'])
+ else:
+- elapsed = utcnow() - start
++ elapsed = datetime.datetime.utcnow() - start
+ if port:
+ module.fail_json(msg=msg or "Timeout when waiting for %s:%s to stop." % (host, port), elapsed=elapsed.seconds)
+ elif path:
+@@ -570,14 +569,14 @@ def main():
+ elif state in ['started', 'present']:
+ # wait for start condition
+ end = start + datetime.timedelta(seconds=timeout)
+- while utcnow() < end:
++ while datetime.datetime.utcnow() < end:
+ if path:
+ try:
+ os.stat(b_path)
+ except OSError as e:
+ # If anything except file not present, throw an error
+ if e.errno != 2:
+- elapsed = utcnow() - start
++ elapsed = datetime.datetime.utcnow() - start
+ module.fail_json(msg=msg or "Failed to stat %s, %s" % (path, e.strerror), elapsed=elapsed.seconds)
+ # file doesn't exist yet, so continue
+ else:
+@@ -585,34 +584,21 @@ def main():
+ if not b_compiled_search_re:
+ # nope, succeed!
+ break
+-
+ try:
+ with open(b_path, 'rb') as f:
+- try:
+- with contextlib.closing(mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)) as mm:
+- search = b_compiled_search_re.search(mm)
+- if search:
+- if search.groupdict():
+- match_groupdict = search.groupdict()
+- if search.groups():
+- match_groups = search.groups()
+- break
+- except (ValueError, OSError) as e:
+- module.debug('wait_for failed to use mmap on "%s": %s. Falling back to file read().' % (path, to_native(e)))
+- # cannot mmap this file, try normal read
+- search = re.search(b_compiled_search_re, f.read())
++ with contextlib.closing(mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)) as mm:
++ search = b_compiled_search_re.search(mm)
+ if search:
+ if search.groupdict():
+ match_groupdict = search.groupdict()
+ if search.groups():
+ match_groups = search.groups()
++
+ break
+- except Exception as e:
+- module.warn('wait_for failed on "%s", unexpected exception(%s): %s.).' % (path, to_native(e.__class__), to_native(e)))
+ except IOError:
+ pass
+ elif port:
+- alt_connect_timeout = math.ceil(_timedelta_total_seconds(end - utcnow()))
++ alt_connect_timeout = math.ceil(_timedelta_total_seconds(end - datetime.datetime.utcnow()))
+ try:
+ s = socket.create_connection((host, port), min(connect_timeout, alt_connect_timeout))
+ except Exception:
+@@ -623,8 +609,8 @@ def main():
+ if b_compiled_search_re:
+ b_data = b''
+ matched = False
+- while utcnow() < end:
+- max_timeout = math.ceil(_timedelta_total_seconds(end - utcnow()))
++ while datetime.datetime.utcnow() < end:
++ max_timeout = math.ceil(_timedelta_total_seconds(end - datetime.datetime.utcnow()))
+ readable = select.select([s], [], [], max_timeout)[0]
+ if not readable:
+ # No new data. Probably means our timeout
+@@ -668,7 +654,7 @@ def main():
+
+ else: # while-else
+ # Timeout expired
+- elapsed = utcnow() - start
++ elapsed = datetime.datetime.utcnow() - start
+ if port:
+ if search_regex:
+ module.fail_json(msg=msg or "Timeout when waiting for search string %s in %s:%s" % (search_regex, host, port), elapsed=elapsed.seconds)
+@@ -684,17 +670,17 @@ def main():
+ # wait until all active connections are gone
+ end = start + datetime.timedelta(seconds=timeout)
+ tcpconns = TCPConnectionInfo(module)
+- while utcnow() < end:
++ while datetime.datetime.utcnow() < end:
+ if tcpconns.get_active_connections_count() == 0:
+ break
+
+ # Conditions not yet met, wait and try again
+ time.sleep(module.params['sleep'])
+ else:
+- elapsed = utcnow() - start
++ elapsed = datetime.datetime.utcnow() - start
+ module.fail_json(msg=msg or "Timeout when waiting for %s:%s to drain" % (host, port), elapsed=elapsed.seconds)
+
+- elapsed = utcnow() - start
++ elapsed = datetime.datetime.utcnow() - start
+ module.exit_json(state=state, port=port, search_regex=search_regex, match_groups=match_groups, match_groupdict=match_groupdict, path=path,
+ elapsed=elapsed.seconds)
+
+--- ansible-core-2.16.5.orig/lib/ansible/modules/wait_for_connection.py
++++ ansible-core-2.16.5/lib/ansible/modules/wait_for_connection.py
+@@ -12,9 +12,9 @@ DOCUMENTATION = r'''
+ module: wait_for_connection
+ short_description: Waits until remote system is reachable/usable
+ description:
+-- Waits for a total of O(timeout) seconds.
+-- Retries the transport connection after a timeout of O(connect_timeout).
+-- Tests the transport connection every O(sleep) seconds.
++- Waits for a total of C(timeout) seconds.
++- Retries the transport connection after a timeout of C(connect_timeout).
++- Tests the transport connection every C(sleep) seconds.
+ - This module makes use of internal ansible transport (and configuration) and the ping/win_ping module to guarantee correct end-to-end functioning.
+ - This module is also supported for Windows targets.
+ version_added: '2.3'
+@@ -101,7 +101,7 @@ EXAMPLES = r'''
+ customization:
+ hostname: '{{ vm_shortname }}'
+ runonce:
+- - cmd.exe /c winrm.cmd quickconfig -quiet -force
++ - powershell.exe -ExecutionPolicy Unrestricted -File C:\Windows\Temp\ConfigureRemotingForAnsible.ps1 -ForceNewSSLCert -EnableCredSSP
+ delegate_to: localhost
+
+ - name: Wait for system to become reachable over WinRM
+--- ansible-core-2.16.5.orig/lib/ansible/modules/yum.py
++++ ansible-core-2.16.5/lib/ansible/modules/yum.py
+@@ -21,49 +21,46 @@ description:
+ options:
+ use_backend:
+ description:
+- - This module supports V(yum) (as it always has), this is known as C(yum3)/C(YUM3)/C(yum-deprecated) by
++ - This module supports C(yum) (as it always has), this is known as C(yum3)/C(YUM3)/C(yum-deprecated) by
+ upstream yum developers. As of Ansible 2.7+, this module also supports C(YUM4), which is the
+- "new yum" and it has an V(dnf) backend. As of ansible-core 2.15+, this module will auto select the backend
+- based on the C(ansible_pkg_mgr) fact.
++ "new yum" and it has an C(dnf) backend.
+ - By default, this module will select the backend based on the C(ansible_pkg_mgr) fact.
+ default: "auto"
+- choices: [ auto, yum, yum4, dnf, dnf4, dnf5 ]
++ choices: [ auto, yum, yum4, dnf ]
+ type: str
+ version_added: "2.7"
+ name:
+ description:
+- - A package name or package specifier with version, like V(name-1.0).
+- - Comparison operators for package version are valid here C(>), C(<), C(>=), C(<=). Example - V(name>=1.0)
+- - If a previous version is specified, the task also needs to turn O(allow_downgrade) on.
+- See the O(allow_downgrade) documentation for caveats with downgrading packages.
+- - When using O(state=latest), this can be V('*') which means run C(yum -y update).
+- - You can also pass a url or a local path to an rpm file (using O(state=present)).
++ - A package name or package specifier with version, like C(name-1.0).
++ - Comparison operators for package version are valid here C(>), C(<), C(>=), C(<=). Example - C(name>=1.0)
++ - If a previous version is specified, the task also needs to turn C(allow_downgrade) on.
++ See the C(allow_downgrade) documentation for caveats with downgrading packages.
++ - When using state=latest, this can be C('*') which means run C(yum -y update).
++ - You can also pass a url or a local path to a rpm file (using state=present).
+ To operate on several packages this can accept a comma separated string of packages or (as of 2.0) a list of packages.
+ aliases: [ pkg ]
+ type: list
+ elements: str
+- default: []
+ exclude:
+ description:
+ - Package name(s) to exclude when state=present, or latest
+ type: list
+ elements: str
+- default: []
+ version_added: "2.0"
+ list:
+ description:
+ - "Package name to run the equivalent of C(yum list --show-duplicates <package>) against. In addition to listing packages,
+- use can also list the following: V(installed), V(updates), V(available) and V(repos)."
+- - This parameter is mutually exclusive with O(name).
++ use can also list the following: C(installed), C(updates), C(available) and C(repos)."
++ - This parameter is mutually exclusive with I(name).
+ type: str
+ state:
+ description:
+- - Whether to install (V(present) or V(installed), V(latest)), or remove (V(absent) or V(removed)) a package.
+- - V(present) and V(installed) will simply ensure that a desired package is installed.
+- - V(latest) will update the specified package if it's not of the latest available version.
+- - V(absent) and V(removed) will remove the specified package.
+- - Default is V(None), however in effect the default action is V(present) unless the O(autoremove) option is
+- enabled for this module, then V(absent) is inferred.
++ - Whether to install (C(present) or C(installed), C(latest)), or remove (C(absent) or C(removed)) a package.
++ - C(present) and C(installed) will simply ensure that a desired package is installed.
++ - C(latest) will update the specified package if it's not of the latest available version.
++ - C(absent) and C(removed) will remove the specified package.
++ - Default is C(None), however in effect the default action is C(present) unless the C(autoremove) option is
++ enabled for this module, then C(absent) is inferred.
+ type: str
+ choices: [ absent, installed, latest, present, removed ]
+ enablerepo:
+@@ -75,7 +72,6 @@ options:
+ separated string
+ type: list
+ elements: str
+- default: []
+ version_added: "0.9"
+ disablerepo:
+ description:
+@@ -86,7 +82,6 @@ options:
+ separated string
+ type: list
+ elements: str
+- default: []
+ version_added: "0.9"
+ conf_file:
+ description:
+@@ -96,7 +91,7 @@ options:
+ disable_gpg_check:
+ description:
+ - Whether to disable the GPG checking of signatures of packages being
+- installed. Has an effect only if O(state) is V(present) or V(latest).
++ installed. Has an effect only if state is I(present) or I(latest).
+ type: bool
+ default: "no"
+ version_added: "1.2"
+@@ -110,30 +105,30 @@ options:
+ update_cache:
+ description:
+ - Force yum to check if cache is out of date and redownload if needed.
+- Has an effect only if O(state) is V(present) or V(latest).
++ Has an effect only if state is I(present) or I(latest).
+ type: bool
+ default: "no"
+ aliases: [ expire-cache ]
+ version_added: "1.9"
+ validate_certs:
+ description:
+- - This only applies if using a https url as the source of the rpm. e.g. for localinstall. If set to V(false), the SSL certificates will not be validated.
+- - This should only set to V(false) used on personally controlled sites using self-signed certificates as it avoids verifying the source site.
+- - Prior to 2.1 the code worked as if this was set to V(true).
++ - This only applies if using a https url as the source of the rpm. e.g. for localinstall. If set to C(false), the SSL certificates will not be validated.
++ - This should only set to C(false) used on personally controlled sites using self-signed certificates as it avoids verifying the source site.
++ - Prior to 2.1 the code worked as if this was set to C(true).
+ type: bool
+ default: "yes"
+ version_added: "2.1"
+ sslverify:
+ description:
+ - Disables SSL validation of the repository server for this transaction.
+- - This should be set to V(false) if one of the configured repositories is using an untrusted or self-signed certificate.
++ - This should be set to C(false) if one of the configured repositories is using an untrusted or self-signed certificate.
+ type: bool
+ default: "yes"
+ version_added: "2.13"
+ update_only:
+ description:
+ - When using latest, only update installed packages. Do not install packages.
+- - Has an effect only if O(state) is V(latest)
++ - Has an effect only if state is I(latest)
+ default: "no"
+ type: bool
+ version_added: "2.5"
+@@ -147,13 +142,13 @@ options:
+ version_added: "2.3"
+ security:
+ description:
+- - If set to V(true), and O(state=latest) then only installs updates that have been marked security related.
++ - If set to C(true), and C(state=latest) then only installs updates that have been marked security related.
+ type: bool
+ default: "no"
+ version_added: "2.4"
+ bugfix:
+ description:
+- - If set to V(true), and O(state=latest) then only installs updates that have been marked bugfix related.
++ - If set to C(true), and C(state=latest) then only installs updates that have been marked bugfix related.
+ default: "no"
+ type: bool
+ version_added: "2.6"
+@@ -176,7 +171,6 @@ options:
+ The enabled plugin will not persist beyond the transaction.
+ type: list
+ elements: str
+- default: []
+ version_added: "2.5"
+ disable_plugin:
+ description:
+@@ -184,7 +178,6 @@ options:
+ The disabled plugins will not persist beyond the transaction.
+ type: list
+ elements: str
+- default: []
+ version_added: "2.5"
+ releasever:
+ description:
+@@ -194,9 +187,9 @@ options:
+ version_added: "2.7"
+ autoremove:
+ description:
+- - If V(true), removes all "leaf" packages from the system that were originally
++ - If C(true), removes all "leaf" packages from the system that were originally
+ installed as dependencies of user-installed packages but which are no longer
+- required by any such package. Should be used alone or when O(state) is V(absent)
++ required by any such package. Should be used alone or when state is I(absent)
+ - "NOTE: This feature requires yum >= 3.4.3 (RHEL/CentOS 7+)"
+ type: bool
+ default: "no"
+@@ -204,9 +197,9 @@ options:
+ disable_excludes:
+ description:
+ - Disable the excludes defined in YUM config files.
+- - If set to V(all), disables all excludes.
+- - If set to V(main), disable excludes defined in [main] in yum.conf.
+- - If set to V(repoid), disable excludes defined for given repo id.
++ - If set to C(all), disables all excludes.
++ - If set to C(main), disable excludes defined in [main] in yum.conf.
++ - If set to C(repoid), disable excludes defined for given repo id.
+ type: str
+ version_added: "2.7"
+ download_only:
+@@ -232,7 +225,7 @@ options:
+ download_dir:
+ description:
+ - Specifies an alternate directory to store packages.
+- - Has an effect only if O(download_only) is specified.
++ - Has an effect only if I(download_only) is specified.
+ type: str
+ version_added: "2.8"
+ install_repoquery:
+@@ -274,7 +267,7 @@ attributes:
+ platforms: rhel
+ notes:
+ - When used with a C(loop:) each package will be processed individually,
+- it is much more efficient to pass the list directly to the O(name) option.
++ it is much more efficient to pass the list directly to the I(name) option.
+ - In versions prior to 1.9.2 this module installed and removed each package
+ given to the yum module separately. This caused problems when packages
+ specified by filename or url had to be installed or removed together. In
+@@ -408,7 +401,8 @@ EXAMPLES = '''
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.common.locale import get_best_parsable_locale
+ from ansible.module_utils.common.respawn import has_respawned, respawn_module
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
++from ansible.module_utils.urls import fetch_url
+ from ansible.module_utils.yumdnf import YumDnf, yumdnf_argument_spec
+
+ import errno
+@@ -569,7 +563,7 @@ class YumModule(YumDnf):
+
+ # A sideeffect of accessing conf is that the configuration is
+ # loaded and plugins are discovered
+- self.yum_base.conf # pylint: disable=pointless-statement
++ self.yum_base.conf
+
+ try:
+ for rid in self.disablerepo:
+@@ -618,7 +612,7 @@ class YumModule(YumDnf):
+ if not repoq:
+ pkgs = []
+ try:
+- e, m, dummy = self.yum_base.rpmdb.matchPackageNames([pkgspec])
++ e, m, _ = self.yum_base.rpmdb.matchPackageNames([pkgspec])
+ pkgs = e + m
+ if not pkgs and not is_pkg:
+ pkgs.extend(self.yum_base.returnInstalledPackagesByDep(pkgspec))
+@@ -670,7 +664,7 @@ class YumModule(YumDnf):
+
+ pkgs = []
+ try:
+- e, m, dummy = self.yum_base.pkgSack.matchPackageNames([pkgspec])
++ e, m, _ = self.yum_base.pkgSack.matchPackageNames([pkgspec])
+ pkgs = e + m
+ if not pkgs:
+ pkgs.extend(self.yum_base.returnPackagesByDep(pkgspec))
+@@ -710,7 +704,7 @@ class YumModule(YumDnf):
+ pkgs = self.yum_base.returnPackagesByDep(pkgspec) + \
+ self.yum_base.returnInstalledPackagesByDep(pkgspec)
+ if not pkgs:
+- e, m, dummy = self.yum_base.pkgSack.matchPackageNames([pkgspec])
++ e, m, _ = self.yum_base.pkgSack.matchPackageNames([pkgspec])
+ pkgs = e + m
+ updates = self.yum_base.doPackageLists(pkgnarrow='updates').updates
+ except Exception as e:
+@@ -928,7 +922,7 @@ class YumModule(YumDnf):
+ cmd = repoq + ["--qf", qf, "-a"]
+ if self.releasever:
+ cmd.extend(['--releasever=%s' % self.releasever])
+- rc, out, err = self.module.run_command(cmd)
++ rc, out, _ = self.module.run_command(cmd)
+ if rc == 0:
+ return set(p for p in out.split('\n') if p.strip())
+ else:
+@@ -1284,13 +1278,15 @@ class YumModule(YumDnf):
+ obsoletes = {}
+ for line in out.split('\n'):
+ line = line.split()
+- # Ignore irrelevant lines:
+- # - '*' in line matches lines like mirror lists: "* base: mirror.corbina.net"
+- # - len(line) != 3 or 6 could be strings like:
+- # "This system is not registered with an entitlement server..."
+- # - len(line) = 6 is package obsoletes
+- # - checking for '.' in line[0] (package name) likely ensures that it is of format:
+- # "package_name.arch" (coreutils.x86_64)
++ """
++ Ignore irrelevant lines:
++ - '*' in line matches lines like mirror lists: "* base: mirror.corbina.net"
++ - len(line) != 3 or 6 could be strings like:
++ "This system is not registered with an entitlement server..."
++ - len(line) = 6 is package obsoletes
++ - checking for '.' in line[0] (package name) likely ensures that it is of format:
++ "package_name.arch" (coreutils.x86_64)
++ """
+ if '*' in line or len(line) not in [3, 6] or '.' not in line[0]:
+ continue
+
+@@ -1419,7 +1415,7 @@ class YumModule(YumDnf):
+ # this contains the full NVR and spec could contain wildcards
+ # or virtual provides (like "python-*" or "smtp-daemon") while
+ # updates contains name only.
+- (pkgname, ver, rel, epoch, arch) = splitFilename(pkg)
++ pkgname, _, _, _, _ = splitFilename(pkg)
+ if spec in pkgs['update'] and pkgname in updates:
+ nothing_to_do = False
+ will_update.add(spec)
+@@ -1619,29 +1615,30 @@ class YumModule(YumDnf):
+ self.yum_basecmd.extend(e_cmd)
+
+ if self.state in ('installed', 'present', 'latest'):
+- # The need of this entire if conditional has to be changed
+- # this function is the ensure function that is called
+- # in the main section.
+- #
+- # This conditional tends to disable/enable repo for
+- # install present latest action, same actually
+- # can be done for remove and absent action
+- #
+- # As solution I would advice to cal
+- # try: self.yum_base.repos.disableRepo(disablerepo)
+- # and
+- # try: self.yum_base.repos.enableRepo(enablerepo)
+- # right before any yum_cmd is actually called regardless
+- # of yum action.
+- #
+- # Please note that enable/disablerepo options are general
+- # options, this means that we can call those with any action
+- # option. https://linux.die.net/man/8/yum
+- #
+- # This docstring will be removed together when issue: #21619
+- # will be solved.
+- #
+- # This has been triggered by: #19587
++ """ The need of this entire if conditional has to be changed
++ this function is the ensure function that is called
++ in the main section.
++
++ This conditional tends to disable/enable repo for
++ install present latest action, same actually
++ can be done for remove and absent action
++
++ As solution I would advice to cal
++ try: self.yum_base.repos.disableRepo(disablerepo)
++ and
++ try: self.yum_base.repos.enableRepo(enablerepo)
++ right before any yum_cmd is actually called regardless
++ of yum action.
++
++ Please note that enable/disablerepo options are general
++ options, this means that we can call those with any action
++ option. https://linux.die.net/man/8/yum
++
++ This docstring will be removed together when issue: #21619
++ will be solved.
++
++ This has been triggered by: #19587
++ """
+
+ if self.update_cache:
+ self.module.run_command(self.yum_basecmd + ['clean', 'expire-cache'])
+@@ -1807,7 +1804,7 @@ def main():
+ # list=repos
+ # list=pkgspec
+
+- yumdnf_argument_spec['argument_spec']['use_backend'] = dict(default='auto', choices=['auto', 'yum', 'yum4', 'dnf', 'dnf4', 'dnf5'])
++ yumdnf_argument_spec['argument_spec']['use_backend'] = dict(default='auto', choices=['auto', 'yum', 'yum4', 'dnf'])
+
+ module = AnsibleModule(
+ **yumdnf_argument_spec
+--- ansible-core-2.16.5.orig/lib/ansible/modules/yum_repository.py
++++ ansible-core-2.16.5/lib/ansible/modules/yum_repository.py
+@@ -21,9 +21,9 @@ description:
+ options:
+ async:
+ description:
+- - If set to V(true) Yum will download packages and metadata from this
++ - If set to C(true) Yum will download packages and metadata from this
+ repo in parallel, if possible.
+- - In ansible-core 2.11, 2.12, and 2.13 the default value is V(true).
++ - In ansible-core 2.11, 2.12, and 2.13 the default value is C(true).
+ - This option has been deprecated in RHEL 8. If you're using one of the
+ versions listed above, you can set this option to None to avoid passing an
+ unknown configuration option.
+@@ -31,19 +31,20 @@ options:
+ bandwidth:
+ description:
+ - Maximum available network bandwidth in bytes/second. Used with the
+- O(throttle) option.
+- - If O(throttle) is a percentage and bandwidth is V(0) then bandwidth
+- throttling will be disabled. If O(throttle) is expressed as a data rate
+- (bytes/sec) then this option is ignored. Default is V(0) (no bandwidth
++ I(throttle) option.
++ - If I(throttle) is a percentage and bandwidth is C(0) then bandwidth
++ throttling will be disabled. If I(throttle) is expressed as a data rate
++ (bytes/sec) then this option is ignored. Default is C(0) (no bandwidth
+ throttling).
+ type: str
++ default: '0'
+ baseurl:
+ description:
+ - URL to the directory where the yum repository's 'repodata' directory
+ lives.
+ - It can also be a list of multiple URLs.
+- - This, the O(metalink) or O(mirrorlist) parameters are required if O(state) is set to
+- V(present).
++ - This, the I(metalink) or I(mirrorlist) parameters are required if I(state) is set to
++ C(present).
+ type: list
+ elements: str
+ cost:
+@@ -51,57 +52,61 @@ options:
+ - Relative cost of accessing this repository. Useful for weighing one
+ repo's packages as greater/less than any other.
+ type: str
++ default: '1000'
+ deltarpm_metadata_percentage:
+ description:
+ - When the relative size of deltarpm metadata vs pkgs is larger than
+ this, deltarpm metadata is not downloaded from the repo. Note that you
+- can give values over V(100), so V(200) means that the metadata is
+- required to be half the size of the packages. Use V(0) to turn off
++ can give values over C(100), so C(200) means that the metadata is
++ required to be half the size of the packages. Use C(0) to turn off
+ this check, and always download metadata.
+ type: str
++ default: '100'
+ deltarpm_percentage:
+ description:
+ - When the relative size of delta vs pkg is larger than this, delta is
+- not used. Use V(0) to turn off delta rpm processing. Local repositories
+- (with file://O(baseurl)) have delta rpms turned off by default.
++ not used. Use C(0) to turn off delta rpm processing. Local repositories
++ (with file:// I(baseurl)) have delta rpms turned off by default.
+ type: str
++ default: '75'
+ description:
+ description:
+ - A human readable string describing the repository. This option corresponds to the "name" property in the repo file.
+- - This parameter is only required if O(state) is set to V(present).
++ - This parameter is only required if I(state) is set to C(present).
+ type: str
+ enabled:
+ description:
+ - This tells yum whether or not use this repository.
+- - Yum default value is V(true).
++ - Yum default value is C(true).
+ type: bool
+ enablegroups:
+ description:
+ - Determines whether yum will allow the use of package groups for this
+ repository.
+- - Yum default value is V(true).
++ - Yum default value is C(true).
+ type: bool
+ exclude:
+ description:
+ - List of packages to exclude from updates or installs. This should be a
+- space separated list. Shell globs using wildcards (for example V(*) and V(?))
++ space separated list. Shell globs using wildcards (eg. C(*) and C(?))
+ are allowed.
+ - The list can also be a regular YAML array.
+ type: list
+ elements: str
+ failovermethod:
+ choices: [roundrobin, priority]
++ default: roundrobin
+ description:
+- - V(roundrobin) randomly selects a URL out of the list of URLs to start
++ - C(roundrobin) randomly selects a URL out of the list of URLs to start
+ with and proceeds through each of them as it encounters a failure
+ contacting the host.
+- - V(priority) starts from the first O(baseurl) listed and reads through
++ - C(priority) starts from the first I(baseurl) listed and reads through
+ them sequentially.
+ type: str
+ file:
+ description:
+ - File name without the C(.repo) extension to save the repo in. Defaults
+- to the value of O(name).
++ to the value of I(name).
+ type: str
+ gpgcakey:
+ description:
+@@ -112,7 +117,7 @@ options:
+ - Tells yum whether or not it should perform a GPG signature check on
+ packages.
+ - No default setting. If the value is not set, the system setting from
+- C(/etc/yum.conf) or system default of V(false) will be used.
++ C(/etc/yum.conf) or system default of C(false) will be used.
+ type: bool
+ gpgkey:
+ description:
+@@ -123,31 +128,32 @@ options:
+ module_hotfixes:
+ description:
+ - Disable module RPM filtering and make all RPMs from the repository
+- available. The default is V(None).
++ available. The default is C(None).
+ version_added: '2.11'
+ type: bool
+ http_caching:
+ description:
+ - Determines how upstream HTTP caches are instructed to handle any HTTP
+ downloads that Yum does.
+- - V(all) means that all HTTP downloads should be cached.
+- - V(packages) means that only RPM package downloads should be cached (but
++ - C(all) means that all HTTP downloads should be cached.
++ - C(packages) means that only RPM package downloads should be cached (but
+ not repository metadata downloads).
+- - V(none) means that no HTTP downloads should be cached.
++ - C(none) means that no HTTP downloads should be cached.
+ choices: [all, packages, none]
+ type: str
++ default: all
+ include:
+ description:
+ - Include external configuration file. Both, local path and URL is
+ supported. Configuration file will be inserted at the position of the
+- C(include=) line. Included files may contain further include lines.
++ I(include=) line. Included files may contain further include lines.
+ Yum will abort with an error if an inclusion loop is detected.
+ type: str
+ includepkgs:
+ description:
+ - List of packages you want to only use from a repository. This should be
+- a space separated list. Shell globs using wildcards (for example V(*) and V(?))
+- are allowed. Substitution variables (for example V($releasever)) are honored
++ a space separated list. Shell globs using wildcards (eg. C(*) and C(?))
++ are allowed. Substitution variables (e.g. C($releasever)) are honored
+ here.
+ - The list can also be a regular YAML array.
+ type: list
+@@ -155,61 +161,65 @@ options:
+ ip_resolve:
+ description:
+ - Determines how yum resolves host names.
+- - V(4) or V(IPv4) - resolve to IPv4 addresses only.
+- - V(6) or V(IPv6) - resolve to IPv6 addresses only.
++ - C(4) or C(IPv4) - resolve to IPv4 addresses only.
++ - C(6) or C(IPv6) - resolve to IPv6 addresses only.
+ choices: ['4', '6', IPv4, IPv6, whatever]
+ type: str
++ default: whatever
+ keepalive:
+ description:
+ - This tells yum whether or not HTTP/1.1 keepalive should be used with
+ this repository. This can improve transfer speeds by using one
+ connection when downloading multiple files from a repository.
+ type: bool
++ default: 'no'
+ keepcache:
+ description:
+- - Either V(1) or V(0). Determines whether or not yum keeps the cache of
++ - Either C(1) or C(0). Determines whether or not yum keeps the cache of
+ headers and packages after successful installation.
+- - This parameter is deprecated and will be removed in version 2.20.
+ choices: ['0', '1']
+ type: str
++ default: '1'
+ metadata_expire:
+ description:
+ - Time (in seconds) after which the metadata will expire.
+ - Default value is 6 hours.
+ type: str
++ default: '21600'
+ metadata_expire_filter:
+ description:
+- - Filter the O(metadata_expire) time, allowing a trade of speed for
++ - Filter the I(metadata_expire) time, allowing a trade of speed for
+ accuracy if a command doesn't require it. Each yum command can specify
+ that it requires a certain level of timeliness quality from the remote
+ repos. from "I'm about to install/upgrade, so this better be current"
+ to "Anything that's available is good enough".
+- - V(never) - Nothing is filtered, always obey O(metadata_expire).
+- - V(read-only:past) - Commands that only care about past information are
+- filtered from metadata expiring. Eg. C(yum history) info (if history
++ - C(never) - Nothing is filtered, always obey I(metadata_expire).
++ - C(read-only:past) - Commands that only care about past information are
++ filtered from metadata expiring. Eg. I(yum history) info (if history
+ needs to lookup anything about a previous transaction, then by
+ definition the remote package was available in the past).
+- - V(read-only:present) - Commands that are balanced between past and
+- future. Eg. C(yum list yum).
+- - V(read-only:future) - Commands that are likely to result in running
++ - C(read-only:present) - Commands that are balanced between past and
++ future. Eg. I(yum list yum).
++ - C(read-only:future) - Commands that are likely to result in running
+ other commands which will require the latest metadata. Eg.
+- C(yum check-update).
++ I(yum check-update).
+ - Note that this option does not override "yum clean expire-cache".
+ choices: [never, 'read-only:past', 'read-only:present', 'read-only:future']
+ type: str
++ default: 'read-only:present'
+ metalink:
+ description:
+ - Specifies a URL to a metalink file for the repomd.xml, a list of
+ mirrors for the entire repository are generated by converting the
+- mirrors for the repomd.xml file to a O(baseurl).
+- - This, the O(baseurl) or O(mirrorlist) parameters are required if O(state) is set to
+- V(present).
++ mirrors for the repomd.xml file to a I(baseurl).
++ - This, the I(baseurl) or I(mirrorlist) parameters are required if I(state) is set to
++ C(present).
+ type: str
+ mirrorlist:
+ description:
+ - Specifies a URL to a file containing a list of baseurls.
+- - This, the O(baseurl) or O(metalink) parameters are required if O(state) is set to
+- V(present).
++ - This, the I(baseurl) or I(metalink) parameters are required if I(state) is set to
++ C(present).
+ type: str
+ mirrorlist_expire:
+ description:
+@@ -217,11 +227,12 @@ options:
+ expire.
+ - Default value is 6 hours.
+ type: str
++ default: '21600'
+ name:
+ description:
+ - Unique repository ID. This option builds the section name of the repository in the repo file.
+- - This parameter is only required if O(state) is set to V(present) or
+- V(absent).
++ - This parameter is only required if I(state) is set to C(present) or
++ C(absent).
+ type: str
+ required: true
+ password:
+@@ -234,13 +245,15 @@ options:
+ from 1 to 99.
+ - This option only works if the YUM Priorities plugin is installed.
+ type: str
++ default: '99'
+ protect:
+ description:
+ - Protect packages from updates from other repositories.
+ type: bool
++ default: 'no'
+ proxy:
+ description:
+- - URL to the proxy server that yum should use. Set to V(_none_) to
++ - URL to the proxy server that yum should use. Set to C(_none_) to
+ disable the global proxy setting.
+ type: str
+ proxy_password:
+@@ -256,6 +269,7 @@ options:
+ - This tells yum whether or not it should perform a GPG signature check
+ on the repodata from this repository.
+ type: bool
++ default: 'no'
+ reposdir:
+ description:
+ - Directory where the C(.repo) files will be stored.
+@@ -264,28 +278,32 @@ options:
+ retries:
+ description:
+ - Set the number of times any attempt to retrieve a file should retry
+- before returning an error. Setting this to V(0) makes yum try forever.
++ before returning an error. Setting this to C(0) makes yum try forever.
+ type: str
++ default: '10'
+ s3_enabled:
+ description:
+ - Enables support for S3 repositories.
+ - This option only works if the YUM S3 plugin is installed.
+ type: bool
++ default: 'no'
+ skip_if_unavailable:
+ description:
+- - If set to V(true) yum will continue running if this repository cannot be
++ - If set to C(true) yum will continue running if this repository cannot be
+ contacted for any reason. This should be set carefully as all repos are
+ consulted for any given command.
+ type: bool
++ default: 'no'
+ ssl_check_cert_permissions:
+ description:
+ - Whether yum should check the permissions on the paths for the
+ certificates on the repository (both remote and local).
+ - If we can't read any of the files then yum will force
+- O(skip_if_unavailable) to be V(true). This is most useful for non-root
++ I(skip_if_unavailable) to be C(true). This is most useful for non-root
+ processes which use yum on repos that have client cert files which are
+ readable only by root.
+ type: bool
++ default: 'no'
+ sslcacert:
+ description:
+ - Path to the directory containing the databases of the certificate
+@@ -308,6 +326,7 @@ options:
+ description:
+ - Defines whether yum should verify SSL certificates/hosts at all.
+ type: bool
++ default: 'yes'
+ aliases: [ validate_certs ]
+ state:
+ description:
+@@ -325,12 +344,14 @@ options:
+ description:
+ - Number of seconds to wait for a connection before timing out.
+ type: str
++ default: '30'
+ ui_repoid_vars:
+ description:
+ - When a repository id is displayed, append these yum variables to the
+- string if they are used in the O(baseurl)/etc. Variables are appended
++ string if they are used in the I(baseurl)/etc. Variables are appended
+ in the order listed (and found).
+ type: str
++ default: releasever basearch
+ username:
+ description:
+ - Username to use for basic authentication to a repo or really any url.
+@@ -354,7 +375,7 @@ notes:
+ - The repo file will be automatically deleted if it contains no repository.
+ - When removing a repository, beware that the metadata cache may still remain
+ on disk until you run C(yum clean all). Use a notification handler for this.
+- - "The O(ignore:params) parameter was removed in Ansible 2.5 due to circumventing Ansible's parameter
++ - "The C(params) parameter was removed in Ansible 2.5 due to circumventing Ansible's parameter
+ handling"
+ '''
+
+@@ -417,7 +438,7 @@ import os
+
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.six.moves import configparser
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+
+ class YumRepo(object):
+@@ -528,11 +549,6 @@ class YumRepo(object):
+
+ # Set the value only if it was defined (default is None)
+ if value is not None and key in self.allowed_params:
+- if key == 'keepcache':
+- self.module.deprecate(
+- "'keepcache' parameter is deprecated.",
+- version='2.20'
+- )
+ self.repofile.set(self.section, key, value)
+
+ def save(self):
+@@ -611,6 +627,7 @@ def main():
+ mirrorlist=dict(),
+ mirrorlist_expire=dict(),
+ name=dict(required=True),
++ params=dict(type='dict'),
+ password=dict(no_log=True),
+ priority=dict(),
+ protect=dict(type='bool'),
+@@ -642,6 +659,11 @@ def main():
+ supports_check_mode=True,
+ )
+
++ # Params was removed
++ # https://meetbot.fedoraproject.org/ansible-meeting/2017-09-28/ansible_dev_meeting.2017-09-28-15.00.log.html
++ if module.params['params']:
++ module.fail_json(msg="The params option to yum_repository was removed in Ansible 2.5 since it circumvents Ansible's option handling")
++
+ name = module.params['name']
+ state = module.params['state']
+
+--- ansible-core-2.16.5.orig/lib/ansible/parsing/ajson.py
++++ ansible-core-2.16.5/lib/ansible/parsing/ajson.py
+@@ -8,7 +8,7 @@ __metaclass__ = type
+ import json
+
+ # Imported for backwards compat
+-from ansible.module_utils.common.json import AnsibleJSONEncoder # pylint: disable=unused-import
++from ansible.module_utils.common.json import AnsibleJSONEncoder
+
+ from ansible.parsing.vault import VaultLib
+ from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
+--- ansible-core-2.16.5.orig/lib/ansible/parsing/dataloader.py
++++ ansible-core-2.16.5/lib/ansible/parsing/dataloader.py
+@@ -11,16 +11,15 @@ import os
+ import os.path
+ import re
+ import tempfile
+-import typing as t
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleFileNotFound, AnsibleParserError
+ from ansible.module_utils.basic import is_executable
+ from ansible.module_utils.six import binary_type, text_type
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.parsing.quoting import unquote
+ from ansible.parsing.utils.yaml import from_yaml
+-from ansible.parsing.vault import VaultLib, b_HEADER, is_encrypted, is_encrypted_file, parse_vaulttext_envelope, PromptVaultSecret
++from ansible.parsing.vault import VaultLib, b_HEADER, is_encrypted, is_encrypted_file, parse_vaulttext_envelope
+ from ansible.utils.path import unfrackpath
+ from ansible.utils.display import Display
+
+@@ -46,7 +45,7 @@ class DataLoader:
+ Usage:
+
+ dl = DataLoader()
+- # optionally: dl.set_vault_secrets([('default', ansible.parsing.vault.PrompVaultSecret(...),)])
++ # optionally: dl.set_vault_password('foo')
+ ds = dl.load('...')
+ ds = dl.load_from_file('/path/to/file')
+ '''
+@@ -67,19 +66,20 @@ class DataLoader:
+ # initialize the vault stuff with an empty password
+ # TODO: replace with a ref to something that can get the password
+ # a creds/auth provider
++ # self.set_vault_password(None)
+ self._vaults = {}
+ self._vault = VaultLib()
+ self.set_vault_secrets(None)
+
+ # TODO: since we can query vault_secrets late, we could provide this to DataLoader init
+- def set_vault_secrets(self, vault_secrets: list[tuple[str, PromptVaultSecret]] | None) -> None:
++ def set_vault_secrets(self, vault_secrets):
+ self._vault.secrets = vault_secrets
+
+- def load(self, data: str, file_name: str = '<string>', show_content: bool = True, json_only: bool = False) -> t.Any:
++ def load(self, data, file_name='<string>', show_content=True, json_only=False):
+ '''Backwards compat for now'''
+ return from_yaml(data, file_name, show_content, self._vault.secrets, json_only=json_only)
+
+- def load_from_file(self, file_name: str, cache: bool = True, unsafe: bool = False, json_only: bool = False) -> t.Any:
++ def load_from_file(self, file_name, cache=True, unsafe=False, json_only=False):
+ ''' Loads data from a file, which can contain either JSON or YAML. '''
+
+ file_name = self.path_dwim(file_name)
+@@ -105,28 +105,28 @@ class DataLoader:
+ # return a deep copy here, so the cache is not affected
+ return copy.deepcopy(parsed_data)
+
+- def path_exists(self, path: str) -> bool:
++ def path_exists(self, path):
+ path = self.path_dwim(path)
+ return os.path.exists(to_bytes(path, errors='surrogate_or_strict'))
+
+- def is_file(self, path: str) -> bool:
++ def is_file(self, path):
+ path = self.path_dwim(path)
+ return os.path.isfile(to_bytes(path, errors='surrogate_or_strict')) or path == os.devnull
+
+- def is_directory(self, path: str) -> bool:
++ def is_directory(self, path):
+ path = self.path_dwim(path)
+ return os.path.isdir(to_bytes(path, errors='surrogate_or_strict'))
+
+- def list_directory(self, path: str) -> list[str]:
++ def list_directory(self, path):
+ path = self.path_dwim(path)
+ return os.listdir(path)
+
+- def is_executable(self, path: str) -> bool:
++ def is_executable(self, path):
+ '''is the given path executable?'''
+ path = self.path_dwim(path)
+ return is_executable(path)
+
+- def _decrypt_if_vault_data(self, b_vault_data: bytes, b_file_name: bytes | None = None) -> tuple[bytes, bool]:
++ def _decrypt_if_vault_data(self, b_vault_data, b_file_name=None):
+ '''Decrypt b_vault_data if encrypted and return b_data and the show_content flag'''
+
+ if not is_encrypted(b_vault_data):
+@@ -139,7 +139,7 @@ class DataLoader:
+ show_content = False
+ return b_data, show_content
+
+- def _get_file_contents(self, file_name: str) -> tuple[bytes, bool]:
++ def _get_file_contents(self, file_name):
+ '''
+ Reads the file contents from the given file name
+
+@@ -168,17 +168,17 @@ class DataLoader:
+ except (IOError, OSError) as e:
+ raise AnsibleParserError("an error occurred while trying to read the file '%s': %s" % (file_name, to_native(e)), orig_exc=e)
+
+- def get_basedir(self) -> str:
++ def get_basedir(self):
+ ''' returns the current basedir '''
+ return self._basedir
+
+- def set_basedir(self, basedir: str) -> None:
++ def set_basedir(self, basedir):
+ ''' sets the base directory, used to find files when a relative path is given '''
+
+ if basedir is not None:
+ self._basedir = to_text(basedir)
+
+- def path_dwim(self, given: str) -> str:
++ def path_dwim(self, given):
+ '''
+ make relative paths work like folks expect.
+ '''
+@@ -194,7 +194,7 @@ class DataLoader:
+
+ return unfrackpath(path, follow=False)
+
+- def _is_role(self, path: str) -> bool:
++ def _is_role(self, path):
+ ''' imperfect role detection, roles are still valid w/o tasks|meta/main.yml|yaml|etc '''
+
+ b_path = to_bytes(path, errors='surrogate_or_strict')
+@@ -228,7 +228,7 @@ class DataLoader:
+
+ return False
+
+- def path_dwim_relative(self, path: str, dirname: str, source: str, is_role: bool = False) -> str:
++ def path_dwim_relative(self, path, dirname, source, is_role=False):
+ '''
+ find one file in either a role or playbook dir with or without
+ explicitly named dirname subdirs
+@@ -283,7 +283,7 @@ class DataLoader:
+
+ return candidate
+
+- def path_dwim_relative_stack(self, paths: list[str], dirname: str, source: str, is_role: bool = False) -> str:
++ def path_dwim_relative_stack(self, paths, dirname, source, is_role=False):
+ '''
+ find one file in first path in stack taking roles into account and adding play basedir as fallback
+
+@@ -342,7 +342,7 @@ class DataLoader:
+
+ return result
+
+- def _create_content_tempfile(self, content: str | bytes) -> str:
++ def _create_content_tempfile(self, content):
+ ''' Create a tempfile containing defined content '''
+ fd, content_tempfile = tempfile.mkstemp(dir=C.DEFAULT_LOCAL_TMP)
+ f = os.fdopen(fd, 'wb')
+@@ -356,7 +356,7 @@ class DataLoader:
+ f.close()
+ return content_tempfile
+
+- def get_real_file(self, file_path: str, decrypt: bool = True) -> str:
++ def get_real_file(self, file_path, decrypt=True):
+ """
+ If the file is vault encrypted return a path to a temporary decrypted file
+ If the file is not encrypted then the path is returned
+@@ -396,7 +396,7 @@ class DataLoader:
+ except (IOError, OSError) as e:
+ raise AnsibleParserError("an error occurred while trying to read the file '%s': %s" % (to_native(real_path), to_native(e)), orig_exc=e)
+
+- def cleanup_tmp_file(self, file_path: str) -> None:
++ def cleanup_tmp_file(self, file_path):
+ """
+ Removes any temporary files created from a previous call to
+ get_real_file. file_path must be the path returned from a
+@@ -406,7 +406,7 @@ class DataLoader:
+ os.unlink(file_path)
+ self._tempfiles.remove(file_path)
+
+- def cleanup_all_tmp_files(self) -> None:
++ def cleanup_all_tmp_files(self):
+ """
+ Removes all temporary files that DataLoader has created
+ NOTE: not thread safe, forks also need special handling see __init__ for details.
+@@ -417,7 +417,7 @@ class DataLoader:
+ except Exception as e:
+ display.warning("Unable to cleanup temp files: %s" % to_text(e))
+
+- def find_vars_files(self, path: str, name: str, extensions: list[str] | None = None, allow_dir: bool = True) -> list[str]:
++ def find_vars_files(self, path, name, extensions=None, allow_dir=True):
+ """
+ Find vars files in a given path with specified name. This will find
+ files in a dir named <name>/ or a file called <name> ending in known
+@@ -447,11 +447,11 @@ class DataLoader:
+ else:
+ continue
+ else:
+- found.append(to_text(full_path))
++ found.append(full_path)
+ break
+ return found
+
+- def _get_dir_vars_files(self, path: str, extensions: list[str]) -> list[str]:
++ def _get_dir_vars_files(self, path, extensions):
+ found = []
+ for spath in sorted(self.list_directory(path)):
+ if not spath.startswith(u'.') and not spath.endswith(u'~'): # skip hidden and backups
+--- ansible-core-2.16.5.orig/lib/ansible/parsing/mod_args.py
++++ ansible-core-2.16.5/lib/ansible/parsing/mod_args.py
+@@ -22,7 +22,7 @@ __metaclass__ = type
+ import ansible.constants as C
+ from ansible.errors import AnsibleParserError, AnsibleError, AnsibleAssertionError
+ from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.parsing.splitter import parse_kv, split_args
+ from ansible.plugins.loader import module_loader, action_loader
+ from ansible.template import Templar
+--- ansible-core-2.16.5.orig/lib/ansible/parsing/plugin_docs.py
++++ ansible-core-2.16.5/lib/ansible/parsing/plugin_docs.py
+@@ -9,7 +9,7 @@ import tokenize
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleParserError
+-from ansible.module_utils.common.text.converters import to_text, to_native
++from ansible.module_utils._text import to_text, to_native
+ from ansible.parsing.yaml.loader import AnsibleLoader
+ from ansible.utils.display import Display
+
+@@ -73,7 +73,7 @@ def read_docstring_from_python_module(fi
+ tokens = tokenize.generate_tokens(f.readline)
+ for token in tokens:
+
+- # found label that looks like variable
++ # found lable that looks like variable
+ if token.type == tokenize.NAME:
+
+ # label is expected value, in correct place and has not been seen before
+@@ -151,10 +151,10 @@ def read_docstring_from_python_file(file
+ if theid == 'EXAMPLES':
+ # examples 'can' be yaml, but even if so, we dont want to parse as such here
+ # as it can create undesired 'objects' that don't display well as docs.
+- data[varkey] = to_text(child.value.value)
++ data[varkey] = to_text(child.value.s)
+ else:
+ # string should be yaml if already not a dict
+- data[varkey] = AnsibleLoader(child.value.value, file_name=filename).get_single_data()
++ data[varkey] = AnsibleLoader(child.value.s, file_name=filename).get_single_data()
+
+ display.debug('Documentation assigned: %s' % varkey)
+
+--- ansible-core-2.16.5.orig/lib/ansible/parsing/splitter.py
++++ ansible-core-2.16.5/lib/ansible/parsing/splitter.py
+@@ -23,7 +23,7 @@ import codecs
+ import re
+
+ from ansible.errors import AnsibleParserError
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.parsing.quoting import unquote
+
+
+@@ -58,7 +58,15 @@ def parse_kv(args, check_raw=False):
+
+ options = {}
+ if args is not None:
+- vargs = split_args(args)
++ try:
++ vargs = split_args(args)
++ except IndexError as e:
++ raise AnsibleParserError("Unable to parse argument string", orig_exc=e)
++ except ValueError as ve:
++ if 'no closing quotation' in str(ve).lower():
++ raise AnsibleParserError("error parsing argument string, try quoting the entire line.", orig_exc=ve)
++ else:
++ raise
+
+ raw_params = []
+ for orig_x in vargs:
+@@ -160,9 +168,6 @@ def split_args(args):
+ how Ansible needs to use it.
+ '''
+
+- if not args:
+- return []
+-
+ # the list of params parsed out of the arg string
+ # this is going to be the result value when we are done
+ params = []
+@@ -199,10 +204,6 @@ def split_args(args):
+ # Empty entries means we have subsequent spaces
+ # We want to hold onto them so we can reconstruct them later
+ if len(token) == 0 and idx != 0:
+- # Make sure there is a params item to store result in.
+- if not params:
+- params.append('')
+-
+ params[-1] += ' '
+ continue
+
+@@ -234,11 +235,13 @@ def split_args(args):
+ elif print_depth or block_depth or comment_depth or inside_quotes or was_inside_quotes:
+ if idx == 0 and was_inside_quotes:
+ params[-1] = "%s%s" % (params[-1], token)
+- else:
++ elif len(tokens) > 1:
+ spacer = ''
+ if idx > 0:
+ spacer = ' '
+ params[-1] = "%s%s%s" % (params[-1], spacer, token)
++ else:
++ params[-1] = "%s\n%s" % (params[-1], token)
+ appended = True
+
+ # if the number of paired block tags is not the same, the depth has changed, so we calculate that here
+@@ -270,12 +273,11 @@ def split_args(args):
+ # one item (meaning we split on newlines), add a newline back here
+ # to preserve the original structure
+ if len(items) > 1 and itemidx != len(items) - 1 and not line_continuation:
+- # Make sure there is a params item to store result in.
+- if not params:
+- params.append('')
+-
+ params[-1] += '\n'
+
++ # always clear the line continuation flag
++ line_continuation = False
++
+ # If we're done and things are not at zero depth or we're still inside quotes,
+ # raise an error to indicate that the args were unbalanced
+ if print_depth or block_depth or comment_depth or inside_quotes:
+--- ansible-core-2.16.5.orig/lib/ansible/parsing/utils/yaml.py
++++ ansible-core-2.16.5/lib/ansible/parsing/utils/yaml.py
+@@ -13,7 +13,7 @@ from yaml import YAMLError
+
+ from ansible.errors import AnsibleParserError
+ from ansible.errors.yaml_strings import YAML_SYNTAX_ERROR
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.parsing.yaml.loader import AnsibleLoader
+ from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject
+ from ansible.parsing.ajson import AnsibleJSONDecoder
+--- ansible-core-2.16.5.orig/lib/ansible/parsing/vault/__init__.py
++++ ansible-core-2.16.5/lib/ansible/parsing/vault/__init__.py
+@@ -55,7 +55,7 @@ except ImportError:
+ from ansible.errors import AnsibleError, AnsibleAssertionError
+ from ansible import constants as C
+ from ansible.module_utils.six import binary_type
+-from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native
++from ansible.module_utils._text import to_bytes, to_text, to_native
+ from ansible.utils.display import Display
+ from ansible.utils.path import makedirs_safe, unfrackpath
+
+@@ -658,10 +658,7 @@ class VaultLib:
+ b_vaulttext = to_bytes(vaulttext, errors='strict', encoding='utf-8')
+
+ if self.secrets is None:
+- msg = "A vault password must be specified to decrypt data"
+- if filename:
+- msg += " in file %s" % to_native(filename)
+- raise AnsibleVaultError(msg)
++ raise AnsibleVaultError("A vault password must be specified to decrypt data")
+
+ if not is_encrypted(b_vaulttext):
+ msg = "input is not vault encrypted data. "
+@@ -787,13 +784,13 @@ class VaultEditor:
+
+ passes = 3
+ with open(tmp_path, "wb") as fh:
+- for dummy in range(passes):
++ for _ in range(passes):
+ fh.seek(0, 0)
+ # get a random chunk of data, each pass with other length
+ chunk_len = random.randint(max_chunk_len // 2, max_chunk_len)
+ data = os.urandom(chunk_len)
+
+- for dummy in range(0, file_len // chunk_len):
++ for _ in range(0, file_len // chunk_len):
+ fh.write(data)
+ fh.write(data[:file_len % chunk_len])
+
+@@ -1044,10 +1041,10 @@ class VaultEditor:
+ since in the plaintext case, the original contents can be of any text encoding
+ or arbitrary binary data.
+
+- When used to write the result of vault encryption, the value of the 'data' arg
+- should be a utf-8 encoded byte string and not a text type.
++ When used to write the result of vault encryption, the val of the 'data' arg
++ should be a utf-8 encoded byte string and not a text typ and not a text type..
+
+- When used to write the result of vault decryption, the value of the 'data' arg
++ When used to write the result of vault decryption, the val of the 'data' arg
+ should be a byte string and not a text type.
+
+ :arg data: the byte string (bytes) data
+@@ -1077,8 +1074,6 @@ class VaultEditor:
+ output = getattr(sys.stdout, 'buffer', sys.stdout)
+ output.write(b_file_data)
+ else:
+- if not os.access(os.path.dirname(thefile), os.W_OK):
+- raise AnsibleError("Destination '%s' not writable" % (os.path.dirname(thefile)))
+ # file names are insecure and prone to race conditions, so remove and create securely
+ if os.path.isfile(thefile):
+ if shred:
+@@ -1128,7 +1123,7 @@ class VaultEditor:
+ os.chown(dest, prev.st_uid, prev.st_gid)
+
+ def _editor_shell_command(self, filename):
+- env_editor = C.config.get_config_value('EDITOR')
++ env_editor = os.environ.get('EDITOR', 'vi')
+ editor = shlex.split(env_editor)
+ editor.append(filename)
+
+@@ -1201,20 +1196,13 @@ class VaultAES256:
+ return to_bytes(hexlify(b_hmac), errors='surrogate_or_strict'), hexlify(b_ciphertext)
+
+ @classmethod
+- def _get_salt(cls):
+- custom_salt = C.config.get_config_value('VAULT_ENCRYPT_SALT')
+- if not custom_salt:
+- custom_salt = os.urandom(32)
+- return to_bytes(custom_salt)
+-
+- @classmethod
+ def encrypt(cls, b_plaintext, secret, salt=None):
+
+ if secret is None:
+ raise AnsibleVaultError('The secret passed to encrypt() was None')
+
+ if salt is None:
+- b_salt = cls._get_salt()
++ b_salt = os.urandom(32)
+ elif not salt:
+ raise AnsibleVaultError('Empty or invalid salt passed to encrypt()')
+ else:
+--- ansible-core-2.16.5.orig/lib/ansible/parsing/yaml/constructor.py
++++ ansible-core-2.16.5/lib/ansible/parsing/yaml/constructor.py
+@@ -23,7 +23,7 @@ from yaml.constructor import SafeConstru
+ from yaml.nodes import MappingNode
+
+ from ansible import constants as C
+-from ansible.module_utils.common.text.converters import to_bytes, to_native
++from ansible.module_utils._text import to_bytes, to_native
+ from ansible.parsing.yaml.objects import AnsibleMapping, AnsibleSequence, AnsibleUnicode, AnsibleVaultEncryptedUnicode
+ from ansible.parsing.vault import VaultLib
+ from ansible.utils.display import Display
+--- ansible-core-2.16.5.orig/lib/ansible/parsing/yaml/objects.py
++++ ansible-core-2.16.5/lib/ansible/parsing/yaml/objects.py
+@@ -19,12 +19,16 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
++import string
+ import sys as _sys
+
+ from collections.abc import Sequence
+
++import sys
++import yaml
++
+ from ansible.module_utils.six import text_type
+-from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native
++from ansible.module_utils._text import to_bytes, to_text, to_native
+
+
+ class AnsibleBaseYAMLObject(object):
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/__init__.py
++++ ansible-core-2.16.5/lib/ansible/playbook/__init__.py
+@@ -23,7 +23,7 @@ import os
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleParserError
+-from ansible.module_utils.common.text.converters import to_text, to_native
++from ansible.module_utils._text import to_text, to_native
+ from ansible.playbook.play import Play
+ from ansible.playbook.playbook_include import PlaybookInclude
+ from ansible.plugins.loader import add_all_plugin_dirs
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/attribute.py
++++ ansible-core-2.16.5/lib/ansible/playbook/attribute.py
+@@ -19,6 +19,8 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
++from copy import copy, deepcopy
++
+ from ansible.utils.sentinel import Sentinel
+
+ _CONTAINERS = frozenset(('list', 'dict', 'set'))
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/base.py
++++ ansible-core-2.16.5/lib/ansible/playbook/base.py
+@@ -19,7 +19,7 @@ from ansible import context
+ from ansible.errors import AnsibleError, AnsibleParserError, AnsibleUndefinedVariable, AnsibleAssertionError
+ from ansible.module_utils.six import string_types
+ from ansible.module_utils.parsing.convert_bool import boolean
+-from ansible.module_utils.common.text.converters import to_text, to_native
++from ansible.module_utils._text import to_text, to_native
+ from ansible.parsing.dataloader import DataLoader
+ from ansible.playbook.attribute import Attribute, FieldAttribute, ConnectionFieldAttribute, NonInheritableFieldAttribute
+ from ansible.plugins.loader import module_loader, action_loader
+@@ -486,8 +486,6 @@ class FieldAttributeBase:
+ if not isinstance(value, attribute.class_type):
+ raise TypeError("%s is not a valid %s (got a %s instead)" % (name, attribute.class_type, type(value)))
+ value.post_validate(templar=templar)
+- else:
+- raise AnsibleAssertionError(f"Unknown value for attribute.isa: {attribute.isa}")
+ return value
+
+ def set_to_context(self, name):
+@@ -590,13 +588,6 @@ class FieldAttributeBase:
+ _validate_variable_keys(ds)
+ return combine_vars(self.vars, ds)
+ elif isinstance(ds, list):
+- display.deprecated(
+- (
+- 'Specifying a list of dictionaries for vars is deprecated in favor of '
+- 'specifying a dictionary.'
+- ),
+- version='2.18'
+- )
+ all_vars = self.vars
+ for item in ds:
+ if not isinstance(item, dict):
+@@ -609,7 +600,7 @@ class FieldAttributeBase:
+ else:
+ raise ValueError
+ except ValueError as e:
+- raise AnsibleParserError("Vars in a %s must be specified as a dictionary" % self.__class__.__name__,
++ raise AnsibleParserError("Vars in a %s must be specified as a dictionary, or a list of dictionaries" % self.__class__.__name__,
+ obj=ds, orig_exc=e)
+ except TypeError as e:
+ raise AnsibleParserError("Invalid variable name in vars specified for %s: %s" % (self.__class__.__name__, e), obj=ds, orig_exc=e)
+@@ -637,7 +628,7 @@ class FieldAttributeBase:
+ else:
+ combined = value + new_value
+
+- return [i for i, dummy in itertools.groupby(combined) if i is not None]
++ return [i for i, _ in itertools.groupby(combined) if i is not None]
+
+ def dump_attrs(self):
+ '''
+@@ -731,7 +722,7 @@ class Base(FieldAttributeBase):
+
+ # flags and misc. settings
+ environment = FieldAttribute(isa='list', extend=True, prepend=True)
+- no_log = FieldAttribute(isa='bool', default=C.DEFAULT_NO_LOG)
++ no_log = FieldAttribute(isa='bool')
+ run_once = FieldAttribute(isa='bool')
+ ignore_errors = FieldAttribute(isa='bool')
+ ignore_unreachable = FieldAttribute(isa='bool')
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/block.py
++++ ansible-core-2.16.5/lib/ansible/playbook/block.py
+@@ -21,25 +21,28 @@ __metaclass__ = type
+
+ import ansible.constants as C
+ from ansible.errors import AnsibleParserError
+-from ansible.playbook.attribute import NonInheritableFieldAttribute
++from ansible.playbook.attribute import FieldAttribute, NonInheritableFieldAttribute
+ from ansible.playbook.base import Base
+ from ansible.playbook.conditional import Conditional
+ from ansible.playbook.collectionsearch import CollectionSearch
+-from ansible.playbook.delegatable import Delegatable
+ from ansible.playbook.helpers import load_list_of_tasks
+-from ansible.playbook.notifiable import Notifiable
+ from ansible.playbook.role import Role
+ from ansible.playbook.taggable import Taggable
+ from ansible.utils.sentinel import Sentinel
+
+
+-class Block(Base, Conditional, CollectionSearch, Taggable, Notifiable, Delegatable):
++class Block(Base, Conditional, CollectionSearch, Taggable):
+
+ # main block fields containing the task lists
+ block = NonInheritableFieldAttribute(isa='list', default=list)
+ rescue = NonInheritableFieldAttribute(isa='list', default=list)
+ always = NonInheritableFieldAttribute(isa='list', default=list)
+
++ # other fields for task compat
++ notify = FieldAttribute(isa='list')
++ delegate_to = FieldAttribute(isa='string')
++ delegate_facts = FieldAttribute(isa='bool')
++
+ # for future consideration? this would be functionally
+ # similar to the 'else' clause for exceptions
+ # otherwise = FieldAttribute(isa='list')
+@@ -377,6 +380,7 @@ class Block(Base, Conditional, Collectio
+ if filtered_block.has_tasks():
+ tmp_list.append(filtered_block)
+ elif ((task.action in C._ACTION_META and task.implicit) or
++ (task.action in C._ACTION_INCLUDE and task.evaluate_tags([], self._play.skip_tags, all_vars=all_vars)) or
+ task.evaluate_tags(self._play.only_tags, self._play.skip_tags, all_vars=all_vars)):
+ tmp_list.append(task)
+ return tmp_list
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/conditional.py
++++ ansible-core-2.16.5/lib/ansible/playbook/conditional.py
+@@ -19,18 +19,28 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-import typing as t
++import ast
++import re
+
++from jinja2.compiler import generate
++from jinja2.exceptions import UndefinedError
++
++from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleUndefinedVariable, AnsibleTemplateError
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils.six import text_type
++from ansible.module_utils._text import to_native, to_text
+ from ansible.playbook.attribute import FieldAttribute
+-from ansible.template import Templar
+ from ansible.utils.display import Display
+
+ display = Display()
+
++DEFINED_REGEX = re.compile(r'(hostvars\[.+\]|[\w_]+)\s+(not\s+is|is|is\s+not)\s+(defined|undefined)')
++LOOKUP_REGEX = re.compile(r'lookup\s*\(')
++VALID_VAR_REGEX = re.compile("^[_A-Za-z][_a-zA-Z0-9]*$")
++
+
+ class Conditional:
++
+ '''
+ This is a mix-in class, to be used with Base to allow the object
+ to be run conditionally when a condition is met or skipped.
+@@ -47,69 +57,166 @@ class Conditional:
+ raise AnsibleError("a loader must be specified when using Conditional() directly")
+ else:
+ self._loader = loader
+- super().__init__()
++ super(Conditional, self).__init__()
+
+ def _validate_when(self, attr, name, value):
+ if not isinstance(value, list):
+ setattr(self, name, [value])
+
+- def evaluate_conditional(self, templar: Templar, all_vars: dict[str, t.Any]) -> bool:
++ def extract_defined_undefined(self, conditional):
++ results = []
++
++ cond = conditional
++ m = DEFINED_REGEX.search(cond)
++ while m:
++ results.append(m.groups())
++ cond = cond[m.end():]
++ m = DEFINED_REGEX.search(cond)
++
++ return results
++
++ def evaluate_conditional(self, templar, all_vars):
+ '''
+ Loops through the conditionals set on this object, returning
+ False if any of them evaluate as such.
+ '''
+- return self.evaluate_conditional_with_result(templar, all_vars)[0]
+
+- def evaluate_conditional_with_result(self, templar: Templar, all_vars: dict[str, t.Any]) -> tuple[bool, t.Optional[str]]:
+- """Loops through the conditionals set on this object, returning
+- False if any of them evaluate as such as well as the condition
+- that was false.
+- """
+- for conditional in self.when:
+- if conditional is None or conditional == "":
+- res = True
+- elif isinstance(conditional, bool):
+- res = conditional
+- else:
+- try:
++ # since this is a mix-in, it may not have an underlying datastructure
++ # associated with it, so we pull it out now in case we need it for
++ # error reporting below
++ ds = None
++ if hasattr(self, '_ds'):
++ ds = getattr(self, '_ds')
++
++ result = True
++ try:
++ for conditional in self.when:
++
++ # do evaluation
++ if conditional is None or conditional == '':
++ res = True
++ elif isinstance(conditional, bool):
++ res = conditional
++ else:
+ res = self._check_conditional(conditional, templar, all_vars)
+- except AnsibleError as e:
+- raise AnsibleError(
+- "The conditional check '%s' failed. The error was: %s" % (to_native(conditional), to_native(e)),
+- obj=getattr(self, '_ds', None)
+- )
+-
+- display.debug("Evaluated conditional (%s): %s" % (conditional, res))
+- if not res:
+- return res, conditional
+
+- return True, None
++ # only update if still true, preserve false
++ if result:
++ result = res
++
++ display.debug("Evaluated conditional (%s): %s" % (conditional, res))
++ if not result:
++ break
++
++ except Exception as e:
++ raise AnsibleError("The conditional check '%s' failed. The error was: %s" % (to_native(conditional), to_native(e)), obj=ds)
++
++ return result
++
++ def _check_conditional(self, conditional, templar, all_vars):
++ '''
++ This method does the low-level evaluation of each conditional
++ set on this object, using jinja2 to wrap the conditionals for
++ evaluation.
++ '''
+
+- def _check_conditional(self, conditional: str, templar: Templar, all_vars: dict[str, t.Any]) -> bool:
+ original = conditional
++
++ if templar.is_template(conditional):
++ display.warning('conditional statements should not include jinja2 '
++ 'templating delimiters such as {{ }} or {%% %%}. '
++ 'Found: %s' % conditional)
++
++ # make sure the templar is using the variables specified with this method
+ templar.available_variables = all_vars
++
+ try:
+- if templar.is_template(conditional):
+- display.warning(
+- "conditional statements should not include jinja2 "
+- "templating delimiters such as {{ }} or {%% %%}. "
+- "Found: %s" % conditional
+- )
+- conditional = templar.template(conditional)
+- if isinstance(conditional, bool):
+- return conditional
+- elif conditional == "":
+- return False
++ # if the conditional is "unsafe", disable lookups
++ disable_lookups = hasattr(conditional, '__UNSAFE__')
++ conditional = templar.template(conditional, disable_lookups=disable_lookups)
++
++ if not isinstance(conditional, text_type) or conditional == "":
++ return conditional
+
+ # 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)
++ class CleansingNodeVisitor(ast.NodeVisitor):
++ def generic_visit(self, node, inside_call=False, inside_yield=False):
++ if isinstance(node, ast.Call):
++ inside_call = True
++ elif isinstance(node, ast.Yield):
++ inside_yield = True
++ elif isinstance(node, ast.Str):
++ if disable_lookups:
++ if inside_call and node.s.startswith("__"):
++ # calling things with a dunder is generally bad at this point...
++ raise AnsibleError(
++ "Invalid access found in the conditional: '%s'" % conditional
++ )
++ elif inside_yield:
++ # we're inside a yield, so recursively parse and traverse the AST
++ # of the result to catch forbidden syntax from executing
++ parsed = ast.parse(node.s, mode='exec')
++ cnv = CleansingNodeVisitor()
++ cnv.visit(parsed)
++ # iterate over all child nodes
++ for child_node in ast.iter_child_nodes(node):
++ self.generic_visit(
++ child_node,
++ inside_call=inside_call,
++ inside_yield=inside_yield
++ )
++ try:
++ res = templar.environment.parse(conditional, None, None)
++ res = generate(res, templar.environment, None, None)
++ parsed = ast.parse(res, mode='exec')
++
++ cnv = CleansingNodeVisitor()
++ cnv.visit(parsed)
++ except Exception as e:
++ raise AnsibleError("Invalid conditional detected: %s" % to_native(e))
++
++ # and finally we generate and template the presented string and look at the resulting string
+ # NOTE The spaces around True and False are intentional to short-circuit literal_eval for
+ # jinja2_native=False and avoid its expensive calls.
+- return templar.template(
+- "{%% if %s %%} True {%% else %%} False {%% endif %%}" % conditional,
+- ).strip() == "True"
+- except AnsibleUndefinedVariable as e:
+- raise AnsibleUndefinedVariable("error while evaluating conditional (%s): %s" % (original, e))
++ presented = "{%% if %s %%} True {%% else %%} False {%% endif %%}" % conditional
++ val = templar.template(presented, disable_lookups=disable_lookups).strip()
++ if val == "True":
++ return True
++ elif val == "False":
++ return False
++ else:
++ raise AnsibleError("unable to evaluate conditional: %s" % original)
++ except (AnsibleUndefinedVariable, UndefinedError) as e:
++ # the templating failed, meaning most likely a variable was undefined. If we happened
++ # to be looking for an undefined variable, return True, otherwise fail
++ try:
++ # first we extract the variable name from the error message
++ var_name = re.compile(r"'(hostvars\[.+\]|[\w_]+)' is undefined").search(str(e)).groups()[0]
++ # next we extract all defined/undefined tests from the conditional string
++ def_undef = self.extract_defined_undefined(conditional)
++ # then we loop through these, comparing the error variable name against
++ # each def/undef test we found above. If there is a match, we determine
++ # whether the logic/state mean the variable should exist or not and return
++ # the corresponding True/False
++ for (du_var, logic, state) in def_undef:
++ # when we compare the var names, normalize quotes because something
++ # like hostvars['foo'] may be tested against hostvars["foo"]
++ if var_name.replace("'", '"') == du_var.replace("'", '"'):
++ # the should exist is a xor test between a negation in the logic portion
++ # against the state (defined or undefined)
++ should_exist = ('not' in logic) != (state == 'defined')
++ if should_exist:
++ return False
++ else:
++ return True
++ # as nothing above matched the failed var name, re-raise here to
++ # trigger the AnsibleUndefinedVariable exception again below
++ raise
++ except Exception:
++ raise AnsibleUndefinedVariable("error while evaluating conditional (%s): %s" % (original, e))
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/handler.py
++++ ansible-core-2.16.5/lib/ansible/playbook/handler.py
+@@ -53,9 +53,6 @@ class Handler(Task):
+ def remove_host(self, host):
+ self.notified_hosts = [h for h in self.notified_hosts if h != host]
+
+- def clear_hosts(self):
+- self.notified_hosts = []
+-
+ def is_host_notified(self, host):
+ return host in self.notified_hosts
+
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/helpers.py
++++ ansible-core-2.16.5/lib/ansible/playbook/helpers.py
+@@ -21,8 +21,9 @@ __metaclass__ = type
+ import os
+
+ from ansible import constants as C
+-from ansible.errors import AnsibleParserError, AnsibleUndefinedVariable, AnsibleAssertionError
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.errors import AnsibleParserError, AnsibleUndefinedVariable, AnsibleFileNotFound, AnsibleAssertionError
++from ansible.module_utils._text import to_native
++from ansible.module_utils.six import string_types
+ from ansible.parsing.mod_args import ModuleArgsParser
+ from ansible.utils.display import Display
+
+@@ -150,9 +151,23 @@ def load_list_of_tasks(ds, play, block=N
+ templar = Templar(loader=loader, variables=all_vars)
+
+ # check to see if this include is dynamic or static:
+- if action in C._ACTION_IMPORT_TASKS:
++ # 1. the user has set the 'static' option to false or true
++ # 2. one of the appropriate config options was set
++ if action in C._ACTION_INCLUDE_TASKS:
++ is_static = False
++ elif action in C._ACTION_IMPORT_TASKS:
++ is_static = True
++ else:
++ include_link = get_versioned_doclink('user_guide/playbooks_reuse_includes.html')
++ display.deprecated('"include" is deprecated, use include_tasks/import_tasks instead. See %s for details' % include_link, "2.16")
++ is_static = not templar.is_template(t.args['_raw_params']) and t.all_parents_static() and not t.loop
++
++ if is_static:
+ if t.loop is not None:
+- raise AnsibleParserError("You cannot use loops on 'import_tasks' statements. You should use 'include_tasks' instead.", obj=task_ds)
++ if action in C._ACTION_IMPORT_TASKS:
++ raise AnsibleParserError("You cannot use loops on 'import_tasks' statements. You should use 'include_tasks' instead.", obj=task_ds)
++ else:
++ raise AnsibleParserError("You cannot use 'static' on an include with a loop", obj=task_ds)
+
+ # we set a flag to indicate this include was static
+ t.statically_loaded = True
+@@ -274,9 +289,18 @@ def load_list_of_tasks(ds, play, block=N
+ loader=loader,
+ )
+
++ # 1. the user has set the 'static' option to false or true
++ # 2. one of the appropriate config options was set
++ is_static = False
+ if action in C._ACTION_IMPORT_ROLE:
++ is_static = True
++
++ if is_static:
+ if ir.loop is not None:
+- raise AnsibleParserError("You cannot use loops on 'import_role' statements. You should use 'include_role' instead.", obj=task_ds)
++ if action in C._ACTION_IMPORT_ROLE:
++ raise AnsibleParserError("You cannot use loops on 'import_role' statements. You should use 'include_role' instead.", obj=task_ds)
++ else:
++ raise AnsibleParserError("You cannot use 'static' on an include_role with a loop", obj=task_ds)
+
+ # we set a flag to indicate this include was static
+ ir.statically_loaded = True
+@@ -288,7 +312,7 @@ def load_list_of_tasks(ds, play, block=N
+ ir._role_name = templar.template(ir._role_name)
+
+ # uses compiled list from object
+- blocks, dummy = ir.get_block_list(variable_manager=variable_manager, loader=loader)
++ blocks, _ = ir.get_block_list(variable_manager=variable_manager, loader=loader)
+ task_list.extend(blocks)
+ else:
+ # passes task object itself for latter generation of list
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/included_file.py
++++ ansible-core-2.16.5/lib/ansible/playbook/included_file.py
+@@ -24,7 +24,7 @@ import os
+ from ansible import constants as C
+ from ansible.errors import AnsibleError
+ from ansible.executor.task_executor import remove_omit
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.playbook.handler import Handler
+ from ansible.playbook.task_include import TaskInclude
+ from ansible.playbook.role_include import IncludeRole
+@@ -72,6 +72,8 @@ class IncludedFile:
+ original_task = res._task
+
+ if original_task.action in C._ACTION_ALL_INCLUDES:
++ if original_task.action in C._ACTION_INCLUDE:
++ display.deprecated('"include" is deprecated, use include_tasks/import_tasks/import_playbook instead', "2.16")
+
+ if original_task.loop:
+ if 'results' not in res._result:
+@@ -116,7 +118,7 @@ class IncludedFile:
+
+ templar = Templar(loader=loader, variables=task_vars)
+
+- if original_task.action in C._ACTION_INCLUDE_TASKS:
++ if original_task.action in C._ACTION_ALL_INCLUDE_TASKS:
+ include_file = None
+
+ if original_task._parent:
+@@ -146,12 +148,9 @@ class IncludedFile:
+ cumulative_path = parent_include_dir
+ include_target = templar.template(include_result['include'])
+ if original_task._role:
+- dirname = 'handlers' if isinstance(original_task, Handler) else 'tasks'
+- new_basedir = os.path.join(original_task._role._role_path, dirname, cumulative_path)
+- candidates = [
+- loader.path_dwim_relative(original_task._role._role_path, dirname, include_target, is_role=True),
+- loader.path_dwim_relative(new_basedir, dirname, include_target, is_role=True)
+- ]
++ new_basedir = os.path.join(original_task._role._role_path, 'tasks', cumulative_path)
++ candidates = [loader.path_dwim_relative(original_task._role._role_path, 'tasks', include_target),
++ loader.path_dwim_relative(new_basedir, 'tasks', include_target)]
+ for include_file in candidates:
+ try:
+ # may throw OSError
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/loop_control.py
++++ ansible-core-2.16.5/lib/ansible/playbook/loop_control.py
+@@ -25,9 +25,9 @@ from ansible.playbook.base import FieldA
+
+ class LoopControl(FieldAttributeBase):
+
+- loop_var = NonInheritableFieldAttribute(isa='string', default='item', always_post_validate=True)
+- index_var = NonInheritableFieldAttribute(isa='string', always_post_validate=True)
+- label = NonInheritableFieldAttribute(isa='string')
++ loop_var = NonInheritableFieldAttribute(isa='str', default='item', always_post_validate=True)
++ index_var = NonInheritableFieldAttribute(isa='str', always_post_validate=True)
++ label = NonInheritableFieldAttribute(isa='str')
+ pause = NonInheritableFieldAttribute(isa='float', default=0, always_post_validate=True)
+ extended = NonInheritableFieldAttribute(isa='bool', always_post_validate=True)
+ extended_allitems = NonInheritableFieldAttribute(isa='bool', default=True, always_post_validate=True)
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/play.py
++++ ansible-core-2.16.5/lib/ansible/playbook/play.py
+@@ -22,7 +22,7 @@ __metaclass__ = type
+ from ansible import constants as C
+ from ansible import context
+ from ansible.errors import AnsibleParserError, AnsibleAssertionError
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.collections import is_sequence
+ from ansible.module_utils.six import binary_type, string_types, text_type
+ from ansible.playbook.attribute import NonInheritableFieldAttribute
+@@ -30,7 +30,7 @@ from ansible.playbook.base import Base
+ from ansible.playbook.block import Block
+ from ansible.playbook.collectionsearch import CollectionSearch
+ from ansible.playbook.helpers import load_list_of_blocks, load_list_of_roles
+-from ansible.playbook.role import Role, hash_params
++from ansible.playbook.role import Role
+ from ansible.playbook.task import Task
+ from ansible.playbook.taggable import Taggable
+ from ansible.vars.manager import preprocess_vars
+@@ -93,7 +93,7 @@ class Play(Base, Taggable, CollectionSea
+ self._included_conditional = None
+ self._included_path = None
+ self._removed_hosts = []
+- self.role_cache = {}
++ self.ROLE_CACHE = {}
+
+ self.only_tags = set(context.CLIARGS.get('tags', [])) or frozenset(('all',))
+ self.skip_tags = set(context.CLIARGS.get('skip_tags', []))
+@@ -104,22 +104,6 @@ class Play(Base, Taggable, CollectionSea
+ def __repr__(self):
+ return self.get_name()
+
+- @property
+- def ROLE_CACHE(self):
+- """Backwards compat for custom strategies using ``play.ROLE_CACHE``
+- """
+- display.deprecated(
+- 'Play.ROLE_CACHE is deprecated in favor of Play.role_cache, or StrategyBase._get_cached_role',
+- version='2.18',
+- )
+- cache = {}
+- for path, roles in self.role_cache.items():
+- for role in roles:
+- name = role.get_name()
+- hashed_params = hash_params(role._get_hash_dict())
+- cache.setdefault(name, {})[hashed_params] = role
+- return cache
+-
+ def _validate_hosts(self, attribute, name, value):
+ # Only validate 'hosts' if a value was passed in to original data set.
+ if 'hosts' in self._ds:
+@@ -409,7 +393,7 @@ class Play(Base, Taggable, CollectionSea
+
+ def copy(self):
+ new_me = super(Play, self).copy()
+- new_me.role_cache = self.role_cache.copy()
++ new_me.ROLE_CACHE = self.ROLE_CACHE.copy()
+ new_me._included_conditional = self._included_conditional
+ new_me._included_path = self._included_path
+ new_me._action_groups = self._action_groups
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/play_context.py
++++ ansible-core-2.16.5/lib/ansible/playbook/play_context.py
+@@ -23,9 +23,11 @@ __metaclass__ = type
+
+ from ansible import constants as C
+ from ansible import context
++from ansible.module_utils.compat.paramiko import paramiko
+ from ansible.playbook.attribute import FieldAttribute
+ from ansible.playbook.base import Base
+ from ansible.utils.display import Display
++from ansible.utils.ssh_functions import check_for_controlpersist
+
+
+ display = Display()
+@@ -119,7 +121,7 @@ class PlayContext(Base):
+ def verbosity(self):
+ display.deprecated(
+ "PlayContext.verbosity is deprecated, use ansible.utils.display.Display.verbosity instead.",
+- version="2.18"
++ version=2.18
+ )
+ return self._internal_verbosity
+
+@@ -127,7 +129,7 @@ class PlayContext(Base):
+ def verbosity(self, value):
+ display.deprecated(
+ "PlayContext.verbosity is deprecated, use ansible.utils.display.Display.verbosity instead.",
+- version="2.18"
++ version=2.18
+ )
+ self._internal_verbosity = value
+
+@@ -318,6 +320,10 @@ class PlayContext(Base):
+ display.warning('The "%s" connection plugin has an improperly configured remote target value, '
+ 'forcing "inventory_hostname" templated value instead of the string' % new_info.connection)
+
++ # set no_log to default if it was not previously set
++ if new_info.no_log is None:
++ new_info.no_log = C.DEFAULT_NO_LOG
++
+ if task.check_mode is not None:
+ new_info.check_mode = task.check_mode
+
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/playbook_include.py
++++ ansible-core-2.16.5/lib/ansible/playbook/playbook_include.py
+@@ -23,9 +23,9 @@ import os
+
+ import ansible.constants as C
+ from ansible.errors import AnsibleParserError, AnsibleAssertionError
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+ from ansible.module_utils.six import string_types
+-from ansible.parsing.splitter import split_args
++from ansible.parsing.splitter import split_args, parse_kv
+ from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping
+ from ansible.playbook.attribute import NonInheritableFieldAttribute
+ from ansible.playbook.base import Base
+@@ -48,7 +48,7 @@ class PlaybookInclude(Base, Conditional,
+ def load(data, basedir, variable_manager=None, loader=None):
+ return PlaybookInclude().load_data(ds=data, basedir=basedir, variable_manager=variable_manager, loader=loader)
+
+- def load_data(self, ds, variable_manager=None, loader=None, basedir=None):
++ def load_data(self, ds, basedir, variable_manager=None, loader=None):
+ '''
+ Overrides the base load_data(), as we're actually going to return a new
+ Playbook() object rather than a PlaybookInclude object
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/role/__init__.py
++++ ansible-core-2.16.5/lib/ansible/playbook/role/__init__.py
+@@ -22,17 +22,15 @@ __metaclass__ = type
+ import os
+
+ from collections.abc import Container, Mapping, Set, Sequence
+-from types import MappingProxyType
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleParserError, AnsibleAssertionError
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.six import binary_type, text_type
+ from ansible.playbook.attribute import FieldAttribute
+ from ansible.playbook.base import Base
+ from ansible.playbook.collectionsearch import CollectionSearch
+ from ansible.playbook.conditional import Conditional
+-from ansible.playbook.delegatable import Delegatable
+ from ansible.playbook.helpers import load_list_of_blocks
+ from ansible.playbook.role.metadata import RoleMetadata
+ from ansible.playbook.taggable import Taggable
+@@ -98,32 +96,22 @@ def hash_params(params):
+ return frozenset((params,))
+
+
+-class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable):
++class Role(Base, Conditional, Taggable, CollectionSearch):
+
+- def __init__(self, play=None, from_files=None, from_include=False, validate=True, public=None, static=True):
++ delegate_to = FieldAttribute(isa='string')
++ delegate_facts = FieldAttribute(isa='bool')
++
++ def __init__(self, play=None, from_files=None, from_include=False, validate=True):
+ self._role_name = None
+ self._role_path = None
+ self._role_collection = None
+ self._role_params = dict()
+ self._loader = None
+- self.static = static
+-
+- # includes (static=false) default to private, while imports (static=true) default to public
+- # but both can be overriden by global config if set
+- if public is None:
+- global_private, origin = C.config.get_config_value_and_origin('DEFAULT_PRIVATE_ROLE_VARS')
+- if origin == 'default':
+- self.public = static
+- else:
+- self.public = not global_private
+- else:
+- self.public = public
+
+- self._metadata = RoleMetadata()
++ self._metadata = None
+ self._play = play
+ self._parents = []
+ self._dependencies = []
+- self._all_dependencies = None
+ self._task_blocks = []
+ self._handler_blocks = []
+ self._compiled_handler_blocks = None
+@@ -140,8 +128,6 @@ class Role(Base, Conditional, Taggable,
+ # Indicates whether this role was included via include/import_role
+ self.from_include = from_include
+
+- self._hash = None
+-
+ super(Role, self).__init__()
+
+ def __repr__(self):
+@@ -152,54 +138,49 @@ class Role(Base, Conditional, Taggable,
+ return '.'.join(x for x in (self._role_collection, self._role_name) if x)
+ return self._role_name
+
+- def get_role_path(self):
+- # Purposefully using realpath for canonical path
+- return os.path.realpath(self._role_path)
+-
+- def _get_hash_dict(self):
+- if self._hash:
+- return self._hash
+- self._hash = MappingProxyType(
+- {
+- 'name': self.get_name(),
+- 'path': self.get_role_path(),
+- 'params': MappingProxyType(self.get_role_params()),
+- 'when': self.when,
+- 'tags': self.tags,
+- 'from_files': MappingProxyType(self._from_files),
+- 'vars': MappingProxyType(self.vars),
+- 'from_include': self.from_include,
+- }
+- )
+- return self._hash
+-
+- def __eq__(self, other):
+- if not isinstance(other, Role):
+- return False
+-
+- return self._get_hash_dict() == other._get_hash_dict()
+-
+ @staticmethod
+- def load(role_include, play, parent_role=None, from_files=None, from_include=False, validate=True, public=None, static=True):
++ def load(role_include, play, parent_role=None, from_files=None, from_include=False, validate=True):
++
+ if from_files is None:
+ from_files = {}
+ try:
++ # The ROLE_CACHE is a dictionary of role names, with each entry
++ # containing another dictionary corresponding to a set of parameters
++ # specified for a role as the key and the Role() object itself.
++ # We use frozenset to make the dictionary hashable.
++
++ params = role_include.get_role_params()
++ if role_include.when is not None:
++ params['when'] = role_include.when
++ if role_include.tags is not None:
++ params['tags'] = role_include.tags
++ if from_files is not None:
++ params['from_files'] = from_files
++ if role_include.vars:
++ params['vars'] = role_include.vars
++
++ params['from_include'] = from_include
++
++ hashed_params = hash_params(params)
++ if role_include.get_name() in play.ROLE_CACHE:
++ for (entry, role_obj) in play.ROLE_CACHE[role_include.get_name()].items():
++ if hashed_params == entry:
++ if parent_role:
++ role_obj.add_parent(parent_role)
++ return role_obj
++
+ # TODO: need to fix cycle detection in role load (maybe use an empty dict
+ # for the in-flight in role cache as a sentinel that we're already trying to load
+ # that role?)
+ # see https://github.com/ansible/ansible/issues/61527
+- r = Role(play=play, from_files=from_files, from_include=from_include, validate=validate, public=public, static=static)
++ r = Role(play=play, from_files=from_files, from_include=from_include, validate=validate)
+ r._load_role_data(role_include, parent_role=parent_role)
+
+- role_path = r.get_role_path()
+- if role_path not in play.role_cache:
+- play.role_cache[role_path] = []
+-
+- # Using the role path as a cache key is done to improve performance when a large number of roles
+- # are in use in the play
+- if r not in play.role_cache[role_path]:
+- play.role_cache[role_path].append(r)
++ if role_include.get_name() not in play.ROLE_CACHE:
++ play.ROLE_CACHE[role_include.get_name()] = dict()
+
++ # FIXME: how to handle cache keys for collection-based roles, since they're technically adjustable per task?
++ play.ROLE_CACHE[role_include.get_name()][hashed_params] = r
+ return r
+
+ except RuntimeError:
+@@ -240,6 +221,8 @@ class Role(Base, Conditional, Taggable,
+ if metadata:
+ self._metadata = RoleMetadata.load(metadata, owner=self, variable_manager=self._variable_manager, loader=self._loader)
+ self._dependencies = self._load_dependencies()
++ else:
++ self._metadata = RoleMetadata()
+
+ # reset collections list; roles do not inherit collections from parents, just use the defaults
+ # FUTURE: use a private config default for this so we can allow it to be overridden later
+@@ -438,9 +421,10 @@ class Role(Base, Conditional, Taggable,
+ '''
+
+ deps = []
+- for role_include in self._metadata.dependencies:
+- r = Role.load(role_include, play=self._play, parent_role=self, static=self.static)
+- deps.append(r)
++ if self._metadata:
++ for role_include in self._metadata.dependencies:
++ r = Role.load(role_include, play=self._play, parent_role=self)
++ deps.append(r)
+
+ return deps
+
+@@ -457,13 +441,6 @@ class Role(Base, Conditional, Taggable,
+ def get_parents(self):
+ return self._parents
+
+- def get_dep_chain(self):
+- dep_chain = []
+- for parent in self._parents:
+- dep_chain.extend(parent.get_dep_chain())
+- dep_chain.append(parent)
+- return dep_chain
+-
+ def get_default_vars(self, dep_chain=None):
+ dep_chain = [] if dep_chain is None else dep_chain
+
+@@ -476,15 +453,14 @@ class Role(Base, Conditional, Taggable,
+ default_vars = combine_vars(default_vars, self._default_vars)
+ return default_vars
+
+- def get_inherited_vars(self, dep_chain=None, only_exports=False):
++ def get_inherited_vars(self, dep_chain=None):
+ dep_chain = [] if dep_chain is None else dep_chain
+
+ inherited_vars = dict()
+
+ if dep_chain:
+ for parent in dep_chain:
+- if not only_exports:
+- inherited_vars = combine_vars(inherited_vars, parent.vars)
++ inherited_vars = combine_vars(inherited_vars, parent.vars)
+ inherited_vars = combine_vars(inherited_vars, parent._role_vars)
+ return inherited_vars
+
+@@ -498,36 +474,18 @@ class Role(Base, Conditional, Taggable,
+ params = combine_vars(params, self._role_params)
+ return params
+
+- def get_vars(self, dep_chain=None, include_params=True, only_exports=False):
++ def get_vars(self, dep_chain=None, include_params=True):
+ dep_chain = [] if dep_chain is None else dep_chain
+
+- all_vars = {}
+-
+- # get role_vars: from parent objects
+- # TODO: is this right precedence for inherited role_vars?
+- all_vars = self.get_inherited_vars(dep_chain, only_exports=only_exports)
++ all_vars = self.get_inherited_vars(dep_chain)
+
+- # get exported variables from meta/dependencies
+- seen = []
+ for dep in self.get_all_dependencies():
+- # Avoid reruning dupe deps since they can have vars from previous invocations and they accumulate in deps
+- # TODO: re-examine dep loading to see if we are somehow improperly adding the same dep too many times
+- if dep not in seen:
+- # only take 'exportable' vars from deps
+- all_vars = combine_vars(all_vars, dep.get_vars(include_params=False, only_exports=True))
+- seen.append(dep)
++ all_vars = combine_vars(all_vars, dep.get_vars(include_params=include_params))
+
+- # role_vars come from vars/ in a role
++ all_vars = combine_vars(all_vars, self.vars)
+ all_vars = combine_vars(all_vars, self._role_vars)
+-
+- if not only_exports:
+- # include_params are 'inline variables' in role invocation. - {role: x, varname: value}
+- if include_params:
+- # TODO: add deprecation notice
+- all_vars = combine_vars(all_vars, self.get_role_params(dep_chain=dep_chain))
+-
+- # these come from vars: keyword in role invocation. - {role: x, vars: {varname: value}}
+- all_vars = combine_vars(all_vars, self.vars)
++ if include_params:
++ all_vars = combine_vars(all_vars, self.get_role_params(dep_chain=dep_chain))
+
+ return all_vars
+
+@@ -539,15 +497,15 @@ class Role(Base, Conditional, Taggable,
+ Returns a list of all deps, built recursively from all child dependencies,
+ in the proper order in which they should be executed or evaluated.
+ '''
+- if self._all_dependencies is None:
+
+- self._all_dependencies = []
+- for dep in self.get_direct_dependencies():
+- for child_dep in dep.get_all_dependencies():
+- self._all_dependencies.append(child_dep)
+- self._all_dependencies.append(dep)
++ child_deps = []
++
++ for dep in self.get_direct_dependencies():
++ for child_dep in dep.get_all_dependencies():
++ child_deps.append(child_dep)
++ child_deps.append(dep)
+
+- return self._all_dependencies
++ return child_deps
+
+ def get_task_blocks(self):
+ return self._task_blocks[:]
+@@ -649,7 +607,8 @@ class Role(Base, Conditional, Taggable,
+ res['_had_task_run'] = self._had_task_run.copy()
+ res['_completed'] = self._completed.copy()
+
+- res['_metadata'] = self._metadata.serialize()
++ if self._metadata:
++ res['_metadata'] = self._metadata.serialize()
+
+ if include_deps:
+ deps = []
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/role/include.py
++++ ansible-core-2.16.5/lib/ansible/playbook/role/include.py
+@@ -22,21 +22,24 @@ __metaclass__ = type
+ from ansible.errors import AnsibleError, AnsibleParserError
+ from ansible.module_utils.six import string_types
+ from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject
+-from ansible.playbook.delegatable import Delegatable
++from ansible.playbook.attribute import FieldAttribute
+ from ansible.playbook.role.definition import RoleDefinition
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+
+ __all__ = ['RoleInclude']
+
+
+-class RoleInclude(RoleDefinition, Delegatable):
++class RoleInclude(RoleDefinition):
+
+ """
+ A derivative of RoleDefinition, used by playbook code when a role
+ is included for execution in a play.
+ """
+
++ delegate_to = FieldAttribute(isa='string')
++ delegate_facts = FieldAttribute(isa='bool', default=False)
++
+ def __init__(self, play=None, role_basedir=None, variable_manager=None, loader=None, collection_list=None):
+ super(RoleInclude, self).__init__(play=play, role_basedir=role_basedir, variable_manager=variable_manager,
+ loader=loader, collection_list=collection_list)
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/role/metadata.py
++++ ansible-core-2.16.5/lib/ansible/playbook/role/metadata.py
+@@ -22,7 +22,7 @@ __metaclass__ = type
+ import os
+
+ from ansible.errors import AnsibleParserError, AnsibleError
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.six import string_types
+ from ansible.playbook.attribute import NonInheritableFieldAttribute
+ from ansible.playbook.base import Base
+@@ -41,7 +41,7 @@ class RoleMetadata(Base, CollectionSearc
+
+ allow_duplicates = NonInheritableFieldAttribute(isa='bool', default=False)
+ dependencies = NonInheritableFieldAttribute(isa='list', default=list)
+- galaxy_info = NonInheritableFieldAttribute(isa='dict')
++ galaxy_info = NonInheritableFieldAttribute(isa='GalaxyInfo')
+ argument_specs = NonInheritableFieldAttribute(isa='dict', default=dict)
+
+ def __init__(self, owner=None):
+@@ -110,6 +110,15 @@ class RoleMetadata(Base, CollectionSearc
+ except AssertionError as e:
+ raise AnsibleParserError("A malformed list of role dependencies was encountered.", obj=self._ds, orig_exc=e)
+
++ def _load_galaxy_info(self, attr, ds):
++ '''
++ This is a helper loading function for the galaxy info entry
++ in the metadata, which returns a GalaxyInfo object rather than
++ a simple dictionary.
++ '''
++
++ return ds
++
+ def serialize(self):
+ return dict(
+ allow_duplicates=self._allow_duplicates,
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/role_include.py
++++ ansible-core-2.16.5/lib/ansible/playbook/role_include.py
+@@ -23,6 +23,7 @@ from os.path import basename
+ import ansible.constants as C
+ from ansible.errors import AnsibleParserError
+ from ansible.playbook.attribute import NonInheritableFieldAttribute
++from ansible.playbook.block import Block
+ from ansible.playbook.task_include import TaskInclude
+ from ansible.playbook.role import Role
+ from ansible.playbook.role.include import RoleInclude
+@@ -49,10 +50,10 @@ class IncludeRole(TaskInclude):
+
+ # =================================================================================
+ # ATTRIBUTES
+- public = NonInheritableFieldAttribute(isa='bool', default=None, private=False, always_post_validate=True)
+
+ # private as this is a 'module options' vs a task property
+ allow_duplicates = NonInheritableFieldAttribute(isa='bool', default=True, private=True, always_post_validate=True)
++ public = NonInheritableFieldAttribute(isa='bool', default=False, private=True, always_post_validate=True)
+ rolespec_validate = NonInheritableFieldAttribute(isa='bool', default=True, private=True, always_post_validate=True)
+
+ def __init__(self, block=None, role=None, task_include=None):
+@@ -88,18 +89,22 @@ class IncludeRole(TaskInclude):
+
+ # build role
+ actual_role = Role.load(ri, myplay, parent_role=self._parent_role, from_files=from_files,
+- from_include=True, validate=self.rolespec_validate, public=self.public, static=self.statically_loaded)
++ from_include=True, validate=self.rolespec_validate)
+ actual_role._metadata.allow_duplicates = self.allow_duplicates
+
+- # add role to play
+- myplay.roles.append(actual_role)
++ if self.statically_loaded or self.public:
++ myplay.roles.append(actual_role)
+
+ # save this for later use
+ self._role_path = actual_role._role_path
+
+ # compile role with parent roles as dependencies to ensure they inherit
+ # variables
+- dep_chain = actual_role.get_dep_chain()
++ if not self._parent_role:
++ dep_chain = []
++ else:
++ dep_chain = list(self._parent_role._parents)
++ dep_chain.append(self._parent_role)
+
+ p_block = self.build_parent_block()
+
+@@ -113,7 +118,7 @@ class IncludeRole(TaskInclude):
+ b.collections = actual_role.collections
+
+ # updated available handlers in play
+- handlers = actual_role.get_handler_blocks(play=myplay, dep_chain=dep_chain)
++ handlers = actual_role.get_handler_blocks(play=myplay)
+ for h in handlers:
+ h._parent = p_block
+ myplay.handlers = myplay.handlers + handlers
+@@ -132,7 +137,6 @@ class IncludeRole(TaskInclude):
+ if ir._role_name is None:
+ raise AnsibleParserError("'name' is a required field for %s." % ir.action, obj=data)
+
+- # public is only valid argument for includes, imports are always 'public' (after they run)
+ if 'public' in ir.args and ir.action not in C._ACTION_INCLUDE_ROLE:
+ raise AnsibleParserError('Invalid options for %s: public' % ir.action, obj=data)
+
+@@ -141,7 +145,7 @@ class IncludeRole(TaskInclude):
+ if bad_opts:
+ raise AnsibleParserError('Invalid options for %s: %s' % (ir.action, ','.join(list(bad_opts))), obj=data)
+
+- # build options for role include/import tasks
++ # build options for role includes
+ for key in my_arg_names.intersection(IncludeRole.FROM_ARGS):
+ from_key = key.removesuffix('_from')
+ args_value = ir.args.get(key)
+@@ -149,7 +153,6 @@ class IncludeRole(TaskInclude):
+ raise AnsibleParserError('Expected a string for %s but got %s instead' % (key, type(args_value)))
+ ir._from_files[from_key] = basename(args_value)
+
+- # apply is only valid for includes, not imports as they inherit directly
+ apply_attrs = ir.args.get('apply', {})
+ if apply_attrs and ir.action not in C._ACTION_INCLUDE_ROLE:
+ raise AnsibleParserError('Invalid options for %s: apply' % ir.action, obj=data)
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/taggable.py
++++ ansible-core-2.16.5/lib/ansible/playbook/taggable.py
+@@ -23,17 +23,6 @@ from ansible.errors import AnsibleError
+ from ansible.module_utils.six import string_types
+ from ansible.playbook.attribute import FieldAttribute
+ from ansible.template import Templar
+-from ansible.utils.sentinel import Sentinel
+-
+-
+-def _flatten_tags(tags: list) -> list:
+- rv = set()
+- for tag in tags:
+- if isinstance(tag, list):
+- rv.update(tag)
+- else:
+- rv.add(tag)
+- return list(rv)
+
+
+ class Taggable:
+@@ -45,7 +34,11 @@ class Taggable:
+ if isinstance(ds, list):
+ return ds
+ elif isinstance(ds, string_types):
+- return [x.strip() for x in ds.split(',')]
++ value = ds.split(',')
++ if isinstance(value, list):
++ return [x.strip() for x in value]
++ else:
++ return [ds]
+ else:
+ raise AnsibleError('tags must be specified as a list', obj=ds)
+
+@@ -54,12 +47,16 @@ class Taggable:
+
+ if self.tags:
+ templar = Templar(loader=self._loader, variables=all_vars)
+- obj = self
+- while obj is not None:
+- if (_tags := getattr(obj, "_tags", Sentinel)) is not Sentinel:
+- obj._tags = _flatten_tags(templar.template(_tags))
+- obj = obj._parent
+- tags = set(self.tags)
++ tags = templar.template(self.tags)
++
++ _temp_tags = set()
++ for tag in tags:
++ if isinstance(tag, list):
++ _temp_tags.update(tag)
++ else:
++ _temp_tags.add(tag)
++ tags = _temp_tags
++ self.tags = list(tags)
+ else:
+ # this makes isdisjoint work for untagged
+ tags = self.untagged
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/task.py
++++ ansible-core-2.16.5/lib/ansible/playbook/task.py
+@@ -21,19 +21,17 @@ __metaclass__ = type
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleParserError, AnsibleUndefinedVariable, AnsibleAssertionError
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.six import string_types
+ from ansible.parsing.mod_args import ModuleArgsParser
+ from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping
+ from ansible.plugins.loader import lookup_loader
+-from ansible.playbook.attribute import NonInheritableFieldAttribute
++from ansible.playbook.attribute import FieldAttribute, NonInheritableFieldAttribute
+ from ansible.playbook.base import Base
+ from ansible.playbook.block import Block
+ from ansible.playbook.collectionsearch import CollectionSearch
+ from ansible.playbook.conditional import Conditional
+-from ansible.playbook.delegatable import Delegatable
+ from ansible.playbook.loop_control import LoopControl
+-from ansible.playbook.notifiable import Notifiable
+ from ansible.playbook.role import Role
+ from ansible.playbook.taggable import Taggable
+ from ansible.utils.collection_loader import AnsibleCollectionConfig
+@@ -45,7 +43,7 @@ __all__ = ['Task']
+ display = Display()
+
+
+-class Task(Base, Conditional, Taggable, CollectionSearch, Notifiable, Delegatable):
++class Task(Base, Conditional, Taggable, CollectionSearch):
+
+ """
+ A task is a language feature that represents a call to a module, with given arguments and other parameters.
+@@ -74,12 +72,15 @@ class Task(Base, Conditional, Taggable,
+ async_val = NonInheritableFieldAttribute(isa='int', default=0, alias='async')
+ changed_when = NonInheritableFieldAttribute(isa='list', default=list)
+ delay = NonInheritableFieldAttribute(isa='int', default=5)
++ delegate_to = FieldAttribute(isa='string')
++ delegate_facts = FieldAttribute(isa='bool')
+ failed_when = NonInheritableFieldAttribute(isa='list', default=list)
+- loop = NonInheritableFieldAttribute(isa='list')
++ loop = NonInheritableFieldAttribute()
+ loop_control = NonInheritableFieldAttribute(isa='class', class_type=LoopControl, default=LoopControl)
++ notify = FieldAttribute(isa='list')
+ poll = NonInheritableFieldAttribute(isa='int', default=C.DEFAULT_POLL_INTERVAL)
+ register = NonInheritableFieldAttribute(isa='string', static=True)
+- retries = NonInheritableFieldAttribute(isa='int') # default is set in TaskExecutor
++ retries = NonInheritableFieldAttribute(isa='int', default=3)
+ until = NonInheritableFieldAttribute(isa='list', default=list)
+
+ # deprecated, used to be loop and loop_args but loop has been repurposed
+@@ -137,7 +138,7 @@ class Task(Base, Conditional, Taggable,
+
+ def __repr__(self):
+ ''' returns a human readable representation of the task '''
+- if self.action in C._ACTION_META:
++ if self.get_name() in C._ACTION_META:
+ return "TASK: meta (%s)" % self.args['_raw_params']
+ else:
+ return "TASK: %s" % self.get_name()
+@@ -532,9 +533,3 @@ class Task(Base, Conditional, Taggable,
+ return self._parent
+ return self._parent.get_first_parent_include()
+ return None
+-
+- def get_play(self):
+- parent = self._parent
+- while not isinstance(parent, Block):
+- parent = parent._parent
+- return parent._play
+--- ansible-core-2.16.5.orig/lib/ansible/playbook/task_include.py
++++ ansible-core-2.16.5/lib/ansible/playbook/task_include.py
+@@ -35,7 +35,7 @@ class TaskInclude(Task):
+
+ """
+ A task include is derived from a regular task to handle the special
+- circumstances related to the `- include_*: ...` task.
++ circumstances related to the `- include: ...` task.
+ """
+
+ BASE = frozenset(('file', '_raw_params')) # directly assigned
+@@ -105,6 +105,29 @@ class TaskInclude(Task):
+ new_me.statically_loaded = self.statically_loaded
+ return new_me
+
++ def get_vars(self):
++ '''
++ We override the parent Task() classes get_vars here because
++ we need to include the args of the include into the vars as
++ they are params to the included tasks. But ONLY for 'include'
++ '''
++ if self.action not in C._ACTION_INCLUDE:
++ all_vars = super(TaskInclude, self).get_vars()
++ else:
++ all_vars = dict()
++ if self._parent:
++ all_vars |= self._parent.get_vars()
++
++ all_vars |= self.vars
++ all_vars |= self.args
++
++ if 'tags' in all_vars:
++ del all_vars['tags']
++ if 'when' in all_vars:
++ del all_vars['when']
++
++ return all_vars
++
+ def build_parent_block(self):
+ '''
+ This method is used to create the parent block for the included tasks
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/__init__.py
+@@ -28,7 +28,7 @@ import typing as t
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.six import string_types
+ from ansible.utils.display import Display
+
+@@ -55,9 +55,6 @@ class AnsiblePlugin(ABC):
+ # allow extra passthrough parameters
+ allow_extras = False
+
+- # Set by plugin loader
+- _load_name: str
+-
+ def __init__(self):
+ self._options = {}
+ self._defs = None
+@@ -72,17 +69,12 @@ class AnsiblePlugin(ABC):
+ possible_fqcns.add(name)
+ return bool(possible_fqcns.intersection(set(self.ansible_aliases)))
+
+- def get_option_and_origin(self, option, hostvars=None):
+- try:
+- option_value, origin = C.config.get_config_value_and_origin(option, plugin_type=self.plugin_type, plugin_name=self._load_name, variables=hostvars)
+- except AnsibleError as e:
+- raise KeyError(to_native(e))
+- return option_value, origin
+-
+ def get_option(self, option, hostvars=None):
+-
+ if option not in self._options:
+- option_value, dummy = self.get_option_and_origin(option, hostvars=hostvars)
++ try:
++ option_value = C.config.get_config_value(option, plugin_type=self.plugin_type, plugin_name=self._load_name, variables=hostvars)
++ except AnsibleError as e:
++ raise KeyError(to_native(e))
+ self.set_option(option, option_value)
+ return self._options.get(option)
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/__init__.py
+@@ -27,7 +27,7 @@ from ansible.module_utils.common.arg_spe
+ from ansible.module_utils.errors import UnsupportedError
+ from ansible.module_utils.json_utils import _filter_non_json_lines
+ from ansible.module_utils.six import binary_type, string_types, text_type
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.parsing.utils.jsonify import jsonify
+ from ansible.release import __version__
+ from ansible.utils.collection_loader import resource_from_fqcr
+@@ -39,18 +39,6 @@ from ansible.utils.plugin_docs import ge
+ display = Display()
+
+
+-def _validate_utf8_json(d):
+- if isinstance(d, text_type):
+- # Purposefully not using to_bytes here for performance reasons
+- d.encode(encoding='utf-8', errors='strict')
+- elif isinstance(d, dict):
+- for o in d.items():
+- _validate_utf8_json(o)
+- elif isinstance(d, (list, tuple)):
+- for o in d:
+- _validate_utf8_json(o)
+-
+-
+ class ActionBase(ABC):
+
+ '''
+@@ -63,13 +51,6 @@ class ActionBase(ABC):
+ # A set of valid arguments
+ _VALID_ARGS = frozenset([]) # type: frozenset[str]
+
+- # behavioral attributes
+- BYPASS_HOST_LOOP = False
+- TRANSFERS_FILES = False
+- _requires_connection = True
+- _supports_check_mode = True
+- _supports_async = False
+-
+ def __init__(self, task, connection, play_context, loader, templar, shared_loader_obj):
+ self._task = task
+ self._connection = connection
+@@ -79,16 +60,20 @@ class ActionBase(ABC):
+ self._shared_loader_obj = shared_loader_obj
+ self._cleanup_remote_tmp = False
+
++ self._supports_check_mode = True
++ self._supports_async = False
++
+ # interpreter discovery state
+ self._discovered_interpreter_key = None
+ self._discovered_interpreter = False
+ self._discovery_deprecation_warnings = []
+ self._discovery_warnings = []
+- self._used_interpreter = None
+
+ # Backwards compat: self._display isn't really needed, just import the global display and use that.
+ self._display = display
+
++ self._used_interpreter = None
++
+ @abstractmethod
+ def run(self, tmp=None, task_vars=None):
+ """ Action Plugins should implement this method to perform their
+@@ -299,8 +284,7 @@ class ActionBase(ABC):
+ try:
+ (module_data, module_style, module_shebang) = modify_module(module_name, module_path, module_args, self._templar,
+ task_vars=use_vars,
+- module_compression=C.config.get_config_value('DEFAULT_MODULE_COMPRESSION',
+- variables=task_vars),
++ module_compression=self._play_context.module_compression,
+ async_timeout=self._task.async_val,
+ environment=final_environment,
+ remote_is_local=bool(getattr(self._connection, '_remote_is_local', False)),
+@@ -739,7 +723,8 @@ class ActionBase(ABC):
+ return remote_paths
+
+ # we'll need this down here
+- become_link = get_versioned_doclink('playbook_guide/playbooks_privilege_escalation.html')
++ become_link = get_versioned_doclink('playbook_guide/playbooks_privilege_escalation.html#risks-of-becoming-an-unprivileged-user')
++
+ # Step 3f: Common group
+ # Otherwise, we're a normal user. We failed to chown the paths to the
+ # unprivileged user, but if we have a common group with them, we should
+@@ -876,6 +861,38 @@ class ActionBase(ABC):
+
+ return mystat['stat']
+
++ def _remote_checksum(self, path, all_vars, follow=False):
++ """Deprecated. Use _execute_remote_stat() instead.
++
++ Produces a remote checksum given a path,
++ Returns a number 0-4 for specific errors instead of checksum, also ensures it is different
++ 0 = unknown error
++ 1 = file does not exist, this might not be an error
++ 2 = permissions issue
++ 3 = its a directory, not a file
++ 4 = stat module failed, likely due to not finding python
++ 5 = appropriate json module not found
++ """
++ self._display.deprecated("The '_remote_checksum()' method is deprecated. "
++ "The plugin author should update the code to use '_execute_remote_stat()' instead", "2.16")
++ x = "0" # unknown error has occurred
++ try:
++ remote_stat = self._execute_remote_stat(path, all_vars, follow=follow)
++ if remote_stat['exists'] and remote_stat['isdir']:
++ x = "3" # its a directory not a file
++ else:
++ x = remote_stat['checksum'] # if 1, file is missing
++ except AnsibleError as e:
++ errormsg = to_text(e)
++ if errormsg.endswith(u'Permission denied'):
++ x = "2" # cannot read file
++ elif errormsg.endswith(u'MODULE FAILURE'):
++ x = "4" # python not found or module uncaught exception
++ elif 'json' in errormsg:
++ x = "5" # json module needed
++ finally:
++ return x # pylint: disable=lost-exception
++
+ def _remote_expand_user(self, path, sudoable=True, pathsep=None):
+ ''' takes a remote path and performs tilde/$HOME expansion on the remote host '''
+
+@@ -1215,18 +1232,6 @@ class ActionBase(ABC):
+ display.warning(w)
+
+ data = json.loads(filtered_output)
+-
+- if C.MODULE_STRICT_UTF8_RESPONSE and not data.pop('_ansible_trusted_utf8', None):
+- try:
+- _validate_utf8_json(data)
+- except UnicodeEncodeError:
+- # When removing this, also remove the loop and latin-1 from ansible.module_utils.common.text.converters.jsonify
+- display.deprecated(
+- f'Module "{self._task.resolved_action or self._task.action}" returned non UTF-8 data in '
+- 'the JSON response. This will become an error in the future',
+- version='2.18',
+- )
+-
+ data['_ansible_parsed'] = True
+ except ValueError:
+ # not valid json, lets try to capture error
+@@ -1339,7 +1344,7 @@ class ActionBase(ABC):
+ display.debug(u"_low_level_execute_command() done: rc=%d, stdout=%s, stderr=%s" % (rc, out, err))
+ return dict(rc=rc, stdout=out, stdout_lines=out.splitlines(), stderr=err, stderr_lines=err.splitlines())
+
+- def _get_diff_data(self, destination, source, task_vars, content, source_file=True):
++ def _get_diff_data(self, destination, source, task_vars, source_file=True):
+
+ # Note: Since we do not diff the source and destination before we transform from bytes into
+ # text the diff between source and destination may not be accurate. To fix this, we'd need
+@@ -1397,10 +1402,7 @@ class ActionBase(ABC):
+ if b"\x00" in src_contents:
+ diff['src_binary'] = 1
+ else:
+- if content:
+- diff['after_header'] = destination
+- else:
+- diff['after_header'] = source
++ diff['after_header'] = source
+ diff['after'] = to_text(src_contents)
+ else:
+ display.debug(u"source of file passed in")
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/add_host.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/add_host.py
+@@ -37,11 +37,12 @@ class ActionModule(ActionBase):
+
+ # We need to be able to modify the inventory
+ BYPASS_HOST_LOOP = True
+- _requires_connection = False
+- _supports_check_mode = True
++ TRANSFERS_FILES = False
+
+ def run(self, tmp=None, task_vars=None):
+
++ self._supports_check_mode = True
++
+ result = super(ActionModule, self).run(tmp, task_vars)
+ del tmp # tmp no longer has any effect
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/assemble.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/assemble.py
+@@ -27,7 +27,7 @@ import tempfile
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleAction, _AnsibleActionDone, AnsibleActionFail
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.plugins.action import ActionBase
+ from ansible.utils.hashing import checksum_s
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/assert.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/assert.py
+@@ -27,8 +27,7 @@ from ansible.module_utils.parsing.conver
+ class ActionModule(ActionBase):
+ ''' Fail with custom message '''
+
+- _requires_connection = False
+-
++ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset(('fail_msg', 'msg', 'quiet', 'success_msg', 'that'))
+
+ def run(self, tmp=None, task_vars=None):
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/async_status.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/async_status.py
+@@ -4,6 +4,7 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
++from ansible.errors import AnsibleActionFail
+ from ansible.plugins.action import ActionBase
+ from ansible.utils.vars import merge_hash
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/command.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/command.py
+@@ -4,6 +4,7 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
++from ansible import constants as C
+ from ansible.plugins.action import ActionBase
+ from ansible.utils.vars import merge_hash
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/copy.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/copy.py
+@@ -30,7 +30,7 @@ import traceback
+ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleFileNotFound
+ from ansible.module_utils.basic import FILE_COMMON_ARGUMENTS
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.plugins.action import ActionBase
+ from ansible.utils.hashing import checksum
+@@ -286,7 +286,7 @@ class ActionModule(ActionBase):
+ # The checksums don't match and we will change or error out.
+
+ if self._play_context.diff and not raw:
+- result['diff'].append(self._get_diff_data(dest_file, source_full, task_vars, content))
++ result['diff'].append(self._get_diff_data(dest_file, source_full, task_vars))
+
+ if self._play_context.check_mode:
+ self._remove_tempfile_if_content_defined(content, content_tempfile)
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/debug.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/debug.py
+@@ -20,7 +20,7 @@ __metaclass__ = type
+
+ from ansible.errors import AnsibleUndefinedVariable
+ from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.plugins.action import ActionBase
+
+
+@@ -29,34 +29,28 @@ class ActionModule(ActionBase):
+
+ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset(('msg', 'var', 'verbosity'))
+- _requires_connection = False
+
+ def run(self, tmp=None, task_vars=None):
+ if task_vars is None:
+ task_vars = dict()
+
+- validation_result, new_module_args = self.validate_argument_spec(
+- argument_spec={
+- 'msg': {'type': 'raw', 'default': 'Hello world!'},
+- 'var': {'type': 'raw'},
+- 'verbosity': {'type': 'int', 'default': 0},
+- },
+- mutually_exclusive=(
+- ('msg', 'var'),
+- ),
+- )
++ if 'msg' in self._task.args and 'var' in self._task.args:
++ return {"failed": True, "msg": "'msg' and 'var' are incompatible options"}
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+ del tmp # tmp no longer has any effect
+
+ # get task verbosity
+- verbosity = new_module_args['verbosity']
++ verbosity = int(self._task.args.get('verbosity', 0))
+
+ if verbosity <= self._display.verbosity:
+- if new_module_args['var']:
++ if 'msg' in self._task.args:
++ result['msg'] = self._task.args['msg']
++
++ elif 'var' in self._task.args:
+ try:
+- results = self._templar.template(new_module_args['var'], convert_bare=True, fail_on_undefined=True)
+- if results == new_module_args['var']:
++ results = self._templar.template(self._task.args['var'], convert_bare=True, fail_on_undefined=True)
++ if results == self._task.args['var']:
+ # if results is not str/unicode type, raise an exception
+ if not isinstance(results, string_types):
+ raise AnsibleUndefinedVariable
+@@ -67,13 +61,13 @@ class ActionModule(ActionBase):
+ if self._display.verbosity > 0:
+ results += u": %s" % to_text(e)
+
+- if isinstance(new_module_args['var'], (list, dict)):
++ if isinstance(self._task.args['var'], (list, dict)):
+ # If var is a list or dict, use the type as key to display
+- result[to_text(type(new_module_args['var']))] = results
++ result[to_text(type(self._task.args['var']))] = results
+ else:
+- result[new_module_args['var']] = results
++ result[self._task.args['var']] = results
+ else:
+- result['msg'] = new_module_args['msg']
++ result['msg'] = 'Hello world!'
+
+ # force flag to make debug output module always verbose
+ result['_ansible_verbose_always'] = True
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/fail.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/fail.py
+@@ -26,7 +26,6 @@ class ActionModule(ActionBase):
+
+ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset(('msg',))
+- _requires_connection = False
+
+ def run(self, tmp=None, task_vars=None):
+ if task_vars is None:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/fetch.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/fetch.py
+@@ -19,7 +19,7 @@ __metaclass__ = type
+
+ import os
+ import base64
+-from ansible.errors import AnsibleConnectionFailure, AnsibleError, AnsibleActionFail, AnsibleActionSkip
++from ansible.errors import AnsibleError, AnsibleActionFail, AnsibleActionSkip
+ from ansible.module_utils.common.text.converters import to_bytes, to_text
+ from ansible.module_utils.six import string_types
+ from ansible.module_utils.parsing.convert_bool import boolean
+@@ -75,8 +75,6 @@ class ActionModule(ActionBase):
+ # Follow symlinks because fetch always follows symlinks
+ try:
+ remote_stat = self._execute_remote_stat(source, all_vars=task_vars, follow=True)
+- except AnsibleConnectionFailure:
+- raise
+ except AnsibleError as ae:
+ result['changed'] = False
+ result['file'] = source
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/gather_facts.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/gather_facts.py
+@@ -6,7 +6,6 @@ __metaclass__ = type
+
+ import os
+ import time
+-import typing as t
+
+ from ansible import constants as C
+ from ansible.executor.module_common import get_action_args_with_defaults
+@@ -17,15 +16,12 @@ from ansible.utils.vars import merge_has
+
+ class ActionModule(ActionBase):
+
+- _supports_check_mode = True
+-
+- def _get_module_args(self, fact_module: str, task_vars: dict[str, t.Any]) -> dict[str, t.Any]:
++ def _get_module_args(self, fact_module, task_vars):
+
+ mod_args = self._task.args.copy()
+
+ # deal with 'setup specific arguments'
+ if fact_module not in C._ACTION_SETUP:
+-
+ # TODO: remove in favor of controller side argspec detecing valid arguments
+ # network facts modules must support gather_subset
+ try:
+@@ -34,16 +30,16 @@ class ActionModule(ActionBase):
+ name = self._connection._load_name.split('.')[-1]
+ if name not in ('network_cli', 'httpapi', 'netconf'):
+ subset = mod_args.pop('gather_subset', None)
+- if subset not in ('all', ['all'], None):
+- self._display.warning('Not passing subset(%s) to %s' % (subset, fact_module))
++ if subset not in ('all', ['all']):
++ self._display.warning('Ignoring subset(%s) for %s' % (subset, fact_module))
+
+ timeout = mod_args.pop('gather_timeout', None)
+ if timeout is not None:
+- self._display.warning('Not passing timeout(%s) to %s' % (timeout, fact_module))
++ self._display.warning('Ignoring timeout(%s) for %s' % (timeout, fact_module))
+
+ fact_filter = mod_args.pop('filter', None)
+ if fact_filter is not None:
+- self._display.warning('Not passing filter(%s) to %s' % (fact_filter, fact_module))
++ self._display.warning('Ignoring filter(%s) for %s' % (fact_filter, fact_module))
+
+ # Strip out keys with ``None`` values, effectively mimicking ``omit`` behavior
+ # This ensures we don't pass a ``None`` value as an argument expecting a specific type
+@@ -61,7 +57,7 @@ class ActionModule(ActionBase):
+
+ return mod_args
+
+- def _combine_task_result(self, result: dict[str, t.Any], task_result: dict[str, t.Any]) -> dict[str, t.Any]:
++ def _combine_task_result(self, result, task_result):
+ filtered_res = {
+ 'ansible_facts': task_result.get('ansible_facts', {}),
+ 'warnings': task_result.get('warnings', []),
+@@ -71,7 +67,9 @@ class ActionModule(ActionBase):
+ # on conflict the last plugin processed wins, but try to do deep merge and append to lists.
+ return merge_hash(result, filtered_res, list_merge='append_rp')
+
+- def run(self, tmp: t.Optional[str] = None, task_vars: t.Optional[dict[str, t.Any]] = None) -> dict[str, t.Any]:
++ def run(self, tmp=None, task_vars=None):
++
++ self._supports_check_mode = True
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+ result['ansible_facts'] = {}
+@@ -89,23 +87,16 @@ class ActionModule(ActionBase):
+ failed = {}
+ skipped = {}
+
+- if parallel is None:
+- if len(modules) > 1:
+- parallel = True
+- else:
+- parallel = False
++ if parallel is None and len(modules) >= 1:
++ parallel = True
+ else:
+ parallel = boolean(parallel)
+
+- timeout = self._task.args.get('gather_timeout', None)
+- async_val = self._task.async_val
+-
+- if not parallel:
++ if parallel:
+ # serially execute each module
+ for fact_module in modules:
+ # just one module, no need for fancy async
+ mod_args = self._get_module_args(fact_module, task_vars)
+- # TODO: use gather_timeout to cut module execution if module itself does not support gather_timeout
+ res = self._execute_module(module_name=fact_module, module_args=mod_args, task_vars=task_vars, wrap_async=False)
+ if res.get('failed', False):
+ failed[fact_module] = res
+@@ -116,21 +107,10 @@ class ActionModule(ActionBase):
+
+ self._remove_tmp_path(self._connection._shell.tmpdir)
+ else:
+- # do it async, aka parallel
++ # do it async
+ jobs = {}
+-
+ for fact_module in modules:
+ mod_args = self._get_module_args(fact_module, task_vars)
+-
+- # if module does not handle timeout, use timeout to handle module, hijack async_val as this is what async_wrapper uses
+- # TODO: make this action compain about async/async settings, use parallel option instead .. or remove parallel in favor of async settings?
+- if timeout and 'gather_timeout' not in mod_args:
+- self._task.async_val = int(timeout)
+- elif async_val != 0:
+- self._task.async_val = async_val
+- else:
+- self._task.async_val = 0
+-
+ self._display.vvvv("Running %s" % fact_module)
+ jobs[fact_module] = (self._execute_module(module_name=fact_module, module_args=mod_args, task_vars=task_vars, wrap_async=True))
+
+@@ -152,10 +132,6 @@ class ActionModule(ActionBase):
+ else:
+ time.sleep(0.5)
+
+- # restore value for post processing
+- if self._task.async_val != async_val:
+- self._task.async_val = async_val
+-
+ if skipped:
+ result['msg'] = "The following modules were skipped: %s\n" % (', '.join(skipped.keys()))
+ result['skipped_modules'] = skipped
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/group_by.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/group_by.py
+@@ -27,7 +27,6 @@ class ActionModule(ActionBase):
+ # We need to be able to modify the inventory
+ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset(('key', 'parents'))
+- _requires_connection = False
+
+ def run(self, tmp=None, task_vars=None):
+ if task_vars is None:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/include_vars.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/include_vars.py
+@@ -6,12 +6,11 @@ __metaclass__ = type
+
+ from os import path, walk
+ import re
+-import pathlib
+
+ import ansible.constants as C
+ from ansible.errors import AnsibleError
+ from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.plugins.action import ActionBase
+ from ansible.utils.vars import combine_vars
+
+@@ -24,7 +23,6 @@ class ActionModule(ActionBase):
+ VALID_DIR_ARGUMENTS = ['dir', 'depth', 'files_matching', 'ignore_files', 'extensions', 'ignore_unknown_extensions']
+ VALID_FILE_ARGUMENTS = ['file', '_raw_params']
+ VALID_ALL = ['name', 'hash_behaviour']
+- _requires_connection = False
+
+ def _set_dir_defaults(self):
+ if not self.depth:
+@@ -183,15 +181,16 @@ class ActionModule(ActionBase):
+ alphabetical order. Do not iterate pass the set depth.
+ The default depth is unlimited.
+ """
+- sorted_walk = list(walk(self.source_dir, onerror=self._log_walk, followlinks=True))
++ current_depth = 0
++ sorted_walk = list(walk(self.source_dir, onerror=self._log_walk))
+ sorted_walk.sort(key=lambda x: x[0])
+ for current_root, current_dir, current_files in sorted_walk:
+- # Depth 1 is the root, relative_to omits the root
+- current_depth = len(pathlib.Path(current_root).relative_to(self.source_dir).parts) + 1
+- if self.depth != 0 and current_depth > self.depth:
+- continue
+- current_files.sort()
+- yield (current_root, current_files)
++ current_depth += 1
++ if current_depth <= self.depth or self.depth == 0:
++ current_files.sort()
++ yield (current_root, current_files)
++ else:
++ break
+
+ def _ignore_file(self, filename):
+ """ Return True if a file matches the list of ignore_files.
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/normal.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/normal.py
+@@ -24,24 +24,33 @@ from ansible.utils.vars import merge_has
+
+ class ActionModule(ActionBase):
+
+- _supports_check_mode = True
+- _supports_async = True
+-
+ def run(self, tmp=None, task_vars=None):
+
+ # individual modules might disagree but as the generic the action plugin, pass at this point.
++ self._supports_check_mode = True
++ self._supports_async = True
++
+ result = super(ActionModule, self).run(tmp, task_vars)
+ del tmp # tmp no longer has any effect
+
+- wrap_async = self._task.async_val and not self._connection.has_native_async
+-
+- # do work!
+- result = merge_hash(result, self._execute_module(task_vars=task_vars, wrap_async=wrap_async))
++ if not result.get('skipped'):
+
+- # hack to keep --verbose from showing all the setup module result
+- # moved from setup module as now we filter out all _ansible_ from result
+- if self._task.action in C._ACTION_SETUP:
+- result['_ansible_verbose_override'] = True
++ if result.get('invocation', {}).get('module_args'):
++ # avoid passing to modules in case of no_log
++ # should not be set anymore but here for backwards compatibility
++ del result['invocation']['module_args']
++
++ # FUTURE: better to let _execute_module calculate this internally?
++ wrap_async = self._task.async_val and not self._connection.has_native_async
++
++ # do work!
++ result = merge_hash(result, self._execute_module(task_vars=task_vars, wrap_async=wrap_async))
++
++ # hack to keep --verbose from showing all the setup module result
++ # moved from setup module as now we filter out all _ansible_ from result
++ # FIXME: is this still accurate with gather_facts etc, or does it need support for FQ and other names?
++ if self._task.action in C._ACTION_SETUP:
++ result['_ansible_verbose_override'] = True
+
+ if not wrap_async:
+ # remove a temporary path we created
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/pause.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/pause.py
+@@ -18,15 +18,92 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ import datetime
++import signal
++import sys
++import termios
+ import time
++import tty
+
+-from ansible.errors import AnsibleError, AnsiblePromptInterrupt, AnsiblePromptNoninteractive
+-from ansible.module_utils.common.text.converters import to_text
++from os import (
++ getpgrp,
++ isatty,
++ tcgetpgrp,
++)
++from ansible.errors import AnsibleError
++from ansible.module_utils._text import to_text, to_native
++from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.plugins.action import ActionBase
+ from ansible.utils.display import Display
+
+ display = Display()
+
++try:
++ import curses
++ import io
++
++ # Nest the try except since curses.error is not available if curses did not import
++ try:
++ curses.setupterm()
++ HAS_CURSES = True
++ except (curses.error, TypeError, io.UnsupportedOperation):
++ HAS_CURSES = False
++except ImportError:
++ HAS_CURSES = False
++
++MOVE_TO_BOL = b'\r'
++CLEAR_TO_EOL = b'\x1b[K'
++if HAS_CURSES:
++ # curses.tigetstr() returns None in some circumstances
++ MOVE_TO_BOL = curses.tigetstr('cr') or MOVE_TO_BOL
++ CLEAR_TO_EOL = curses.tigetstr('el') or CLEAR_TO_EOL
++
++
++def setraw(fd, when=termios.TCSAFLUSH):
++ """Put terminal into a raw mode.
++
++ Copied from ``tty`` from CPython 3.11.0, and modified to not remove OPOST from OFLAG
++
++ OPOST is kept to prevent an issue with multi line prompts from being corrupted now that display
++ is proxied via the queue from forks. The problem is a race condition, in that we proxy the display
++ over the fork, but before it can be displayed, this plugin will have continued executing, potentially
++ setting stdout and stdin to raw which remove output post processing that commonly converts NL to CRLF
++ """
++ mode = termios.tcgetattr(fd)
++ mode[tty.IFLAG] = mode[tty.IFLAG] & ~(termios.BRKINT | termios.ICRNL | termios.INPCK | termios.ISTRIP | termios.IXON)
++ # mode[tty.OFLAG] = mode[tty.OFLAG] & ~(termios.OPOST)
++ mode[tty.CFLAG] = mode[tty.CFLAG] & ~(termios.CSIZE | termios.PARENB)
++ mode[tty.CFLAG] = mode[tty.CFLAG] | termios.CS8
++ mode[tty.LFLAG] = mode[tty.LFLAG] & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG)
++ mode[tty.CC][termios.VMIN] = 1
++ mode[tty.CC][termios.VTIME] = 0
++ termios.tcsetattr(fd, when, mode)
++
++
++class AnsibleTimeoutExceeded(Exception):
++ pass
++
++
++def timeout_handler(signum, frame):
++ raise AnsibleTimeoutExceeded
++
++
++def clear_line(stdout):
++ stdout.write(b'\x1b[%s' % MOVE_TO_BOL)
++ stdout.write(b'\x1b[%s' % CLEAR_TO_EOL)
++
++
++def is_interactive(fd=None):
++ if fd is None:
++ return False
++
++ if isatty(fd):
++ # Compare the current process group to the process group associated
++ # with terminal of the given file descriptor to determine if the process
++ # is running in the background.
++ return getpgrp() == tcgetpgrp(fd)
++ else:
++ return False
++
+
+ class ActionModule(ActionBase):
+ ''' pauses execution for a length or time, or until input is received '''
+@@ -92,57 +169,143 @@ class ActionModule(ActionBase):
+ result['start'] = to_text(datetime.datetime.now())
+ result['user_input'] = b''
+
+- default_input_complete = None
+- if seconds is not None:
+- if seconds < 1:
+- seconds = 1
+-
+- # show the timer and control prompts
+- display.display("Pausing for %d seconds%s" % (seconds, echo_prompt))
+-
+- # show the prompt specified in the task
+- if new_module_args['prompt']:
+- display.display("(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r")
+- else:
+- # corner case where enter does not continue, wait for timeout/interrupt only
+- prompt = "(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r"
++ stdin_fd = None
++ old_settings = None
++ try:
++ if seconds is not None:
++ if seconds < 1:
++ seconds = 1
++
++ # setup the alarm handler
++ signal.signal(signal.SIGALRM, timeout_handler)
++ signal.alarm(seconds)
++
++ # show the timer and control prompts
++ display.display("Pausing for %d seconds%s" % (seconds, echo_prompt))
++ display.display("(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r"),
++
++ # show the prompt specified in the task
++ if new_module_args['prompt']:
++ display.display(prompt)
+
+- # don't complete on LF/CR; we expect a timeout/interrupt and ignore user input when a pause duration is specified
+- default_input_complete = tuple()
++ else:
++ display.display(prompt)
+
+- # Only echo input if no timeout is specified
+- echo = seconds is None and echo
++ # save the attributes on the existing (duped) stdin so
++ # that we can restore them later after we set raw mode
++ stdin_fd = None
++ stdout_fd = None
++ try:
++ stdin = self._connection._new_stdin.buffer
++ stdout = sys.stdout.buffer
++ stdin_fd = stdin.fileno()
++ stdout_fd = stdout.fileno()
++ except (ValueError, AttributeError):
++ # ValueError: someone is using a closed file descriptor as stdin
++ # AttributeError: someone is using a null file descriptor as stdin on windoze
++ stdin = None
++ interactive = is_interactive(stdin_fd)
++ if interactive:
++ # grab actual Ctrl+C sequence
++ try:
++ intr = termios.tcgetattr(stdin_fd)[6][termios.VINTR]
++ except Exception:
++ # unsupported/not present, use default
++ intr = b'\x03' # value for Ctrl+C
++
++ # get backspace sequences
++ try:
++ backspace = termios.tcgetattr(stdin_fd)[6][termios.VERASE]
++ except Exception:
++ backspace = [b'\x7f', b'\x08']
++
++ old_settings = termios.tcgetattr(stdin_fd)
++ setraw(stdin_fd)
++
++ # Only set stdout to raw mode if it is a TTY. This is needed when redirecting
++ # stdout to a file since a file cannot be set to raw mode.
++ if isatty(stdout_fd):
++ setraw(stdout_fd)
++
++ # Only echo input if no timeout is specified
++ if not seconds and echo:
++ new_settings = termios.tcgetattr(stdin_fd)
++ new_settings[3] = new_settings[3] | termios.ECHO
++ termios.tcsetattr(stdin_fd, termios.TCSANOW, new_settings)
++
++ # flush the buffer to make sure no previous key presses
++ # are read in below
++ termios.tcflush(stdin, termios.TCIFLUSH)
++
++ while True:
++ if not interactive:
++ if seconds is None:
++ display.warning("Not waiting for response to prompt as stdin is not interactive")
++ if seconds is not None:
++ # Give the signal handler enough time to timeout
++ time.sleep(seconds + 1)
++ break
++
++ try:
++ key_pressed = stdin.read(1)
++
++ if key_pressed == intr: # value for Ctrl+C
++ clear_line(stdout)
++ raise KeyboardInterrupt
++
++ if not seconds:
++ # read key presses and act accordingly
++ if key_pressed in (b'\r', b'\n'):
++ clear_line(stdout)
++ break
++ elif key_pressed in backspace:
++ # delete a character if backspace is pressed
++ result['user_input'] = result['user_input'][:-1]
++ clear_line(stdout)
++ if echo:
++ stdout.write(result['user_input'])
++ stdout.flush()
++ else:
++ result['user_input'] += key_pressed
++
++ except KeyboardInterrupt:
++ signal.alarm(0)
++ display.display("Press 'C' to continue the play or 'A' to abort \r"),
++ if self._c_or_a(stdin):
++ clear_line(stdout)
++ break
++
++ clear_line(stdout)
++
++ raise AnsibleError('user requested abort!')
++
++ except AnsibleTimeoutExceeded:
++ # this is the exception we expect when the alarm signal
++ # fires, so we simply ignore it to move into the cleanup
++ pass
++ finally:
++ # cleanup and save some information
++ # restore the old settings for the duped stdin stdin_fd
++ if not (None in (stdin_fd, old_settings)) and isatty(stdin_fd):
++ termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings)
++
++ duration = time.time() - start
++ result['stop'] = to_text(datetime.datetime.now())
++ result['delta'] = int(duration)
+
+- user_input = b''
+- try:
+- _user_input = display.prompt_until(prompt, private=not echo, seconds=seconds, complete_input=default_input_complete)
+- except AnsiblePromptInterrupt:
+- user_input = None
+- except AnsiblePromptNoninteractive:
+- if seconds is None:
+- display.warning("Not waiting for response to prompt as stdin is not interactive")
++ if duration_unit == 'minutes':
++ duration = round(duration / 60.0, 2)
+ else:
+- # wait specified duration
+- time.sleep(seconds)
+- else:
+- if seconds is None:
+- user_input = _user_input
+- # user interrupt
+- if user_input is None:
+- prompt = "Press 'C' to continue the play or 'A' to abort \r"
+- try:
+- user_input = display.prompt_until(prompt, private=not echo, interrupt_input=(b'a',), complete_input=(b'c',))
+- except AnsiblePromptInterrupt:
+- raise AnsibleError('user requested abort!')
+-
+- duration = time.time() - start
+- result['stop'] = to_text(datetime.datetime.now())
+- result['delta'] = int(duration)
++ duration = round(duration, 2)
++ result['stdout'] = "Paused for %s %s" % (duration, duration_unit)
+
+- if duration_unit == 'minutes':
+- duration = round(duration / 60.0, 2)
+- else:
+- duration = round(duration, 2)
+- result['stdout'] = "Paused for %s %s" % (duration, duration_unit)
+- result['user_input'] = to_text(user_input, errors='surrogate_or_strict')
++ result['user_input'] = to_text(result['user_input'], errors='surrogate_or_strict')
+ return result
++
++ def _c_or_a(self, stdin):
++ while True:
++ key_pressed = stdin.read(1)
++ if key_pressed.lower() == b'a':
++ return False
++ elif key_pressed.lower() == b'c':
++ return True
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/reboot.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/reboot.py
+@@ -8,10 +8,10 @@ __metaclass__ = type
+ import random
+ import time
+
+-from datetime import datetime, timedelta, timezone
++from datetime import datetime, timedelta
+
+ from ansible.errors import AnsibleError, AnsibleConnectionFailure
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.common.validation import check_type_list, check_type_str
+ from ansible.plugins.action import ActionBase
+ from ansible.utils.display import Display
+@@ -129,7 +129,7 @@ class ActionModule(ActionBase):
+ else:
+ args = self._get_value_from_facts('SHUTDOWN_COMMAND_ARGS', distribution, 'DEFAULT_SHUTDOWN_COMMAND_ARGS')
+
+- # Convert seconds to minutes. If less than 60, set it to 0.
++ # Convert seconds to minutes. If less that 60, set it to 0.
+ delay_min = self.pre_reboot_delay // 60
+ reboot_message = self._task.args.get('msg', self.DEFAULT_REBOOT_MESSAGE)
+ return args.format(delay_sec=self.pre_reboot_delay, delay_min=delay_min, message=reboot_message)
+@@ -236,7 +236,7 @@ class ActionModule(ActionBase):
+ display.vvv("{action}: attempting to get system boot time".format(action=self._task.action))
+ connect_timeout = self._task.args.get('connect_timeout', self._task.args.get('connect_timeout_sec', self.DEFAULT_CONNECT_TIMEOUT))
+
+- # override connection timeout from defaults to the custom value
++ # override connection timeout from defaults to custom value
+ if connect_timeout:
+ try:
+ display.debug("{action}: setting connect_timeout to {value}".format(action=self._task.action, value=connect_timeout))
+@@ -280,15 +280,14 @@ class ActionModule(ActionBase):
+ display.vvv("{action}: system successfully rebooted".format(action=self._task.action))
+
+ def do_until_success_or_timeout(self, action, reboot_timeout, action_desc, distribution, action_kwargs=None):
+- max_end_time = datetime.now(timezone.utc) + timedelta(seconds=reboot_timeout)
++ max_end_time = datetime.utcnow() + timedelta(seconds=reboot_timeout)
+ if action_kwargs is None:
+ action_kwargs = {}
+
+ fail_count = 0
+ max_fail_sleep = 12
+- last_error_msg = ''
+
+- while datetime.now(timezone.utc) < max_end_time:
++ while datetime.utcnow() < max_end_time:
+ try:
+ action(distribution=distribution, **action_kwargs)
+ if action_desc:
+@@ -300,7 +299,7 @@ class ActionModule(ActionBase):
+ self._connection.reset()
+ except AnsibleConnectionFailure:
+ pass
+- # Use exponential backoff with a max timeout, plus a little bit of randomness
++ # Use exponential backoff with a max timout, plus a little bit of randomness
+ random_int = random.randint(0, 1000) / 1000
+ fail_sleep = 2 ** fail_count + random_int
+ if fail_sleep > max_fail_sleep:
+@@ -311,18 +310,14 @@ class ActionModule(ActionBase):
+ error = to_text(e).splitlines()[-1]
+ except IndexError as e:
+ error = to_text(e)
+- last_error_msg = f"{self._task.action}: {action_desc} fail '{error}'"
+- msg = f"{last_error_msg}, retrying in {fail_sleep:.4f} seconds..."
+-
+- display.debug(msg)
+- display.vvv(msg)
++ display.debug("{action}: {desc} fail '{err}', retrying in {sleep:.4} seconds...".format(
++ action=self._task.action,
++ desc=action_desc,
++ err=error,
++ sleep=fail_sleep))
+ fail_count += 1
+ time.sleep(fail_sleep)
+
+- if last_error_msg:
+- msg = f"Last error message before the timeout exception - {last_error_msg}"
+- display.debug(msg)
+- display.vvv(msg)
+ raise TimedOutException('Timed out waiting for {desc} (timeout={timeout})'.format(desc=action_desc, timeout=reboot_timeout))
+
+ def perform_reboot(self, task_vars, distribution):
+@@ -341,7 +336,7 @@ class ActionModule(ActionBase):
+ display.debug('{action}: AnsibleConnectionFailure caught and handled: {error}'.format(action=self._task.action, error=to_text(e)))
+ reboot_result['rc'] = 0
+
+- result['start'] = datetime.now(timezone.utc)
++ result['start'] = datetime.utcnow()
+
+ if reboot_result['rc'] != 0:
+ result['failed'] = True
+@@ -411,7 +406,7 @@ class ActionModule(ActionBase):
+ self._supports_check_mode = True
+ self._supports_async = True
+
+- # If running with local connection, fail so we don't reboot ourselves
++ # If running with local connection, fail so we don't reboot ourself
+ if self._connection.transport == 'local':
+ msg = 'Running {0} with local connection would reboot the control node.'.format(self._task.action)
+ return {'changed': False, 'elapsed': 0, 'rebooted': False, 'failed': True, 'msg': msg}
+@@ -452,7 +447,7 @@ class ActionModule(ActionBase):
+
+ if reboot_result['failed']:
+ result = reboot_result
+- elapsed = datetime.now(timezone.utc) - reboot_result['start']
++ elapsed = datetime.utcnow() - reboot_result['start']
+ result['elapsed'] = elapsed.seconds
+ return result
+
+@@ -464,7 +459,7 @@ class ActionModule(ActionBase):
+ # Make sure reboot was successful
+ result = self.validate_reboot(distribution, original_connection_timeout, action_kwargs={'previous_boot_time': previous_boot_time})
+
+- elapsed = datetime.now(timezone.utc) - reboot_result['start']
++ elapsed = datetime.utcnow() - reboot_result['start']
+ result['elapsed'] = elapsed.seconds
+
+ return result
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/script.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/script.py
+@@ -23,7 +23,7 @@ import shlex
+
+ from ansible.errors import AnsibleError, AnsibleAction, _AnsibleActionDone, AnsibleActionFail, AnsibleActionSkip
+ from ansible.executor.powershell import module_manifest as ps_manifest
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.plugins.action import ActionBase
+
+
+@@ -40,25 +40,11 @@ class ActionModule(ActionBase):
+ if task_vars is None:
+ task_vars = dict()
+
+- validation_result, new_module_args = self.validate_argument_spec(
+- argument_spec={
+- '_raw_params': {},
+- 'cmd': {'type': 'str'},
+- 'creates': {'type': 'str'},
+- 'removes': {'type': 'str'},
+- 'chdir': {'type': 'str'},
+- 'executable': {'type': 'str'},
+- },
+- required_one_of=[
+- ['_raw_params', 'cmd']
+- ]
+- )
+-
+ result = super(ActionModule, self).run(tmp, task_vars)
+ del tmp # tmp no longer has any effect
+
+ try:
+- creates = new_module_args['creates']
++ creates = self._task.args.get('creates')
+ if creates:
+ # do not run the command if the line contains creates=filename
+ # and the filename already exists. This allows idempotence
+@@ -66,7 +52,7 @@ class ActionModule(ActionBase):
+ if self._remote_file_exists(creates):
+ raise AnsibleActionSkip("%s exists, matching creates option" % creates)
+
+- removes = new_module_args['removes']
++ removes = self._task.args.get('removes')
+ if removes:
+ # do not run the command if the line contains removes=filename
+ # and the filename does not exist. This allows idempotence
+@@ -76,7 +62,7 @@ class ActionModule(ActionBase):
+
+ # The chdir must be absolute, because a relative path would rely on
+ # remote node behaviour & user config.
+- chdir = new_module_args['chdir']
++ chdir = self._task.args.get('chdir')
+ if chdir:
+ # Powershell is the only Windows-path aware shell
+ if getattr(self._connection._shell, "_IS_WINDOWS", False) and \
+@@ -89,14 +75,13 @@ class ActionModule(ActionBase):
+ # Split out the script as the first item in raw_params using
+ # shlex.split() in order to support paths and files with spaces in the name.
+ # Any arguments passed to the script will be added back later.
+- raw_params = to_native(new_module_args.get('_raw_params', ''), errors='surrogate_or_strict')
++ raw_params = to_native(self._task.args.get('_raw_params', ''), errors='surrogate_or_strict')
+ parts = [to_text(s, errors='surrogate_or_strict') for s in shlex.split(raw_params.strip())]
+ source = parts[0]
+
+ # Support executable paths and files with spaces in the name.
+- executable = new_module_args['executable']
+- if executable:
+- executable = to_native(new_module_args['executable'], errors='surrogate_or_strict')
++ executable = to_native(self._task.args.get('executable', ''), errors='surrogate_or_strict')
++
+ try:
+ source = self._loader.get_real_file(self._find_needle('files', source), decrypt=self._task.args.get('decrypt', True))
+ except AnsibleError as e:
+@@ -105,7 +90,7 @@ class ActionModule(ActionBase):
+ if self._task.check_mode:
+ # check mode is supported if 'creates' or 'removes' are provided
+ # the task has already been skipped if a change would not occur
+- if new_module_args['creates'] or new_module_args['removes']:
++ if self._task.args.get('creates') or self._task.args.get('removes'):
+ result['changed'] = True
+ raise _AnsibleActionDone(result=result)
+ # If the script doesn't return changed in the result, it defaults to True,
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/set_fact.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/set_fact.py
+@@ -30,7 +30,6 @@ import ansible.constants as C
+ class ActionModule(ActionBase):
+
+ TRANSFERS_FILES = False
+- _requires_connection = False
+
+ def run(self, tmp=None, task_vars=None):
+ if task_vars is None:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/set_stats.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/set_stats.py
+@@ -18,6 +18,7 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
++from ansible.module_utils.six import string_types
+ from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.plugins.action import ActionBase
+ from ansible.utils.vars import isidentifier
+@@ -27,7 +28,6 @@ class ActionModule(ActionBase):
+
+ TRANSFERS_FILES = False
+ _VALID_ARGS = frozenset(('aggregate', 'data', 'per_host'))
+- _requires_connection = False
+
+ # TODO: document this in non-empty set_stats.py module
+ def run(self, tmp=None, task_vars=None):
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/shell.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/shell.py
+@@ -4,7 +4,6 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-from ansible.errors import AnsibleActionFail
+ from ansible.plugins.action import ActionBase
+
+
+@@ -16,11 +15,6 @@ class ActionModule(ActionBase):
+ # Shell module is implemented via command with a special arg
+ self._task.args['_uses_shell'] = True
+
+- # Shell shares the same module code as command. Fail if command
+- # specific options are set.
+- if "expand_argument_vars" in self._task.args:
+- raise AnsibleActionFail(f"Unsupported parameters for ({self._task.action}) module: expand_argument_vars")
+-
+ command_action = self._shared_loader_obj.action_loader.get('ansible.legacy.command',
+ task=self._task,
+ connection=self._connection,
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/template.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/template.py
+@@ -10,19 +10,10 @@ import shutil
+ import stat
+ import tempfile
+
+-from jinja2.defaults import (
+- BLOCK_END_STRING,
+- BLOCK_START_STRING,
+- COMMENT_END_STRING,
+- COMMENT_START_STRING,
+- VARIABLE_END_STRING,
+- VARIABLE_START_STRING,
+-)
+-
+ from ansible import constants as C
+ from ansible.config.manager import ensure_type
+ from ansible.errors import AnsibleError, AnsibleFileNotFound, AnsibleAction, AnsibleActionFail
+-from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native
++from ansible.module_utils._text import to_bytes, to_text, to_native
+ from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.module_utils.six import string_types
+ from ansible.plugins.action import ActionBase
+@@ -66,12 +57,12 @@ class ActionModule(ActionBase):
+ dest = self._task.args.get('dest', None)
+ state = self._task.args.get('state', None)
+ newline_sequence = self._task.args.get('newline_sequence', self.DEFAULT_NEWLINE_SEQUENCE)
+- variable_start_string = self._task.args.get('variable_start_string', VARIABLE_START_STRING)
+- variable_end_string = self._task.args.get('variable_end_string', VARIABLE_END_STRING)
+- block_start_string = self._task.args.get('block_start_string', BLOCK_START_STRING)
+- block_end_string = self._task.args.get('block_end_string', BLOCK_END_STRING)
+- comment_start_string = self._task.args.get('comment_start_string', COMMENT_START_STRING)
+- comment_end_string = self._task.args.get('comment_end_string', COMMENT_END_STRING)
++ variable_start_string = self._task.args.get('variable_start_string', None)
++ variable_end_string = self._task.args.get('variable_end_string', None)
++ block_start_string = self._task.args.get('block_start_string', None)
++ block_end_string = self._task.args.get('block_end_string', None)
++ comment_start_string = self._task.args.get('comment_start_string', None)
++ comment_end_string = self._task.args.get('comment_end_string', None)
+ output_encoding = self._task.args.get('output_encoding', 'utf-8') or 'utf-8'
+
+ wrong_sequences = ["\\n", "\\r", "\\r\\n"]
+@@ -138,18 +129,16 @@ class ActionModule(ActionBase):
+ templar = self._templar.copy_with_new_env(environment_class=AnsibleEnvironment,
+ searchpath=searchpath,
+ newline_sequence=newline_sequence,
++ block_start_string=block_start_string,
++ block_end_string=block_end_string,
++ variable_start_string=variable_start_string,
++ variable_end_string=variable_end_string,
++ comment_start_string=comment_start_string,
++ comment_end_string=comment_end_string,
++ trim_blocks=trim_blocks,
++ lstrip_blocks=lstrip_blocks,
+ available_variables=temp_vars)
+- overrides = dict(
+- block_start_string=block_start_string,
+- block_end_string=block_end_string,
+- variable_start_string=variable_start_string,
+- variable_end_string=variable_end_string,
+- comment_start_string=comment_start_string,
+- comment_end_string=comment_end_string,
+- trim_blocks=trim_blocks,
+- lstrip_blocks=lstrip_blocks
+- )
+- resultant = templar.do_template(template_data, preserve_trailing_newlines=True, escape_backslashes=False, overrides=overrides)
++ resultant = templar.do_template(template_data, preserve_trailing_newlines=True, escape_backslashes=False)
+ except AnsibleAction:
+ raise
+ except Exception as e:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/unarchive.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/unarchive.py
+@@ -21,7 +21,7 @@ __metaclass__ = type
+ import os
+
+ from ansible.errors import AnsibleError, AnsibleAction, AnsibleActionFail, AnsibleActionSkip
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.plugins.action import ActionBase
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/uri.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/uri.py
+@@ -10,9 +10,10 @@ __metaclass__ = type
+ import os
+
+ from ansible.errors import AnsibleError, AnsibleAction, _AnsibleActionDone, AnsibleActionFail
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.collections import Mapping, MutableMapping
+ from ansible.module_utils.parsing.convert_bool import boolean
++from ansible.module_utils.six import text_type
+ from ansible.plugins.action import ActionBase
+
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/validate_argument_spec.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/validate_argument_spec.py
+@@ -6,7 +6,9 @@ __metaclass__ = type
+
+ from ansible.errors import AnsibleError
+ from ansible.plugins.action import ActionBase
++from ansible.module_utils.six import string_types
+ from ansible.module_utils.common.arg_spec import ArgumentSpecValidator
++from ansible.module_utils.errors import AnsibleValidationErrorMultiple
+ from ansible.utils.vars import combine_vars
+
+
+@@ -14,7 +16,6 @@ class ActionModule(ActionBase):
+ ''' Validate an arg spec'''
+
+ TRANSFERS_FILES = False
+- _requires_connection = False
+
+ def get_args_from_task_vars(self, argument_spec, task_vars):
+ '''
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/wait_for_connection.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/wait_for_connection.py
+@@ -20,9 +20,9 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ import time
+-from datetime import datetime, timedelta, timezone
++from datetime import datetime, timedelta
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.plugins.action import ActionBase
+ from ansible.utils.display import Display
+
+@@ -43,10 +43,10 @@ class ActionModule(ActionBase):
+ DEFAULT_TIMEOUT = 600
+
+ def do_until_success_or_timeout(self, what, timeout, connect_timeout, what_desc, sleep=1):
+- max_end_time = datetime.now(timezone.utc) + timedelta(seconds=timeout)
++ max_end_time = datetime.utcnow() + timedelta(seconds=timeout)
+
+ e = None
+- while datetime.now(timezone.utc) < max_end_time:
++ while datetime.utcnow() < max_end_time:
+ try:
+ what(connect_timeout)
+ if what_desc:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/action/yum.py
++++ ansible-core-2.16.5/lib/ansible/plugins/action/yum.py
+@@ -23,7 +23,7 @@ from ansible.utils.display import Displa
+
+ display = Display()
+
+-VALID_BACKENDS = frozenset(('yum', 'yum4', 'dnf', 'dnf4', 'dnf5'))
++VALID_BACKENDS = frozenset(('yum', 'yum4', 'dnf'))
+
+
+ class ActionModule(ActionBase):
+@@ -53,9 +53,6 @@ class ActionModule(ActionBase):
+
+ module = self._task.args.get('use', self._task.args.get('use_backend', 'auto'))
+
+- if module == 'dnf':
+- module = 'auto'
+-
+ if module == 'auto':
+ try:
+ if self._task.delegate_to: # if we delegate, we should use delegated host's facts
+@@ -84,7 +81,7 @@ class ActionModule(ActionBase):
+ )
+
+ else:
+- if module in {"yum4", "dnf4"}:
++ if module == "yum4":
+ module = "dnf"
+
+ # eliminate collisions with collections search while still allowing local override
+@@ -93,6 +90,7 @@ class ActionModule(ActionBase):
+ if not self._shared_loader_obj.module_loader.has_plugin(module):
+ result.update({'failed': True, 'msg': "Could not find a yum module backend for %s." % module})
+ else:
++ # run either the yum (yum3) or dnf (yum4) backend module
+ new_module_args = self._task.args.copy()
+ if 'use_backend' in new_module_args:
+ del new_module_args['use_backend']
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/become/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/become/__init__.py
+@@ -12,7 +12,7 @@ from string import ascii_lowercase
+ from gettext import dgettext
+
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+ from ansible.plugins import AnsiblePlugin
+
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/become/su.py
++++ ansible-core-2.16.5/lib/ansible/plugins/become/su.py
+@@ -94,7 +94,7 @@ DOCUMENTATION = """
+ import re
+ import shlex
+
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+ from ansible.plugins.become import BecomeBase
+
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/cache/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/cache/__init__.py
+@@ -29,7 +29,7 @@ from collections.abc import MutableMappi
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.plugins import AnsiblePlugin
+ from ansible.plugins.loader import cache_loader
+ from ansible.utils.collection_loader import resource_from_fqcr
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/cache/base.py
++++ ansible-core-2.16.5/lib/ansible/plugins/cache/base.py
+@@ -18,4 +18,4 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ # moved actual classes to __init__ kept here for backward compat with 3rd parties
+-from ansible.plugins.cache import BaseCacheModule, BaseFileCacheModule # pylint: disable=unused-import
++from ansible.plugins.cache import BaseCacheModule, BaseFileCacheModule
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/callback/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/callback/__init__.py
+@@ -165,7 +165,7 @@ class CallbackBase(AnsiblePlugin):
+
+ self._hide_in_debug = ('changed', 'failed', 'skipped', 'invocation', 'skip_reason')
+
+- # helper for callbacks, so they don't all have to include deepcopy
++ ''' helper for callbacks, so they don't all have to include deepcopy '''
+ _copy_result = deepcopy
+
+ def set_option(self, k, v):
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/callback/junit.py
++++ ansible-core-2.16.5/lib/ansible/plugins/callback/junit.py
+@@ -88,7 +88,7 @@ import time
+ import re
+
+ from ansible import constants as C
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.plugins.callback import CallbackBase
+ from ansible.utils._junit_xml import (
+ TestCase,
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/callback/oneline.py
++++ ansible-core-2.16.5/lib/ansible/plugins/callback/oneline.py
+@@ -12,7 +12,7 @@ DOCUMENTATION = '''
+ short_description: oneline Ansible screen output
+ version_added: historical
+ description:
+- - This is the output callback used by the C(-o)/C(--one-line) command line option.
++ - This is the output callback used by the -o/--one-line command line option.
+ '''
+
+ from ansible.plugins.callback import CallbackBase
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/callback/tree.py
++++ ansible-core-2.16.5/lib/ansible/plugins/callback/tree.py
+@@ -31,7 +31,7 @@ DOCUMENTATION = '''
+ import os
+
+ from ansible.constants import TREE_DIR
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.plugins.callback import CallbackBase
+ from ansible.utils.path import makedirs_safe, unfrackpath
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/cliconf/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/cliconf/__init__.py
+@@ -24,7 +24,7 @@ from functools import wraps
+
+ from ansible.plugins import AnsiblePlugin
+ from ansible.errors import AnsibleError, AnsibleConnectionFailure
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+
+ try:
+ from scp import SCPClient
+@@ -276,7 +276,7 @@ class CliconfBase(AnsiblePlugin):
+ 'diff_replace': [list of supported replace values],
+ 'output': [list of supported command output format]
+ }
+- :return: capability as dict
++ :return: capability as json string
+ """
+ result = {}
+ result['rpc'] = self.get_base_rpc()
+@@ -360,6 +360,7 @@ class CliconfBase(AnsiblePlugin):
+ remote host before triggering timeout exception
+ :return: None
+ """
++ """Fetch file over scp/sftp from remote device"""
+ ssh = self._connection.paramiko_conn._connect_uncached()
+ if proto == 'scp':
+ if not HAS_SCP:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/connection/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/connection/__init__.py
+@@ -2,12 +2,10 @@
+ # (c) 2015 Toshio Kuratomi <tkuratomi@ansible.com>
+ # (c) 2017, Peter Sprygada <psprygad@redhat.com>
+ # (c) 2017 Ansible Project
+-from __future__ import (annotations, absolute_import, division, print_function)
++from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-import collections.abc as c
+ import fcntl
+-import io
+ import os
+ import shlex
+ import typing as t
+@@ -16,11 +14,8 @@ from abc import abstractmethod
+ from functools import wraps
+
+ from ansible import constants as C
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
+-from ansible.playbook.play_context import PlayContext
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.plugins import AnsiblePlugin
+-from ansible.plugins.become import BecomeBase
+-from ansible.plugins.shell import ShellBase
+ from ansible.utils.display import Display
+ from ansible.plugins.loader import connection_loader, get_shell_plugin
+ from ansible.utils.path import unfrackpath
+@@ -32,15 +27,10 @@ __all__ = ['ConnectionBase', 'ensure_con
+
+ BUFSIZE = 65536
+
+-P = t.ParamSpec('P')
+-T = t.TypeVar('T')
+
+-
+-def ensure_connect(
+- func: c.Callable[t.Concatenate[ConnectionBase, P], T],
+-) -> c.Callable[t.Concatenate[ConnectionBase, P], T]:
++def ensure_connect(func):
+ @wraps(func)
+- def wrapped(self: ConnectionBase, *args: P.args, **kwargs: P.kwargs) -> T:
++ def wrapped(self, *args, **kwargs):
+ if not self._connected:
+ self._connect()
+ return func(self, *args, **kwargs)
+@@ -67,16 +57,9 @@ class ConnectionBase(AnsiblePlugin):
+ supports_persistence = False
+ force_persistence = False
+
+- default_user: str | None = None
++ default_user = None
+
+- def __init__(
+- self,
+- play_context: PlayContext,
+- new_stdin: io.TextIOWrapper | None = None,
+- shell: ShellBase | None = None,
+- *args: t.Any,
+- **kwargs: t.Any,
+- ) -> None:
++ def __init__(self, play_context, new_stdin, shell=None, *args, **kwargs):
+
+ super(ConnectionBase, self).__init__()
+
+@@ -84,17 +67,18 @@ class ConnectionBase(AnsiblePlugin):
+ if not hasattr(self, '_play_context'):
+ # Backwards compat: self._play_context isn't really needed, using set_options/get_option
+ self._play_context = play_context
+- # Delete once the deprecation period is over for WorkerProcess._new_stdin
+- if not hasattr(self, '__new_stdin'):
+- self.__new_stdin = new_stdin
++ if not hasattr(self, '_new_stdin'):
++ self._new_stdin = new_stdin
+ if not hasattr(self, '_display'):
+ # Backwards compat: self._display isn't really needed, just import the global display and use that.
+ self._display = display
++ if not hasattr(self, '_connected'):
++ self._connected = False
+
+ self.success_key = None
+ self.prompt = None
+ self._connected = False
+- self._socket_path: str | None = None
++ self._socket_path = None
+
+ # helper plugins
+ self._shell = shell
+@@ -104,32 +88,23 @@ class ConnectionBase(AnsiblePlugin):
+ shell_type = play_context.shell if play_context.shell else getattr(self, '_shell_type', None)
+ self._shell = get_shell_plugin(shell_type=shell_type, executable=self._play_context.executable)
+
+- self.become: BecomeBase | None = None
+-
+- @property
+- def _new_stdin(self) -> io.TextIOWrapper | None:
+- display.deprecated(
+- "The connection's stdin object is deprecated. "
+- "Call display.prompt_until(msg) instead.",
+- version='2.19',
+- )
+- return self.__new_stdin
++ self.become = None
+
+- def set_become_plugin(self, plugin: BecomeBase) -> None:
++ def set_become_plugin(self, plugin):
+ self.become = plugin
+
+ @property
+- def connected(self) -> bool:
++ def connected(self):
+ '''Read-only property holding whether the connection to the remote host is active or closed.'''
+ return self._connected
+
+ @property
+- def socket_path(self) -> str | None:
++ def socket_path(self):
+ '''Read-only property holding the connection socket path for this remote host'''
+ return self._socket_path
+
+ @staticmethod
+- def _split_ssh_args(argstring: str) -> list[str]:
++ def _split_ssh_args(argstring):
+ """
+ Takes a string like '-o Foo=1 -o Bar="foo bar"' and returns a
+ list ['-o', 'Foo=1', '-o', 'Bar=foo bar'] that can be added to
+@@ -140,17 +115,17 @@ class ConnectionBase(AnsiblePlugin):
+
+ @property
+ @abstractmethod
+- def transport(self) -> str:
++ def transport(self):
+ """String used to identify this Connection class from other classes"""
+ pass
+
+ @abstractmethod
+- def _connect(self: T) -> T:
++ def _connect(self):
+ """Connect to the host we've been initialized with"""
+
+ @ensure_connect
+ @abstractmethod
+- def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]:
++ def exec_command(self, cmd, in_data=None, sudoable=True):
+ """Run a command on the remote host.
+
+ :arg cmd: byte string containing the command
+@@ -218,36 +193,36 @@ class ConnectionBase(AnsiblePlugin):
+
+ @ensure_connect
+ @abstractmethod
+- def put_file(self, in_path: str, out_path: str) -> None:
++ def put_file(self, in_path, out_path):
+ """Transfer a file from local to remote"""
+ pass
+
+ @ensure_connect
+ @abstractmethod
+- def fetch_file(self, in_path: str, out_path: str) -> None:
++ def fetch_file(self, in_path, out_path):
+ """Fetch a file from remote to local; callers are expected to have pre-created the directory chain for out_path"""
+ pass
+
+ @abstractmethod
+- def close(self) -> None:
++ def close(self):
+ """Terminate the connection"""
+ pass
+
+- def connection_lock(self) -> None:
++ def connection_lock(self):
+ f = self._play_context.connection_lockfd
+ display.vvvv('CONNECTION: pid %d waiting for lock on %d' % (os.getpid(), f), host=self._play_context.remote_addr)
+ fcntl.lockf(f, fcntl.LOCK_EX)
+ display.vvvv('CONNECTION: pid %d acquired lock on %d' % (os.getpid(), f), host=self._play_context.remote_addr)
+
+- def connection_unlock(self) -> None:
++ def connection_unlock(self):
+ f = self._play_context.connection_lockfd
+ fcntl.lockf(f, fcntl.LOCK_UN)
+ display.vvvv('CONNECTION: pid %d released lock on %d' % (os.getpid(), f), host=self._play_context.remote_addr)
+
+- def reset(self) -> None:
++ def reset(self):
+ display.warning("Reset is not implemented for this connection")
+
+- def update_vars(self, variables: dict[str, t.Any]) -> None:
++ def update_vars(self, variables):
+ '''
+ Adds 'magic' variables relating to connections to the variable dictionary provided.
+ In case users need to access from the play, this is a legacy from runner.
+@@ -263,7 +238,7 @@ class ConnectionBase(AnsiblePlugin):
+ elif varname == 'ansible_connection':
+ # its me mom!
+ value = self._load_name
+- elif varname == 'ansible_shell_type' and self._shell:
++ elif varname == 'ansible_shell_type':
+ # its my cousin ...
+ value = self._shell._load_name
+ else:
+@@ -296,15 +271,9 @@ class NetworkConnectionBase(ConnectionBa
+ # Do not use _remote_is_local in other connections
+ _remote_is_local = True
+
+- def __init__(
+- self,
+- play_context: PlayContext,
+- new_stdin: io.TextIOWrapper | None = None,
+- *args: t.Any,
+- **kwargs: t.Any,
+- ) -> None:
++ def __init__(self, play_context, new_stdin, *args, **kwargs):
+ super(NetworkConnectionBase, self).__init__(play_context, new_stdin, *args, **kwargs)
+- self._messages: list[tuple[str, str]] = []
++ self._messages = []
+ self._conn_closed = False
+
+ self._network_os = self._play_context.network_os
+@@ -312,7 +281,7 @@ class NetworkConnectionBase(ConnectionBa
+ self._local = connection_loader.get('local', play_context, '/dev/null')
+ self._local.set_options()
+
+- self._sub_plugin: dict[str, t.Any] = {}
++ self._sub_plugin = {}
+ self._cached_variables = (None, None, None)
+
+ # reconstruct the socket_path and set instance values accordingly
+@@ -331,10 +300,10 @@ class NetworkConnectionBase(ConnectionBa
+ return method
+ raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))
+
+- def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]:
++ def exec_command(self, cmd, in_data=None, sudoable=True):
+ return self._local.exec_command(cmd, in_data, sudoable)
+
+- def queue_message(self, level: str, message: str) -> None:
++ def queue_message(self, level, message):
+ """
+ Adds a message to the queue of messages waiting to be pushed back to the controller process.
+
+@@ -344,19 +313,19 @@ class NetworkConnectionBase(ConnectionBa
+ """
+ self._messages.append((level, message))
+
+- def pop_messages(self) -> list[tuple[str, str]]:
++ def pop_messages(self):
+ messages, self._messages = self._messages, []
+ return messages
+
+- def put_file(self, in_path: str, out_path: str) -> None:
++ def put_file(self, in_path, out_path):
+ """Transfer a file from local to remote"""
+ return self._local.put_file(in_path, out_path)
+
+- def fetch_file(self, in_path: str, out_path: str) -> None:
++ def fetch_file(self, in_path, out_path):
+ """Fetch a file from remote to local"""
+ return self._local.fetch_file(in_path, out_path)
+
+- def reset(self) -> None:
++ def reset(self):
+ '''
+ Reset the connection
+ '''
+@@ -365,17 +334,12 @@ class NetworkConnectionBase(ConnectionBa
+ self.close()
+ self.queue_message('vvvv', 'reset call on connection instance')
+
+- def close(self) -> None:
++ def close(self):
+ self._conn_closed = True
+ if self._connected:
+ self._connected = False
+
+- def set_options(
+- self,
+- task_keys: dict[str, t.Any] | None = None,
+- var_options: dict[str, t.Any] | None = None,
+- direct: dict[str, t.Any] | None = None,
+- ) -> None:
++ def set_options(self, task_keys=None, var_options=None, direct=None):
+ super(NetworkConnectionBase, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
+ if self.get_option('persistent_log_messages'):
+ warning = "Persistent connection logging is enabled for %s. This will log ALL interactions" % self._play_context.remote_addr
+@@ -390,7 +354,7 @@ class NetworkConnectionBase(ConnectionBa
+ except AttributeError:
+ pass
+
+- def _update_connection_state(self) -> None:
++ def _update_connection_state(self):
+ '''
+ Reconstruct the connection socket_path and check if it exists
+
+@@ -413,6 +377,6 @@ class NetworkConnectionBase(ConnectionBa
+ self._connected = True
+ self._socket_path = socket_path
+
+- def _log_messages(self, message: str) -> None:
++ def _log_messages(self, message):
+ if self.get_option('persistent_log_messages'):
+ self.queue_message('log', message)
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/connection/local.py
++++ ansible-core-2.16.5/lib/ansible/plugins/connection/local.py
+@@ -2,7 +2,7 @@
+ # (c) 2015, 2017 Toshio Kuratomi <tkuratomi@ansible.com>
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+-from __future__ import (annotations, absolute_import, division, print_function)
++from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+ DOCUMENTATION = '''
+@@ -24,13 +24,12 @@ import os
+ import pty
+ import shutil
+ import subprocess
+-import typing as t
+
+ import ansible.constants as C
+ from ansible.errors import AnsibleError, AnsibleFileNotFound
+ from ansible.module_utils.compat import selectors
+ from ansible.module_utils.six import text_type, binary_type
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.plugins.connection import ConnectionBase
+ from ansible.utils.display import Display
+ from ansible.utils.path import unfrackpath
+@@ -44,7 +43,7 @@ class Connection(ConnectionBase):
+ transport = 'local'
+ has_pipelining = True
+
+- def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
++ def __init__(self, *args, **kwargs):
+
+ super(Connection, self).__init__(*args, **kwargs)
+ self.cwd = None
+@@ -54,7 +53,7 @@ class Connection(ConnectionBase):
+ display.vv("Current user (uid=%s) does not seem to exist on this system, leaving user empty." % os.getuid())
+ self.default_user = ""
+
+- def _connect(self) -> Connection:
++ def _connect(self):
+ ''' connect to the local host; nothing to do here '''
+
+ # Because we haven't made any remote connection we're running as
+@@ -66,7 +65,7 @@ class Connection(ConnectionBase):
+ self._connected = True
+ return self
+
+- def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]:
++ def exec_command(self, cmd, in_data=None, sudoable=True):
+ ''' run a command on the local host '''
+
+ super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+@@ -164,7 +163,7 @@ class Connection(ConnectionBase):
+ display.debug("done with local.exec_command()")
+ return (p.returncode, stdout, stderr)
+
+- def put_file(self, in_path: str, out_path: str) -> None:
++ def put_file(self, in_path, out_path):
+ ''' transfer a file from local to local '''
+
+ super(Connection, self).put_file(in_path, out_path)
+@@ -182,7 +181,7 @@ class Connection(ConnectionBase):
+ except IOError as e:
+ raise AnsibleError("failed to transfer file to {0}: {1}".format(to_native(out_path), to_native(e)))
+
+- def fetch_file(self, in_path: str, out_path: str) -> None:
++ def fetch_file(self, in_path, out_path):
+ ''' fetch a file from local to local -- for compatibility '''
+
+ super(Connection, self).fetch_file(in_path, out_path)
+@@ -190,6 +189,6 @@ class Connection(ConnectionBase):
+ display.vvv(u"FETCH {0} TO {1}".format(in_path, out_path), host=self._play_context.remote_addr)
+ self.put_file(in_path, out_path)
+
+- def close(self) -> None:
++ def close(self):
+ ''' terminate the connection; nothing to do here '''
+ self._connected = False
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/connection/paramiko_ssh.py
++++ ansible-core-2.16.5/lib/ansible/plugins/connection/paramiko_ssh.py
+@@ -1,15 +1,15 @@
+ # (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+ # (c) 2017 Ansible Project
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+-from __future__ import (annotations, absolute_import, division, print_function)
++from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+ DOCUMENTATION = """
+ author: Ansible Core Team
+ name: paramiko
+- short_description: Run tasks via Python SSH (paramiko)
++ short_description: Run tasks via python ssh (paramiko)
+ description:
+- - Use the Python SSH implementation (Paramiko) to connect to targets
++ - Use the python ssh implementation (Paramiko) to connect to targets
+ - The paramiko transport is provided because many distributions, in particular EL6 and before do not support ControlPersist
+ in their SSH implementations.
+ - This is needed on the Ansible control machine to be reasonably efficient with connections.
+@@ -22,38 +22,15 @@ DOCUMENTATION = """
+ description:
+ - Address of the remote target
+ default: inventory_hostname
+- type: string
+ vars:
+ - name: inventory_hostname
+ - name: ansible_host
+ - name: ansible_ssh_host
+ - name: ansible_paramiko_host
+- port:
+- description: Remote port to connect to.
+- type: int
+- default: 22
+- ini:
+- - section: defaults
+- key: remote_port
+- - section: paramiko_connection
+- key: remote_port
+- version_added: '2.15'
+- env:
+- - name: ANSIBLE_REMOTE_PORT
+- - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+- version_added: '2.15'
+- vars:
+- - name: ansible_port
+- - name: ansible_ssh_port
+- - name: ansible_paramiko_port
+- version_added: '2.15'
+- keyword:
+- - name: port
+ remote_user:
+ description:
+ - User to login/authenticate as
+ - Can be set from the CLI via the C(--user) or C(-u) options.
+- type: string
+ vars:
+ - name: ansible_user
+ - name: ansible_ssh_user
+@@ -74,7 +51,6 @@ DOCUMENTATION = """
+ description:
+ - Secret used to either login the ssh server or as a passphrase for ssh keys that require it
+ - Can be set from the CLI via the C(--ask-pass) option.
+- type: string
+ vars:
+ - name: ansible_password
+ - name: ansible_ssh_pass
+@@ -86,7 +62,7 @@ DOCUMENTATION = """
+ description:
+ - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys
+ - On paramiko versions older than 2.9, this only affects hostkeys
+- - For behavior matching paramiko<2.9 set this to V(False)
++ - For behavior matching paramiko<2.9 set this to C(False)
+ vars:
+ - name: ansible_paramiko_use_rsa_sha2_algorithms
+ ini:
+@@ -114,17 +90,12 @@ DOCUMENTATION = """
+ description:
+ - Proxy information for running the connection via a jumphost
+ - Also this plugin will scan 'ssh_args', 'ssh_extra_args' and 'ssh_common_args' from the 'ssh' plugin settings for proxy information if set.
+- type: string
+ env: [{name: ANSIBLE_PARAMIKO_PROXY_COMMAND}]
+ ini:
+ - {key: proxy_command, section: paramiko_connection}
+- vars:
+- - name: ansible_paramiko_proxy_command
+- version_added: '2.15'
+ ssh_args:
+ description: Only used in parsing ProxyCommand for use in this plugin.
+ default: ''
+- type: string
+ ini:
+ - section: 'ssh_connection'
+ key: 'ssh_args'
+@@ -133,13 +104,8 @@ DOCUMENTATION = """
+ vars:
+ - name: ansible_ssh_args
+ version_added: '2.7'
+- deprecated:
+- why: In favor of the "proxy_command" option.
+- version: "2.18"
+- alternatives: proxy_command
+ ssh_common_args:
+ description: Only used in parsing ProxyCommand for use in this plugin.
+- type: string
+ ini:
+ - section: 'ssh_connection'
+ key: 'ssh_common_args'
+@@ -152,13 +118,8 @@ DOCUMENTATION = """
+ cli:
+ - name: ssh_common_args
+ default: ''
+- deprecated:
+- why: In favor of the "proxy_command" option.
+- version: "2.18"
+- alternatives: proxy_command
+ ssh_extra_args:
+ description: Only used in parsing ProxyCommand for use in this plugin.
+- type: string
+ vars:
+ - name: ansible_ssh_extra_args
+ env:
+@@ -171,10 +132,6 @@ DOCUMENTATION = """
+ cli:
+ - name: ssh_extra_args
+ default: ''
+- deprecated:
+- why: In favor of the "proxy_command" option.
+- version: "2.18"
+- alternatives: proxy_command
+ pty:
+ default: True
+ description: 'SUDO usually requires a PTY, True to give a PTY and False to not give a PTY.'
+@@ -237,54 +194,8 @@ DOCUMENTATION = """
+ key: banner_timeout
+ env:
+ - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+- timeout:
+- type: int
+- default: 10
+- description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+- ini:
+- - section: defaults
+- key: timeout
+- - section: ssh_connection
+- key: timeout
+- version_added: '2.11'
+- - section: paramiko_connection
+- key: timeout
+- version_added: '2.15'
+- env:
+- - name: ANSIBLE_TIMEOUT
+- - name: ANSIBLE_SSH_TIMEOUT
+- version_added: '2.11'
+- - name: ANSIBLE_PARAMIKO_TIMEOUT
+- version_added: '2.15'
+- vars:
+- - name: ansible_ssh_timeout
+- version_added: '2.11'
+- - name: ansible_paramiko_timeout
+- version_added: '2.15'
+- cli:
+- - name: timeout
+- private_key_file:
+- description:
+- - Path to private key file to use for authentication.
+- type: string
+- ini:
+- - section: defaults
+- key: private_key_file
+- - section: paramiko_connection
+- key: private_key_file
+- version_added: '2.15'
+- env:
+- - name: ANSIBLE_PRIVATE_KEY_FILE
+- - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+- version_added: '2.15'
+- vars:
+- - name: ansible_private_key_file
+- - name: ansible_ssh_private_key_file
+- - name: ansible_paramiko_private_key_file
+- version_added: '2.15'
+- cli:
+- - name: private_key_file
+- option: '--private-key'
++# TODO:
++#timeout=self._play_context.timeout,
+ """
+
+ import os
+@@ -292,9 +203,10 @@ import socket
+ import tempfile
+ import traceback
+ import fcntl
++import sys
+ import re
+-import typing as t
+
++from termios import tcflush, TCIFLUSH
+ from ansible.module_utils.compat.version import LooseVersion
+ from binascii import hexlify
+
+@@ -308,7 +220,7 @@ from ansible.module_utils.compat.paramik
+ from ansible.plugins.connection import ConnectionBase
+ from ansible.utils.display import Display
+ from ansible.utils.path import makedirs_safe
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+
+ display = Display()
+
+@@ -322,12 +234,8 @@ Are you sure you want to continue connec
+ # SSH Options Regex
+ SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)')
+
+-MissingHostKeyPolicy: type = object
+-if paramiko:
+- MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+-
+
+-class MyAddPolicy(MissingHostKeyPolicy):
++class MyAddPolicy(object):
+ """
+ Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+@@ -337,13 +245,14 @@ class MyAddPolicy(MissingHostKeyPolicy):
+ local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+ """
+
+- def __init__(self, connection: Connection) -> None:
++ def __init__(self, new_stdin, connection):
++ self._new_stdin = new_stdin
+ self.connection = connection
+ self._options = connection._options
+
+- def missing_host_key(self, client, hostname, key) -> None:
++ def missing_host_key(self, client, hostname, key):
+
+- if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
++ if all((self._options['host_key_checking'], not self._options['host_key_auto_add'])):
+
+ fingerprint = hexlify(key.get_fingerprint())
+ ktype = key.get_name()
+@@ -353,10 +262,18 @@ class MyAddPolicy(MissingHostKeyPolicy):
+ # to the question anyway
+ raise AnsibleError(AUTHENTICITY_MSG[1:92] % (hostname, ktype, fingerprint))
+
+- inp = to_text(
+- display.prompt_until(AUTHENTICITY_MSG % (hostname, ktype, fingerprint), private=False),
+- errors='surrogate_or_strict'
+- )
++ self.connection.connection_lock()
++
++ old_stdin = sys.stdin
++ sys.stdin = self._new_stdin
++
++ # clear out any premature input on sys.stdin
++ tcflush(sys.stdin, TCIFLUSH)
++
++ inp = input(AUTHENTICITY_MSG % (hostname, ktype, fingerprint))
++ sys.stdin = old_stdin
++
++ self.connection.connection_unlock()
+
+ if inp not in ['yes', 'y', '']:
+ raise AnsibleError("host connection rejected by user")
+@@ -372,20 +289,20 @@ class MyAddPolicy(MissingHostKeyPolicy):
+
+ # keep connection objects on a per host basis to avoid repeated attempts to reconnect
+
+-SSH_CONNECTION_CACHE: dict[str, paramiko.client.SSHClient] = {}
+-SFTP_CONNECTION_CACHE: dict[str, paramiko.sftp_client.SFTPClient] = {}
++SSH_CONNECTION_CACHE = {} # type: dict[str, paramiko.client.SSHClient]
++SFTP_CONNECTION_CACHE = {} # type: dict[str, paramiko.sftp_client.SFTPClient]
+
+
+ class Connection(ConnectionBase):
+ ''' SSH based connections with Paramiko '''
+
+ transport = 'paramiko'
+- _log_channel: str | None = None
++ _log_channel = None
+
+- def _cache_key(self) -> str:
+- return "%s__%s__" % (self.get_option('remote_addr'), self.get_option('remote_user'))
++ def _cache_key(self):
++ return "%s__%s__" % (self._play_context.remote_addr, self._play_context.remote_user)
+
+- def _connect(self) -> Connection:
++ def _connect(self):
+ cache_key = self._cache_key()
+ if cache_key in SSH_CONNECTION_CACHE:
+ self.ssh = SSH_CONNECTION_CACHE[cache_key]
+@@ -395,11 +312,11 @@ class Connection(ConnectionBase):
+ self._connected = True
+ return self
+
+- def _set_log_channel(self, name: str) -> None:
++ def _set_log_channel(self, name):
+ '''Mimic paramiko.SSHClient.set_log_channel'''
+ self._log_channel = name
+
+- def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
++ def _parse_proxy_command(self, port=22):
+ proxy_command = None
+ # Parse ansible_ssh_common_args, specifically looking for ProxyCommand
+ ssh_args = [
+@@ -428,15 +345,15 @@ class Connection(ConnectionBase):
+ sock_kwarg = {}
+ if proxy_command:
+ replacers = {
+- '%h': self.get_option('remote_addr'),
++ '%h': self._play_context.remote_addr,
+ '%p': port,
+- '%r': self.get_option('remote_user')
++ '%r': self._play_context.remote_user
+ }
+ for find, replace in replacers.items():
+ proxy_command = proxy_command.replace(find, str(replace))
+ try:
+ sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+- display.vvv("CONFIGURE PROXY COMMAND FOR CONNECTION: %s" % proxy_command, host=self.get_option('remote_addr'))
++ display.vvv("CONFIGURE PROXY COMMAND FOR CONNECTION: %s" % proxy_command, host=self._play_context.remote_addr)
+ except AttributeError:
+ display.warning('Paramiko ProxyCommand support unavailable. '
+ 'Please upgrade to Paramiko 1.9.0 or newer. '
+@@ -444,25 +361,24 @@ class Connection(ConnectionBase):
+
+ return sock_kwarg
+
+- def _connect_uncached(self) -> paramiko.SSHClient:
++ def _connect_uncached(self):
+ ''' activates the connection object '''
+
+ if paramiko is None:
+ raise AnsibleError("paramiko is not installed: %s" % to_native(PARAMIKO_IMPORT_ERR))
+
+- port = self.get_option('port')
+- display.vvv("ESTABLISH PARAMIKO SSH CONNECTION FOR USER: %s on PORT %s TO %s" % (self.get_option('remote_user'), port, self.get_option('remote_addr')),
+- host=self.get_option('remote_addr'))
++ port = self._play_context.port or 22
++ display.vvv("ESTABLISH PARAMIKO SSH CONNECTION FOR USER: %s on PORT %s TO %s" % (self._play_context.remote_user, port, self._play_context.remote_addr),
++ host=self._play_context.remote_addr)
+
+ ssh = paramiko.SSHClient()
+
+ # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+ # is keeping or omitting rsa-sha2 algorithms
+- # default_keys: t.Tuple[str] = ()
+ paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+ paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+ use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+- disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
++ disabled_algorithms = {}
+ if not use_rsa_sha2_algorithms:
+ if paramiko_preferred_pubkeys:
+ disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+@@ -487,9 +403,9 @@ class Connection(ConnectionBase):
+
+ ssh_connect_kwargs = self._parse_proxy_command(port)
+
+- ssh.set_missing_host_key_policy(MyAddPolicy(self))
++ ssh.set_missing_host_key_policy(MyAddPolicy(self._new_stdin, self))
+
+- conn_password = self.get_option('password')
++ conn_password = self.get_option('password') or self._play_context.password
+
+ allow_agent = True
+
+@@ -498,25 +414,25 @@ class Connection(ConnectionBase):
+
+ try:
+ key_filename = None
+- if self.get_option('private_key_file'):
+- key_filename = os.path.expanduser(self.get_option('private_key_file'))
++ if self._play_context.private_key_file:
++ key_filename = os.path.expanduser(self._play_context.private_key_file)
+
+ # paramiko 2.2 introduced auth_timeout parameter
+ if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+- ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
++ ssh_connect_kwargs['auth_timeout'] = self._play_context.timeout
+
+ # paramiko 1.15 introduced banner timeout parameter
+ if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+ ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+ ssh.connect(
+- self.get_option('remote_addr').lower(),
+- username=self.get_option('remote_user'),
++ self._play_context.remote_addr.lower(),
++ username=self._play_context.remote_user,
+ allow_agent=allow_agent,
+ look_for_keys=self.get_option('look_for_keys'),
+ key_filename=key_filename,
+ password=conn_password,
+- timeout=self.get_option('timeout'),
++ timeout=self._play_context.timeout,
+ port=port,
+ disabled_algorithms=disabled_algorithms,
+ **ssh_connect_kwargs,
+@@ -532,14 +448,14 @@ class Connection(ConnectionBase):
+ raise AnsibleError("paramiko version issue, please upgrade paramiko on the machine running ansible")
+ elif u"Private key file is encrypted" in msg:
+ msg = 'ssh %s@%s:%s : %s\nTo connect as a different user, use -u <username>.' % (
+- self.get_option('remote_user'), self.get_options('remote_addr'), port, msg)
++ self._play_context.remote_user, self._play_context.remote_addr, port, msg)
+ raise AnsibleConnectionFailure(msg)
+ else:
+ raise AnsibleConnectionFailure(msg)
+
+ return ssh
+
+- def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]:
++ def exec_command(self, cmd, in_data=None, sudoable=True):
+ ''' run a command on the remote host '''
+
+ super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+@@ -565,7 +481,7 @@ class Connection(ConnectionBase):
+ if self.get_option('pty') and sudoable:
+ chan.get_pty(term=os.getenv('TERM', 'vt100'), width=int(os.getenv('COLUMNS', 0)), height=int(os.getenv('LINES', 0)))
+
+- display.vvv("EXEC %s" % cmd, host=self.get_option('remote_addr'))
++ display.vvv("EXEC %s" % cmd, host=self._play_context.remote_addr)
+
+ cmd = to_bytes(cmd, errors='surrogate_or_strict')
+
+@@ -582,10 +498,11 @@ class Connection(ConnectionBase):
+ display.debug('Waiting for Privilege Escalation input')
+
+ chunk = chan.recv(bufsize)
+- display.debug("chunk is: %r" % chunk)
++ display.debug("chunk is: %s" % chunk)
+ if not chunk:
+ if b'unknown user' in become_output:
+- n_become_user = to_native(self.become.get_option('become_user'))
++ n_become_user = to_native(self.become.get_option('become_user',
++ playcontext=self._play_context))
+ raise AnsibleError('user %s does not exist' % n_become_user)
+ else:
+ break
+@@ -594,17 +511,17 @@ class Connection(ConnectionBase):
+
+ # need to check every line because we might get lectured
+ # and we might get the middle of a line in a chunk
+- for line in become_output.splitlines(True):
+- if self.become.check_success(line):
++ for l in become_output.splitlines(True):
++ if self.become.check_success(l):
+ become_sucess = True
+ break
+- elif self.become.check_password_prompt(line):
++ elif self.become.check_password_prompt(l):
+ passprompt = True
+ break
+
+ if passprompt:
+ if self.become:
+- become_pass = self.become.get_option('become_pass')
++ become_pass = self.become.get_option('become_pass', playcontext=self._play_context)
+ chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
+ else:
+ raise AnsibleError("A password is required but none was supplied")
+@@ -612,19 +529,19 @@ class Connection(ConnectionBase):
+ no_prompt_out += become_output
+ no_prompt_err += become_output
+ except socket.timeout:
+- raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output))
++ raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + become_output)
+
+ stdout = b''.join(chan.makefile('rb', bufsize))
+ stderr = b''.join(chan.makefile_stderr('rb', bufsize))
+
+ return (chan.recv_exit_status(), no_prompt_out + stdout, no_prompt_out + stderr)
+
+- def put_file(self, in_path: str, out_path: str) -> None:
++ def put_file(self, in_path, out_path):
+ ''' transfer a file from local to remote '''
+
+ super(Connection, self).put_file(in_path, out_path)
+
+- display.vvv("PUT %s TO %s" % (in_path, out_path), host=self.get_option('remote_addr'))
++ display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr)
+
+ if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')):
+ raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
+@@ -639,21 +556,21 @@ class Connection(ConnectionBase):
+ except IOError:
+ raise AnsibleError("failed to transfer file to %s" % out_path)
+
+- def _connect_sftp(self) -> paramiko.sftp_client.SFTPClient:
++ def _connect_sftp(self):
+
+- cache_key = "%s__%s__" % (self.get_option('remote_addr'), self.get_option('remote_user'))
++ cache_key = "%s__%s__" % (self._play_context.remote_addr, self._play_context.remote_user)
+ if cache_key in SFTP_CONNECTION_CACHE:
+ return SFTP_CONNECTION_CACHE[cache_key]
+ else:
+ result = SFTP_CONNECTION_CACHE[cache_key] = self._connect().ssh.open_sftp()
+ return result
+
+- def fetch_file(self, in_path: str, out_path: str) -> None:
++ def fetch_file(self, in_path, out_path):
+ ''' save a remote file to the specified path '''
+
+ super(Connection, self).fetch_file(in_path, out_path)
+
+- display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self.get_option('remote_addr'))
++ display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr)
+
+ try:
+ self.sftp = self._connect_sftp()
+@@ -665,7 +582,7 @@ class Connection(ConnectionBase):
+ except IOError:
+ raise AnsibleError("failed to transfer file from %s" % in_path)
+
+- def _any_keys_added(self) -> bool:
++ def _any_keys_added(self):
+
+ for hostname, keys in self.ssh._host_keys.items():
+ for keytype, key in keys.items():
+@@ -674,14 +591,14 @@ class Connection(ConnectionBase):
+ return True
+ return False
+
+- def _save_ssh_host_keys(self, filename: str) -> None:
++ def _save_ssh_host_keys(self, filename):
+ '''
+ not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+ don't complain about it :)
+ '''
+
+ if not self._any_keys_added():
+- return
++ return False
+
+ path = os.path.expanduser("~/.ssh")
+ makedirs_safe(path)
+@@ -704,13 +621,13 @@ class Connection(ConnectionBase):
+ if added_this_time:
+ f.write("%s %s %s\n" % (hostname, keytype, key.get_base64()))
+
+- def reset(self) -> None:
++ def reset(self):
+ if not self._connected:
+ return
+ self.close()
+ self._connect()
+
+- def close(self) -> None:
++ def close(self):
+ ''' terminate the connection '''
+
+ cache_key = self._cache_key()
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/connection/psrp.py
++++ ansible-core-2.16.5/lib/ansible/plugins/connection/psrp.py
+@@ -1,7 +1,7 @@
+ # Copyright (c) 2018 Ansible Project
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+-from __future__ import (annotations, absolute_import, division, print_function)
++from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+ DOCUMENTATION = """
+@@ -10,7 +10,7 @@ name: psrp
+ short_description: Run tasks over Microsoft PowerShell Remoting Protocol
+ description:
+ - Run commands or put/fetch on a target via PSRP (WinRM plugin)
+-- This is similar to the P(ansible.builtin.winrm#connection) connection plugin which uses the same
++- This is similar to the I(winrm) connection plugin which uses the same
+ underlying transport but instead runs in a PowerShell interpreter.
+ version_added: "2.7"
+ requirements:
+@@ -38,7 +38,7 @@ options:
+ keyword:
+ - name: remote_user
+ remote_password:
+- description: Authentication password for the O(remote_user). Can be supplied as CLI option.
++ description: Authentication password for the C(remote_user). Can be supplied as CLI option.
+ type: str
+ vars:
+ - name: ansible_password
+@@ -49,8 +49,8 @@ options:
+ port:
+ description:
+ - The port for PSRP to connect on the remote target.
+- - Default is V(5986) if O(protocol) is not defined or is V(https),
+- otherwise the port is V(5985).
++ - Default is C(5986) if I(protocol) is not defined or is C(https),
++ otherwise the port is C(5985).
+ type: int
+ vars:
+ - name: ansible_port
+@@ -60,7 +60,7 @@ options:
+ protocol:
+ description:
+ - Set the protocol to use for the connection.
+- - Default is V(https) if O(port) is not defined or O(port) is not V(5985).
++ - Default is C(https) if I(port) is not defined or I(port) is not C(5985).
+ choices:
+ - http
+ - https
+@@ -77,8 +77,8 @@ options:
+ auth:
+ description:
+ - The authentication protocol to use when authenticating the remote user.
+- - The default, V(negotiate), will attempt to use Kerberos (V(kerberos)) if it is
+- available and fall back to NTLM (V(ntlm)) if it isn't.
++ - The default, C(negotiate), will attempt to use C(Kerberos) if it is
++ available and fall back to C(NTLM) if it isn't.
+ type: str
+ vars:
+ - name: ansible_psrp_auth
+@@ -93,8 +93,8 @@ options:
+ cert_validation:
+ description:
+ - Whether to validate the remote server's certificate or not.
+- - Set to V(ignore) to not validate any certificates.
+- - O(ca_cert) can be set to the path of a PEM certificate chain to
++ - Set to C(ignore) to not validate any certificates.
++ - I(ca_cert) can be set to the path of a PEM certificate chain to
+ use in the validation.
+ choices:
+ - validate
+@@ -107,7 +107,7 @@ options:
+ description:
+ - The path to a PEM certificate chain to use when validating the server's
+ certificate.
+- - This value is ignored if O(cert_validation) is set to V(ignore).
++ - This value is ignored if I(cert_validation) is set to C(ignore).
+ type: path
+ vars:
+ - name: ansible_psrp_cert_trust_path
+@@ -124,7 +124,7 @@ options:
+ read_timeout:
+ description:
+ - The read timeout for receiving data from the remote host.
+- - This value must always be greater than O(operation_timeout).
++ - This value must always be greater than I(operation_timeout).
+ - This option requires pypsrp >= 0.3.
+ - This is measured in seconds.
+ type: int
+@@ -156,15 +156,15 @@ options:
+ message_encryption:
+ description:
+ - Controls the message encryption settings, this is different from TLS
+- encryption when O(protocol) is V(https).
+- - Only the auth protocols V(negotiate), V(kerberos), V(ntlm), and
+- V(credssp) can do message encryption. The other authentication protocols
+- only support encryption when V(protocol) is set to V(https).
+- - V(auto) means means message encryption is only used when not using
++ encryption when I(ansible_psrp_protocol) is C(https).
++ - Only the auth protocols C(negotiate), C(kerberos), C(ntlm), and
++ C(credssp) can do message encryption. The other authentication protocols
++ only support encryption when C(protocol) is set to C(https).
++ - C(auto) means means message encryption is only used when not using
+ TLS/HTTPS.
+- - V(always) is the same as V(auto) but message encryption is always used
++ - C(always) is the same as C(auto) but message encryption is always used
+ even when running over TLS/HTTPS.
+- - V(never) disables any encryption checks that are in place when running
++ - C(never) disables any encryption checks that are in place when running
+ over HTTP and disables any authentication encryption processes.
+ type: str
+ vars:
+@@ -184,11 +184,11 @@ options:
+ description:
+ - Will disable any environment proxy settings and connect directly to the
+ remote host.
+- - This option is ignored if O(proxy) is set.
++ - This option is ignored if C(proxy) is set.
+ vars:
+ - name: ansible_psrp_ignore_proxy
+ type: bool
+- default: false
++ default: 'no'
+
+ # auth options
+ certificate_key_pem:
+@@ -206,7 +206,7 @@ options:
+ credssp_auth_mechanism:
+ description:
+ - The sub authentication mechanism to use with CredSSP auth.
+- - When V(auto), both Kerberos and NTLM is attempted with kerberos being
++ - When C(auto), both Kerberos and NTLM is attempted with kerberos being
+ preferred.
+ type: str
+ choices:
+@@ -219,16 +219,16 @@ options:
+ credssp_disable_tlsv1_2:
+ description:
+ - Disables the use of TLSv1.2 on the CredSSP authentication channel.
+- - This should not be set to V(yes) unless dealing with a host that does not
++ - This should not be set to C(yes) unless dealing with a host that does not
+ have TLSv1.2.
+- default: false
++ default: no
+ type: bool
+ vars:
+ - name: ansible_psrp_credssp_disable_tlsv1_2
+ credssp_minimum_version:
+ description:
+ - The minimum CredSSP server authentication version that will be accepted.
+- - Set to V(5) to ensure the server has been patched and is not vulnerable
++ - Set to C(5) to ensure the server has been patched and is not vulnerable
+ to CVE 2018-0886.
+ default: 2
+ type: int
+@@ -262,7 +262,7 @@ options:
+ - CBT is used to provide extra protection against Man in the Middle C(MitM)
+ attacks by binding the outer transport channel to the auth channel.
+ - CBT is not used when using just C(HTTP), only C(HTTPS).
+- default: true
++ default: yes
+ type: bool
+ vars:
+ - name: ansible_psrp_negotiate_send_cbt
+@@ -282,7 +282,7 @@ options:
+ description:
+ - Sets the WSMan timeout for each operation.
+ - This is measured in seconds.
+- - This should not exceed the value for O(connection_timeout).
++ - This should not exceed the value for C(connection_timeout).
+ type: int
+ vars:
+ - name: ansible_psrp_operation_timeout
+@@ -309,15 +309,13 @@ import base64
+ import json
+ import logging
+ import os
+-import typing as t
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleConnectionFailure, AnsibleError
+ from ansible.errors import AnsibleFileNotFound
+ from ansible.module_utils.parsing.convert_bool import boolean
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.plugins.connection import ConnectionBase
+-from ansible.plugins.shell.powershell import ShellModule as PowerShellPlugin
+ from ansible.plugins.shell.powershell import _common_args
+ from ansible.utils.display import Display
+ from ansible.utils.hashing import sha1
+@@ -347,16 +345,13 @@ class Connection(ConnectionBase):
+ has_pipelining = True
+ allow_extras = True
+
+- # Satifies mypy as this connection only ever runs with this plugin
+- _shell: PowerShellPlugin
+-
+- def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
++ def __init__(self, *args, **kwargs):
+ self.always_pipeline_modules = True
+ self.has_native_async = True
+
+- self.runspace: RunspacePool | None = None
+- self.host: PSHost | None = None
+- self._last_pipeline: PowerShell | None = None
++ self.runspace = None
++ self.host = None
++ self._last_pipeline = False
+
+ self._shell_type = 'powershell'
+ super(Connection, self).__init__(*args, **kwargs)
+@@ -366,7 +361,7 @@ class Connection(ConnectionBase):
+ logging.getLogger('requests_credssp').setLevel(logging.INFO)
+ logging.getLogger('urllib3').setLevel(logging.INFO)
+
+- def _connect(self) -> Connection:
++ def _connect(self):
+ if not HAS_PYPSRP:
+ raise AnsibleError("pypsrp or dependencies are not installed: %s"
+ % to_native(PYPSRP_IMP_ERR))
+@@ -413,7 +408,7 @@ class Connection(ConnectionBase):
+ self._last_pipeline = None
+ return self
+
+- def reset(self) -> None:
++ def reset(self):
+ if not self._connected:
+ self.runspace = None
+ return
+@@ -429,27 +424,26 @@ class Connection(ConnectionBase):
+ self.runspace = None
+ self._connect()
+
+- def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]:
++ def exec_command(self, cmd, in_data=None, sudoable=True):
+ super(Connection, self).exec_command(cmd, in_data=in_data,
+ sudoable=sudoable)
+
+- pwsh_in_data: bytes | str | None = None
+-
+ if cmd.startswith(" ".join(_common_args) + " -EncodedCommand"):
+ # This is a PowerShell script encoded by the shell plugin, we will
+ # decode the script and execute it in the runspace instead of
+ # starting a new interpreter to save on time
+ b_command = base64.b64decode(cmd.split(" ")[-1])
+ script = to_text(b_command, 'utf-16-le')
+- pwsh_in_data = to_text(in_data, errors="surrogate_or_strict", nonstring="passthru")
++ in_data = to_text(in_data, errors="surrogate_or_strict", nonstring="passthru")
+
+- if pwsh_in_data and isinstance(pwsh_in_data, str) and pwsh_in_data.startswith("#!"):
++ if in_data and in_data.startswith(u"#!"):
+ # ANSIBALLZ wrapper, we need to get the interpreter and execute
+ # that as the script - note this won't work as basic.py relies
+ # on packages not available on Windows, once fixed we can enable
+ # this path
+- interpreter = to_native(pwsh_in_data.splitlines()[0][2:])
++ interpreter = to_native(in_data.splitlines()[0][2:])
+ # script = "$input | &'%s' -" % interpreter
++ # in_data = to_text(in_data)
+ raise AnsibleError("cannot run the interpreter '%s' on the psrp "
+ "connection plugin" % interpreter)
+
+@@ -464,13 +458,12 @@ class Connection(ConnectionBase):
+ # In other cases we want to execute the cmd as the script. We add on the 'exit $LASTEXITCODE' to ensure the
+ # rc is propagated back to the connection plugin.
+ script = to_text(u"%s\nexit $LASTEXITCODE" % cmd)
+- pwsh_in_data = in_data
+ display.vvv(u"PSRP: EXEC %s" % script, host=self._psrp_host)
+
+- rc, stdout, stderr = self._exec_psrp_script(script, pwsh_in_data)
++ rc, stdout, stderr = self._exec_psrp_script(script, in_data)
+ return rc, stdout, stderr
+
+- def put_file(self, in_path: str, out_path: str) -> None:
++ def put_file(self, in_path, out_path):
+ super(Connection, self).put_file(in_path, out_path)
+
+ out_path = self._shell._unquote(out_path)
+@@ -618,7 +611,7 @@ end {
+ raise AnsibleError("Remote sha1 hash %s does not match local hash %s"
+ % (to_native(remote_sha1), to_native(local_sha1)))
+
+- def fetch_file(self, in_path: str, out_path: str) -> None:
++ def fetch_file(self, in_path, out_path):
+ super(Connection, self).fetch_file(in_path, out_path)
+ display.vvv("FETCH %s TO %s" % (in_path, out_path),
+ host=self._psrp_host)
+@@ -696,7 +689,7 @@ if ($bytes_read -gt 0) {
+ display.warning("failed to close remote file stream of file "
+ "'%s': %s" % (in_path, to_native(stderr)))
+
+- def close(self) -> None:
++ def close(self):
+ if self.runspace and self.runspace.state == RunspacePoolState.OPENED:
+ display.vvvvv("PSRP CLOSE RUNSPACE: %s" % (self.runspace.id),
+ host=self._psrp_host)
+@@ -705,7 +698,7 @@ if ($bytes_read -gt 0) {
+ self._connected = False
+ self._last_pipeline = None
+
+- def _build_kwargs(self) -> None:
++ def _build_kwargs(self):
+ self._psrp_host = self.get_option('remote_addr')
+ self._psrp_user = self.get_option('remote_user')
+ self._psrp_pass = self.get_option('remote_password')
+@@ -809,13 +802,7 @@ if ($bytes_read -gt 0) {
+ option = self.get_option('_extras')['ansible_psrp_%s' % arg]
+ self._psrp_conn_kwargs[arg] = option
+
+- def _exec_psrp_script(
+- self,
+- script: str,
+- input_data: bytes | str | t.Iterable | None = None,
+- use_local_scope: bool = True,
+- arguments: t.Iterable[str] | None = None,
+- ) -> tuple[int, bytes, bytes]:
++ def _exec_psrp_script(self, script, input_data=None, use_local_scope=True, arguments=None):
+ # Check if there's a command on the current pipeline that still needs to be closed.
+ if self._last_pipeline:
+ # Current pypsrp versions raise an exception if the current state was not RUNNING. We manually set it so we
+@@ -841,7 +828,7 @@ if ($bytes_read -gt 0) {
+
+ return rc, stdout, stderr
+
+- def _parse_pipeline_result(self, pipeline: PowerShell) -> tuple[int, bytes, bytes]:
++ def _parse_pipeline_result(self, pipeline):
+ """
+ PSRP doesn't have the same concept as other protocols with its output.
+ We need some extra logic to convert the pipeline streams and host
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/connection/ssh.py
++++ ansible-core-2.16.5/lib/ansible/plugins/connection/ssh.py
+@@ -4,7 +4,7 @@
+ # Copyright (c) 2017 Ansible Project
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+-from __future__ import (annotations, absolute_import, division, print_function)
++from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+ DOCUMENTATION = '''
+@@ -20,7 +20,7 @@ DOCUMENTATION = '''
+ - connection_pipelining
+ version_added: historical
+ notes:
+- - Many options default to V(None) here but that only means we do not override the SSH tool's defaults and/or configuration.
++ - Many options default to C(None) here but that only means we do not override the SSH tool's defaults and/or configuration.
+ For example, if you specify the port in this plugin it will override any C(Port) entry in your C(.ssh/config).
+ - The ssh CLI tool uses return code 255 as a 'connection error', this can conflict with commands/tools that
+ also return 255 as an error code and will look like an 'unreachable' condition or 'connection error' to this plugin.
+@@ -28,7 +28,6 @@ DOCUMENTATION = '''
+ host:
+ description: Hostname/IP to connect to.
+ default: inventory_hostname
+- type: string
+ vars:
+ - name: inventory_hostname
+ - name: ansible_host
+@@ -55,8 +54,7 @@ DOCUMENTATION = '''
+ - name: ansible_ssh_host_key_checking
+ version_added: '2.5'
+ password:
+- description: Authentication password for the O(remote_user). Can be supplied as CLI option.
+- type: string
++ description: Authentication password for the C(remote_user). Can be supplied as CLI option.
+ vars:
+ - name: ansible_password
+ - name: ansible_ssh_pass
+@@ -66,7 +64,6 @@ DOCUMENTATION = '''
+ - Password prompt that sshpass should search for. Supported by sshpass 1.06 and up.
+ - Defaults to C(Enter PIN for) when pkcs11_provider is set.
+ default: ''
+- type: string
+ ini:
+ - section: 'ssh_connection'
+ key: 'sshpass_prompt'
+@@ -78,7 +75,6 @@ DOCUMENTATION = '''
+ ssh_args:
+ description: Arguments to pass to all SSH CLI tools.
+ default: '-C -o ControlMaster=auto -o ControlPersist=60s'
+- type: string
+ ini:
+ - section: 'ssh_connection'
+ key: 'ssh_args'
+@@ -89,7 +85,6 @@ DOCUMENTATION = '''
+ version_added: '2.7'
+ ssh_common_args:
+ description: Common extra args for all SSH CLI tools.
+- type: string
+ ini:
+ - section: 'ssh_connection'
+ key: 'ssh_common_args'
+@@ -105,10 +100,9 @@ DOCUMENTATION = '''
+ ssh_executable:
+ default: ssh
+ description:
+- - This defines the location of the SSH binary. It defaults to V(ssh) which will use the first SSH binary available in $PATH.
++ - This defines the location of the SSH binary. It defaults to C(ssh) which will use the first SSH binary available in $PATH.
+ - This option is usually not required, it might be useful when access to system SSH is restricted,
+ or when using SSH wrappers to connect to remote hosts.
+- type: string
+ env: [{name: ANSIBLE_SSH_EXECUTABLE}]
+ ini:
+ - {key: ssh_executable, section: ssh_connection}
+@@ -120,8 +114,7 @@ DOCUMENTATION = '''
+ sftp_executable:
+ default: sftp
+ description:
+- - This defines the location of the sftp binary. It defaults to V(sftp) which will use the first binary available in $PATH.
+- type: string
++ - This defines the location of the sftp binary. It defaults to C(sftp) which will use the first binary available in $PATH.
+ env: [{name: ANSIBLE_SFTP_EXECUTABLE}]
+ ini:
+ - {key: sftp_executable, section: ssh_connection}
+@@ -132,8 +125,7 @@ DOCUMENTATION = '''
+ scp_executable:
+ default: scp
+ description:
+- - This defines the location of the scp binary. It defaults to V(scp) which will use the first binary available in $PATH.
+- type: string
++ - This defines the location of the scp binary. It defaults to C(scp) which will use the first binary available in $PATH.
+ env: [{name: ANSIBLE_SCP_EXECUTABLE}]
+ ini:
+ - {key: scp_executable, section: ssh_connection}
+@@ -143,7 +135,6 @@ DOCUMENTATION = '''
+ version_added: '2.7'
+ scp_extra_args:
+ description: Extra exclusive to the C(scp) CLI
+- type: string
+ vars:
+ - name: ansible_scp_extra_args
+ env:
+@@ -158,7 +149,6 @@ DOCUMENTATION = '''
+ default: ''
+ sftp_extra_args:
+ description: Extra exclusive to the C(sftp) CLI
+- type: string
+ vars:
+ - name: ansible_sftp_extra_args
+ env:
+@@ -173,7 +163,6 @@ DOCUMENTATION = '''
+ default: ''
+ ssh_extra_args:
+ description: Extra exclusive to the SSH CLI.
+- type: string
+ vars:
+ - name: ansible_ssh_extra_args
+ env:
+@@ -220,7 +209,6 @@ DOCUMENTATION = '''
+ description:
+ - User name with which to login to the remote server, normally set by the remote_user keyword.
+ - If no user is supplied, Ansible will let the SSH client binary choose the user as it normally.
+- type: string
+ ini:
+ - section: defaults
+ key: remote_user
+@@ -251,7 +239,6 @@ DOCUMENTATION = '''
+ private_key_file:
+ description:
+ - Path to private key file to use for authentication.
+- type: string
+ ini:
+ - section: defaults
+ key: private_key_file
+@@ -270,7 +257,6 @@ DOCUMENTATION = '''
+ - Since 2.3, if null (default), ansible will generate a unique hash. Use ``%(directory)s`` to indicate where to use the control dir path setting.
+ - Before 2.3 it defaulted to ``control_path=%(directory)s/ansible-ssh-%%h-%%p-%%r``.
+ - Be aware that this setting is ignored if C(-o ControlPath) is set in ssh args.
+- type: string
+ env:
+ - name: ANSIBLE_SSH_CONTROL_PATH
+ ini:
+@@ -284,7 +270,6 @@ DOCUMENTATION = '''
+ description:
+ - This sets the directory to use for ssh control path if the control path setting is null.
+ - Also, provides the ``%(directory)s`` variable for the control path setting.
+- type: string
+ env:
+ - name: ANSIBLE_SSH_CONTROL_PATH_DIR
+ ini:
+@@ -294,7 +279,7 @@ DOCUMENTATION = '''
+ - name: ansible_control_path_dir
+ version_added: '2.7'
+ sftp_batch_mode:
+- default: true
++ default: 'yes'
+ description: 'TODO: write it'
+ env: [{name: ANSIBLE_SFTP_BATCH_MODE}]
+ ini:
+@@ -310,7 +295,6 @@ DOCUMENTATION = '''
+ - For OpenSSH >=9.0 you must add an additional option to enable scp (scp_extra_args="-O")
+ - Using 'piped' creates an ssh pipe with C(dd) on either side to copy the data
+ choices: ['sftp', 'scp', 'piped', 'smart']
+- type: string
+ env: [{name: ANSIBLE_SSH_TRANSFER_METHOD}]
+ ini:
+ - {key: transfer_method, section: ssh_connection}
+@@ -319,16 +303,16 @@ DOCUMENTATION = '''
+ version_added: '2.12'
+ scp_if_ssh:
+ deprecated:
+- why: In favor of the O(ssh_transfer_method) option.
++ why: In favor of the "ssh_transfer_method" option.
+ version: "2.17"
+- alternatives: O(ssh_transfer_method)
++ alternatives: ssh_transfer_method
+ default: smart
+ description:
+ - "Preferred method to use when transferring files over SSH."
+- - When set to V(smart), Ansible will try them until one succeeds or they all fail.
+- - If set to V(True), it will force 'scp', if V(False) it will use 'sftp'.
+- - For OpenSSH >=9.0 you must add an additional option to enable scp (C(scp_extra_args="-O"))
+- - This setting will overridden by O(ssh_transfer_method) if set.
++ - When set to I(smart), Ansible will try them until one succeeds or they all fail.
++ - If set to I(True), it will force 'scp', if I(False) it will use 'sftp'.
++ - For OpenSSH >=9.0 you must add an additional option to enable scp (scp_extra_args="-O")
++ - This setting will overridden by ssh_transfer_method if set.
+ env: [{name: ANSIBLE_SCP_IF_SSH}]
+ ini:
+ - {key: scp_if_ssh, section: ssh_connection}
+@@ -337,7 +321,7 @@ DOCUMENTATION = '''
+ version_added: '2.7'
+ use_tty:
+ version_added: '2.5'
+- default: true
++ default: 'yes'
+ description: add -tt to ssh commands to force tty allocation.
+ env: [{name: ANSIBLE_SSH_USETTY}]
+ ini:
+@@ -370,7 +354,6 @@ DOCUMENTATION = '''
+ pkcs11_provider:
+ version_added: '2.12'
+ default: ""
+- type: string
+ description:
+ - "PKCS11 SmartCard provider such as opensc, example: /usr/local/lib/opensc-pkcs11.so"
+ - Requires sshpass version 1.06+, sshpass must support the -P option.
+@@ -381,18 +364,15 @@ DOCUMENTATION = '''
+ - name: ansible_ssh_pkcs11_provider
+ '''
+
+-import collections.abc as c
+ import errno
+ import fcntl
+ import hashlib
+-import io
+ import os
+ import pty
+ import re
+ import shlex
+ import subprocess
+ import time
+-import typing as t
+
+ from functools import wraps
+ from ansible.errors import (
+@@ -404,7 +384,7 @@ from ansible.errors import (
+ from ansible.errors import AnsibleOptionsError
+ from ansible.module_utils.compat import selectors
+ from ansible.module_utils.six import PY3, text_type, binary_type
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.parsing.convert_bool import BOOLEANS, boolean
+ from ansible.plugins.connection import ConnectionBase, BUFSIZE
+ from ansible.plugins.shell.powershell import _parse_clixml
+@@ -413,8 +393,6 @@ from ansible.utils.path import unfrackpa
+
+ display = Display()
+
+-P = t.ParamSpec('P')
+-
+ # error messages that indicate 255 return code is not from ssh itself.
+ b_NOT_SSH_ERRORS = (b'Traceback (most recent call last):', # Python-2.6 when there's an exception
+ # while invoking a script via -m
+@@ -432,14 +410,7 @@ class AnsibleControlPersistBrokenPipeErr
+ pass
+
+
+-def _handle_error(
+- remaining_retries: int,
+- command: bytes,
+- return_tuple: tuple[int, bytes, bytes],
+- no_log: bool,
+- host: str,
+- display: Display = display,
+-) -> None:
++def _handle_error(remaining_retries, command, return_tuple, no_log, host, display=display):
+
+ # sshpass errors
+ if command == b'sshpass':
+@@ -495,9 +466,7 @@ def _handle_error(
+ display.vvv(msg, host=host)
+
+
+-def _ssh_retry(
+- func: c.Callable[t.Concatenate[Connection, P], tuple[int, bytes, bytes]],
+-) -> c.Callable[t.Concatenate[Connection, P], tuple[int, bytes, bytes]]:
++def _ssh_retry(func):
+ """
+ Decorator to retry ssh/scp/sftp in the case of a connection failure
+
+@@ -510,12 +479,12 @@ def _ssh_retry(
+ * retries limit reached
+ """
+ @wraps(func)
+- def wrapped(self: Connection, *args: P.args, **kwargs: P.kwargs) -> tuple[int, bytes, bytes]:
++ def wrapped(self, *args, **kwargs):
+ remaining_tries = int(self.get_option('reconnection_retries')) + 1
+ cmd_summary = u"%s..." % to_text(args[0])
+ conn_password = self.get_option('password') or self._play_context.password
+ for attempt in range(remaining_tries):
+- cmd = t.cast(list[bytes], args[0])
++ cmd = args[0]
+ if attempt != 0 and conn_password and isinstance(cmd, list):
+ # If this is a retry, the fd/pipe for sshpass is closed, and we need a new one
+ self.sshpass_pipe = os.pipe()
+@@ -528,13 +497,13 @@ def _ssh_retry(
+ if self._play_context.no_log:
+ display.vvv(u'rc=%s, stdout and stderr censored due to no log' % return_tuple[0], host=self.host)
+ else:
+- display.vvv(str(return_tuple), host=self.host)
++ display.vvv(return_tuple, host=self.host)
+ # 0 = success
+ # 1-254 = remote command return code
+ # 255 could be a failure from the ssh command itself
+ except (AnsibleControlPersistBrokenPipeError):
+ # Retry one more time because of the ControlPersist broken pipe (see #16731)
+- cmd = t.cast(list[bytes], args[0])
++ cmd = args[0]
+ if conn_password and isinstance(cmd, list):
+ # This is a retry, so the fd/pipe for sshpass is closed, and we need a new one
+ self.sshpass_pipe = os.pipe()
+@@ -582,15 +551,15 @@ class Connection(ConnectionBase):
+ transport = 'ssh'
+ has_pipelining = True
+
+- def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
++ def __init__(self, *args, **kwargs):
+ super(Connection, self).__init__(*args, **kwargs)
+
+ # TODO: all should come from get_option(), but not might be set at this point yet
+ self.host = self._play_context.remote_addr
+ self.port = self._play_context.port
+ self.user = self._play_context.remote_user
+- self.control_path: str | None = None
+- self.control_path_dir: str | None = None
++ self.control_path = None
++ self.control_path_dir = None
+
+ # Windows operates differently from a POSIX connection/shell plugin,
+ # we need to set various properties to ensure SSH on Windows continues
+@@ -605,17 +574,11 @@ class Connection(ConnectionBase):
+ # put_file, and fetch_file methods, so we don't need to do any connection
+ # management here.
+
+- def _connect(self) -> Connection:
++ def _connect(self):
+ return self
+
+ @staticmethod
+- def _create_control_path(
+- host: str | None,
+- port: int | None,
+- user: str | None,
+- connection: ConnectionBase | None = None,
+- pid: int | None = None,
+- ) -> str:
++ def _create_control_path(host, port, user, connection=None, pid=None):
+ '''Make a hash for the controlpath based on con attributes'''
+ pstring = '%s-%s-%s' % (host, port, user)
+ if connection:
+@@ -629,7 +592,7 @@ class Connection(ConnectionBase):
+ return cpath
+
+ @staticmethod
+- def _sshpass_available() -> bool:
++ def _sshpass_available():
+ global SSHPASS_AVAILABLE
+
+ # We test once if sshpass is available, and remember the result. It
+@@ -647,7 +610,7 @@ class Connection(ConnectionBase):
+ return SSHPASS_AVAILABLE
+
+ @staticmethod
+- def _persistence_controls(b_command: list[bytes]) -> tuple[bool, bool]:
++ def _persistence_controls(b_command):
+ '''
+ Takes a command array and scans it for ControlPersist and ControlPath
+ settings and returns two booleans indicating whether either was found.
+@@ -666,7 +629,7 @@ class Connection(ConnectionBase):
+
+ return controlpersist, controlpath
+
+- def _add_args(self, b_command: list[bytes], b_args: t.Iterable[bytes], explanation: str) -> None:
++ def _add_args(self, b_command, b_args, explanation):
+ """
+ Adds arguments to the ssh command and displays a caller-supplied explanation of why.
+
+@@ -682,7 +645,7 @@ class Connection(ConnectionBase):
+ display.vvvvv(u'SSH: %s: (%s)' % (explanation, ')('.join(to_text(a) for a in b_args)), host=self.host)
+ b_command += b_args
+
+- def _build_command(self, binary: str, subsystem: str, *other_args: bytes | str) -> list[bytes]:
++ def _build_command(self, binary, subsystem, *other_args):
+ '''
+ Takes a executable (ssh, scp, sftp or wrapper) and optional extra arguments and returns the remote command
+ wrapped in local ssh shell commands and ready for execution.
+@@ -739,7 +702,6 @@ class Connection(ConnectionBase):
+ # be disabled if the client side doesn't support the option. However,
+ # sftp batch mode does not prompt for passwords so it must be disabled
+ # if not using controlpersist and using sshpass
+- b_args: t.Iterable[bytes]
+ if subsystem == 'sftp' and self.get_option('sftp_batch_mode'):
+ if conn_password:
+ b_args = [b'-o', b'BatchMode=no']
+@@ -839,7 +801,7 @@ class Connection(ConnectionBase):
+
+ return b_command
+
+- def _send_initial_data(self, fh: io.IOBase, in_data: bytes, ssh_process: subprocess.Popen) -> None:
++ def _send_initial_data(self, fh, in_data, ssh_process):
+ '''
+ Writes initial data to the stdin filehandle of the subprocess and closes
+ it. (The handle must be closed; otherwise, for example, "sftp -b -" will
+@@ -866,7 +828,7 @@ class Connection(ConnectionBase):
+
+ # Used by _run() to kill processes on failures
+ @staticmethod
+- def _terminate_process(p: subprocess.Popen) -> None:
++ def _terminate_process(p):
+ """ Terminate a process, ignoring errors """
+ try:
+ p.terminate()
+@@ -875,7 +837,7 @@ class Connection(ConnectionBase):
+
+ # This is separate from _run() because we need to do the same thing for stdout
+ # and stderr.
+- def _examine_output(self, source: str, state: str, b_chunk: bytes, sudoable: bool) -> tuple[bytes, bytes]:
++ def _examine_output(self, source, state, b_chunk, sudoable):
+ '''
+ Takes a string, extracts complete lines from it, tests to see if they
+ are a prompt, error message, etc., and sets appropriate flags in self.
+@@ -924,7 +886,7 @@ class Connection(ConnectionBase):
+
+ return b''.join(output), remainder
+
+- def _bare_run(self, cmd: list[bytes], in_data: bytes | None, sudoable: bool = True, checkrc: bool = True) -> tuple[int, bytes, bytes]:
++ def _bare_run(self, cmd, in_data, sudoable=True, checkrc=True):
+ '''
+ Starts the command and communicates with it until it ends.
+ '''
+@@ -970,7 +932,7 @@ class Connection(ConnectionBase):
+ else:
+ p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+- stdin = p.stdin # type: ignore[assignment] # stdin will be set and not None due to the calls above
++ stdin = p.stdin
+ except (OSError, IOError) as e:
+ raise AnsibleError('Unable to execute ssh command line on a controller due to: %s' % to_native(e))
+
+@@ -1220,13 +1182,13 @@ class Connection(ConnectionBase):
+ return (p.returncode, b_stdout, b_stderr)
+
+ @_ssh_retry
+- def _run(self, cmd: list[bytes], in_data: bytes | None, sudoable: bool = True, checkrc: bool = True) -> tuple[int, bytes, bytes]:
++ def _run(self, cmd, in_data, sudoable=True, checkrc=True):
+ """Wrapper around _bare_run that retries the connection
+ """
+ return self._bare_run(cmd, in_data, sudoable=sudoable, checkrc=checkrc)
+
+ @_ssh_retry
+- def _file_transport_command(self, in_path: str, out_path: str, sftp_action: str) -> tuple[int, bytes, bytes]:
++ def _file_transport_command(self, in_path, out_path, sftp_action):
+ # scp and sftp require square brackets for IPv6 addresses, but
+ # accept them for hostnames and IPv4 addresses too.
+ host = '[%s]' % self.host
+@@ -1314,7 +1276,7 @@ class Connection(ConnectionBase):
+ raise AnsibleError("failed to transfer file to %s %s:\n%s\n%s" %
+ (to_native(in_path), to_native(out_path), to_native(stdout), to_native(stderr)))
+
+- def _escape_win_path(self, path: str) -> str:
++ def _escape_win_path(self, path):
+ """ converts a Windows path to one that's supported by SFTP and SCP """
+ # If using a root path then we need to start with /
+ prefix = ""
+@@ -1327,7 +1289,7 @@ class Connection(ConnectionBase):
+ #
+ # Main public methods
+ #
+- def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]:
++ def exec_command(self, cmd, in_data=None, sudoable=True):
+ ''' run a command on the remote host '''
+
+ super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+@@ -1344,10 +1306,8 @@ class Connection(ConnectionBase):
+
+ # Make sure our first command is to set the console encoding to
+ # utf-8, this must be done via chcp to get utf-8 (65001)
+- # union-attr ignores rely on internal powershell shell plugin details,
+- # this should be fixed at a future point in time.
+- cmd_parts = ["chcp.com", "65001", self._shell._SHELL_REDIRECT_ALLNULL, self._shell._SHELL_AND] # type: ignore[union-attr]
+- cmd_parts.extend(self._shell._encode_script(cmd, as_list=True, strict_mode=False, preserve_rc=False)) # type: ignore[union-attr]
++ cmd_parts = ["chcp.com", "65001", self._shell._SHELL_REDIRECT_ALLNULL, self._shell._SHELL_AND]
++ cmd_parts.extend(self._shell._encode_script(cmd, as_list=True, strict_mode=False, preserve_rc=False))
+ cmd = ' '.join(cmd_parts)
+
+ # we can only use tty when we are not pipelining the modules. piping
+@@ -1361,7 +1321,6 @@ class Connection(ConnectionBase):
+ # to disable it as a troubleshooting method.
+ use_tty = self.get_option('use_tty')
+
+- args: tuple[str, ...]
+ if not in_data and sudoable and use_tty:
+ args = ('-tt', self.host, cmd)
+ else:
+@@ -1376,7 +1335,7 @@ class Connection(ConnectionBase):
+
+ return (returncode, stdout, stderr)
+
+- def put_file(self, in_path: str, out_path: str) -> tuple[int, bytes, bytes]: # type: ignore[override] # Used by tests and would break API
++ def put_file(self, in_path, out_path):
+ ''' transfer a file from local to remote '''
+
+ super(Connection, self).put_file(in_path, out_path)
+@@ -1392,7 +1351,7 @@ class Connection(ConnectionBase):
+
+ return self._file_transport_command(in_path, out_path, 'put')
+
+- def fetch_file(self, in_path: str, out_path: str) -> tuple[int, bytes, bytes]: # type: ignore[override] # Used by tests and would break API
++ def fetch_file(self, in_path, out_path):
+ ''' fetch a file from remote to local '''
+
+ super(Connection, self).fetch_file(in_path, out_path)
+@@ -1407,7 +1366,7 @@ class Connection(ConnectionBase):
+
+ return self._file_transport_command(in_path, out_path, 'get')
+
+- def reset(self) -> None:
++ def reset(self):
+
+ run_reset = False
+ self.host = self.get_option('host') or self._play_context.remote_addr
+@@ -1436,5 +1395,5 @@ class Connection(ConnectionBase):
+
+ self.close()
+
+- def close(self) -> None:
++ def close(self):
+ self._connected = False
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/connection/winrm.py
++++ ansible-core-2.16.5/lib/ansible/plugins/connection/winrm.py
+@@ -2,7 +2,7 @@
+ # Copyright (c) 2017 Ansible Project
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+-from __future__ import (annotations, absolute_import, division, print_function)
++from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+ DOCUMENTATION = """
+@@ -39,7 +39,7 @@ DOCUMENTATION = """
+ - name: remote_user
+ type: str
+ remote_password:
+- description: Authentication password for the O(remote_user). Can be supplied as CLI option.
++ description: Authentication password for the C(remote_user). Can be supplied as CLI option.
+ vars:
+ - name: ansible_password
+ - name: ansible_winrm_pass
+@@ -61,8 +61,8 @@ DOCUMENTATION = """
+ scheme:
+ description:
+ - URI scheme to use
+- - If not set, then will default to V(https) or V(http) if O(port) is
+- V(5985).
++ - If not set, then will default to C(https) or C(http) if I(port) is
++ C(5985).
+ choices: [http, https]
+ vars:
+ - name: ansible_winrm_scheme
+@@ -119,7 +119,7 @@ DOCUMENTATION = """
+ - The managed option means Ansible will obtain kerberos ticket.
+ - While the manual one means a ticket must already have been obtained by the user.
+ - If having issues with Ansible freezing when trying to obtain the
+- Kerberos ticket, you can either set this to V(manual) and obtain
++ Kerberos ticket, you can either set this to C(manual) and obtain
+ it outside Ansible or install C(pexpect) through pip and try
+ again.
+ choices: [managed, manual]
+@@ -128,29 +128,8 @@ DOCUMENTATION = """
+ type: str
+ connection_timeout:
+ description:
+- - Despite its name, sets both the 'operation' and 'read' timeout settings for the WinRM
++ - Sets the operation and read timeout settings for the WinRM
+ connection.
+- - The operation timeout belongs to the WS-Man layer and runs on the winRM-service on the
+- managed windows host.
+- - The read timeout belongs to the underlying python Request call (http-layer) and runs
+- on the ansible controller.
+- - The operation timeout sets the WS-Man 'Operation timeout' that runs on the managed
+- windows host. The operation timeout specifies how long a command will run on the
+- winRM-service before it sends the message 'WinRMOperationTimeoutError' back to the
+- client. The client (silently) ignores this message and starts a new instance of the
+- operation timeout, waiting for the command to finish (long running commands).
+- - The read timeout sets the client HTTP-request timeout and specifies how long the
+- client (ansible controller) will wait for data from the server to come back over
+- the HTTP-connection (timeout for waiting for in-between messages from the server).
+- When this timer expires, an exception will be thrown and the ansible connection
+- will be terminated with the error message 'Read timed out'
+- - To avoid the above exception to be thrown, the read timeout will be set to 10
+- seconds higher than the WS-Man operation timeout, thus make the connection more
+- robust on networks with long latency and/or many hops between server and client
+- network wise.
+- - Setting the difference bewteen the operation and the read timeout to 10 seconds
+- alligns it to the defaults used in the winrm-module and the PSRP-module which also
+- uses 10 seconds (30 seconds for read timeout and 20 seconds for operation timeout)
+ - Corresponds to the C(operation_timeout_sec) and
+ C(read_timeout_sec) args in pywinrm so avoid setting these vars
+ with this one.
+@@ -171,15 +150,13 @@ import tempfile
+ import shlex
+ import subprocess
+ import time
+-import typing as t
+-import xml.etree.ElementTree as ET
+
+ from inspect import getfullargspec
+ from urllib.parse import urlunsplit
+
+ HAVE_KERBEROS = False
+ try:
+- import kerberos # pylint: disable=unused-import
++ import kerberos
+ HAVE_KERBEROS = True
+ except ImportError:
+ pass
+@@ -189,16 +166,17 @@ from ansible.errors import AnsibleError,
+ from ansible.errors import AnsibleFileNotFound
+ from ansible.module_utils.json_utils import _filter_non_json_lines
+ from ansible.module_utils.parsing.convert_bool import boolean
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
++from ansible.module_utils.six import binary_type
+ from ansible.plugins.connection import ConnectionBase
+ from ansible.plugins.shell.powershell import _parse_clixml
+-from ansible.plugins.shell.powershell import ShellBase as PowerShellBase
+ from ansible.utils.hashing import secure_hash
+ from ansible.utils.display import Display
+
+
+ try:
+ import winrm
++ from winrm import Response
+ from winrm.exceptions import WinRMError, WinRMOperationTimeoutError
+ from winrm.protocol import Protocol
+ import requests.exceptions
+@@ -248,15 +226,14 @@ class Connection(ConnectionBase):
+ has_pipelining = True
+ allow_extras = True
+
+- def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
++ def __init__(self, *args, **kwargs):
+
+ self.always_pipeline_modules = True
+ self.has_native_async = True
+
+- self.protocol: winrm.Protocol | None = None
+- self.shell_id: str | None = None
++ self.protocol = None
++ self.shell_id = None
+ self.delegate = None
+- self._shell: PowerShellBase
+ self._shell_type = 'powershell'
+
+ super(Connection, self).__init__(*args, **kwargs)
+@@ -266,7 +243,7 @@ class Connection(ConnectionBase):
+ logging.getLogger('requests_kerberos').setLevel(logging.INFO)
+ logging.getLogger('urllib3').setLevel(logging.INFO)
+
+- def _build_winrm_kwargs(self) -> None:
++ def _build_winrm_kwargs(self):
+ # this used to be in set_options, as win_reboot needs to be able to
+ # override the conn timeout, we need to be able to build the args
+ # after setting individual options. This is called by _connect before
+@@ -340,7 +317,7 @@ class Connection(ConnectionBase):
+
+ # Until pykerberos has enough goodies to implement a rudimentary kinit/klist, simplest way is to let each connection
+ # auth itself with a private CCACHE.
+- def _kerb_auth(self, principal: str, password: str) -> None:
++ def _kerb_auth(self, principal, password):
+ if password is None:
+ password = ""
+
+@@ -405,8 +382,8 @@ class Connection(ConnectionBase):
+ rc = child.exitstatus
+ else:
+ proc_mechanism = "subprocess"
+- b_password = to_bytes(password, encoding='utf-8',
+- errors='surrogate_or_strict')
++ password = to_bytes(password, encoding='utf-8',
++ errors='surrogate_or_strict')
+
+ display.vvvv("calling kinit with subprocess for principal %s"
+ % principal)
+@@ -421,7 +398,7 @@ class Connection(ConnectionBase):
+ "'%s': %s" % (self._kinit_cmd, to_native(err))
+ raise AnsibleConnectionFailure(err_msg)
+
+- stdout, stderr = p.communicate(b_password + b'\n')
++ stdout, stderr = p.communicate(password + b'\n')
+ rc = p.returncode != 0
+
+ if rc != 0:
+@@ -436,7 +413,7 @@ class Connection(ConnectionBase):
+
+ display.vvvvv("kinit succeeded for principal %s" % principal)
+
+- def _winrm_connect(self) -> winrm.Protocol:
++ def _winrm_connect(self):
+ '''
+ Establish a WinRM connection over HTTP/HTTPS.
+ '''
+@@ -468,7 +445,7 @@ class Connection(ConnectionBase):
+ winrm_kwargs = self._winrm_kwargs.copy()
+ if self._winrm_connection_timeout:
+ winrm_kwargs['operation_timeout_sec'] = self._winrm_connection_timeout
+- winrm_kwargs['read_timeout_sec'] = self._winrm_connection_timeout + 10
++ winrm_kwargs['read_timeout_sec'] = self._winrm_connection_timeout + 1
+ protocol = Protocol(endpoint, transport=transport, **winrm_kwargs)
+
+ # open the shell from connect so we know we're able to talk to the server
+@@ -495,7 +472,7 @@ class Connection(ConnectionBase):
+ else:
+ raise AnsibleError('No transport found for WinRM connection')
+
+- def _winrm_write_stdin(self, command_id: str, stdin_iterator: t.Iterable[tuple[bytes, bool]]) -> None:
++ def _winrm_write_stdin(self, command_id, stdin_iterator):
+ for (data, is_last) in stdin_iterator:
+ for attempt in range(1, 4):
+ try:
+@@ -532,7 +509,7 @@ class Connection(ConnectionBase):
+
+ break
+
+- def _winrm_send_input(self, protocol: winrm.Protocol, shell_id: str, command_id: str, stdin: bytes, eof: bool = False) -> None:
++ def _winrm_send_input(self, protocol, shell_id, command_id, stdin, eof=False):
+ rq = {'env:Envelope': protocol._get_soap_header(
+ resource_uri='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
+ action='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send',
+@@ -546,84 +523,7 @@ class Connection(ConnectionBase):
+ stream['@End'] = 'true'
+ protocol.send_message(xmltodict.unparse(rq))
+
+- def _winrm_get_raw_command_output(
+- self,
+- protocol: winrm.Protocol,
+- shell_id: str,
+- command_id: str,
+- ) -> tuple[bytes, bytes, int, bool]:
+- rq = {'env:Envelope': protocol._get_soap_header(
+- resource_uri='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
+- action='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive',
+- shell_id=shell_id)}
+-
+- stream = rq['env:Envelope'].setdefault('env:Body', {}).setdefault('rsp:Receive', {})\
+- .setdefault('rsp:DesiredStream', {})
+- stream['@CommandId'] = command_id
+- stream['#text'] = 'stdout stderr'
+-
+- res = protocol.send_message(xmltodict.unparse(rq))
+- root = ET.fromstring(res)
+- stream_nodes = [
+- node for node in root.findall('.//*')
+- if node.tag.endswith('Stream')]
+- stdout = []
+- stderr = []
+- return_code = -1
+- for stream_node in stream_nodes:
+- if not stream_node.text:
+- continue
+- if stream_node.attrib['Name'] == 'stdout':
+- stdout.append(base64.b64decode(stream_node.text.encode('ascii')))
+- elif stream_node.attrib['Name'] == 'stderr':
+- stderr.append(base64.b64decode(stream_node.text.encode('ascii')))
+-
+- command_done = len([
+- node for node in root.findall('.//*')
+- if node.get('State', '').endswith('CommandState/Done')]) == 1
+- if command_done:
+- return_code = int(
+- next(node for node in root.findall('.//*')
+- if node.tag.endswith('ExitCode')).text)
+-
+- return b"".join(stdout), b"".join(stderr), return_code, command_done
+-
+- def _winrm_get_command_output(
+- self,
+- protocol: winrm.Protocol,
+- shell_id: str,
+- command_id: str,
+- try_once: bool = False,
+- ) -> tuple[bytes, bytes, int]:
+- stdout_buffer, stderr_buffer = [], []
+- command_done = False
+- return_code = -1
+-
+- while not command_done:
+- try:
+- stdout, stderr, return_code, command_done = \
+- self._winrm_get_raw_command_output(protocol, shell_id, command_id)
+- stdout_buffer.append(stdout)
+- stderr_buffer.append(stderr)
+-
+- # If we were able to get output at least once then we should be
+- # able to get the rest.
+- try_once = False
+- except WinRMOperationTimeoutError:
+- # This is an expected error when waiting for a long-running process,
+- # just silently retry if we haven't been set to do one attempt.
+- if try_once:
+- break
+- continue
+- return b''.join(stdout_buffer), b''.join(stderr_buffer), return_code
+-
+- def _winrm_exec(
+- self,
+- command: str,
+- args: t.Iterable[bytes] = (),
+- from_exec: bool = False,
+- stdin_iterator: t.Iterable[tuple[bytes, bool]] = None,
+- ) -> tuple[int, bytes, bytes]:
++ def _winrm_exec(self, command, args=(), from_exec=False, stdin_iterator=None):
+ if not self.protocol:
+ self.protocol = self._winrm_connect()
+ self._connected = True
+@@ -646,47 +546,45 @@ class Connection(ConnectionBase):
+ display.debug(traceback.format_exc())
+ stdin_push_failed = True
+
+- # Even on a failure above we try at least once to get the output
+- # in case the stdin was actually written and it an normally.
+- b_stdout, b_stderr, rc = self._winrm_get_command_output(
+- self.protocol,
+- self.shell_id,
+- command_id,
+- try_once=stdin_push_failed,
+- )
+- stdout = to_text(b_stdout)
+- stderr = to_text(b_stderr)
++ # NB: this can hang if the receiver is still running (eg, network failed a Send request but the server's still happy).
++ # FUTURE: Consider adding pywinrm status check/abort operations to see if the target is still running after a failure.
++ resptuple = self.protocol.get_command_output(self.shell_id, command_id)
++ # ensure stdout/stderr are text for py3
++ # FUTURE: this should probably be done internally by pywinrm
++ response = Response(tuple(to_text(v) if isinstance(v, binary_type) else v for v in resptuple))
+
++ # TODO: check result from response and set stdin_push_failed if we have nonzero
+ if from_exec:
+- display.vvvvv('WINRM RESULT <Response code %d, out %r, err %r>' % (rc, stdout, stderr), host=self._winrm_host)
+- display.vvvvvv('WINRM RC %d' % rc, host=self._winrm_host)
+- display.vvvvvv('WINRM STDOUT %s' % stdout, host=self._winrm_host)
+- display.vvvvvv('WINRM STDERR %s' % stderr, host=self._winrm_host)
+-
+- # This is done after logging so we can still see the raw stderr for
+- # debugging purposes.
+- if b_stderr.startswith(b"#< CLIXML"):
+- b_stderr = _parse_clixml(b_stderr)
+- stderr = to_text(stderr)
++ display.vvvvv('WINRM RESULT %r' % to_text(response), host=self._winrm_host)
++ else:
++ display.vvvvvv('WINRM RESULT %r' % to_text(response), host=self._winrm_host)
++
++ display.vvvvvv('WINRM STDOUT %s' % to_text(response.std_out), host=self._winrm_host)
++ display.vvvvvv('WINRM STDERR %s' % to_text(response.std_err), host=self._winrm_host)
+
+ if stdin_push_failed:
+ # There are cases where the stdin input failed but the WinRM service still processed it. We attempt to
+ # see if stdout contains a valid json return value so we can ignore this error
+ try:
+- filtered_output, dummy = _filter_non_json_lines(stdout)
++ filtered_output, dummy = _filter_non_json_lines(response.std_out)
+ json.loads(filtered_output)
+ except ValueError:
+ # stdout does not contain a return response, stdin input was a fatal error
+- raise AnsibleError(f'winrm send_input failed; \nstdout: {stdout}\nstderr {stderr}')
++ stderr = to_bytes(response.std_err, encoding='utf-8')
++ if stderr.startswith(b"#< CLIXML"):
++ stderr = _parse_clixml(stderr)
++
++ raise AnsibleError('winrm send_input failed; \nstdout: %s\nstderr %s'
++ % (to_native(response.std_out), to_native(stderr)))
+
+- return rc, b_stdout, b_stderr
++ return response
+ except requests.exceptions.Timeout as exc:
+ raise AnsibleConnectionFailure('winrm connection error: %s' % to_native(exc))
+ finally:
+ if command_id:
+ self.protocol.cleanup_command(self.shell_id, command_id)
+
+- def _connect(self) -> Connection:
++ def _connect(self):
+
+ if not HAS_WINRM:
+ raise AnsibleError("winrm or requests is not installed: %s" % to_native(WINRM_IMPORT_ERR))
+@@ -700,20 +598,20 @@ class Connection(ConnectionBase):
+ self._connected = True
+ return self
+
+- def reset(self) -> None:
++ def reset(self):
+ if not self._connected:
+ return
+ self.protocol = None
+ self.shell_id = None
+ self._connect()
+
+- def _wrapper_payload_stream(self, payload: bytes, buffer_size: int = 200000) -> t.Iterable[tuple[bytes, bool]]:
++ def _wrapper_payload_stream(self, payload, buffer_size=200000):
+ payload_bytes = to_bytes(payload)
+ byte_count = len(payload_bytes)
+ for i in range(0, byte_count, buffer_size):
+ yield payload_bytes[i:i + buffer_size], i + buffer_size >= byte_count
+
+- def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]:
++ def exec_command(self, cmd, in_data=None, sudoable=True):
+ super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+ cmd_parts = self._shell._encode_script(cmd, as_list=True, strict_mode=False, preserve_rc=False)
+
+@@ -725,10 +623,23 @@ class Connection(ConnectionBase):
+ if in_data:
+ stdin_iterator = self._wrapper_payload_stream(in_data)
+
+- return self._winrm_exec(cmd_parts[0], cmd_parts[1:], from_exec=True, stdin_iterator=stdin_iterator)
++ result = self._winrm_exec(cmd_parts[0], cmd_parts[1:], from_exec=True, stdin_iterator=stdin_iterator)
++
++ result.std_out = to_bytes(result.std_out)
++ result.std_err = to_bytes(result.std_err)
++
++ # parse just stderr from CLIXML output
++ if result.std_err.startswith(b"#< CLIXML"):
++ try:
++ result.std_err = _parse_clixml(result.std_err)
++ except Exception:
++ # unsure if we're guaranteed a valid xml doc- use raw output in case of error
++ pass
++
++ return (result.status_code, result.std_out, result.std_err)
+
+ # FUTURE: determine buffer size at runtime via remote winrm config?
+- def _put_file_stdin_iterator(self, in_path: str, out_path: str, buffer_size: int = 250000) -> t.Iterable[tuple[bytes, bool]]:
++ def _put_file_stdin_iterator(self, in_path, out_path, buffer_size=250000):
+ in_size = os.path.getsize(to_bytes(in_path, errors='surrogate_or_strict'))
+ offset = 0
+ with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as in_file:
+@@ -741,9 +652,9 @@ class Connection(ConnectionBase):
+ yield b64_data, (in_file.tell() == in_size)
+
+ if offset == 0: # empty file, return an empty buffer + eof to close it
+- yield b"", True
++ yield "", True
+
+- def put_file(self, in_path: str, out_path: str) -> None:
++ def put_file(self, in_path, out_path):
+ super(Connection, self).put_file(in_path, out_path)
+ out_path = self._shell._unquote(out_path)
+ display.vvv('PUT "%s" TO "%s"' % (in_path, out_path), host=self._winrm_host)
+@@ -783,18 +694,19 @@ class Connection(ConnectionBase):
+ script = script_template.format(self._shell._escape(out_path))
+ cmd_parts = self._shell._encode_script(script, as_list=True, strict_mode=False, preserve_rc=False)
+
+- status_code, b_stdout, b_stderr = self._winrm_exec(cmd_parts[0], cmd_parts[1:], stdin_iterator=self._put_file_stdin_iterator(in_path, out_path))
+- stdout = to_text(b_stdout)
+- stderr = to_text(b_stderr)
+-
+- if status_code != 0:
+- raise AnsibleError(stderr)
++ result = self._winrm_exec(cmd_parts[0], cmd_parts[1:], stdin_iterator=self._put_file_stdin_iterator(in_path, out_path))
++ # TODO: improve error handling
++ if result.status_code != 0:
++ raise AnsibleError(to_native(result.std_err))
+
+ try:
+- put_output = json.loads(stdout)
++ put_output = json.loads(result.std_out)
+ except ValueError:
+ # stdout does not contain a valid response
+- raise AnsibleError('winrm put_file failed; \nstdout: %s\nstderr %s' % (stdout, stderr))
++ stderr = to_bytes(result.std_err, encoding='utf-8')
++ if stderr.startswith(b"#< CLIXML"):
++ stderr = _parse_clixml(stderr)
++ raise AnsibleError('winrm put_file failed; \nstdout: %s\nstderr %s' % (to_native(result.std_out), to_native(stderr)))
+
+ remote_sha1 = put_output.get("sha1")
+ if not remote_sha1:
+@@ -805,7 +717,7 @@ class Connection(ConnectionBase):
+ if not remote_sha1 == local_sha1:
+ raise AnsibleError("Remote sha1 hash {0} does not match local hash {1}".format(to_native(remote_sha1), to_native(local_sha1)))
+
+- def fetch_file(self, in_path: str, out_path: str) -> None:
++ def fetch_file(self, in_path, out_path):
+ super(Connection, self).fetch_file(in_path, out_path)
+ in_path = self._shell._unquote(in_path)
+ out_path = out_path.replace('\\', '/')
+@@ -819,7 +731,7 @@ class Connection(ConnectionBase):
+ try:
+ script = '''
+ $path = '%(path)s'
+- If (Test-Path -LiteralPath $path -PathType Leaf)
++ If (Test-Path -Path $path -PathType Leaf)
+ {
+ $buffer_size = %(buffer_size)d
+ $offset = %(offset)d
+@@ -834,7 +746,7 @@ class Connection(ConnectionBase):
+ }
+ $stream.Close() > $null
+ }
+- ElseIf (Test-Path -LiteralPath $path -PathType Container)
++ ElseIf (Test-Path -Path $path -PathType Container)
+ {
+ Write-Host "[DIR]";
+ }
+@@ -846,16 +758,13 @@ class Connection(ConnectionBase):
+ ''' % dict(buffer_size=buffer_size, path=self._shell._escape(in_path), offset=offset)
+ display.vvvvv('WINRM FETCH "%s" to "%s" (offset=%d)' % (in_path, out_path, offset), host=self._winrm_host)
+ cmd_parts = self._shell._encode_script(script, as_list=True, preserve_rc=False)
+- status_code, b_stdout, b_stderr = self._winrm_exec(cmd_parts[0], cmd_parts[1:])
+- stdout = to_text(b_stdout)
+- stderr = to_text(b_stderr)
+-
+- if status_code != 0:
+- raise IOError(stderr)
+- if stdout.strip() == '[DIR]':
++ result = self._winrm_exec(cmd_parts[0], cmd_parts[1:])
++ if result.status_code != 0:
++ raise IOError(to_native(result.std_err))
++ if result.std_out.strip() == '[DIR]':
+ data = None
+ else:
+- data = base64.b64decode(stdout.strip())
++ data = base64.b64decode(result.std_out.strip())
+ if data is None:
+ break
+ else:
+@@ -875,7 +784,7 @@ class Connection(ConnectionBase):
+ if out_file:
+ out_file.close()
+
+- def close(self) -> None:
++ def close(self):
+ if self.protocol and self.shell_id:
+ display.vvvvv('WINRM CLOSE SHELL: %s' % self.shell_id, host=self._winrm_host)
+ self.protocol.close_shell(self.shell_id)
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/doc_fragments/constructed.py
++++ ansible-core-2.16.5/lib/ansible/plugins/doc_fragments/constructed.py
+@@ -12,7 +12,7 @@ class ModuleDocFragment(object):
+ options:
+ strict:
+ description:
+- - If V(yes) make invalid entries a fatal error, otherwise skip and continue.
++ - If C(yes) make invalid entries a fatal error, otherwise skip and continue.
+ - Since it is possible to use facts in the expressions they might not always be available
+ and we ignore those errors by default.
+ type: bool
+@@ -49,13 +49,13 @@ options:
+ default_value:
+ description:
+ - The default value when the host variable's value is an empty string.
+- - This option is mutually exclusive with O(keyed_groups[].trailing_separator).
++ - This option is mutually exclusive with C(trailing_separator).
+ type: str
+ version_added: '2.12'
+ trailing_separator:
+ description:
+- - Set this option to V(False) to omit the O(keyed_groups[].separator) after the host variable when the value is an empty string.
+- - This option is mutually exclusive with O(keyed_groups[].default_value).
++ - Set this option to I(False) to omit the C(separator) after the host variable when the value is an empty string.
++ - This option is mutually exclusive with C(default_value).
+ type: bool
+ default: True
+ version_added: '2.12'
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/doc_fragments/files.py
++++ ansible-core-2.16.5/lib/ansible/plugins/doc_fragments/files.py
+@@ -18,18 +18,17 @@ options:
+ description:
+ - The permissions the resulting filesystem object should have.
+ - For those used to I(/usr/bin/chmod) remember that modes are actually octal numbers.
+- You must give Ansible enough information to parse them correctly.
+- For consistent results, quote octal numbers (for example, V('644') or V('1777')) so Ansible receives
++ You must either add a leading zero so that Ansible's YAML parser knows it is an octal number
++ (like C(0644) or C(01777)) or quote it (like C('644') or C('1777')) so Ansible receives
+ a string and can do its own conversion from string into number.
+- Adding a leading zero (for example, V(0755)) works sometimes, but can fail in loops and some other circumstances.
+- - Giving Ansible a number without following either of these rules will end up with a decimal
++ - Giving Ansible a number without following one of these rules will end up with a decimal
+ number which will have unexpected results.
+- - As of Ansible 1.8, the mode may be specified as a symbolic mode (for example, V(u+rwx) or
+- V(u=rw,g=r,o=r)).
+- - If O(mode) is not specified and the destination filesystem object B(does not) exist, the default C(umask) on the system will be used
++ - As of Ansible 1.8, the mode may be specified as a symbolic mode (for example, C(u+rwx) or
++ C(u=rw,g=r,o=r)).
++ - If C(mode) is not specified and the destination filesystem object B(does not) exist, the default C(umask) on the system will be used
+ when setting the mode for the newly created filesystem object.
+- - If O(mode) is not specified and the destination filesystem object B(does) exist, the mode of the existing filesystem object will be used.
+- - Specifying O(mode) is the best way to ensure filesystem objects are created with the correct permissions.
++ - If C(mode) is not specified and the destination filesystem object B(does) exist, the mode of the existing filesystem object will be used.
++ - Specifying C(mode) is the best way to ensure filesystem objects are created with the correct permissions.
+ See CVE-2020-1736 for further details.
+ type: raw
+ owner:
+@@ -49,24 +48,24 @@ options:
+ seuser:
+ description:
+ - The user part of the SELinux filesystem object context.
+- - By default it uses the V(system) policy, where applicable.
+- - When set to V(_default), it will use the C(user) portion of the policy if available.
++ - By default it uses the C(system) policy, where applicable.
++ - When set to C(_default), it will use the C(user) portion of the policy if available.
+ type: str
+ serole:
+ description:
+ - The role part of the SELinux filesystem object context.
+- - When set to V(_default), it will use the C(role) portion of the policy if available.
++ - When set to C(_default), it will use the C(role) portion of the policy if available.
+ type: str
+ setype:
+ description:
+ - The type part of the SELinux filesystem object context.
+- - When set to V(_default), it will use the C(type) portion of the policy if available.
++ - When set to C(_default), it will use the C(type) portion of the policy if available.
+ type: str
+ selevel:
+ description:
+ - The level part of the SELinux filesystem object context.
+ - This is the MLS/MCS attribute, sometimes known as the C(range).
+- - When set to V(_default), it will use the C(level) portion of the policy if available.
++ - When set to C(_default), it will use the C(level) portion of the policy if available.
+ type: str
+ unsafe_writes:
+ description:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/doc_fragments/inventory_cache.py
++++ ansible-core-2.16.5/lib/ansible/plugins/doc_fragments/inventory_cache.py
+@@ -67,6 +67,12 @@ options:
+ - name: ANSIBLE_CACHE_PLUGIN_PREFIX
+ - name: ANSIBLE_INVENTORY_CACHE_PLUGIN_PREFIX
+ ini:
++ - section: default
++ key: fact_caching_prefix
++ deprecated:
++ alternatives: Use the 'defaults' section instead
++ why: Fixes typing error in INI section name
++ version: '2.16'
+ - section: defaults
+ key: fact_caching_prefix
+ - section: inventory
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/doc_fragments/result_format_callback.py
++++ ansible-core-2.16.5/lib/ansible/plugins/doc_fragments/result_format_callback.py
+@@ -31,14 +31,14 @@ class ModuleDocFragment(object):
+ name: Configure output for readability
+ description:
+ - Configure the result format to be more readable
+- - When O(result_format) is set to V(yaml) this option defaults to V(True), and defaults
+- to V(False) when configured to V(json).
+- - Setting this option to V(True) will force V(json) and V(yaml) results to always be pretty
++ - When the result format is set to C(yaml) this option defaults to C(True), and defaults
++ to C(False) when configured to C(json).
++ - Setting this option to C(True) will force C(json) and C(yaml) results to always be pretty
+ printed regardless of verbosity.
+- - When set to V(True) and used with the V(yaml) result format, this option will
++ - When set to C(True) and used with the C(yaml) result format, this option will
+ modify module responses in an attempt to produce a more human friendly output at the expense
+ of correctness, and should not be relied upon to aid in writing variable manipulations
+- or conditionals. For correctness, set this option to V(False) or set O(result_format) to V(json).
++ or conditionals. For correctness, set this option to C(False) or set the result format to C(json).
+ type: bool
+ default: null
+ env:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/doc_fragments/shell_common.py
++++ ansible-core-2.16.5/lib/ansible/plugins/doc_fragments/shell_common.py
+@@ -35,11 +35,11 @@ options:
+ system_tmpdirs:
+ description:
+ - "List of valid system temporary directories on the managed machine for Ansible to validate
+- O(remote_tmp) against, when specific permissions are needed. These must be world
++ C(remote_tmp) against, when specific permissions are needed. These must be world
+ readable, writable, and executable. This list should only contain directories which the
+ system administrator has pre-created with the proper ownership and permissions otherwise
+ security issues can arise."
+- - When O(remote_tmp) is required to be a system temp dir and it does not match any in the list,
++ - When C(remote_tmp) is required to be a system temp dir and it does not match any in the list,
+ the first one from the list will be used instead.
+ default: [ /var/tmp, /tmp ]
+ type: list
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/doc_fragments/shell_windows.py
++++ ansible-core-2.16.5/lib/ansible/plugins/doc_fragments/shell_windows.py
+@@ -35,7 +35,7 @@ options:
+ description:
+ - Controls if we set the locale for modules when executing on the
+ target.
+- - Windows only supports V(no) as an option.
++ - Windows only supports C(no) as an option.
+ type: bool
+ default: 'no'
+ choices: ['no', False]
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/doc_fragments/template_common.py
++++ ansible-core-2.16.5/lib/ansible/plugins/doc_fragments/template_common.py
+@@ -29,7 +29,7 @@ options:
+ description:
+ - Path of a Jinja2 formatted template on the Ansible controller.
+ - This can be a relative or an absolute path.
+- - The file must be encoded with C(utf-8) but O(output_encoding) can be used to control the encoding of the output
++ - The file must be encoded with C(utf-8) but I(output_encoding) can be used to control the encoding of the output
+ template.
+ type: path
+ required: yes
+@@ -82,14 +82,14 @@ options:
+ trim_blocks:
+ description:
+ - Determine when newlines should be removed from blocks.
+- - When set to V(yes) the first newline after a block is removed (block, not variable tag!).
++ - When set to C(yes) the first newline after a block is removed (block, not variable tag!).
+ type: bool
+ default: yes
+ version_added: '2.4'
+ lstrip_blocks:
+ description:
+ - Determine when leading spaces and tabs should be stripped.
+- - When set to V(yes) leading spaces and tabs are stripped from the start of a line to a block.
++ - When set to C(yes) leading spaces and tabs are stripped from the start of a line to a block.
+ type: bool
+ default: no
+ version_added: '2.6'
+@@ -102,7 +102,7 @@ options:
+ default: yes
+ output_encoding:
+ description:
+- - Overrides the encoding used to write the template file defined by O(dest).
++ - Overrides the encoding used to write the template file defined by C(dest).
+ - It defaults to C(utf-8), but any encoding supported by python can be used.
+ - The source template file must always be encoded using C(utf-8), for homogeneity.
+ type: str
+@@ -110,10 +110,10 @@ options:
+ version_added: '2.7'
+ notes:
+ - Including a string that uses a date in the template will result in the template being marked 'changed' each time.
+-- Since Ansible 0.9, templates are loaded with O(trim_blocks=True).
++- Since Ansible 0.9, templates are loaded with C(trim_blocks=True).
+ - >
+ Also, you can override jinja2 settings by adding a special header to template file.
+- that is C(#jinja2:variable_start_string:'[%', variable_end_string:'%]', trim_blocks: False)
++ i.e. C(#jinja2:variable_start_string:'[%', variable_end_string:'%]', trim_blocks: False)
+ which changes the variable interpolation markers to C([% var %]) instead of C({{ var }}).
+ This is the best way to prevent evaluation of things that look like, but should not be Jinja2.
+ - To find Byte Order Marks in files, use C(Format-Hex <file> -Count 16) on Windows, and use C(od -a -t x1 -N 16 <file>)
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/doc_fragments/url.py
++++ ansible-core-2.16.5/lib/ansible/plugins/doc_fragments/url.py
+@@ -17,7 +17,7 @@ options:
+ type: str
+ force:
+ description:
+- - If V(yes) do not get a cached copy.
++ - If C(yes) do not get a cached copy.
+ type: bool
+ default: no
+ http_agent:
+@@ -27,48 +27,48 @@ options:
+ default: ansible-httpget
+ use_proxy:
+ description:
+- - If V(no), it will not use a proxy, even if one is defined in an environment variable on the target hosts.
++ - If C(no), it will not use a proxy, even if one is defined in an environment variable on the target hosts.
+ type: bool
+ default: yes
+ validate_certs:
+ description:
+- - If V(no), SSL certificates will not be validated.
++ - If C(no), SSL certificates will not be validated.
+ - This should only be used on personally controlled sites using self-signed certificates.
+ type: bool
+ default: yes
+ url_username:
+ description:
+ - The username for use in HTTP basic authentication.
+- - This parameter can be used without O(url_password) for sites that allow empty passwords
++ - This parameter can be used without I(url_password) for sites that allow empty passwords
+ type: str
+ url_password:
+ description:
+ - The password for use in HTTP basic authentication.
+- - If the O(url_username) parameter is not specified, the O(url_password) parameter will not be used.
++ - If the I(url_username) parameter is not specified, the I(url_password) parameter will not be used.
+ type: str
+ force_basic_auth:
+ description:
+- - Credentials specified with O(url_username) and O(url_password) should be passed in HTTP Header.
++ - Credentials specified with I(url_username) and I(url_password) should be passed in HTTP Header.
+ type: bool
+ default: no
+ client_cert:
+ description:
+ - PEM formatted certificate chain file to be used for SSL client authentication.
+- - This file can also include the key as well, and if the key is included, O(client_key) is not required.
++ - This file can also include the key as well, and if the key is included, C(client_key) is not required.
+ type: path
+ client_key:
+ description:
+ - PEM formatted file that contains your private key to be used for SSL client authentication.
+- - If O(client_cert) contains both the certificate and key, this option is not required.
++ - If C(client_cert) contains both the certificate and key, this option is not required.
+ type: path
+ use_gssapi:
+ description:
+ - Use GSSAPI to perform the authentication, typically this is for Kerberos or Kerberos through Negotiate
+ authentication.
+ - Requires the Python library L(gssapi,https://github.com/pythongssapi/python-gssapi) to be installed.
+- - Credentials for GSSAPI can be specified with O(url_username)/O(url_password) or with the GSSAPI env var
++ - Credentials for GSSAPI can be specified with I(url_username)/I(url_password) or with the GSSAPI env var
+ C(KRB5CCNAME) that specified a custom Kerberos credential cache.
+- - NTLM authentication is B(not) supported even if the GSSAPI mech for NTLM has been installed.
++ - NTLM authentication is C(not) supported even if the GSSAPI mech for NTLM has been installed.
+ type: bool
+ default: no
+ version_added: '2.11'
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/doc_fragments/url_windows.py
++++ ansible-core-2.16.5/lib/ansible/plugins/doc_fragments/url_windows.py
+@@ -19,9 +19,9 @@ options:
+ follow_redirects:
+ description:
+ - Whether or the module should follow redirects.
+- - V(all) will follow all redirect.
+- - V(none) will not follow any redirect.
+- - V(safe) will follow only "safe" redirects, where "safe" means that the
++ - C(all) will follow all redirect.
++ - C(none) will not follow any redirect.
++ - C(safe) will follow only "safe" redirects, where "safe" means that the
+ client is only doing a C(GET) or C(HEAD) on the URI to which it is being
+ redirected.
+ - When following a redirected URL, the C(Authorization) header and any
+@@ -48,7 +48,7 @@ options:
+ description:
+ - Specify how many times the module will redirect a connection to an
+ alternative URI before the connection fails.
+- - If set to V(0) or O(follow_redirects) is set to V(none), or V(safe) when
++ - If set to C(0) or I(follow_redirects) is set to C(none), or C(safe) when
+ not doing a C(GET) or C(HEAD) it prevents all redirection.
+ default: 50
+ type: int
+@@ -56,12 +56,12 @@ options:
+ description:
+ - Specifies how long the request can be pending before it times out (in
+ seconds).
+- - Set to V(0) to specify an infinite timeout.
++ - Set to C(0) to specify an infinite timeout.
+ default: 30
+ type: int
+ validate_certs:
+ description:
+- - If V(no), SSL certificates will not be validated.
++ - If C(no), SSL certificates will not be validated.
+ - This should only be used on personally controlled sites using self-signed
+ certificates.
+ default: yes
+@@ -74,12 +74,12 @@ options:
+ C(Cert:\CurrentUser\My\<thumbprint>).
+ - The WinRM connection must be authenticated with C(CredSSP) or C(become)
+ is used on the task if the certificate file is not password protected.
+- - Other authentication types can set O(client_cert_password) when the cert
++ - Other authentication types can set I(client_cert_password) when the cert
+ is password protected.
+ type: str
+ client_cert_password:
+ description:
+- - The password for O(client_cert) if the cert is password protected.
++ - The password for I(client_cert) if the cert is password protected.
+ type: str
+ force_basic_auth:
+ description:
+@@ -96,14 +96,14 @@ options:
+ type: str
+ url_password:
+ description:
+- - The password for O(url_username).
++ - The password for I(url_username).
+ type: str
+ use_default_credential:
+ description:
+ - Uses the current user's credentials when authenticating with a server
+ protected with C(NTLM), C(Kerberos), or C(Negotiate) authentication.
+ - Sites that use C(Basic) auth will still require explicit credentials
+- through the O(url_username) and O(url_password) options.
++ through the I(url_username) and I(url_password) options.
+ - The module will only have access to the user's credentials if using
+ C(become) with a password, you are connecting with SSH using a password,
+ or connecting with WinRM using C(CredSSP) or C(Kerberos with delegation).
+@@ -114,14 +114,14 @@ options:
+ type: bool
+ use_proxy:
+ description:
+- - If V(no), it will not use the proxy defined in IE for the current user.
++ - If C(no), it will not use the proxy defined in IE for the current user.
+ default: yes
+ type: bool
+ proxy_url:
+ description:
+ - An explicit proxy to use for the request.
+- - By default, the request will use the IE defined proxy unless O(use_proxy)
+- is set to V(no).
++ - By default, the request will use the IE defined proxy unless I(use_proxy)
++ is set to C(no).
+ type: str
+ proxy_username:
+ description:
+@@ -129,14 +129,14 @@ options:
+ type: str
+ proxy_password:
+ description:
+- - The password for O(proxy_username).
++ - The password for I(proxy_username).
+ type: str
+ proxy_use_default_credential:
+ description:
+ - Uses the current user's credentials when authenticating with a proxy host
+ protected with C(NTLM), C(Kerberos), or C(Negotiate) authentication.
+ - Proxies that use C(Basic) auth will still require explicit credentials
+- through the O(proxy_username) and O(proxy_password) options.
++ through the I(proxy_username) and I(proxy_password) options.
+ - The module will only have access to the user's credentials if using
+ C(become) with a password, you are connecting with SSH using a password,
+ or connecting with WinRM using C(CredSSP) or C(Kerberos with delegation).
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/doc_fragments/vars_plugin_staging.py
++++ ansible-core-2.16.5/lib/ansible/plugins/doc_fragments/vars_plugin_staging.py
+@@ -14,10 +14,10 @@ options:
+ stage:
+ description:
+ - Control when this vars plugin may be executed.
+- - Setting this option to V(all) will run the vars plugin after importing inventory and whenever it is demanded by a task.
+- - Setting this option to V(task) will only run the vars plugin whenever it is demanded by a task.
+- - Setting this option to V(inventory) will only run the vars plugin after parsing inventory.
+- - If this option is omitted, the global C(RUN_VARS_PLUGINS) configuration is used to determine when to execute the vars plugin.
++ - Setting this option to C(all) will run the vars plugin after importing inventory and whenever it is demanded by a task.
++ - Setting this option to C(task) will only run the vars plugin whenever it is demanded by a task.
++ - Setting this option to C(inventory) will only run the vars plugin after parsing inventory.
++ - If this option is omitted, the global I(RUN_VARS_PLUGINS) configuration is used to determine when to execute the vars plugin.
+ choices: ['all', 'task', 'inventory']
+ version_added: "2.10"
+ type: str
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/__init__.py
+@@ -11,4 +11,4 @@ from ansible.plugins import AnsibleJinja
+ class AnsibleJinja2Filter(AnsibleJinja2Plugin):
+
+ def _no_options(self, *args, **kwargs):
+- raise NotImplementedError("Jinja2 filter plugins do not support option functions, they use direct arguments instead.")
++ raise NotImplementedError("Jinaj2 filter plugins do not support option functions, they use direct arguments instead.")
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/b64decode.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/b64decode.yml
+@@ -7,7 +7,7 @@ DOCUMENTATION:
+ - Base64 decoding function.
+ - The return value is a string.
+ - Trying to store a binary blob in a string most likely corrupts the binary. To base64 decode a binary blob,
+- use the ``base64`` command and pipe the encoded data through standard input.
++ use the ``base64`` command and pipe the encoded data through standard input.
+ For example, in the ansible.builtin.shell`` module, ``cmd="base64 --decode > myfile.bin" stdin="{{ encoded }}"``.
+ positional: _input
+ options:
+@@ -21,7 +21,7 @@ EXAMPLES: |
+ lola: "{{ 'bG9sYQ==' | b64decode }}"
+
+ # b64 decode the content of 'b64stuff' variable
+- stuff: "{{ b64stuff | b64decode }}"
++ stuff: "{{ b64stuff | b64encode }}"
+
+ RETURN:
+ _value:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/b64encode.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/b64encode.yml
+@@ -14,10 +14,10 @@ DOCUMENTATION:
+
+ EXAMPLES: |
+ # b64 encode a string
+- b64lola: "{{ 'lola'| b64encode }}"
++ b64lola: "{{ 'lola'|b64encode }}"
+
+ # b64 encode the content of 'stuff' variable
+- b64stuff: "{{ stuff | b64encode }}"
++ b64stuff: "{{ stuff|b64encode }}"
+
+ RETURN:
+ _value:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/bool.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/bool.yml
+@@ -3,7 +3,7 @@ DOCUMENTATION:
+ version_added: "historical"
+ short_description: cast into a boolean
+ description:
+- - Attempt to cast the input into a boolean (V(True) or V(False)) value.
++ - Attempt to cast the input into a boolean (C(True) or C(False)) value.
+ positional: _input
+ options:
+ _input:
+@@ -13,10 +13,10 @@ DOCUMENTATION:
+
+ EXAMPLES: |
+
+- # in vars
++ # simply encrypt my key in a vault
+ vars:
+- isbool: "{{ (a == b) | bool }} "
+- otherbool: "{{ anothervar | bool }} "
++ isbool: "{{ (a == b)|bool }} "
++ otherbool: "{{ anothervar|bool }} "
+
+ # in a task
+ ...
+@@ -24,5 +24,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: The boolean resulting of casting the input expression into a V(True) or V(False) value.
++ description: The boolean resulting of casting the input expression into a C(True) or C(False) value.
+ type: bool
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/combine.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/combine.yml
+@@ -16,7 +16,7 @@ DOCUMENTATION:
+ elements: dictionary
+ required: true
+ recursive:
+- description: If V(True), merge elements recursively.
++ description: If C(True), merge elements recursively.
+ type: bool
+ default: false
+ list_merge:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/comment.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/comment.yml
+@@ -38,7 +38,7 @@ DOCUMENTATION:
+ postfix:
+ description: Indicator of the end of each line inside a comment block, only available for styles that support multiline comments.
+ type: string
+- postfix_count:
++ protfix_count:
+ description: Number of times to add a postfix at the end of a line, when a prefix exists and is usable.
+ type: int
+ default: 1
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/core.py
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/core.py
+@@ -27,14 +27,14 @@ from jinja2.filters import pass_environm
+
+ from ansible.errors import AnsibleError, AnsibleFilterError, AnsibleFilterTypeError
+ from ansible.module_utils.six import string_types, integer_types, reraise, text_type
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.common.collections import is_sequence
+ from ansible.module_utils.common.yaml import yaml_load, yaml_load_all
+ from ansible.parsing.ajson import AnsibleJSONEncoder
+ from ansible.parsing.yaml.dumper import AnsibleDumper
+ from ansible.template import recursive_check_defined
+ from ansible.utils.display import Display
+-from ansible.utils.encrypt import do_encrypt, PASSLIB_AVAILABLE
++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
+@@ -193,8 +193,8 @@ def ternary(value, true_val, false_val,
+
+
+ def regex_escape(string, re_type='python'):
+- """Escape all regular expressions special characters from STRING."""
+ string = to_text(string, errors='surrogate_or_strict', nonstring='simplerepr')
++ '''Escape all regular expressions special characters from STRING.'''
+ if re_type == 'python':
+ return re.escape(string)
+ elif re_type == 'posix_basic':
+@@ -286,27 +286,10 @@ def get_encrypted_password(password, has
+ }
+
+ hashtype = passlib_mapping.get(hashtype, hashtype)
+-
+- unknown_passlib_hashtype = False
+- if PASSLIB_AVAILABLE and hashtype not in passlib_mapping and hashtype not in passlib_mapping.values():
+- unknown_passlib_hashtype = True
+- display.deprecated(
+- f"Checking for unsupported password_hash passlib hashtype '{hashtype}'. "
+- "This will be an error in the future as all supported hashtypes must be documented.",
+- version='2.19'
+- )
+-
+ try:
+- return do_encrypt(password, hashtype, salt=salt, salt_size=salt_size, rounds=rounds, ident=ident)
++ return passlib_or_crypt(password, hashtype, salt=salt, salt_size=salt_size, rounds=rounds, ident=ident)
+ except AnsibleError as e:
+ reraise(AnsibleFilterError, AnsibleFilterError(to_native(e), orig_exc=e), sys.exc_info()[2])
+- except Exception as e:
+- if unknown_passlib_hashtype:
+- # This can occur if passlib.hash has the hashtype attribute, but it has a different signature than the valid choices.
+- # In 2.19 this will replace the deprecation warning above and the extra exception handling can be deleted.
+- choices = ', '.join(passlib_mapping)
+- raise AnsibleFilterError(f"{hashtype} is not in the list of supported passlib algorithms: {choices}") from e
+- raise
+
+
+ def to_uuid(string, namespace=UUID_NAMESPACE_ANSIBLE):
+@@ -321,9 +304,9 @@ def to_uuid(string, namespace=UUID_NAMES
+
+
+ def mandatory(a, msg=None):
+- """Make a variable mandatory."""
+ from jinja2.runtime import Undefined
+
++ ''' Make a variable mandatory '''
+ if isinstance(a, Undefined):
+ if a._undefined_name is not None:
+ name = "'%s' " % to_text(a._undefined_name)
+@@ -332,7 +315,8 @@ def mandatory(a, msg=None):
+
+ if msg is not None:
+ raise AnsibleFilterError(to_native(msg))
+- raise AnsibleFilterError("Mandatory variable %s not defined." % name)
++ else:
++ raise AnsibleFilterError("Mandatory variable %s not defined." % name)
+
+ return a
+
+@@ -580,24 +564,10 @@ def path_join(paths):
+ of the different members '''
+ if isinstance(paths, string_types):
+ return os.path.join(paths)
+- if is_sequence(paths):
++ elif is_sequence(paths):
+ return os.path.join(*paths)
+- raise AnsibleFilterTypeError("|path_join expects string or sequence, got %s instead." % type(paths))
+-
+-
+-def commonpath(paths):
+- """
+- Retrieve the longest common path from the given list.
+-
+- :param paths: A list of file system paths.
+- :type paths: List[str]
+- :returns: The longest common path.
+- :rtype: str
+- """
+- if not is_sequence(paths):
+- raise AnsibleFilterTypeError("|path_join expects sequence, got %s instead." % type(paths))
+-
+- return os.path.commonpath(paths)
++ else:
++ raise AnsibleFilterTypeError("|path_join expects string or sequence, got %s instead." % type(paths))
+
+
+ class FilterModule(object):
+@@ -635,8 +605,6 @@ class FilterModule(object):
+ 'win_basename': partial(unicode_wrap, ntpath.basename),
+ 'win_dirname': partial(unicode_wrap, ntpath.dirname),
+ 'win_splitdrive': partial(unicode_wrap, ntpath.splitdrive),
+- 'commonpath': commonpath,
+- 'normpath': partial(unicode_wrap, os.path.normpath),
+
+ # file glob
+ 'fileglob': fileglob,
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/dict2items.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/dict2items.yml
+@@ -30,18 +30,8 @@ DOCUMENTATION:
+ EXAMPLES: |
+
+ # items => [ { "key": "a", "value": 1 }, { "key": "b", "value": 2 } ]
+- items: "{{ {'a': 1, 'b': 2}| dict2items }}"
++ items: "{{ {'a': 1, 'b': 2}| dict2items}}"
+
+- # files_dicts: [
+- # {
+- # "file": "users",
+- # "path": "/etc/passwd"
+- # },
+- # {
+- # "file": "groups",
+- # "path": "/etc/group"
+- # }
+- # ]
+ vars:
+ files:
+ users: /etc/passwd
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/difference.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/difference.yml
+@@ -5,7 +5,6 @@ DOCUMENTATION:
+ short_description: the difference of one list from another
+ description:
+ - Provide a unique list of all the elements of the first list that do not appear in the second one.
+- - Items in the resulting list are returned in arbitrary order.
+ options:
+ _input:
+ description: A list.
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/encryption.py
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/encryption.py
+@@ -8,7 +8,7 @@ from jinja2.runtime import Undefined
+ from jinja2.exceptions import UndefinedError
+
+ from ansible.errors import AnsibleFilterError, AnsibleFilterTypeError
+-from ansible.module_utils.common.text.converters import to_native, to_bytes
++from ansible.module_utils._text import to_native, to_bytes
+ from ansible.module_utils.six import string_types, binary_type
+ from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
+ from ansible.parsing.vault import is_encrypted, VaultSecret, VaultLib
+@@ -17,7 +17,7 @@ from ansible.utils.display import Displa
+ display = Display()
+
+
+-def do_vault(data, secret, salt=None, vault_id='filter_default', wrap_object=False, vaultid=None):
++def do_vault(data, secret, salt=None, vaultid='filter_default', wrap_object=False):
+
+ if not isinstance(secret, (string_types, binary_type, Undefined)):
+ raise AnsibleFilterTypeError("Secret passed is required to be a string, instead we got: %s" % type(secret))
+@@ -25,18 +25,11 @@ def do_vault(data, secret, salt=None, va
+ if not isinstance(data, (string_types, binary_type, Undefined)):
+ raise AnsibleFilterTypeError("Can only vault strings, instead we got: %s" % type(data))
+
+- if vaultid is not None:
+- display.deprecated("Use of undocumented 'vaultid', use 'vault_id' instead", version='2.20')
+- if vault_id == 'filter_default':
+- vault_id = vaultid
+- else:
+- display.warning("Ignoring vaultid as vault_id is already set.")
+-
+ vault = ''
+ vs = VaultSecret(to_bytes(secret))
+ vl = VaultLib()
+ try:
+- vault = vl.encrypt(to_bytes(data), vs, vault_id, salt)
++ vault = vl.encrypt(to_bytes(data), vs, vaultid, salt)
+ except UndefinedError:
+ raise
+ except Exception as e:
+@@ -50,7 +43,7 @@ def do_vault(data, secret, salt=None, va
+ return vault
+
+
+-def do_unvault(vault, secret, vault_id='filter_default', vaultid=None):
++def do_unvault(vault, secret, vaultid='filter_default'):
+
+ if not isinstance(secret, (string_types, binary_type, Undefined)):
+ raise AnsibleFilterTypeError("Secret passed is required to be as string, instead we got: %s" % type(secret))
+@@ -58,16 +51,9 @@ def do_unvault(vault, secret, vault_id='
+ if not isinstance(vault, (string_types, binary_type, AnsibleVaultEncryptedUnicode, Undefined)):
+ raise AnsibleFilterTypeError("Vault should be in the form of a string, instead we got: %s" % type(vault))
+
+- if vaultid is not None:
+- display.deprecated("Use of undocumented 'vaultid', use 'vault_id' instead", version='2.20')
+- if vault_id == 'filter_default':
+- vault_id = vaultid
+- else:
+- display.warning("Ignoring vaultid as vault_id is already set.")
+-
+ data = ''
+ vs = VaultSecret(to_bytes(secret))
+- vl = VaultLib([(vault_id, vs)])
++ vl = VaultLib([(vaultid, vs)])
+ if isinstance(vault, AnsibleVaultEncryptedUnicode):
+ vault.vault = vl
+ data = vault.data
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/extract.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/extract.yml
+@@ -12,7 +12,7 @@ DOCUMENTATION:
+ description: Index or key to extract.
+ type: raw
+ required: true
+- container:
++ contianer:
+ description: Dictionary or list from which to extract a value.
+ type: raw
+ required: true
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/flatten.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/flatten.yml
+@@ -14,7 +14,7 @@ DOCUMENTATION:
+ description: Number of recursive list depths to flatten.
+ type: int
+ skip_nulls:
+- description: Skip V(null)/V(None) elements when inserting into the top list.
++ description: Skip C(null)/C(None) elements when inserting into the top list.
+ type: bool
+ default: true
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/from_yaml.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/from_yaml.yml
+@@ -14,7 +14,7 @@ DOCUMENTATION:
+ required: true
+ EXAMPLES: |
+ # variable from string variable containing a YAML document
+- {{ github_workflow | from_yaml }}
++ {{ github_workflow | from_yaml}}
+
+ # variable from string JSON document
+ {{ '{"a": true, "b": 54, "c": [1,2,3]}' | from_yaml }}
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/from_yaml_all.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/from_yaml_all.yml
+@@ -8,7 +8,7 @@ DOCUMENTATION:
+ - If multiple YAML documents are not supplied, this is the equivalend of using C(from_yaml).
+ notes:
+ - This filter functions as a wrapper to the Python C(yaml.safe_load_all) function, part of the L(pyyaml Python library, https://pypi.org/project/PyYAML/).
+- - Possible conflicts in variable names from the multiple documents are resolved directly by the pyyaml library.
++ - Possible conflicts in variable names from the mulitple documents are resolved directly by the pyyaml library.
+ options:
+ _input:
+ description: A YAML string.
+@@ -20,7 +20,7 @@ EXAMPLES: |
+ {{ multidoc_yaml_string | from_yaml_all }}
+
+ # variable from multidocument YAML string
+- {{ '---\n{"a": true, "b": 54, "c": [1,2,3]}\n...\n---{"x": 1}\n...\n' | from_yaml_all }}
++ {{ '---\n{"a": true, "b": 54, "c": [1,2,3]}\n...\n---{"x": 1}\n...\n' | from_yaml_all}}
+
+ RETURN:
+ _value:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/hash.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/hash.yml
+@@ -24,5 +24,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: The checksum of the input, as configured in O(hashtype).
++ description: The checksum of the input, as configured in I(hashtype).
+ type: string
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/human_readable.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/human_readable.yml
+@@ -7,7 +7,7 @@ DOCUMENTATION:
+ positional: _input, isbits, unit
+ options:
+ _input:
+- description: Number of bytes, or bits. Depends on O(isbits).
++ description: Number of bytes, or bits. Depends on I(isbits).
+ type: int
+ required: true
+ isbits:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/human_to_bytes.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/human_to_bytes.yml
+@@ -15,7 +15,7 @@ DOCUMENTATION:
+ type: str
+ choices: ['Y', 'Z', 'E', 'P', 'T', 'G', 'M', 'K', 'B']
+ isbits:
+- description: If V(True), force to interpret only bit input; if V(False), force bytes. Otherwise use the notation to guess.
++ description: If C(True), force to interpret only bit input; if C(False), force bytes. Otherwise use the notation to guess.
+ type: bool
+ EXAMPLES: |
+
+@@ -23,7 +23,7 @@ EXAMPLES: |
+ size: '{{ "1.15 GB" | human_to_bytes }}'
+
+ # size => 1234803098
+- size: '{{ "1.15" | human_to_bytes(default_unit="G") }}'
++ size: '{{ "1.15" | human_to_bytes(deafult_unit="G") }}'
+
+ # this is an error, wants bits, got bytes
+ ERROR: '{{ "1.15 GB" | human_to_bytes(isbits=true) }}'
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/intersect.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/intersect.yml
+@@ -5,7 +5,6 @@ DOCUMENTATION:
+ short_description: intersection of lists
+ description:
+ - Provide a list with the common elements from other lists.
+- - Items in the resulting list are returned in arbitrary order.
+ options:
+ _input:
+ description: A list.
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/mandatory.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/mandatory.yml
+@@ -10,18 +10,11 @@ DOCUMENTATION:
+ description: Mandatory expression.
+ type: raw
+ required: true
+- msg:
+- description: The customized message that is printed when the given variable is not defined.
+- type: str
+- required: false
+ EXAMPLES: |
+
+ # results in a Filter Error
+ {{ notdefined | mandatory }}
+
+- # print a custom error message
+- {{ notdefined | mandatory(msg='This variable is required.') }}
+-
+ RETURN:
+ _value:
+ description: The input if defined, otherwise an error.
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/mathstuff.py
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/mathstuff.py
+@@ -18,19 +18,21 @@
+ # You should have received a copy of the GNU General Public License
+ # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+-from __future__ import annotations
++# Make coding more python3-ish
++from __future__ import (absolute_import, division, print_function)
++__metaclass__ = type
+
+ import itertools
+ import math
+
+-from collections.abc import Mapping, Iterable
++from collections.abc import Hashable, Mapping, Iterable
+
+ from jinja2.filters import pass_environment
+
+ from ansible.errors import AnsibleFilterError, AnsibleFilterTypeError
+ from ansible.module_utils.common.text import formatters
+ from ansible.module_utils.six import binary_type, text_type
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.utils.display import Display
+
+ try:
+@@ -82,27 +84,27 @@ def unique(environment, a, case_sensitiv
+
+ @pass_environment
+ def intersect(environment, a, b):
+- try:
+- c = list(set(a) & set(b))
+- except TypeError:
++ if isinstance(a, Hashable) and isinstance(b, Hashable):
++ c = set(a) & set(b)
++ else:
+ c = unique(environment, [x for x in a if x in b], True)
+ return c
+
+
+ @pass_environment
+ def difference(environment, a, b):
+- try:
+- c = list(set(a) - set(b))
+- except TypeError:
++ if isinstance(a, Hashable) and isinstance(b, Hashable):
++ c = set(a) - set(b)
++ else:
+ c = unique(environment, [x for x in a if x not in b], True)
+ return c
+
+
+ @pass_environment
+ def symmetric_difference(environment, a, b):
+- try:
+- c = list(set(a) ^ set(b))
+- except TypeError:
++ if isinstance(a, Hashable) and isinstance(b, Hashable):
++ c = set(a) ^ set(b)
++ else:
+ isect = intersect(environment, a, b)
+ c = [x for x in union(environment, a, b) if x not in isect]
+ return c
+@@ -110,9 +112,9 @@ def symmetric_difference(environment, a,
+
+ @pass_environment
+ def union(environment, a, b):
+- try:
+- c = list(set(a) | set(b))
+- except TypeError:
++ if isinstance(a, Hashable) and isinstance(b, Hashable):
++ c = set(a) | set(b)
++ else:
+ c = unique(environment, a + b, True)
+ return c
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/path_join.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/path_join.yml
+@@ -6,8 +6,6 @@ DOCUMENTATION:
+ positional: _input
+ description:
+ - Returns a path obtained by joining one or more path components.
+- - If a path component is an absolute path, then all previous components
+- are ignored and joining continues from the absolute path. See examples for details.
+ options:
+ _input:
+ description: A path, or a list of paths.
+@@ -23,14 +21,9 @@ EXAMPLES: |
+ # equivalent to '/etc/subdir/{{filename}}'
+ wheremyfile: "{{ ['/etc', 'subdir', filename] | path_join }}"
+
+- # trustme => '/etc/apt/trusted.d/mykey.gpg'
++ # trustme => '/etc/apt/trusted.d/mykey.gpgp'
+ trustme: "{{ ['/etc', 'apt', 'trusted.d', 'mykey.gpg'] | path_join }}"
+
+- # If one of the paths is absolute, then path_join ignores all previous path components
+- # If backup_dir == '/tmp' and backup_file == '/sample/baz.txt', the result is '/sample/baz.txt'
+- # backup_path => "/sample/baz.txt"
+- backup_path: "{{ ('/etc', backup_dir, backup_file) | path_join }}"
+-
+ RETURN:
+ _value:
+ description: The concatenated path.
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/realpath.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/realpath.yml
+@@ -4,8 +4,8 @@ DOCUMENTATION:
+ version_added: "1.8"
+ short_description: Turn path into real path
+ description:
+- - Resolves/follows symlinks to return the 'real path' from a given path.
+- - Filters always run on the controller so this path is resolved using the controller's filesystem.
++ - Resolves/follows symliknks to return the 'real path' from a given path.
++ - Filters alwasy run on controller so this path is resolved using the controller's filesystem.
+ options:
+ _input:
+ description: A path.
+@@ -13,7 +13,6 @@ DOCUMENTATION:
+ required: true
+ EXAMPLES: |
+
+- # realpath => /usr/bin/somebinary
+ realpath: {{ '/path/to/synlink' | realpath }}
+
+ RETURN:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/regex_findall.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/regex_findall.yml
+@@ -14,11 +14,11 @@ DOCUMENTATION:
+ description: Regular expression string that defines the match.
+ type: str
+ multiline:
+- description: Search across line endings if V(True), do not if otherwise.
++ description: Search across line endings if C(True), do not if otherwise.
+ type: bool
+ default: no
+ ignorecase:
+- description: Force the search to be case insensitive if V(True), case sensitive otherwise.
++ description: Force the search to be case insensitive if C(True), case sensitive otherwise.
+ type: bool
+ default: no
+
+@@ -27,12 +27,6 @@ EXAMPLES: |
+ # all_pirates => ['CAR', 'tar', 'bar']
+ all_pirates: "{{ 'CAR\ntar\nfoo\nbar\n' | regex_findall('^.ar$', multiline=True, ignorecase=True) }}"
+
+- # Using inline regex flags instead of passing options to filter
+- # See https://docs.python.org/3/library/re.html for more information
+- # on inline regex flags
+- # all_pirates => ['CAR', 'tar', 'bar']
+- all_pirates: "{{ 'CAR\ntar\nfoo\nbar\n' | regex_findall('(?im)^.ar$') }}"
+-
+ # get_ips => ['8.8.8.8', '8.8.4.4']
+ get_ips: "{{ 'Some DNS servers are 8.8.8.8 and 8.8.4.4' | regex_findall('\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b') }}"
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/regex_replace.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/regex_replace.yml
+@@ -5,7 +5,7 @@ DOCUMENTATION:
+ description:
+ - Replace a substring defined by a regular expression with another defined by another regular expression based on the first match.
+ notes:
+- - Maps to Python's C(re.sub).
++ - Maps to Python's C(re.replace).
+ positional: _input, _regex_match, _regex_replace
+ options:
+ _input:
+@@ -21,11 +21,11 @@ DOCUMENTATION:
+ type: int
+ required: true
+ multiline:
+- description: Search across line endings if V(True), do not if otherwise.
++ description: Search across line endings if C(True), do not if otherwise.
+ type: bool
+ default: no
+ ignorecase:
+- description: Force the search to be case insensitive if V(True), case sensitive otherwise.
++ description: Force the search to be case insensitive if C(True), case sensitive otherwise.
+ type: bool
+ default: no
+
+@@ -40,12 +40,6 @@ EXAMPLES: |
+ # piratecomment => '#CAR\n#tar\nfoo\n#bar\n'
+ piratecomment: "{{ 'CAR\ntar\nfoo\nbar\n' | regex_replace('^(.ar)$', '#\\1', multiline=True, ignorecase=True) }}"
+
+- # Using inline regex flags instead of passing options to filter
+- # See https://docs.python.org/3/library/re.html for more information
+- # on inline regex flags
+- # piratecomment => '#CAR\n#tar\nfoo\n#bar\n'
+- piratecomment: "{{ 'CAR\ntar\nfoo\nbar\n' | regex_replace('(?im)^(.ar)$', '#\\1') }}"
+-
+ RETURN:
+ _value:
+ description: String with substitution (or original if no match).
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/regex_search.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/regex_search.yml
+@@ -16,11 +16,11 @@ DOCUMENTATION:
+ description: Regular expression string that defines the match.
+ type: str
+ multiline:
+- description: Search across line endings if V(True), do not if otherwise.
++ description: Search across line endings if C(True), do not if otherwise.
+ type: bool
+ default: no
+ ignorecase:
+- description: Force the search to be case insensitive if V(True), case sensitive otherwise.
++ description: Force the search to be case insensitive if C(True), case sensitive otherwise.
+ type: bool
+ default: no
+
+@@ -29,12 +29,6 @@ EXAMPLES: |
+ # db => 'database42'
+ db: "{{ 'server1/database42' | regex_search('database[0-9]+') }}"
+
+- # Using inline regex flags instead of passing options to filter
+- # See https://docs.python.org/3/library/re.html for more information
+- # on inline regex flags
+- # server => 'sErver1'
+- db: "{{ 'sErver1/database42' | regex_search('(?i)server([0-9]+)') }}"
+-
+ # drinkat => 'BAR'
+ drinkat: "{{ 'foo\nBAR' | regex_search('^bar', multiline=True, ignorecase=True) }}"
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/relpath.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/relpath.yml
+@@ -5,8 +5,8 @@ DOCUMENTATION:
+ short_description: Make a path relative
+ positional: _input, start
+ description:
+- - Converts the given path to a relative path from the O(start),
+- or relative to the directory given in O(start).
++ - Converts the given path to a relative path from the I(start),
++ or relative to the directory given in I(start).
+ options:
+ _input:
+ description: A path.
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/root.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/root.yml
+@@ -18,7 +18,7 @@ DOCUMENTATION:
+ EXAMPLES: |
+
+ # => 8
+- fiveroot: "{{ 32768 | root(5) }}"
++ fiveroot: "{{ 32768 | root (5) }}"
+
+ # 2
+ sqrt_of_2: "{{ 4 | root }}"
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/split.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/split.yml
+@@ -3,7 +3,7 @@ DOCUMENTATION:
+ version_added: 2.11
+ short_description: split a string into a list
+ description:
+- - Using Python's text object method C(split) we turn strings into lists via a 'splitting character'.
++ - Using Python's text object method C(split) we turn strings into lists via a 'spliting character'.
+ notes:
+ - This is a passthrough to Python's C(str.split).
+ positional: _input, _split_string
+@@ -23,7 +23,7 @@ EXAMPLES: |
+ listjojo: "{{ 'jojo is a' | split }}"
+
+ # listjojocomma => [ "jojo is", "a" ]
+- listjojocomma: "{{ 'jojo is, a' | split(',') }}"
++ listjojocomma: "{{ 'jojo is, a' | split(',' }}"
+
+ RETURN:
+ _value:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/splitext.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/splitext.yml
+@@ -21,7 +21,7 @@ EXAMPLES: |
+ file_n_ext: "{{ 'ansible.cfg' | splitext }}"
+
+ # hoax => ['/etc/hoasdf', '']
+- hoax: '{{ "/etc//hoasdf/" | splitext }}'
++ hoax: '{{ "/etc//hoasdf/"|splitext }}'
+
+ RETURN:
+ _value:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/strftime.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/strftime.yml
+@@ -5,7 +5,7 @@ DOCUMENTATION:
+ description:
+ - Using Python's C(strftime) function, take a data formating string and a date/time to create a formated date.
+ notes:
+- - This is a passthrough to Python's C(stftime), for a complete set of formatting options go to https://strftime.org/.
++ - This is a passthrough to Python's C(stftime).
+ positional: _input, second, utc
+ options:
+ _input:
+@@ -23,8 +23,6 @@ DOCUMENTATION:
+ default: false
+
+ EXAMPLES: |
+- # for a complete set of features go to https://strftime.org/
+-
+ # Display year-month-day
+ {{ '%Y-%m-%d' | strftime }}
+ # => "2021-03-19"
+@@ -41,14 +39,6 @@ EXAMPLES: |
+ {{ '%Y-%m-%d' | strftime(0) }} # => 1970-01-01
+ {{ '%Y-%m-%d' | strftime(1441357287) }} # => 2015-09-04
+
+- # complex examples
+- vars:
+- date1: '2022-11-15T03:23:13.686956868Z'
+- date2: '2021-12-15T16:06:24.400087Z'
+- date_short: '{{ date1|regex_replace("([^.]+)(\.\d{6})(\d*)(.+)", "\1\2\4") }}' #shorten microseconds
+- iso8601format: '%Y-%m-%dT%H:%M:%S.%fZ'
+- date_diff_isoed: '{{ (date1|to_datetime(isoformat) - date2|to_datetime(isoformat)).total_seconds() }}'
+-
+ RETURN:
+ _value:
+ description: A formatted date/time string.
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/subelements.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/subelements.yml
+@@ -4,7 +4,7 @@ DOCUMENTATION:
+ short_description: returns a product of a list and its elements
+ positional: _input, _subelement, skip_missing
+ description:
+- - This produces a product of an object and the subelement values of that object, similar to the subelements lookup. This lets you specify individual subelements to use in a template O(_input).
++ - This produces a product of an object and the subelement values of that object, similar to the subelements lookup. This lets you specify individual subelements to use in a template I(_input).
+ options:
+ _input:
+ description: Original list.
+@@ -16,7 +16,7 @@ DOCUMENTATION:
+ type: str
+ required: yes
+ skip_missing:
+- description: If V(True), ignore missing subelements, otherwise missing subelements generate an error.
++ description: If C(True), ignore missing subelements, otherwise missing subelements generate an error.
+ type: bool
+ default: no
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/symmetric_difference.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/symmetric_difference.yml
+@@ -5,7 +5,6 @@ DOCUMENTATION:
+ short_description: different items from two lists
+ description:
+ - Provide a unique list of all the elements unique to each list.
+- - Items in the resulting list are returned in arbitrary order.
+ options:
+ _input:
+ description: A list.
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/ternary.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/ternary.yml
+@@ -4,22 +4,22 @@ DOCUMENTATION:
+ version_added: '1.9'
+ short_description: Ternary operation filter
+ description:
+- - Return the first value if the input is V(True), the second if V(False).
++ - Return the first value if the input is C(True), the second if C(False).
+ positional: true_val, false_val
+ options:
+ _input:
+- description: A boolean expression, must evaluate to V(True) or V(False).
++ description: A boolean expression, must evaluate to C(True) or C(False).
+ type: bool
+ required: true
+ true_val:
+- description: Value to return if the input is V(True).
++ description: Value to return if the input is C(True).
+ type: any
+ required: true
+ false_val:
+- description: Value to return if the input is V(False).
++ description: Value to return if the input is C(False).
+ type: any
+ none_val:
+- description: Value to return if the input is V(None). If not set, V(None) will be treated as V(False).
++ description: Value to return if the input is C(None). If not set, C(None) will be treated as C(False).
+ type: any
+ version_added: '2.8'
+ notes:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/to_json.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/to_json.yml
+@@ -23,8 +23,8 @@ DOCUMENTATION:
+ default: True
+ version_added: '2.9'
+ allow_nan:
+- description: When V(False), strict adherence to float value limits of the JSON specifications, so C(nan), C(inf) and C(-inf) values will produce errors.
+- When V(True), JavaScript equivalents will be used (C(NaN), C(Infinity), C(-Infinity)).
++ description: When C(False), strict adherence to float value limits of the JSON specifications, so C(nan), C(inf) and C(-inf) values will produce errors.
++ When C(True), JavaScript equivalents will be used (C(NaN), C(Infinity), C(-Infinity)).
+ default: True
+ type: bool
+ check_circular:
+@@ -41,11 +41,11 @@ DOCUMENTATION:
+ type: integer
+ separators:
+ description: The C(item) and C(key) separator to be used in the serialized output,
+- default may change depending on O(indent) and Python version.
++ default may change depending on I(indent) and Python version.
+ default: "(', ', ': ')"
+ type: tuple
+ skipkeys:
+- description: If V(True), keys that are not basic Python types will be skipped.
++ description: If C(True), keys that are not basic Python types will be skipped.
+ default: False
+ type: bool
+ sort_keys:
+@@ -53,15 +53,15 @@ DOCUMENTATION:
+ default: False
+ type: bool
+ notes:
+- - Both O(vault_to_text) and O(preprocess_unsafe) defaulted to V(False) between Ansible 2.9 and 2.12.
+- - 'These parameters to C(json.dumps) will be ignored, as they are overridden internally: I(cls), I(default)'
++ - Both I(vault_to_text) and I(preprocess_unsafe) defaulted to C(False) between Ansible 2.9 and 2.12.
++ - 'These parameters to C(json.dumps) will be ignored, as they are overriden internally: I(cls), I(default)'
+
+ EXAMPLES: |
+ # dump variable in a template to create a JSON document
+- {{ docker_config | to_json }}
++ {{ docker_config|to_json }}
+
+ # same as above but 'prettier' (equivalent to to_nice_json filter)
+- {{ docker_config | to_json(indent=4, sort_keys=True) }}
++ {{ docker_config|to_json(indent=4, sort_keys=True) }}
+
+ RETURN:
+ _value:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/to_nice_json.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/to_nice_json.yml
+@@ -23,8 +23,8 @@ DOCUMENTATION:
+ default: True
+ version_added: '2.9'
+ allow_nan:
+- description: When V(False), strict adherence to float value limits of the JSON specification, so C(nan), C(inf) and C(-inf) values will produce errors.
+- When V(True), JavaScript equivalents will be used (C(NaN), C(Infinity), C(-Infinity)).
++ description: When C(False), strict adherence to float value limits of the JSON specification, so C(nan), C(inf) and C(-inf) values will produce errors.
++ When C(True), JavaScript equivalents will be used (C(NaN), C(Infinity), C(-Infinity)).
+ default: True
+ type: bool
+ check_circular:
+@@ -36,16 +36,16 @@ DOCUMENTATION:
+ default: True
+ type: bool
+ skipkeys:
+- description: If V(True), keys that are not basic Python types will be skipped.
++ description: If C(True), keys that are not basic Python types will be skipped.
+ default: False
+ type: bool
+ notes:
+- - Both O(vault_to_text) and O(preprocess_unsafe) defaulted to V(False) between Ansible 2.9 and 2.12.
+- - 'These parameters to C(json.dumps) will be ignored, they are overridden for internal use: I(cls), I(default), I(indent), I(separators), I(sort_keys).'
++ - Both I(vault_to_text) and I(preprocess_unsafe) defaulted to C(False) between Ansible 2.9 and 2.12.
++ - 'These parameters to C(json.dumps) will be ignored, they are overriden for internal use: I(cls), I(default), I(indent), I(separators), I(sort_keys).'
+
+ EXAMPLES: |
+ # dump variable in a template to create a nicely formatted JSON document
+- {{ docker_config | to_nice_json }}
++ {{ docker_config|to_nice_json }}
+
+
+ RETURN:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/to_nice_yaml.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/to_nice_yaml.yml
+@@ -27,7 +27,7 @@ DOCUMENTATION:
+ #default_style=None, canonical=None, width=None, line_break=None, encoding=None, explicit_start=None, explicit_end=None, version=None, tags=None
+ notes:
+ - More options may be available, see L(PyYAML documentation, https://pyyaml.org/wiki/PyYAMLDocumentation) for details.
+- - 'These parameters to C(yaml.dump) will be ignored, as they are overridden internally: I(default_flow_style)'
++ - 'These parameters to C(yaml.dump) will be ignored, as they are overriden internally: I(default_flow_style)'
+
+ EXAMPLES: |
+ # dump variable in a template to create a YAML document
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/to_yaml.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/to_yaml.yml
+@@ -25,26 +25,26 @@ DOCUMENTATION:
+
+ # TODO: find docs for these
+ #allow_unicode:
+- # description:
++ # description:
+ # type: bool
+ # default: true
+ #default_flow_style
+ #default_style
+- #canonical=None,
+- #width=None,
+- #line_break=None,
+- #encoding=None,
+- #explicit_start=None,
+- #explicit_end=None,
+- #version=None,
++ #canonical=None,
++ #width=None,
++ #line_break=None,
++ #encoding=None,
++ #explicit_start=None,
++ #explicit_end=None,
++ #version=None,
+ #tags=None
+
+ EXAMPLES: |
+ # dump variable in a template to create a YAML document
+- {{ github_workflow | to_yaml }}
++ {{ github_workflow |to_yaml}}
+
+ # same as above but 'prettier' (equivalent to to_nice_yaml filter)
+- {{ docker_config | to_yaml(indent=4) }}
++ {{ docker_config|to_json(indent=4) }}
+
+ RETURN:
+ _value:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/type_debug.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/type_debug.yml
+@@ -16,5 +16,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: The Python 'type' of the O(_input) provided.
++ description: The Python 'type' of the I(_input) provided.
+ type: string
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/union.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/union.yml
+@@ -5,7 +5,6 @@ DOCUMENTATION:
+ short_description: union of lists
+ description:
+ - Provide a unique list of all the elements of two lists.
+- - Items in the resulting list are returned in arbitrary order.
+ options:
+ _input:
+ description: A list.
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/unvault.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/unvault.yml
+@@ -23,12 +23,12 @@ DOCUMENTATION:
+ EXAMPLES: |
+ # simply decrypt my key from a vault
+ vars:
+- mykey: "{{ myvaultedkey | unvault(passphrase) }} "
++ mykey: "{{ myvaultedkey|unvault(passphrase) }} "
+
+ - name: save templated unvaulted data
+ template: src=dump_template_data.j2 dest=/some/key/clear.txt
+ vars:
+- template_data: '{{ secretdata | unvault(vaultsecret) }}'
++ template_data: '{{ secretdata|unvault(vaultsecret) }}'
+
+ RETURN:
+ _value:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/urldecode.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/urldecode.yml
+@@ -1,29 +1,48 @@
+ DOCUMENTATION:
+- name: urldecode
++ name: urlsplit
+ version_added: "2.4"
+- short_description: Decode percent-encoded sequences
++ short_description: get components from URL
+ description:
+- - Replace %xx escapes with their single-character equivalent in the given string.
+- - Also replace plus signs with spaces, as required for unquoting HTML form values.
+- positional: _input
++ - Split a URL into its component parts.
++ positional: _input, query
+ options:
+ _input:
+- description: URL encoded string to decode.
++ description: URL string to split.
+ type: str
+ required: true
++ query:
++ description: Specify a single component to return.
++ type: str
++ choices: ["fragment", "hostname", "netloc", "password", "path", "port", "query", "scheme", "username"]
+
+ RETURN:
+ _value:
+ description:
+- - URL decoded value for the given string
++ - A dictionary with components as keyword and their value.
++ - If I(query) is provided, a string or integer will be returned instead, depending on I(query).
+ type: any
+
+ EXAMPLES: |
+
+- # Decode urlencoded string
+- {{ '%7e/abc+def' | urldecode }}
+- # => "~/abc def"
+-
+- # Decode plus sign as well
+- {{ 'El+Ni%C3%B1o' | urldecode }}
+- # => "El Niño"
++ {{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit }}
++ # =>
++ # {
++ # "fragment": "fragment",
++ # "hostname": "www.acme.com",
++ # "netloc": "user:password@www.acme.com:9000",
++ # "password": "password",
++ # "path": "/dir/index.html",
++ # "port": 9000,
++ # "query": "query=term",
++ # "scheme": "http",
++ # "username": "user"
++ # }
++
++ {{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('hostname') }}
++ # => 'www.acme.com'
++
++ {{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('query') }}
++ # => 'query=term'
++
++ {{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('path') }}
++ # => '/dir/index.html'
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/urlsplit.py
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/urlsplit.py
+@@ -53,7 +53,7 @@ RETURN = r'''
+ _value:
+ description:
+ - A dictionary with components as keyword and their value.
+- - If O(query) is provided, a string or integer will be returned instead, depending on O(query).
++ - If I(query) is provided, a string or integer will be returned instead, depending on I(query).
+ type: any
+ '''
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/vault.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/vault.yml
+@@ -26,7 +26,7 @@ DOCUMENTATION:
+ default: 'filter_default'
+ wrap_object:
+ description:
+- - This toggle can force the return of an C(AnsibleVaultEncryptedUnicode) string object, when V(False), you get a simple string.
++ - This toggle can force the return of an C(AnsibleVaultEncryptedUnicode) string object, when C(False), you get a simple string.
+ - Mostly useful when combining with the C(to_yaml) filter to output the 'inline vault' format.
+ type: bool
+ default: False
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/zip.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/zip.yml
+@@ -18,7 +18,7 @@ DOCUMENTATION:
+ elements: any
+ required: yes
+ strict:
+- description: If V(True) return an error on mismatching list length, otherwise shortest list determines output.
++ description: If C(True) return an error on mismatching list length, otherwise shortest list determines output.
+ type: bool
+ default: no
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/filter/zip_longest.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/filter/zip_longest.yml
+@@ -5,7 +5,7 @@ DOCUMENTATION:
+ positional: _input, _additional_lists
+ description:
+ - Make an iterator that aggregates elements from each of the iterables.
+- If the iterables are of uneven length, missing values are filled-in with O(fillvalue).
++ If the iterables are of uneven length, missing values are filled-in with I(fillvalue).
+ Iteration continues until the longest iterable is exhausted.
+ notes:
+ - This is mostly a passhtrough to Python's C(itertools.zip_longest) function
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/inventory/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/inventory/__init__.py
+@@ -30,7 +30,7 @@ from ansible.inventory.group import to_s
+ from ansible.parsing.utils.addresses import parse_address
+ from ansible.plugins import AnsiblePlugin
+ from ansible.plugins.cache import CachePluginAdjudicator as CacheObject
+-from ansible.module_utils.common.text.converters import to_bytes, to_native
++from ansible.module_utils._text import to_bytes, to_native
+ from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.module_utils.six import string_types
+ from ansible.template import Templar
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/inventory/advanced_host_list.py
++++ ansible-core-2.16.5/lib/ansible/plugins/inventory/advanced_host_list.py
+@@ -24,7 +24,7 @@ EXAMPLES = '''
+ import os
+
+ from ansible.errors import AnsibleError, AnsibleParserError
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.plugins.inventory import BaseInventoryPlugin
+
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/inventory/constructed.py
++++ ansible-core-2.16.5/lib/ansible/plugins/inventory/constructed.py
+@@ -13,7 +13,7 @@ DOCUMENTATION = '''
+ - The Jinja2 conditionals that qualify a host for membership.
+ - The Jinja2 expressions are calculated and assigned to the variables
+ - Only variables already available from previous inventories or the fact cache can be used for templating.
+- - When O(strict) is False, failed expressions will be ignored (assumes vars were missing).
++ - When I(strict) is False, failed expressions will be ignored (assumes vars were missing).
+ options:
+ plugin:
+ description: token that ensures this is a source file for the 'constructed' plugin.
+@@ -84,7 +84,7 @@ from ansible import constants as C
+ from ansible.errors import AnsibleParserError, AnsibleOptionsError
+ from ansible.inventory.helpers import get_group_vars
+ from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.utils.vars import combine_vars
+ from ansible.vars.fact_cache import FactCache
+ from ansible.vars.plugins import get_vars_from_inventory_sources
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/inventory/host_list.py
++++ ansible-core-2.16.5/lib/ansible/plugins/inventory/host_list.py
+@@ -27,7 +27,7 @@ EXAMPLES = r'''
+ import os
+
+ from ansible.errors import AnsibleError, AnsibleParserError
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.parsing.utils.addresses import parse_address
+ from ansible.plugins.inventory import BaseInventoryPlugin
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/inventory/ini.py
++++ ansible-core-2.16.5/lib/ansible/plugins/inventory/ini.py
+@@ -75,13 +75,12 @@ host4 # same host as above, but member o
+
+ import ast
+ import re
+-import warnings
+
+ from ansible.inventory.group import to_safe_group_name
+ from ansible.plugins.inventory import BaseFileInventoryPlugin
+
+ from ansible.errors import AnsibleError, AnsibleParserError
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.utils.shlex import shlex_split
+
+
+@@ -342,11 +341,9 @@ class InventoryModule(BaseFileInventoryP
+ (int, dict, list, unicode string, etc).
+ '''
+ try:
+- with warnings.catch_warnings():
+- warnings.simplefilter("ignore", SyntaxWarning)
+- v = ast.literal_eval(v)
++ v = ast.literal_eval(v)
+ # Using explicit exceptions.
+- # Likely a string that literal_eval does not like. We will then just set it.
++ # Likely a string that literal_eval does not like. We wil then just set it.
+ except ValueError:
+ # For some reason this was thought to be malformed.
+ pass
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/inventory/script.py
++++ ansible-core-2.16.5/lib/ansible/plugins/inventory/script.py
+@@ -28,8 +28,6 @@ DOCUMENTATION = '''
+ notes:
+ - Enabled in configuration by default.
+ - The plugin does not cache results because external inventory scripts are responsible for their own caching.
+- - To write your own inventory script see (R(Developing dynamic inventory,developing_inventory) from the documentation site.
+- - To find the scripts that used to be part of the code release, go to U(https://github.com/ansible-community/contrib-scripts/).
+ '''
+
+ import os
+@@ -39,7 +37,7 @@ from collections.abc import Mapping
+
+ from ansible.errors import AnsibleError, AnsibleParserError
+ from ansible.module_utils.basic import json_dict_bytes_to_unicode
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.plugins.inventory import BaseInventoryPlugin
+ from ansible.utils.display import Display
+
+@@ -189,11 +187,7 @@ class InventoryModule(BaseInventoryPlugi
+ sp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ except OSError as e:
+ raise AnsibleError("problem running %s (%s)" % (' '.join(cmd), e))
+- (out, stderr) = sp.communicate()
+-
+- if sp.returncode != 0:
+- raise AnsibleError("Inventory script (%s) had an execution error: %s" % (path, to_native(stderr)))
+-
++ (out, err) = sp.communicate()
+ if out.strip() == '':
+ return {}
+ try:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/inventory/toml.py
++++ ansible-core-2.16.5/lib/ansible/plugins/inventory/toml.py
+@@ -94,7 +94,7 @@ from collections.abc import MutableMappi
+ from functools import partial
+
+ from ansible.errors import AnsibleFileNotFound, AnsibleParserError, AnsibleRuntimeError
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.six import string_types, text_type
+ from ansible.parsing.yaml.objects import AnsibleSequence, AnsibleUnicode
+ from ansible.plugins.inventory import BaseFileInventoryPlugin
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/inventory/yaml.py
++++ ansible-core-2.16.5/lib/ansible/plugins/inventory/yaml.py
+@@ -72,7 +72,7 @@ from collections.abc import MutableMappi
+
+ from ansible.errors import AnsibleError, AnsibleParserError
+ from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.plugins.inventory import BaseFileInventoryPlugin
+
+ NoneType = type(None)
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/list.py
++++ ansible-core-2.16.5/lib/ansible/plugins/list.py
+@@ -11,10 +11,10 @@ from ansible import context
+ from ansible import constants as C
+ from ansible.collections.list import list_collections
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_native, to_bytes
++from ansible.module_utils._text import to_native, to_bytes
+ from ansible.plugins import loader
+ from ansible.utils.display import Display
+-from ansible.utils.collection_loader._collection_finder import _get_collection_path
++from ansible.utils.collection_loader._collection_finder import _get_collection_path, AnsibleCollectionRef
+
+ display = Display()
+
+@@ -44,7 +44,6 @@ def get_composite_name(collection, name,
+
+
+ def _list_plugins_from_paths(ptype, dirs, collection, depth=0):
+- # TODO: update to use importlib.resources
+
+ plugins = {}
+
+@@ -118,7 +117,6 @@ def _list_j2_plugins_from_file(collectio
+
+
+ def list_collection_plugins(ptype, collections, search_paths=None):
+- # TODO: update to use importlib.resources
+
+ # starts at {plugin_name: filepath, ...}, but changes at the end
+ plugins = {}
+@@ -171,32 +169,28 @@ def list_collection_plugins(ptype, colle
+ return plugins
+
+
+-def list_plugins(ptype, collections=None, search_paths=None):
+- if isinstance(collections, str):
+- collections = [collections]
++def list_plugins(ptype, collection=None, search_paths=None):
+
+ # {plugin_name: (filepath, class), ...}
+ plugins = {}
+- plugin_collections = {}
+- if collections is None:
++ collections = {}
++ if collection is None:
+ # list all collections, add synthetic ones
+- plugin_collections['ansible.builtin'] = b''
+- plugin_collections['ansible.legacy'] = b''
+- plugin_collections.update(list_collections(search_paths=search_paths, dedupe=True))
++ collections['ansible.builtin'] = b''
++ collections['ansible.legacy'] = b''
++ collections.update(list_collections(search_paths=search_paths, dedupe=True))
++ elif collection == 'ansible.legacy':
++ # add builtin, since legacy also resolves to these
++ collections[collection] = b''
++ collections['ansible.builtin'] = b''
+ else:
+- for collection in collections:
+- if collection == 'ansible.legacy':
+- # add builtin, since legacy also resolves to these
+- plugin_collections[collection] = b''
+- plugin_collections['ansible.builtin'] = b''
+- else:
+- try:
+- plugin_collections[collection] = to_bytes(_get_collection_path(collection))
+- except ValueError as e:
+- raise AnsibleError("Cannot use supplied collection {0}: {1}".format(collection, to_native(e)), orig_exc=e)
++ try:
++ collections[collection] = to_bytes(_get_collection_path(collection))
++ except ValueError as e:
++ raise AnsibleError("Cannot use supplied collection {0}: {1}".format(collection, to_native(e)), orig_exc=e)
+
+- if plugin_collections:
+- plugins.update(list_collection_plugins(ptype, plugin_collections))
++ if collections:
++ plugins.update(list_collection_plugins(ptype, collections))
+
+ return plugins
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/loader.py
++++ ansible-core-2.16.5/lib/ansible/plugins/loader.py
+@@ -17,7 +17,6 @@ import warnings
+ from collections import defaultdict, namedtuple
+ from traceback import format_exc
+
+-import ansible.module_utils.compat.typing as t
+
+ from .filter import AnsibleJinja2Filter
+ from .test import AnsibleJinja2Test
+@@ -25,7 +24,7 @@ from .test import AnsibleJinja2Test
+ from ansible import __version__ as ansible_version
+ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsiblePluginCircularRedirect, AnsiblePluginRemovedError, AnsibleCollectionUnsupportedVersionError
+-from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native
++from ansible.module_utils._text import to_bytes, to_text, to_native
+ from ansible.module_utils.compat.importlib import import_module
+ from ansible.module_utils.six import string_types
+ from ansible.parsing.utils.yaml import from_yaml
+@@ -34,8 +33,7 @@ from ansible.plugins import get_plugin_c
+ from ansible.utils.collection_loader import AnsibleCollectionConfig, AnsibleCollectionRef
+ from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionFinder, _get_collection_metadata
+ from ansible.utils.display import Display
+-from ansible.utils.plugin_docs import add_fragments
+-from ansible.utils.unsafe_proxy import _is_unsafe
++from ansible.utils.plugin_docs import add_fragments, find_plugin_docfile
+
+ # TODO: take the packaging dep, or vendor SpecifierSet?
+
+@@ -48,7 +46,6 @@ except ImportError:
+
+ import importlib.util
+
+-_PLUGIN_FILTERS = defaultdict(frozenset) # type: t.DefaultDict[str, frozenset]
+ display = Display()
+
+ get_with_context_result = namedtuple('get_with_context_result', ['object', 'plugin_load_context'])
+@@ -239,7 +236,6 @@ class PluginLoader:
+ self._module_cache = MODULE_CACHE[class_name]
+ self._paths = PATH_CACHE[class_name]
+ self._plugin_path_cache = PLUGIN_PATH_CACHE[class_name]
+- self._plugin_instance_cache = {} if self.subdir == 'vars_plugins' else None
+
+ self._searched_paths = set()
+
+@@ -264,7 +260,6 @@ class PluginLoader:
+ self._module_cache = MODULE_CACHE[self.class_name]
+ self._paths = PATH_CACHE[self.class_name]
+ self._plugin_path_cache = PLUGIN_PATH_CACHE[self.class_name]
+- self._plugin_instance_cache = {} if self.subdir == 'vars_plugins' else None
+ self._searched_paths = set()
+
+ def __setstate__(self, data):
+@@ -863,52 +858,29 @@ class PluginLoader:
+
+ def get_with_context(self, name, *args, **kwargs):
+ ''' instantiates a plugin of the given name using arguments '''
+- if _is_unsafe(name):
+- # Objects constructed using the name wrapped as unsafe remain
+- # (correctly) unsafe. Using such unsafe objects in places
+- # where underlying types (builtin string in this case) are
+- # expected can cause problems.
+- # One such case is importlib.abc.Loader.exec_module failing
+- # with "ValueError: unmarshallable object" because the module
+- # object is created with the __path__ attribute being wrapped
+- # as unsafe which isn't marshallable.
+- # Manually removing the unsafe wrapper prevents such issues.
+- name = name._strip_unsafe()
+
+ found_in_cache = True
+ class_only = kwargs.pop('class_only', False)
+ collection_list = kwargs.pop('collection_list', None)
+ if name in self.aliases:
+ name = self.aliases[name]
+-
+- if (cached_result := (self._plugin_instance_cache or {}).get(name)) and cached_result[1].resolved:
+- # Resolving the FQCN is slow, even if we've passed in the resolved FQCN.
+- # Short-circuit here if we've previously resolved this name.
+- # This will need to be restricted if non-vars plugins start using the cache, since
+- # some non-fqcn plugin need to be resolved again with the collections list.
+- return get_with_context_result(*cached_result)
+-
+ plugin_load_context = self.find_plugin_with_context(name, collection_list=collection_list)
+ if not plugin_load_context.resolved or not plugin_load_context.plugin_resolved_path:
+ # FIXME: this is probably an error (eg removed plugin)
+ return get_with_context_result(None, plugin_load_context)
+
+ fq_name = plugin_load_context.resolved_fqcn
+- if '.' not in fq_name and plugin_load_context.plugin_resolved_collection:
++ if '.' not in fq_name:
+ fq_name = '.'.join((plugin_load_context.plugin_resolved_collection, fq_name))
+- resolved_type_name = plugin_load_context.plugin_resolved_name
++ name = plugin_load_context.plugin_resolved_name
+ path = plugin_load_context.plugin_resolved_path
+- if (cached_result := (self._plugin_instance_cache or {}).get(fq_name)) and cached_result[1].resolved:
+- # This is unused by vars plugins, but it's here in case the instance cache expands to other plugin types.
+- # We get here if we've seen this plugin before, but it wasn't called with the resolved FQCN.
+- return get_with_context_result(*cached_result)
+ redirected_names = plugin_load_context.redirect_list or []
+
+ if path not in self._module_cache:
+- self._module_cache[path] = self._load_module_source(resolved_type_name, path)
++ self._module_cache[path] = self._load_module_source(name, path)
+ found_in_cache = False
+
+- self._load_config_defs(resolved_type_name, self._module_cache[path], path)
++ self._load_config_defs(name, self._module_cache[path], path)
+
+ obj = getattr(self._module_cache[path], self.class_name)
+
+@@ -925,29 +897,24 @@ class PluginLoader:
+ return get_with_context_result(None, plugin_load_context)
+
+ # FIXME: update this to use the load context
+- self._display_plugin_load(self.class_name, resolved_type_name, self._searched_paths, path, found_in_cache=found_in_cache, class_only=class_only)
++ self._display_plugin_load(self.class_name, name, self._searched_paths, path, found_in_cache=found_in_cache, class_only=class_only)
+
+ if not class_only:
+ try:
+ # A plugin may need to use its _load_name in __init__ (for example, to set
+ # or get options from config), so update the object before using the constructor
+ instance = object.__new__(obj)
+- self._update_object(instance, resolved_type_name, path, redirected_names, fq_name)
++ self._update_object(instance, name, path, redirected_names, fq_name)
+ obj.__init__(instance, *args, **kwargs) # pylint: disable=unnecessary-dunder-call
+ obj = instance
+ except TypeError as e:
+ if "abstract" in e.args[0]:
+ # Abstract Base Class or incomplete plugin, don't load
+- display.v('Returning not found on "%s" as it has unimplemented abstract methods; %s' % (resolved_type_name, to_native(e)))
++ display.v('Returning not found on "%s" as it has unimplemented abstract methods; %s' % (name, to_native(e)))
+ return get_with_context_result(None, plugin_load_context)
+ raise
+
+- self._update_object(obj, resolved_type_name, path, redirected_names, fq_name)
+- if self._plugin_instance_cache is not None and getattr(obj, 'is_stateless', False):
+- self._plugin_instance_cache[fq_name] = (obj, plugin_load_context)
+- elif self._plugin_instance_cache is not None:
+- # The cache doubles as the load order, so record the FQCN even if the plugin hasn't set is_stateless = True
+- self._plugin_instance_cache[fq_name] = (None, PluginLoadContext())
++ self._update_object(obj, name, path, redirected_names, fq_name)
+ return get_with_context_result(obj, plugin_load_context)
+
+ def _display_plugin_load(self, class_name, name, searched_paths, path, found_in_cache=None, class_only=None):
+@@ -1017,47 +984,28 @@ class PluginLoader:
+
+ loaded_modules = set()
+ for path in all_matches:
+-
+ name = os.path.splitext(path)[0]
+ basename = os.path.basename(name)
+- is_j2 = isinstance(self, Jinja2Loader)
+
+- if is_j2:
+- ref_name = path
+- else:
+- ref_name = basename
+-
+- if not is_j2 and basename in _PLUGIN_FILTERS[self.package]:
+- # j2 plugins get processed in own class, here they would just be container files
++ if basename in _PLUGIN_FILTERS[self.package]:
+ display.debug("'%s' skipped due to a defined plugin filter" % basename)
+ continue
+
+ if basename == '__init__' or (basename == 'base' and self.package == 'ansible.plugins.cache'):
+ # cache has legacy 'base.py' file, which is wrapper for __init__.py
+- display.debug("'%s' skipped due to reserved name" % name)
++ display.debug("'%s' skipped due to reserved name" % basename)
+ continue
+
+- if dedupe and ref_name in loaded_modules:
+- # for j2 this is 'same file', other plugins it is basename
+- display.debug("'%s' skipped as duplicate" % ref_name)
++ if dedupe and basename in loaded_modules:
++ display.debug("'%s' skipped as duplicate" % basename)
+ continue
+
+- loaded_modules.add(ref_name)
++ loaded_modules.add(basename)
+
+ if path_only:
+ yield path
+ continue
+
+- if path in legacy_excluding_builtin:
+- fqcn = basename
+- else:
+- fqcn = f"ansible.builtin.{basename}"
+-
+- if (cached_result := (self._plugin_instance_cache or {}).get(fqcn)) and cached_result[1].resolved:
+- # Here just in case, but we don't call all() multiple times for vars plugins, so this should not be used.
+- yield cached_result[0]
+- continue
+-
+ if path not in self._module_cache:
+ if self.type in ('filter', 'test'):
+ # filter and test plugin files can contain multiple plugins
+@@ -1105,20 +1053,11 @@ class PluginLoader:
+ except TypeError as e:
+ display.warning("Skipping plugin (%s) as it seems to be incomplete: %s" % (path, to_text(e)))
+
++ if path in legacy_excluding_builtin:
++ fqcn = basename
++ else:
++ fqcn = f"ansible.builtin.{basename}"
+ self._update_object(obj, basename, path, resolved=fqcn)
+-
+- if self._plugin_instance_cache is not None:
+- needs_enabled = False
+- if hasattr(obj, 'REQUIRES_ENABLED'):
+- needs_enabled = obj.REQUIRES_ENABLED
+- elif hasattr(obj, 'REQUIRES_WHITELIST'):
+- needs_enabled = obj.REQUIRES_WHITELIST
+- display.deprecated("The VarsModule class variable 'REQUIRES_WHITELIST' is deprecated. "
+- "Use 'REQUIRES_ENABLED' instead.", version=2.18)
+- if not needs_enabled:
+- # Use get_with_context to cache the plugin the first time we see it.
+- self.get_with_context(fqcn)[0]
+-
+ yield obj
+
+
+@@ -1394,7 +1333,7 @@ def get_fqcr_and_name(resource, collecti
+
+
+ def _load_plugin_filter():
+- filters = _PLUGIN_FILTERS
++ filters = defaultdict(frozenset)
+ user_set = False
+ if C.PLUGIN_FILTERS_CFG is None:
+ filter_cfg = '/etc/ansible/plugin_filters.yml'
+@@ -1422,21 +1361,15 @@ def _load_plugin_filter():
+ version = to_text(version)
+ version = version.strip()
+
+- # Modules and action plugins share the same reject list since the difference between the
+- # two isn't visible to the users
+ if version == u'1.0':
+-
+- if 'module_blacklist' in filter_data:
+- display.deprecated("'module_blacklist' is being removed in favor of 'module_rejectlist'", version='2.18')
+- if 'module_rejectlist' not in filter_data:
+- filter_data['module_rejectlist'] = filter_data['module_blacklist']
+- del filter_data['module_blacklist']
+-
++ # Modules and action plugins share the same blacklist since the difference between the
++ # two isn't visible to the users
+ try:
+- filters['ansible.modules'] = frozenset(filter_data['module_rejectlist'])
++ # reject list was documented but we never changed the code from blacklist, will be deprected in 2.15
++ filters['ansible.modules'] = frozenset(filter_data.get('module_rejectlist)', filter_data['module_blacklist']))
+ except TypeError:
+ display.warning(u'Unable to parse the plugin filter file {0} as'
+- u' module_rejectlist is not a list.'
++ u' module_blacklist is not a list.'
+ u' Skipping.'.format(filter_cfg))
+ return filters
+ filters['ansible.plugins.action'] = filters['ansible.modules']
+@@ -1448,11 +1381,11 @@ def _load_plugin_filter():
+ display.warning(u'The plugin filter file, {0} does not exist.'
+ u' Skipping.'.format(filter_cfg))
+
+- # Specialcase the stat module as Ansible can run very few things if stat is rejected
++ # Specialcase the stat module as Ansible can run very few things if stat is blacklisted.
+ if 'stat' in filters['ansible.modules']:
+- raise AnsibleError('The stat module was specified in the module reject list file, {0}, but'
++ raise AnsibleError('The stat module was specified in the module blacklist file, {0}, but'
+ ' Ansible will not function without the stat module. Please remove stat'
+- ' from the reject list.'.format(to_native(filter_cfg)))
++ ' from the blacklist.'.format(to_native(filter_cfg)))
+ return filters
+
+
+@@ -1492,38 +1425,25 @@ def _does_collection_support_ansible_ver
+ return ss.contains(base_ansible_version)
+
+
+-def _configure_collection_loader(prefix_collections_path=None):
++def _configure_collection_loader():
+ if AnsibleCollectionConfig.collection_finder:
+ # this must be a Python warning so that it can be filtered out by the import sanity test
+ warnings.warn('AnsibleCollectionFinder has already been configured')
+ return
+
+- if prefix_collections_path is None:
+- prefix_collections_path = []
+-
+- paths = list(prefix_collections_path) + C.COLLECTIONS_PATHS
+- finder = _AnsibleCollectionFinder(paths, C.COLLECTIONS_SCAN_SYS_PATH)
++ finder = _AnsibleCollectionFinder(C.COLLECTIONS_PATHS, C.COLLECTIONS_SCAN_SYS_PATH)
+ finder._install()
+
+ # this should succeed now
+ AnsibleCollectionConfig.on_collection_load += _on_collection_load_handler
+
+
+-def init_plugin_loader(prefix_collections_path=None):
+- """Initialize the plugin filters and the collection loaders
+-
+- This method must be called to configure and insert the collection python loaders
+- into ``sys.meta_path`` and ``sys.path_hooks``.
+-
+- This method is only called in ``CLI.run`` after CLI args have been parsed, so that
+- instantiation of the collection finder can utilize parsed CLI args, and to not cause
+- side effects.
+- """
+- _load_plugin_filter()
+- _configure_collection_loader(prefix_collections_path)
++# TODO: All of the following is initialization code It should be moved inside of an initialization
++# function which is called at some point early in the ansible and ansible-playbook CLI startup.
+
++_PLUGIN_FILTERS = _load_plugin_filter()
+
+-# TODO: Evaluate making these class instantiations lazy, but keep them in the global scope
++_configure_collection_loader()
+
+ # doc fragments first
+ fragment_loader = PluginLoader(
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/__init__.py
+@@ -100,7 +100,7 @@ class LookupBase(AnsiblePlugin):
+ must be converted into python's unicode type as the strings will be run
+ through jinja2 which has this requirement. You can use::
+
+- from ansible.module_utils.common.text.converters import to_text
++ from ansible.module_utils._text import to_text
+ result_string = to_text(result_string)
+ """
+ pass
+@@ -117,7 +117,7 @@ class LookupBase(AnsiblePlugin):
+
+ result = None
+ try:
+- result = self._loader.path_dwim_relative_stack(paths, subdir, needle, is_role=bool('role_path' in myvars))
++ result = self._loader.path_dwim_relative_stack(paths, subdir, needle)
+ except AnsibleFileNotFound:
+ if not ignore_missing:
+ self._display.warning("Unable to find '%s' in expected paths (use -vvvvv to see paths)" % needle)
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/config.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/config.py
+@@ -33,10 +33,6 @@ DOCUMENTATION = """
+ description: name of the plugin for which you want to retrieve configuration settings.
+ type: string
+ version_added: '2.12'
+- show_origin:
+- description: toggle the display of what configuration subsystem the value came from
+- type: bool
+- version_added: '2.16'
+ """
+
+ EXAMPLES = """
+@@ -71,8 +67,7 @@ EXAMPLES = """
+ RETURN = """
+ _raw:
+ description:
+- - A list of value(s) of the key(s) in the config if show_origin is false (default)
+- - Optionally, a list of 2 element lists (value, origin) if show_origin is true
++ - value(s) of the key(s) in the config
+ type: raw
+ """
+
+@@ -80,7 +75,7 @@ import ansible.plugins.loader as plugin_
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleLookupError, AnsibleOptionsError
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.six import string_types
+ from ansible.plugins.lookup import LookupBase
+ from ansible.utils.sentinel import Sentinel
+@@ -97,7 +92,7 @@ def _get_plugin_config(pname, ptype, con
+ p = loader.get(pname, class_only=True)
+ if p is None:
+ raise AnsibleLookupError('Unable to load %s plugin "%s"' % (ptype, pname))
+- result, origin = C.config.get_config_value_and_origin(config, plugin_type=ptype, plugin_name=p._load_name, variables=variables)
++ result = C.config.get_config_value(config, plugin_type=ptype, plugin_name=p._load_name, variables=variables)
+ except AnsibleLookupError:
+ raise
+ except AnsibleError as e:
+@@ -106,7 +101,7 @@ def _get_plugin_config(pname, ptype, con
+ raise MissingSetting(msg, orig_exc=e)
+ raise e
+
+- return result, origin
++ return result
+
+
+ def _get_global_config(config):
+@@ -129,7 +124,6 @@ class LookupModule(LookupBase):
+ missing = self.get_option('on_missing')
+ ptype = self.get_option('plugin_type')
+ pname = self.get_option('plugin_name')
+- show_origin = self.get_option('show_origin')
+
+ if (ptype or pname) and not (ptype and pname):
+ raise AnsibleOptionsError('Both plugin_type and plugin_name are required, cannot use one without the other')
+@@ -144,10 +138,9 @@ class LookupModule(LookupBase):
+ raise AnsibleOptionsError('Invalid setting identifier, "%s" is not a string, its a %s' % (term, type(term)))
+
+ result = Sentinel
+- origin = None
+ try:
+ if pname:
+- result, origin = _get_plugin_config(pname, ptype, term, variables)
++ result = _get_plugin_config(pname, ptype, term, variables)
+ else:
+ result = _get_global_config(term)
+ except MissingSetting as e:
+@@ -159,8 +152,5 @@ class LookupModule(LookupBase):
+ pass # this is not needed, but added to have all 3 options stated
+
+ if result is not Sentinel:
+- if show_origin:
+- ret.append((result, origin))
+- else:
+- ret.append(result)
++ ret.append(result)
+ return ret
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/csvfile.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/csvfile.py
+@@ -12,7 +12,7 @@ DOCUMENTATION = r"""
+ description:
+ - The csvfile lookup reads the contents of a file in CSV (comma-separated value) format.
+ The lookup looks for the row where the first column matches keyname (which can be multiple words)
+- and returns the value in the O(col) column (default 1, which indexed from 0 means the second column in the file).
++ and returns the value in the C(col) column (default 1, which indexed from 0 means the second column in the file).
+ options:
+ col:
+ description: column to return (0 indexed).
+@@ -20,7 +20,7 @@ DOCUMENTATION = r"""
+ default:
+ description: what to return if the value is not found in the file.
+ delimiter:
+- description: field separator in the file, for a tab you can specify V(TAB) or V(\\t).
++ description: field separator in the file, for a tab you can specify C(TAB) or C(\t).
+ default: TAB
+ file:
+ description: name of the CSV/TSV file to open.
+@@ -35,9 +35,6 @@ DOCUMENTATION = r"""
+ - For historical reasons, in the search keyname, quotes are treated
+ literally and cannot be used around the string unless they appear
+ (escaped as required) in the first column of the file you are parsing.
+- seealso:
+- - ref: playbook_task_paths
+- description: Search paths used for relative files.
+ """
+
+ EXAMPLES = """
+@@ -57,7 +54,7 @@ EXAMPLES = """
+ neighbor_as: "{{ csvline[5] }}"
+ neigh_int_ip: "{{ csvline[6] }}"
+ vars:
+- csvline: "{{ lookup('ansible.builtin.csvfile', bgp_neighbor_ip, file='bgp_neighbors.csv', delimiter=',') }}"
++ csvline = "{{ lookup('ansible.builtin.csvfile', bgp_neighbor_ip, file='bgp_neighbors.csv', delimiter=',') }}"
+ delegate_to: localhost
+ """
+
+@@ -78,7 +75,7 @@ from ansible.errors import AnsibleError,
+ from ansible.parsing.splitter import parse_kv
+ from ansible.plugins.lookup import LookupBase
+ from ansible.module_utils.six import PY2
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+
+
+ class CSVRecoder:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/env.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/env.py
+@@ -23,7 +23,7 @@ DOCUMENTATION = """
+ default: ''
+ version_added: '2.13'
+ notes:
+- - You can pass the C(Undefined) object as O(default) to force an undefined error
++ - You can pass the C(Undefined) object as C(default) to force an undefined error
+ """
+
+ EXAMPLES = """
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/file.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/file.py
+@@ -28,14 +28,11 @@ DOCUMENTATION = """
+ notes:
+ - if read in variable context, the file can be interpreted as YAML if the content is valid to the parser.
+ - this lookup does not understand 'globbing', use the fileglob lookup instead.
+- seealso:
+- - ref: playbook_task_paths
+- description: Search paths used for relative files.
+ """
+
+ EXAMPLES = """
+ - ansible.builtin.debug:
+- msg: "the value of foo.txt is {{ lookup('ansible.builtin.file', '/etc/foo.txt') }}"
++ msg: "the value of foo.txt is {{lookup('ansible.builtin.file', '/etc/foo.txt') }}"
+
+ - name: display multiple file contents
+ ansible.builtin.debug: var=item
+@@ -53,9 +50,9 @@ RETURN = """
+ elements: str
+ """
+
+-from ansible.errors import AnsibleError, AnsibleOptionsError, AnsibleLookupError
++from ansible.errors import AnsibleError, AnsibleParserError
+ from ansible.plugins.lookup import LookupBase
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.utils.display import Display
+
+ display = Display()
+@@ -70,10 +67,11 @@ class LookupModule(LookupBase):
+
+ for term in terms:
+ display.debug("File lookup term: %s" % term)
++
+ # Find the file in the expected search path
++ lookupfile = self.find_file_in_search_path(variables, 'files', term)
++ display.vvvv(u"File lookup using %s as file" % lookupfile)
+ try:
+- lookupfile = self.find_file_in_search_path(variables, 'files', term, ignore_missing=True)
+- display.vvvv(u"File lookup using %s as file" % lookupfile)
+ if lookupfile:
+ b_contents, show_data = self._loader._get_file_contents(lookupfile)
+ contents = to_text(b_contents, errors='surrogate_or_strict')
+@@ -83,9 +81,8 @@ class LookupModule(LookupBase):
+ contents = contents.rstrip()
+ ret.append(contents)
+ else:
+- # TODO: only add search info if abs path?
+- raise AnsibleOptionsError("file not found, use -vvvvv to see paths searched")
+- except AnsibleError as e:
+- raise AnsibleLookupError("The 'file' lookup had an issue accessing the file '%s'" % term, orig_exc=e)
++ raise AnsibleParserError()
++ except AnsibleParserError:
++ raise AnsibleError("could not locate file in lookup: %s" % term)
+
+ return ret
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/fileglob.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/fileglob.py
+@@ -21,10 +21,7 @@ DOCUMENTATION = """
+ - See R(Ansible task paths,playbook_task_paths) to understand how file lookup occurs with paths.
+ - Matching is against local system files on the Ansible controller.
+ To iterate a list of files on a remote node, use the M(ansible.builtin.find) module.
+- - Returns a string list of paths joined by commas, or an empty list if no files match. For a 'true list' pass O(ignore:wantlist=True) to the lookup.
+- seealso:
+- - ref: playbook_task_paths
+- description: Search paths used for relative files.
++ - Returns a string list of paths joined by commas, or an empty list if no files match. For a 'true list' pass C(wantlist=True) to the lookup.
+ """
+
+ EXAMPLES = """
+@@ -53,7 +50,8 @@ import os
+ import glob
+
+ from ansible.plugins.lookup import LookupBase
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.errors import AnsibleFileNotFound
++from ansible.module_utils._text import to_bytes, to_text
+
+
+ class LookupModule(LookupBase):
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/first_found.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/first_found.py
+@@ -15,9 +15,9 @@ DOCUMENTATION = """
+ to the containing locations of role / play / include and so on.
+ - The list of files has precedence over the paths searched.
+ For example, A task in a role has a 'file1' in the play's relative path, this will be used, 'file2' in role's relative path will not.
+- - Either a list of files O(_terms) or a key O(files) with a list of files is required for this plugin to operate.
++ - Either a list of files C(_terms) or a key C(files) with a list of files is required for this plugin to operate.
+ notes:
+- - This lookup can be used in 'dual mode', either passing a list of file names or a dictionary that has O(files) and O(paths).
++ - This lookup can be used in 'dual mode', either passing a list of file names or a dictionary that has C(files) and C(paths).
+ options:
+ _terms:
+ description: A list of file names.
+@@ -35,19 +35,16 @@ DOCUMENTATION = """
+ type: boolean
+ default: False
+ description:
+- - When V(True), return an empty list when no files are matched.
++ - When C(True), return an empty list when no files are matched.
+ - This is useful when used with C(with_first_found), as an empty list return to C(with_) calls
+ causes the calling task to be skipped.
+- - When used as a template via C(lookup) or C(query), setting O(skip=True) will *not* cause the task to skip.
++ - When used as a template via C(lookup) or C(query), setting I(skip=True) will *not* cause the task to skip.
+ Tasks must handle the empty list return from the template.
+- - When V(False) and C(lookup) or C(query) specifies O(ignore:errors='ignore') all errors (including no file found,
++ - When C(False) and C(lookup) or C(query) specifies I(errors='ignore') all errors (including no file found,
+ but potentially others) return an empty string or an empty list respectively.
+- - When V(True) and C(lookup) or C(query) specifies O(ignore:errors='ignore'), no file found will return an empty
++ - When C(True) and C(lookup) or C(query) specifies I(errors='ignore'), no file found will return an empty
+ list and other potential errors return an empty string or empty list depending on the template call
+- (in other words return values of C(lookup) vs C(query)).
+- seealso:
+- - ref: playbook_task_paths
+- description: Search paths used for relative paths/files.
++ (in other words return values of C(lookup) v C(query)).
+ """
+
+ EXAMPLES = """
+@@ -183,9 +180,8 @@ class LookupModule(LookupBase):
+ for term in terms:
+ if isinstance(term, Mapping):
+ self.set_options(var_options=variables, direct=term)
+- files = self.get_option('files')
+ elif isinstance(term, string_types):
+- files = [term]
++ self.set_options(var_options=variables, direct=kwargs)
+ elif isinstance(term, Sequence):
+ partial, skip = self._process_terms(term, variables, kwargs)
+ total_search.extend(partial)
+@@ -193,6 +189,7 @@ class LookupModule(LookupBase):
+ else:
+ raise AnsibleLookupError("Invalid term supplied, can handle string, mapping or list of strings but got: %s for %s" % (type(term), term))
+
++ files = self.get_option('files')
+ paths = self.get_option('paths')
+
+ # NOTE: this is used as 'global' but can be set many times?!?!?
+@@ -209,8 +206,8 @@ class LookupModule(LookupBase):
+ f = os.path.join(path, fn)
+ total_search.append(f)
+ elif filelist:
+- # NOTE: this is now 'extend', previouslly it would clobber all options, but we deemed that a bug
+- total_search.extend(filelist)
++ # NOTE: this seems wrong, should be 'extend' as any option/entry can clobber all
++ total_search = filelist
+ else:
+ total_search.append(term)
+
+@@ -218,10 +215,6 @@ class LookupModule(LookupBase):
+
+ def run(self, terms, variables, **kwargs):
+
+- if not terms:
+- self.set_options(var_options=variables, direct=kwargs)
+- terms = self.get_option('files')
+-
+ total_search, skip = self._process_terms(terms, variables, kwargs)
+
+ # NOTE: during refactor noticed that the 'using a dict' as term
+@@ -237,8 +230,6 @@ class LookupModule(LookupBase):
+ try:
+ fn = self._templar.template(fn)
+ except (AnsibleUndefinedVariable, UndefinedError):
+- # NOTE: backwards compat ff behaviour is to ignore errors when vars are undefined.
+- # moved here from task_executor.
+ continue
+
+ # get subdir if set by task executor, default to files otherwise
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/ini.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/ini.py
+@@ -39,7 +39,7 @@ DOCUMENTATION = """
+ default: ''
+ case_sensitive:
+ description:
+- Whether key names read from O(file) should be case sensitive. This prevents
++ Whether key names read from C(file) should be case sensitive. This prevents
+ duplicate key errors if keys only differ in case.
+ default: False
+ version_added: '2.12'
+@@ -50,9 +50,6 @@ DOCUMENTATION = """
+ default: False
+ aliases: ['allow_none']
+ version_added: '2.12'
+- seealso:
+- - ref: playbook_task_paths
+- description: Search paths used for relative files.
+ """
+
+ EXAMPLES = """
+@@ -88,7 +85,7 @@ from collections import defaultdict
+ from collections.abc import MutableSequence
+
+ from ansible.errors import AnsibleLookupError, AnsibleOptionsError
+-from ansible.module_utils.common.text.converters import to_text, to_native
++from ansible.module_utils._text import to_text, to_native
+ from ansible.plugins.lookup import LookupBase
+
+
+@@ -190,7 +187,7 @@ class LookupModule(LookupBase):
+ config.seek(0, os.SEEK_SET)
+
+ try:
+- self.cp.read_file(config)
++ self.cp.readfp(config)
+ except configparser.DuplicateOptionError as doe:
+ raise AnsibleLookupError("Duplicate option in '{file}': {error}".format(file=paramvals['file'], error=to_native(doe)))
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/lines.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/lines.py
+@@ -20,7 +20,6 @@ DOCUMENTATION = """
+ - Like all lookups, this runs on the Ansible controller and is unaffected by other keywords such as 'become'.
+ If you need to use different permissions, you must change the command or run Ansible as another user.
+ - Alternatively, you can use a shell/command task that runs against localhost and registers the result.
+- - The directory of the play is used as the current working directory.
+ """
+
+ EXAMPLES = """
+@@ -45,7 +44,7 @@ RETURN = """
+ import subprocess
+ from ansible.errors import AnsibleError
+ from ansible.plugins.lookup import LookupBase
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+
+
+ class LookupModule(LookupBase):
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/password.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/password.py
+@@ -28,26 +28,23 @@ DOCUMENTATION = """
+ required: True
+ encrypt:
+ description:
+- - Which hash scheme to encrypt the returning password, should be one hash scheme from C(passlib.hash);
+- V(md5_crypt), V(bcrypt), V(sha256_crypt), V(sha512_crypt).
++ - Which hash scheme to encrypt the returning password, should be one hash scheme from C(passlib.hash; md5_crypt, bcrypt, sha256_crypt, sha512_crypt).
+ - If not provided, the password will be returned in plain text.
+ - Note that the password is always stored as plain text, only the returning password is encrypted.
+ - Encrypt also forces saving the salt value for idempotence.
+ - Note that before 2.6 this option was incorrectly labeled as a boolean for a long time.
+ ident:
+ description:
+- - Specify version of Bcrypt algorithm to be used while using O(encrypt) as V(bcrypt).
+- - The parameter is only available for V(bcrypt) - U(https://passlib.readthedocs.io/en/stable/lib/passlib.hash.bcrypt.html#passlib.hash.bcrypt).
++ - Specify version of Bcrypt algorithm to be used while using C(encrypt) as C(bcrypt).
++ - The parameter is only available for C(bcrypt) - U(https://passlib.readthedocs.io/en/stable/lib/passlib.hash.bcrypt.html#passlib.hash.bcrypt).
+ - Other hash types will simply ignore this parameter.
+- - 'Valid values for this parameter are: V(2), V(2a), V(2y), V(2b).'
++ - 'Valid values for this parameter are: C(2), C(2a), C(2y), C(2b).'
+ type: string
+ version_added: "2.12"
+ chars:
+ version_added: "1.4"
+ description:
+ - A list of names that compose a custom character set in the generated passwords.
+- - This parameter defines the possible character sets in the resulting password, not the required character sets.
+- If you want to require certain character sets for passwords, you can use the P(community.general.random_string#lookup) lookup plugin.
+ - 'By default generated passwords contain a random mix of upper and lowercase ASCII letters, the numbers 0-9, and punctuation (". , : - _").'
+ - "They can be either parts of Python's string module attributes or represented literally ( :, -)."
+ - "Though string modules can vary by Python version, valid values for both major releases include:
+@@ -133,7 +130,7 @@ import time
+ import hashlib
+
+ from ansible.errors import AnsibleError, AnsibleAssertionError
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.six import string_types
+ from ansible.parsing.splitter import parse_kv
+ from ansible.plugins.lookup import LookupBase
+@@ -367,7 +364,6 @@ class LookupModule(LookupBase):
+ try:
+ # make sure only one process finishes all the job first
+ first_process, lockfile = _get_lock(b_path)
+-
+ content = _read_password_file(b_path)
+
+ if content is None or b_path == to_bytes('/dev/null'):
+@@ -385,18 +381,34 @@ class LookupModule(LookupBase):
+ except KeyError:
+ salt = random_salt()
+
+- if not ident:
+- ident = params['ident']
+- elif params['ident'] and ident != params['ident']:
+- raise AnsibleError('The ident parameter provided (%s) does not match the stored one (%s).' % (ident, params['ident']))
+-
++ ident = params['ident']
+ if encrypt and not ident:
++ changed = True
+ try:
+ ident = BaseHash.algorithms[encrypt].implicit_ident
+ except KeyError:
+ ident = None
+- if ident:
++
++ encrypt = params['encrypt']
++ if encrypt and not salt:
+ changed = True
++ try:
++ salt = random_salt(BaseHash.algorithms[encrypt].salt_size)
++ except KeyError:
++ salt = random_salt()
++
++ if not ident:
++ ident = params['ident']
++ elif params['ident'] and ident != params['ident']:
++ raise AnsibleError('The ident parameter provided (%s) does not match the stored one (%s).' % (ident, params['ident']))
++
++ if encrypt and not ident:
++ try:
++ ident = BaseHash.algorithms[encrypt].implicit_ident
++ except KeyError:
++ ident = None
++ if ident:
++ changed = True
+
+ if changed and b_path != to_bytes('/dev/null'):
+ content = _format_content(plaintext_password, salt, encrypt=encrypt, ident=ident)
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/pipe.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/pipe.py
+@@ -24,7 +24,6 @@ DOCUMENTATION = r"""
+ It is strongly recommended to pass user input or variable input via quote filter before using with pipe lookup.
+ See example section for this.
+ Read more about this L(Bandit B602 docs,https://bandit.readthedocs.io/en/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html)
+- - The directory of the play is used as the current working directory.
+ """
+
+ EXAMPLES = r"""
+@@ -57,13 +56,15 @@ class LookupModule(LookupBase):
+
+ ret = []
+ for term in terms:
+- # https://docs.python.org/3/library/subprocess.html#popen-constructor
+- #
+- # The shell argument (which defaults to False) specifies whether to use the
+- # shell as the program to execute. If shell is True, it is recommended to pass
+- # args as a string rather than as a sequence
+- #
+- # https://github.com/ansible/ansible/issues/6550
++ '''
++ https://docs.python.org/3/library/subprocess.html#popen-constructor
++
++ The shell argument (which defaults to False) specifies whether to use the
++ shell as the program to execute. If shell is True, it is recommended to pass
++ args as a string rather than as a sequence
++
++ https://github.com/ansible/ansible/issues/6550
++ '''
+ term = str(term)
+
+ p = subprocess.Popen(term, cwd=self._loader.get_basedir(), shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/random_choice.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/random_choice.py
+@@ -35,13 +35,13 @@ RETURN = """
+ import random
+
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.plugins.lookup import LookupBase
+
+
+ class LookupModule(LookupBase):
+
+- def run(self, terms, variables=None, **kwargs):
++ def run(self, terms, inject=None, **kwargs):
+
+ ret = terms
+ if terms:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/sequence.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/sequence.py
+@@ -175,7 +175,7 @@ class LookupModule(LookupBase):
+ if not match:
+ return False
+
+- dummy, start, end, dummy, stride, dummy, format = match.groups()
++ _, start, end, _, stride, _, format = match.groups()
+
+ if start is not None:
+ try:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/subelements.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/subelements.py
+@@ -19,8 +19,8 @@ DOCUMENTATION = """
+ default: False
+ description:
+ - Lookup accepts this flag from a dictionary as optional. See Example section for more information.
+- - If set to V(True), the lookup plugin will skip the lists items that do not contain the given subkey.
+- - If set to V(False), the plugin will yield an error and complain about the missing subkey.
++ - If set to C(True), the lookup plugin will skip the lists items that do not contain the given subkey.
++ - If set to C(False), the plugin will yield an error and complain about the missing subkey.
+ """
+
+ EXAMPLES = """
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/template.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/template.py
+@@ -50,15 +50,10 @@ DOCUMENTATION = """
+ description: The string marking the beginning of a comment statement.
+ version_added: '2.12'
+ type: str
+- default: '{#'
+ comment_end_string:
+ description: The string marking the end of a comment statement.
+ version_added: '2.12'
+ type: str
+- default: '#}'
+- seealso:
+- - ref: playbook_task_paths
+- description: Search paths used for relative templates.
+ """
+
+ EXAMPLES = """
+@@ -89,7 +84,7 @@ import ansible.constants as C
+
+ from ansible.errors import AnsibleError
+ from ansible.plugins.lookup import LookupBase
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.template import generate_ansible_template_vars, AnsibleEnvironment
+ from ansible.utils.display import Display
+ from ansible.utils.native_jinja import NativeJinjaText
+@@ -150,16 +145,13 @@ class LookupModule(LookupBase):
+ vars.update(generate_ansible_template_vars(term, lookupfile))
+ vars.update(lookup_template_vars)
+
+- with templar.set_temporary_context(available_variables=vars, searchpath=searchpath):
+- overrides = dict(
+- variable_start_string=variable_start_string,
+- variable_end_string=variable_end_string,
+- comment_start_string=comment_start_string,
+- comment_end_string=comment_end_string
+- )
++ with templar.set_temporary_context(variable_start_string=variable_start_string,
++ variable_end_string=variable_end_string,
++ comment_start_string=comment_start_string,
++ comment_end_string=comment_end_string,
++ available_variables=vars, searchpath=searchpath):
+ res = templar.template(template_data, preserve_trailing_newlines=True,
+- convert_data=convert_data_p, escape_backslashes=False,
+- overrides=overrides)
++ convert_data=convert_data_p, escape_backslashes=False)
+
+ if (C.DEFAULT_JINJA2_NATIVE and not jinja2_native) or not convert_data_p:
+ # jinja2_native is true globally but off for the lookup, we need this text
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/unvault.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/unvault.py
+@@ -16,9 +16,6 @@ DOCUMENTATION = """
+ required: True
+ notes:
+ - This lookup does not understand 'globbing' nor shell environment variables.
+- seealso:
+- - ref: playbook_task_paths
+- description: Search paths used for relative files.
+ """
+
+ EXAMPLES = """
+@@ -35,7 +32,7 @@ RETURN = """
+
+ from ansible.errors import AnsibleParserError
+ from ansible.plugins.lookup import LookupBase
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.utils.display import Display
+
+ display = Display()
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/url.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/url.py
+@@ -64,7 +64,7 @@ options:
+ - section: url_lookup
+ key: timeout
+ http_agent:
+- description: User-Agent to use in the request. The default was changed in 2.11 to V(ansible-httpget).
++ description: User-Agent to use in the request. The default was changed in 2.11 to C(ansible-httpget).
+ type: string
+ version_added: "2.10"
+ default: ansible-httpget
+@@ -81,12 +81,12 @@ options:
+ version_added: "2.10"
+ default: False
+ vars:
+- - name: ansible_lookup_url_force_basic_auth
++ - name: ansible_lookup_url_agent
+ env:
+- - name: ANSIBLE_LOOKUP_URL_FORCE_BASIC_AUTH
++ - name: ANSIBLE_LOOKUP_URL_AGENT
+ ini:
+ - section: url_lookup
+- key: force_basic_auth
++ key: agent
+ follow_redirects:
+ description: String of urllib2, all/yes, safe, none to determine how redirects are followed, see RedirectHandlerFactory for more information
+ type: string
+@@ -102,7 +102,7 @@ options:
+ use_gssapi:
+ description:
+ - Use GSSAPI handler of requests
+- - As of Ansible 2.11, GSSAPI credentials can be specified with O(username) and O(password).
++ - As of Ansible 2.11, GSSAPI credentials can be specified with I(username) and I(password).
+ type: boolean
+ version_added: "2.10"
+ default: False
+@@ -211,7 +211,7 @@ RETURN = """
+ from urllib.error import HTTPError, URLError
+
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_text, to_native
++from ansible.module_utils._text import to_text, to_native
+ from ansible.module_utils.urls import open_url, ConnectionError, SSLValidationError
+ from ansible.plugins.lookup import LookupBase
+ from ansible.utils.display import Display
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/lookup/varnames.py
++++ ansible-core-2.16.5/lib/ansible/plugins/lookup/varnames.py
+@@ -46,7 +46,7 @@ _value:
+ import re
+
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.six import string_types
+ from ansible.plugins.lookup import LookupBase
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/netconf/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/netconf/__init__.py
+@@ -24,7 +24,7 @@ from functools import wraps
+
+ from ansible.errors import AnsibleError
+ from ansible.plugins import AnsiblePlugin
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.basic import missing_required_lib
+
+ try:
+@@ -62,8 +62,8 @@ class NetconfBase(AnsiblePlugin):
+ :class:`TerminalBase` plugins are byte strings. This is because of
+ how close to the underlying platform these plugins operate. Remember
+ to mark literal strings as byte string (``b"string"``) and to use
+- :func:`~ansible.module_utils.common.text.converters.to_bytes` and
+- :func:`~ansible.module_utils.common.text.converters.to_text` to avoid unexpected
++ :func:`~ansible.module_utils._text.to_bytes` and
++ :func:`~ansible.module_utils._text.to_text` to avoid unexpected
+ problems.
+
+ List of supported rpc's:
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/shell/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/shell/__init__.py
+@@ -24,11 +24,10 @@ import re
+ import shlex
+ import time
+
+-from collections.abc import Mapping, Sequence
+-
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.six import text_type, string_types
++from ansible.module_utils.common._collections_compat import Mapping, Sequence
+ from ansible.plugins import AnsiblePlugin
+
+ _USER_HOME_PATH_RE = re.compile(r'^~[_.A-Za-z0-9][-_.A-Za-z0-9]*$')
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/shell/cmd.py
++++ ansible-core-2.16.5/lib/ansible/plugins/shell/cmd.py
+@@ -34,24 +34,24 @@ class ShellModule(PSShellModule):
+ # Used by various parts of Ansible to do Windows specific changes
+ _IS_WINDOWS = True
+
+- def quote(self, cmd):
++ def quote(self, s):
+ # cmd does not support single quotes that the shlex_quote uses. We need to override the quoting behaviour to
+ # better match cmd.exe.
+ # https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
+
+ # Return an empty argument
+- if not cmd:
++ if not s:
+ return '""'
+
+- if _find_unsafe(cmd) is None:
+- return cmd
++ if _find_unsafe(s) is None:
++ return s
+
+ # Escape the metachars as we are quoting the string to stop cmd from interpreting that metachar. For example
+ # 'file &whoami.exe' would result in 'file $(whoami.exe)' instead of the literal string
+ # https://stackoverflow.com/questions/3411771/multiple-character-replace-with-python
+ for c in '^()%!"<>&|': # '^' must be the first char that we scan and replace
+- if c in cmd:
++ if c in s:
+ # I can't find any docs that explicitly say this but to escape ", it needs to be prefixed with \^.
+- cmd = cmd.replace(c, ("\\^" if c == '"' else "^") + c)
++ s = s.replace(c, ("\\^" if c == '"' else "^") + c)
+
+- return '^"' + cmd + '^"'
++ return '^"' + s + '^"'
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/shell/powershell.py
++++ ansible-core-2.16.5/lib/ansible/plugins/shell/powershell.py
+@@ -23,7 +23,7 @@ import pkgutil
+ import xml.etree.ElementTree as ET
+ import ntpath
+
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.plugins.shell import ShellBase
+
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/strategy/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/strategy/__init__.py
+@@ -27,7 +27,6 @@ import queue
+ import sys
+ import threading
+ import time
+-import typing as t
+
+ from collections import deque
+ from multiprocessing import Lock
+@@ -38,12 +37,12 @@ from ansible import constants as C
+ from ansible import context
+ from ansible.errors import AnsibleError, AnsibleFileNotFound, AnsibleUndefinedVariable, AnsibleParserError
+ from ansible.executor import action_write_locks
+-from ansible.executor.play_iterator import IteratingStates, PlayIterator
++from ansible.executor.play_iterator import IteratingStates
+ from ansible.executor.process.worker import WorkerProcess
+ from ansible.executor.task_result import TaskResult
+-from ansible.executor.task_queue_manager import CallbackSend, DisplaySend, PromptSend
++from ansible.executor.task_queue_manager import CallbackSend, DisplaySend
+ from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.connection import Connection, ConnectionError
+ from ansible.playbook.conditional import Conditional
+ from ansible.playbook.handler import Handler
+@@ -55,7 +54,6 @@ from ansible.template import Templar
+ from ansible.utils.display import Display
+ from ansible.utils.fqcn import add_internal_fqcns
+ from ansible.utils.unsafe_proxy import wrap_var
+-from ansible.utils.sentinel import Sentinel
+ from ansible.utils.vars import combine_vars, isidentifier
+ from ansible.vars.clean import strip_internal_keys, module_response_deepcopy
+
+@@ -117,8 +115,7 @@ def results_thread_main(strategy):
+ if isinstance(result, StrategySentinel):
+ break
+ elif isinstance(result, DisplaySend):
+- dmethod = getattr(display, result.method)
+- dmethod(*result.args, **result.kwargs)
++ display.display(*result.args, **result.kwargs)
+ elif isinstance(result, CallbackSend):
+ for arg in result.args:
+ if isinstance(arg, TaskResult):
+@@ -129,24 +126,6 @@ def results_thread_main(strategy):
+ strategy.normalize_task_result(result)
+ with strategy._results_lock:
+ strategy._results.append(result)
+- elif isinstance(result, PromptSend):
+- try:
+- value = display.prompt_until(
+- result.prompt,
+- private=result.private,
+- seconds=result.seconds,
+- complete_input=result.complete_input,
+- interrupt_input=result.interrupt_input,
+- )
+- except AnsibleError as e:
+- value = e
+- except BaseException as e:
+- # relay unexpected errors so bugs in display are reported and don't cause workers to hang
+- try:
+- raise AnsibleError(f"{e}") from e
+- except AnsibleError as e:
+- value = e
+- strategy._workers[result.worker_id].worker_queue.put(value)
+ else:
+ display.warning('Received an invalid object (%s) in the result queue: %r' % (type(result), result))
+ except (IOError, EOFError):
+@@ -263,8 +242,6 @@ class StrategyBase:
+ self._results = deque()
+ self._results_lock = threading.Condition(threading.Lock())
+
+- self._worker_queues = dict()
+-
+ # create the result processing thread for reading results in the background
+ self._results_thread = threading.Thread(target=results_thread_main, args=(self,))
+ self._results_thread.daemon = True
+@@ -408,10 +385,7 @@ class StrategyBase:
+ 'play_context': play_context
+ }
+
+- # Pass WorkerProcess its strategy worker number so it can send an identifier along with intra-task requests
+- worker_prc = WorkerProcess(
+- self._final_q, task_vars, host, task, play_context, self._loader, self._variable_manager, plugin_loader, self._cur_worker,
+- )
++ worker_prc = WorkerProcess(self._final_q, task_vars, host, task, play_context, self._loader, self._variable_manager, plugin_loader)
+ self._workers[self._cur_worker] = worker_prc
+ self._tqm.send_callback('v2_runner_on_start', host, task)
+ worker_prc.start()
+@@ -508,71 +482,56 @@ class StrategyBase:
+
+ return task_result
+
+- def search_handlers_by_notification(self, notification: str, iterator: PlayIterator) -> t.Generator[Handler, None, None]:
+- templar = Templar(None)
+- handlers = [h for b in reversed(iterator._play.handlers) for h in b.block]
+- # iterate in reversed order since last handler loaded with the same name wins
+- for handler in handlers:
+- if not handler.name:
+- continue
+- if not handler.cached_name:
+- if templar.is_template(handler.name):
+- templar.available_variables = self._variable_manager.get_vars(
+- play=iterator._play,
+- task=handler,
+- _hosts=self._hosts_cache,
+- _hosts_all=self._hosts_cache_all
+- )
+- try:
+- handler.name = templar.template(handler.name)
+- except (UndefinedError, AnsibleUndefinedVariable) as e:
+- # We skip this handler due to the fact that it may be using
+- # a variable in the name that was conditionally included via
+- # set_fact or some other method, and we don't want to error
+- # out unnecessarily
+- if not handler.listen:
+- display.warning(
+- "Handler '%s' is unusable because it has no listen topics and "
+- "the name could not be templated (host-specific variables are "
+- "not supported in handler names). The error: %s" % (handler.name, to_text(e))
+- )
+- continue
+- handler.cached_name = True
+-
+- # first we check with the full result of get_name(), which may
+- # include the role name (if the handler is from a role). If that
+- # is not found, we resort to the simple name field, which doesn't
+- # have anything extra added to it.
+- if notification in {
+- handler.name,
+- handler.get_name(include_role_fqcn=False),
+- handler.get_name(include_role_fqcn=True),
+- }:
+- yield handler
+- break
+-
+- templar.available_variables = {}
+- seen = []
+- for handler in handlers:
+- if listeners := handler.listen:
+- if notification in handler.get_validated_value(
+- 'listen',
+- handler.fattributes.get('listen'),
+- listeners,
+- templar,
+- ):
+- if handler.name and handler.name in seen:
+- continue
+- seen.append(handler.name)
+- yield handler
+-
+ @debug_closure
+ def _process_pending_results(self, iterator, one_pass=False, max_passes=None):
+ '''
+ Reads results off the final queue and takes appropriate action
+ based on the result (executing callbacks, updating state, etc.).
+ '''
++
+ ret_results = []
++ handler_templar = Templar(self._loader)
++
++ def search_handler_blocks_by_name(handler_name, handler_blocks):
++ # iterate in reversed order since last handler loaded with the same name wins
++ for handler_block in reversed(handler_blocks):
++ for handler_task in handler_block.block:
++ if handler_task.name:
++ try:
++ if not handler_task.cached_name:
++ if handler_templar.is_template(handler_task.name):
++ handler_templar.available_variables = self._variable_manager.get_vars(play=iterator._play,
++ task=handler_task,
++ _hosts=self._hosts_cache,
++ _hosts_all=self._hosts_cache_all)
++ handler_task.name = handler_templar.template(handler_task.name)
++ handler_task.cached_name = True
++
++ # first we check with the full result of get_name(), which may
++ # include the role name (if the handler is from a role). If that
++ # is not found, we resort to the simple name field, which doesn't
++ # have anything extra added to it.
++ candidates = (
++ handler_task.name,
++ handler_task.get_name(include_role_fqcn=False),
++ handler_task.get_name(include_role_fqcn=True),
++ )
++
++ if handler_name in candidates:
++ return handler_task
++ except (UndefinedError, AnsibleUndefinedVariable) as e:
++ # We skip this handler due to the fact that it may be using
++ # a variable in the name that was conditionally included via
++ # set_fact or some other method, and we don't want to error
++ # out unnecessarily
++ if not handler_task.listen:
++ display.warning(
++ "Handler '%s' is unusable because it has no listen topics and "
++ "the name could not be templated (host-specific variables are "
++ "not supported in handler names). The error: %s" % (handler_task.name, to_text(e))
++ )
++ continue
++
+ cur_pass = 0
+ while True:
+ try:
+@@ -603,7 +562,7 @@ class StrategyBase:
+ else:
+ iterator.mark_host_failed(original_host)
+
+- state, dummy = iterator.get_next_task_for_host(original_host, peek=True)
++ state, _ = iterator.get_next_task_for_host(original_host, peek=True)
+
+ if iterator.is_failed(original_host) and state and state.run_state == IteratingStates.COMPLETE:
+ self._tqm._failed_hosts[original_host.name] = True
+@@ -653,33 +612,49 @@ class StrategyBase:
+ result_items = [task_result._result]
+
+ for result_item in result_items:
+- if '_ansible_notify' in result_item and task_result.is_changed():
+- # only ensure that notified handlers exist, if so save the notifications for when
+- # handlers are actually flushed so the last defined handlers are exexcuted,
+- # otherwise depending on the setting either error or warn
+- host_state = iterator.get_state_for_host(original_host.name)
+- for notification in result_item['_ansible_notify']:
+- handler = Sentinel
+- for handler in self.search_handlers_by_notification(notification, iterator):
+- if host_state.run_state == IteratingStates.HANDLERS:
+- # we're currently iterating handlers, so we need to expand this now
+- if handler.notify_host(original_host):
+- # NOTE even with notifications deduplicated this can still happen in case of handlers being
+- # notified multiple times using different names, like role name or fqcn
+- self._tqm.send_callback('v2_playbook_on_notify', handler, original_host)
+- else:
+- iterator.add_notification(original_host.name, notification)
+- display.vv(f"Notification for handler {notification} has been saved.")
+- break
+- if handler is Sentinel:
+- msg = (
+- f"The requested handler '{notification}' was not found in either the main handlers"
+- " list nor in the listening handlers list"
+- )
+- if C.ERROR_ON_MISSING_HANDLER:
+- raise AnsibleError(msg)
+- else:
+- display.warning(msg)
++ if '_ansible_notify' in result_item:
++ if task_result.is_changed():
++ # The shared dictionary for notified handlers is a proxy, which
++ # does not detect when sub-objects within the proxy are modified.
++ # So, per the docs, we reassign the list so the proxy picks up and
++ # notifies all other threads
++ for handler_name in result_item['_ansible_notify']:
++ found = False
++ # Find the handler using the above helper. First we look up the
++ # dependency chain of the current task (if it's from a role), otherwise
++ # we just look through the list of handlers in the current play/all
++ # roles and use the first one that matches the notify name
++ target_handler = search_handler_blocks_by_name(handler_name, iterator._play.handlers)
++ if target_handler is not None:
++ found = True
++ if target_handler.notify_host(original_host):
++ self._tqm.send_callback('v2_playbook_on_notify', target_handler, original_host)
++
++ for listening_handler_block in iterator._play.handlers:
++ for listening_handler in listening_handler_block.block:
++ listeners = getattr(listening_handler, 'listen', []) or []
++ if not listeners:
++ continue
++
++ listeners = listening_handler.get_validated_value(
++ 'listen', listening_handler.fattributes.get('listen'), listeners, handler_templar
++ )
++ if handler_name not in listeners:
++ continue
++ else:
++ found = True
++
++ if listening_handler.notify_host(original_host):
++ self._tqm.send_callback('v2_playbook_on_notify', listening_handler, original_host)
++
++ # and if none were found, then we raise an error
++ if not found:
++ msg = ("The requested handler '%s' was not found in either the main handlers list nor in the listening "
++ "handlers list" % handler_name)
++ if C.ERROR_ON_MISSING_HANDLER:
++ raise AnsibleError(msg)
++ else:
++ display.warning(msg)
+
+ if 'add_host' in result_item:
+ # this task added a new host (add_host module)
+@@ -701,7 +676,7 @@ class StrategyBase:
+ else:
+ all_task_vars = found_task_vars
+ all_task_vars[original_task.register] = wrap_var(result_item)
+- post_process_whens(result_item, original_task, Templar(self._loader), all_task_vars)
++ post_process_whens(result_item, original_task, handler_templar, all_task_vars)
+ if original_task.loop or original_task.loop_with:
+ new_item_result = TaskResult(
+ task_result._host,
+@@ -795,13 +770,18 @@ class StrategyBase:
+ # If this is a role task, mark the parent role as being run (if
+ # the task was ok or failed, but not skipped or unreachable)
+ if original_task._role is not None and role_ran: # TODO: and original_task.action not in C._ACTION_INCLUDE_ROLE:?
+- # lookup the role in the role cache to make sure we're dealing
++ # lookup the role in the ROLE_CACHE to make sure we're dealing
+ # with the correct object and mark it as executed
+- role_obj = self._get_cached_role(original_task, iterator._play)
+- role_obj._had_task_run[original_host.name] = True
++ for (entry, role_obj) in iterator._play.ROLE_CACHE[original_task._role.get_name()].items():
++ if role_obj._uuid == original_task._role._uuid:
++ role_obj._had_task_run[original_host.name] = True
+
+ ret_results.append(task_result)
+
++ if isinstance(original_task, Handler):
++ for handler in (h for b in iterator._play.handlers for h in b.block if h._uuid == original_task._uuid):
++ handler.remove_host(original_host)
++
+ if one_pass or max_passes is not None and (cur_pass + 1) >= max_passes:
+ break
+
+@@ -954,15 +934,6 @@ class StrategyBase:
+ elif meta_action == 'flush_handlers':
+ if _evaluate_conditional(target_host):
+ host_state = iterator.get_state_for_host(target_host.name)
+- # actually notify proper handlers based on all notifications up to this point
+- for notification in list(host_state.handler_notifications):
+- for handler in self.search_handlers_by_notification(notification, iterator):
+- if handler.notify_host(target_host):
+- # NOTE even with notifications deduplicated this can still happen in case of handlers being
+- # notified multiple times using different names, like role name or fqcn
+- self._tqm.send_callback('v2_playbook_on_notify', handler, target_host)
+- iterator.clear_notification(target_host.name, notification)
+-
+ if host_state.run_state == IteratingStates.HANDLERS:
+ raise AnsibleError('flush_handlers cannot be used as a handler')
+ if target_host.name not in self._tqm._unreachable_hosts:
+@@ -1030,9 +1001,8 @@ class StrategyBase:
+ # Allow users to use this in a play as reported in https://github.com/ansible/ansible/issues/22286?
+ # How would this work with allow_duplicates??
+ if task.implicit:
+- role_obj = self._get_cached_role(task, iterator._play)
+- if target_host.name in role_obj._had_task_run:
+- role_obj._completed[target_host.name] = True
++ if target_host.name in task._role._had_task_run:
++ task._role._completed[target_host.name] = True
+ msg = 'role_complete for %s' % target_host.name
+ elif meta_action == 'reset_connection':
+ all_vars = self._variable_manager.get_vars(play=iterator._play, host=target_host, task=task,
+@@ -1089,20 +1059,14 @@ class StrategyBase:
+ header = skip_reason if skipped else msg
+ display.vv(f"META: {header}")
+
++ if isinstance(task, Handler):
++ task.remove_host(target_host)
++
+ res = TaskResult(target_host, task, result)
+ if skipped:
+ self._tqm.send_callback('v2_runner_on_skipped', res)
+ return [res]
+
+- def _get_cached_role(self, task, play):
+- role_path = task._role.get_role_path()
+- role_cache = play.role_cache[role_path]
+- try:
+- idx = role_cache.index(task._role)
+- return role_cache[idx]
+- except ValueError:
+- raise AnsibleError(f'Cannot locate {task._role.get_name()} in role cache')
+-
+ def get_hosts_left(self, iterator):
+ ''' returns list of available hosts for this iterator by filtering out unreachables '''
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/strategy/debug.py
++++ ansible-core-2.16.5/lib/ansible/plugins/strategy/debug.py
+@@ -24,6 +24,10 @@ DOCUMENTATION = '''
+ author: Kishin Yagami (!UNKNOWN)
+ '''
+
++import cmd
++import pprint
++import sys
++
+ from ansible.plugins.strategy.linear import StrategyModule as LinearStrategyModule
+
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/strategy/free.py
++++ ansible-core-2.16.5/lib/ansible/plugins/strategy/free.py
+@@ -40,7 +40,7 @@ from ansible.playbook.included_file impo
+ from ansible.plugins.loader import action_loader
+ from ansible.plugins.strategy import StrategyBase
+ from ansible.template import Templar
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.utils.display import Display
+
+ display = Display()
+@@ -146,8 +146,6 @@ class StrategyModule(StrategyBase):
+ # advance the host, mark the host blocked, and queue it
+ self._blocked_hosts[host_name] = True
+ iterator.set_state_for_host(host.name, state)
+- if isinstance(task, Handler):
+- task.remove_host(host)
+
+ try:
+ action = action_loader.get(task.action, class_only=True, collection_list=task.collections)
+@@ -175,9 +173,10 @@ class StrategyModule(StrategyBase):
+
+ # check to see if this task should be skipped, due to it being a member of a
+ # role which has already run (and whether that role allows duplicate execution)
+- if not isinstance(task, Handler) and task._role:
+- role_obj = self._get_cached_role(task, iterator._play)
+- if role_obj.has_run(host) and role_obj._metadata.allow_duplicates is False:
++ if not isinstance(task, Handler) and task._role and task._role.has_run(host):
++ # If there is no metadata, the default behavior is to not allow duplicates,
++ # if there is metadata, check to see if the allow_duplicates flag was set to true
++ if task._role._metadata is None or task._role._metadata and not task._role._metadata.allow_duplicates:
+ display.debug("'%s' skipped because role has already run" % task, host=host_name)
+ del self._blocked_hosts[host_name]
+ continue
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/strategy/linear.py
++++ ansible-core-2.16.5/lib/ansible/plugins/strategy/linear.py
+@@ -34,7 +34,7 @@ DOCUMENTATION = '''
+ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleAssertionError, AnsibleParserError
+ from ansible.executor.play_iterator import IteratingStates, FailedStates
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.playbook.handler import Handler
+ from ansible.playbook.included_file import IncludedFile
+ from ansible.playbook.task import Task
+@@ -77,7 +77,7 @@ class StrategyModule(StrategyBase):
+
+ if self._in_handlers and not any(filter(
+ lambda rs: rs == IteratingStates.HANDLERS,
+- (s.run_state for s, dummy in state_task_per_host.values()))
++ (s.run_state for s, _ in state_task_per_host.values()))
+ ):
+ self._in_handlers = False
+
+@@ -170,9 +170,10 @@ class StrategyModule(StrategyBase):
+
+ # check to see if this task should be skipped, due to it being a member of a
+ # role which has already run (and whether that role allows duplicate execution)
+- if not isinstance(task, Handler) and task._role:
+- role_obj = self._get_cached_role(task, iterator._play)
+- if role_obj.has_run(host) and role_obj._metadata.allow_duplicates is False:
++ if not isinstance(task, Handler) and task._role and task._role.has_run(host):
++ # If there is no metadata, the default behavior is to not allow duplicates,
++ # if there is metadata, check to see if the allow_duplicates flag was set to true
++ if task._role._metadata is None or task._role._metadata and not task._role._metadata.allow_duplicates:
+ display.debug("'%s' skipped because role has already run" % task)
+ continue
+
+@@ -242,12 +243,6 @@ class StrategyModule(StrategyBase):
+ self._queue_task(host, task, task_vars, play_context)
+ del task_vars
+
+- if isinstance(task, Handler):
+- if run_once:
+- task.clear_hosts()
+- else:
+- task.remove_host(host)
+-
+ # if we're bypassing the host loop, break out now
+ if run_once:
+ break
+@@ -367,7 +362,7 @@ class StrategyModule(StrategyBase):
+ if any_errors_fatal and (len(failed_hosts) > 0 or len(unreachable_hosts) > 0):
+ dont_fail_states = frozenset([IteratingStates.RESCUE, IteratingStates.ALWAYS])
+ for host in hosts_left:
+- (s, dummy) = iterator.get_next_task_for_host(host, peek=True)
++ (s, _) = iterator.get_next_task_for_host(host, peek=True)
+ # the state may actually be in a child state, use the get_active_state()
+ # method in the iterator to figure out the true active state
+ s = iterator.get_active_state(s)
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/terminal/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/terminal/__init__.py
+@@ -34,8 +34,8 @@ class TerminalBase(ABC):
+ :class:`TerminalBase` plugins are byte strings. This is because of
+ how close to the underlying platform these plugins operate. Remember
+ to mark literal strings as byte string (``b"string"``) and to use
+- :func:`~ansible.module_utils.common.text.converters.to_bytes` and
+- :func:`~ansible.module_utils.common.text.converters.to_text` to avoid unexpected
++ :func:`~ansible.module_utils._text.to_bytes` and
++ :func:`~ansible.module_utils._text.to_text` to avoid unexpected
+ problems.
+ '''
+
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/abs.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/abs.yml
+@@ -19,5 +19,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the path is absolute, V(False) if it is relative.
++ description: Returns C(True) if the path is absolute, C(False) if it is relative.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/all.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/all.yml
+@@ -19,5 +19,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if all elements of the list were True, V(False) otherwise.
++ description: Returns C(True) if all elements of the list were True, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/any.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/any.yml
+@@ -19,5 +19,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if any element of the list was true, V(False) otherwise.
++ description: Returns C(True) if any element of the list was true, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/change.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/change.yml
+@@ -6,7 +6,7 @@ DOCUMENTATION:
+ aliases: [change]
+ description:
+ - Tests if task required changes to complete
+- - This test checks for the existance of a C(changed) key in the input dictionary and that it is V(True) if present
++ - This test checks for the existance of a C(changed) key in the input dictionary and that it is C(True) if present
+ options:
+ _input:
+ description: registered result from an Ansible task
+@@ -14,9 +14,9 @@ DOCUMENTATION:
+ required: True
+ EXAMPLES: |
+ # test 'status' to know how to respond
+- {{ taskresults is changed }}
++ {{ (taskresults is changed }}
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the task was required changes, V(False) otherwise.
++ description: Returns C(True) if the task was required changes, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/changed.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/changed.yml
+@@ -6,7 +6,7 @@ DOCUMENTATION:
+ aliases: [change]
+ description:
+ - Tests if task required changes to complete
+- - This test checks for the existance of a C(changed) key in the input dictionary and that it is V(True) if present
++ - This test checks for the existance of a C(changed) key in the input dictionary and that it is C(True) if present
+ options:
+ _input:
+ description: registered result from an Ansible task
+@@ -14,9 +14,9 @@ DOCUMENTATION:
+ required: True
+ EXAMPLES: |
+ # test 'status' to know how to respond
+- {{ taskresults is changed }}
++ {{ (taskresults is changed }}
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the task was required changes, V(False) otherwise.
++ description: Returns C(True) if the task was required changes, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/contains.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/contains.yml
+@@ -45,5 +45,5 @@ EXAMPLES: |
+ - em4
+ RETURN:
+ _value:
+- description: Returns V(True) if the specified element is contained in the supplied sequence, V(False) otherwise.
++ description: Returns C(True) if the specified element is contained in the supplied sequence, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/core.py
++++ ansible-core-2.16.5/lib/ansible/plugins/test/core.py
+@@ -27,7 +27,7 @@ from collections.abc import MutableMappi
+ from ansible.module_utils.compat.version import LooseVersion, StrictVersion
+
+ from ansible import errors
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.utils.display import Display
+ from ansible.utils.version import SemanticVersion
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/directory.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/directory.yml
+@@ -17,5 +17,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the path corresponds to an existing directory on the filesystem on the controller, V(False) if otherwise.
++ description: Returns C(True) if the path corresponds to an existing directory on the filesystem on the controller, c(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/exists.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/exists.yml
+@@ -5,8 +5,7 @@ DOCUMENTATION:
+ short_description: does the path exist, follow symlinks
+ description:
+ - Check if the provided path maps to an existing filesystem object on the controller (localhost).
+- - Follows symlinks and checks the target of the symlink instead of the link itself, use the P(ansible.builtin.link#test)
+- or P(ansible.builtin.link_exists#test) tests to check on the link.
++ - Follows symlinks and checks the target of the symlink instead of the link itself, use the C(link) or C(link_exists) tests to check on the link.
+ options:
+ _input:
+ description: a path
+@@ -19,5 +18,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the path corresponds to an existing filesystem object on the controller (after following symlinks), V(False) if otherwise.
++ description: Returns C(True) if the path corresponds to an existing filesystem object on the controller (after following symlinks), C(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/failed.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/failed.yml
+@@ -6,7 +6,7 @@ DOCUMENTATION:
+ aliases: [failure]
+ description:
+ - Tests if task finished in failure, opposite of C(succeeded).
+- - This test checks for the existance of a C(failed) key in the input dictionary and that it is V(True) if present.
++ - This test checks for the existance of a C(failed) key in the input dictionary and that it is C(True) if present.
+ - Tasks that get skipped or not executed due to other failures (syntax, templating, unreachable host, etc) do not return a 'failed' status.
+ options:
+ _input:
+@@ -19,5 +19,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the task was failed, V(False) otherwise.
++ description: Returns C(True) if the task was failed, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/failure.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/failure.yml
+@@ -6,7 +6,7 @@ DOCUMENTATION:
+ aliases: [failure]
+ description:
+ - Tests if task finished in failure, opposite of C(succeeded).
+- - This test checks for the existance of a C(failed) key in the input dictionary and that it is V(True) if present.
++ - This test checks for the existance of a C(failed) key in the input dictionary and that it is C(True) if present.
+ - Tasks that get skipped or not executed due to other failures (syntax, templating, unreachable host, etc) do not return a 'failed' status.
+ options:
+ _input:
+@@ -19,5 +19,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the task was failed, V(False) otherwise.
++ description: Returns C(True) if the task was failed, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/falsy.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/falsy.yml
+@@ -12,7 +12,7 @@ DOCUMENTATION:
+ type: string
+ required: True
+ convert_bool:
+- description: Attempts to convert the result to a strict Python boolean vs normally acceptable values (V(yes)/V(no), V(on)/V(off), V(0)/V(1), etc).
++ description: Attempts to convert the result to a strict Python boolean vs normally acceptable values (C(yes)/C(no), C(on)/C(off), C(0)/C(1), etc).
+ type: bool
+ default: false
+ EXAMPLES: |
+@@ -20,5 +20,5 @@ EXAMPLES: |
+ thisistrue: '{{ "" is falsy }}'
+ RETURN:
+ _value:
+- description: Returns V(False) if the condition is not "Python truthy", V(True) otherwise.
++ description: Returns C(False) if the condition is not "Python truthy", C(True) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/file.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/file.yml
+@@ -18,5 +18,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the path corresponds to an existing file on the filesystem on the controller, V(False) if otherwise.
++ description: Returns C(True) if the path corresponds to an existing file on the filesystem on the controller, C(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/files.py
++++ ansible-core-2.16.5/lib/ansible/plugins/test/files.py
+@@ -20,6 +20,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ from os.path import isdir, isfile, isabs, exists, lexists, islink, samefile, ismount
++from ansible import errors
+
+
+ class TestModule(object):
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/finished.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/finished.yml
+@@ -5,7 +5,7 @@ DOCUMENTATION:
+ short_description: Did async task finish
+ description:
+ - Used to test if an async task has finished, it will aslo work with normal tasks but will issue a warning.
+- - This test checks for the existance of a C(finished) key in the input dictionary and that it is V(1) if present
++ - This test checks for the existance of a C(finished) key in the input dictionary and that it is C(1) if present
+ options:
+ _input:
+ description: registered result from an Ansible task
+@@ -17,5 +17,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the aysnc task has finished, V(False) otherwise.
++ description: Returns C(True) if the aysnc task has finished, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/is_abs.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/is_abs.yml
+@@ -19,5 +19,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the path is absolute, V(False) if it is relative.
++ description: Returns C(True) if the path is absolute, C(False) if it is relative.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/is_dir.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/is_dir.yml
+@@ -17,5 +17,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the path corresponds to an existing directory on the filesystem on the controller, V(False) if otherwise.
++ description: Returns C(True) if the path corresponds to an existing directory on the filesystem on the controller, c(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/is_file.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/is_file.yml
+@@ -18,5 +18,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the path corresponds to an existing file on the filesystem on the controller, V(False) if otherwise.
++ description: Returns C(True) if the path corresponds to an existing file on the filesystem on the controller, C(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/is_link.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/is_link.yml
+@@ -17,5 +17,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the path corresponds to an existing symlink on the filesystem on the controller, V(False) if otherwise.
++ description: Returns C(True) if the path corresponds to an existing symlink on the filesystem on the controller, C(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/is_mount.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/is_mount.yml
+@@ -18,5 +18,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the path corresponds to a mount point on the controller, V(False) if otherwise.
++ description: Returns C(True) if the path corresponds to a mount point on the controller, C(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/is_same_file.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/is_same_file.yml
+@@ -20,5 +20,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the paths correspond to the same location on the filesystem on the controller, V(False) if otherwise.
++ description: Returns C(True) if the paths correspond to the same location on the filesystem on the controller, C(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/isnan.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/isnan.yml
+@@ -16,5 +16,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the input is NaN, V(False) if otherwise.
++ description: Returns C(True) if the input is NaN, C(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/issubset.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/issubset.yml
+@@ -6,6 +6,7 @@ DOCUMENTATION:
+ short_description: is the list a subset of this other list
+ description:
+ - Validate if the first list is a sub set (is included) of the second list.
++ - Same as the C(all) Python function.
+ options:
+ _input:
+ description: List.
+@@ -23,5 +24,5 @@ EXAMPLES: |
+ issmallinbig: '{{ small is subset(big) }}'
+ RETURN:
+ _value:
+- description: Returns V(True) if the specified list is a subset of the provided list, V(False) otherwise.
++ description: Returns C(True) if the specified list is a subset of the provided list, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/issuperset.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/issuperset.yml
+@@ -6,6 +6,7 @@ DOCUMENTATION:
+ aliases: [issuperset]
+ description:
+ - Validate if the first list is a super set (includes) the second list.
++ - Same as the C(all) Python function.
+ options:
+ _input:
+ description: List.
+@@ -23,5 +24,5 @@ EXAMPLES: |
+ issmallinbig: '{{ big is superset(small) }}'
+ RETURN:
+ _value:
+- description: Returns V(True) if the specified list is a superset of the provided list, V(False) otherwise.
++ description: Returns C(True) if the specified list is a superset of the provided list, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/link.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/link.yml
+@@ -17,5 +17,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the path corresponds to an existing symlink on the filesystem on the controller, V(False) if otherwise.
++ description: Returns C(True) if the path corresponds to an existing symlink on the filesystem on the controller, C(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/link_exists.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/link_exists.yml
+@@ -17,5 +17,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the path corresponds to an existing filesystem object on the controller, V(False) if otherwise.
++ description: Returns C(True) if the path corresponds to an existing filesystem object on the controller, C(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/match.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/match.yml
+@@ -19,7 +19,7 @@ DOCUMENTATION:
+ type: boolean
+ default: False
+ multiline:
+- description: Match against multiple lines in string.
++ description: Match against mulitple lines in string.
+ type: boolean
+ default: False
+ EXAMPLES: |
+@@ -28,5 +28,5 @@ EXAMPLES: |
+ nomatch: url is match("/users/.*/resources")
+ RETURN:
+ _value:
+- description: Returns V(True) if there is a match, V(False) otherwise.
++ description: Returns C(True) if there is a match, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/mount.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/mount.yml
+@@ -18,5 +18,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the path corresponds to a mount point on the controller, V(False) if otherwise.
++ description: Returns C(True) if the path corresponds to a mount point on the controller, C(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/nan.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/nan.yml
+@@ -16,5 +16,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the input is NaN, V(False) if otherwise.
++ description: Returns C(True) if the input is NaN, C(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/reachable.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/reachable.yml
+@@ -5,7 +5,7 @@ DOCUMENTATION:
+ short_description: Task did not end due to unreachable host
+ description:
+ - Tests if task was able to reach the host for execution
+- - This test checks for the existance of a C(unreachable) key in the input dictionary and that it is V(False) if present
++ - This test checks for the existance of a C(unreachable) key in the input dictionary and that it is C(False) if present
+ options:
+ _input:
+ description: registered result from an Ansible task
+@@ -13,9 +13,9 @@ DOCUMENTATION:
+ required: True
+ EXAMPLES: |
+ # test 'status' to know how to respond
+- {{ taskresults is reachable }}
++ {{ (taskresults is reachable }}
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the task did not flag the host as unreachable, V(False) otherwise.
++ description: Returns C(True) if the task did not flag the host as unreachable, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/regex.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/regex.yml
+@@ -33,5 +33,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if there is a match, V(False) otherwise.
++ description: Returns C(True) if there is a match, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/same_file.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/same_file.yml
+@@ -20,5 +20,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the paths correspond to the same location on the filesystem on the controller, V(False) if otherwise.
++ description: Returns C(True) if the paths correspond to the same location on the filesystem on the controller, C(False) if otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/search.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/search.yml
+@@ -18,7 +18,7 @@ DOCUMENTATION:
+ type: boolean
+ default: False
+ multiline:
+- description: Match against multiple lines in string.
++ description: Match against mulitple lines in string.
+ type: boolean
+ default: False
+
+@@ -29,5 +29,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if there is a match, V(False) otherwise.
++ description: Returns C(True) if there is a match, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/skip.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/skip.yml
+@@ -6,7 +6,7 @@ DOCUMENTATION:
+ aliases: [skip]
+ description:
+ - Tests if task was skipped
+- - This test checks for the existance of a C(skipped) key in the input dictionary and that it is V(True) if present
++ - This test checks for the existance of a C(skipped) key in the input dictionary and that it is C(True) if present
+ options:
+ _input:
+ description: registered result from an Ansible task
+@@ -14,9 +14,9 @@ DOCUMENTATION:
+ required: True
+ EXAMPLES: |
+ # test 'status' to know how to respond
+- {{ taskresults is skipped }}
++ {{ (taskresults is skipped}}
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the task was skipped, V(False) otherwise.
++ description: Returns C(True) if the task was skipped, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/skipped.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/skipped.yml
+@@ -6,7 +6,7 @@ DOCUMENTATION:
+ aliases: [skip]
+ description:
+ - Tests if task was skipped
+- - This test checks for the existance of a C(skipped) key in the input dictionary and that it is V(True) if present
++ - This test checks for the existance of a C(skipped) key in the input dictionary and that it is C(True) if present
+ options:
+ _input:
+ description: registered result from an Ansible task
+@@ -14,9 +14,9 @@ DOCUMENTATION:
+ required: True
+ EXAMPLES: |
+ # test 'status' to know how to respond
+- {{ taskresults is skipped }}
++ {{ (taskresults is skipped}}
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the task was skipped, V(False) otherwise.
++ description: Returns C(True) if the task was skipped, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/started.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/started.yml
+@@ -5,7 +5,7 @@ DOCUMENTATION:
+ short_description: Was async task started
+ description:
+ - Used to check if an async task has started, will also work with non async tasks but will issue a warning.
+- - This test checks for the existance of a C(started) key in the input dictionary and that it is V(1) if present
++ - This test checks for the existance of a C(started) key in the input dictionary and that it is C(1) if present
+ options:
+ _input:
+ description: registered result from an Ansible task
+@@ -17,5 +17,5 @@ EXAMPLES: |
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the task has started, V(False) otherwise.
++ description: Returns C(True) if the task has started, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/subset.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/subset.yml
+@@ -6,6 +6,7 @@ DOCUMENTATION:
+ short_description: is the list a subset of this other list
+ description:
+ - Validate if the first list is a sub set (is included) of the second list.
++ - Same as the C(all) Python function.
+ options:
+ _input:
+ description: List.
+@@ -23,5 +24,5 @@ EXAMPLES: |
+ issmallinbig: '{{ small is subset(big) }}'
+ RETURN:
+ _value:
+- description: Returns V(True) if the specified list is a subset of the provided list, V(False) otherwise.
++ description: Returns C(True) if the specified list is a subset of the provided list, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/succeeded.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/succeeded.yml
+@@ -6,7 +6,7 @@ DOCUMENTATION:
+ aliases: [succeeded, successful]
+ description:
+ - Tests if task finished successfully, opposite of C(failed).
+- - This test checks for the existance of a C(failed) key in the input dictionary and that it is V(False) if present
++ - This test checks for the existance of a C(failed) key in the input dictionary and that it is C(False) if present
+ options:
+ _input:
+ description: registered result from an Ansible task
+@@ -14,9 +14,9 @@ DOCUMENTATION:
+ required: True
+ EXAMPLES: |
+ # test 'status' to know how to respond
+- {{ taskresults is success }}
++ {{ (taskresults is success }}
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the task was successfully completed, V(False) otherwise.
++ description: Returns C(True) if the task was successfully completed, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/success.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/success.yml
+@@ -6,7 +6,7 @@ DOCUMENTATION:
+ aliases: [succeeded, successful]
+ description:
+ - Tests if task finished successfully, opposite of C(failed).
+- - This test checks for the existance of a C(failed) key in the input dictionary and that it is V(False) if present
++ - This test checks for the existance of a C(failed) key in the input dictionary and that it is C(False) if present
+ options:
+ _input:
+ description: registered result from an Ansible task
+@@ -14,9 +14,9 @@ DOCUMENTATION:
+ required: True
+ EXAMPLES: |
+ # test 'status' to know how to respond
+- {{ taskresults is success }}
++ {{ (taskresults is success }}
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the task was successfully completed, V(False) otherwise.
++ description: Returns C(True) if the task was successfully completed, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/successful.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/successful.yml
+@@ -6,7 +6,7 @@ DOCUMENTATION:
+ aliases: [succeeded, successful]
+ description:
+ - Tests if task finished successfully, opposite of C(failed).
+- - This test checks for the existance of a C(failed) key in the input dictionary and that it is V(False) if present
++ - This test checks for the existance of a C(failed) key in the input dictionary and that it is C(False) if present
+ options:
+ _input:
+ description: registered result from an Ansible task
+@@ -14,9 +14,9 @@ DOCUMENTATION:
+ required: True
+ EXAMPLES: |
+ # test 'status' to know how to respond
+- {{ taskresults is success }}
++ {{ (taskresults is success }}
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the task was successfully completed, V(False) otherwise.
++ description: Returns C(True) if the task was successfully completed, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/superset.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/superset.yml
+@@ -6,6 +6,7 @@ DOCUMENTATION:
+ aliases: [issuperset]
+ description:
+ - Validate if the first list is a super set (includes) the second list.
++ - Same as the C(all) Python function.
+ options:
+ _input:
+ description: List.
+@@ -23,5 +24,5 @@ EXAMPLES: |
+ issmallinbig: '{{ big is superset(small) }}'
+ RETURN:
+ _value:
+- description: Returns V(True) if the specified list is a superset of the provided list, V(False) otherwise.
++ description: Returns C(True) if the specified list is a superset of the provided list, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/truthy.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/truthy.yml
+@@ -5,14 +5,14 @@ DOCUMENTATION:
+ short_description: Pythonic true
+ description:
+ - This check is a more Python version of what is 'true'.
+- - It is the opposite of P(ansible.builtin.falsy#test).
++ - It is the opposite of C(falsy).
+ options:
+ _input:
+ description: An expression that can be expressed in a boolean context.
+ type: string
+ required: True
+ convert_bool:
+- description: Attempts to convert to strict python boolean vs normally acceptable values (V(yes)/V(no), V(on)/V(off), V(0)/V(1), etc).
++ description: Attempts to convert to strict python boolean vs normally acceptable values (C(yes)/C(no), C(on)/C(off), C(0)/C(1), etc).
+ type: bool
+ default: false
+ EXAMPLES: |
+@@ -20,5 +20,5 @@ EXAMPLES: |
+ thisisfalse: '{{ "" is truthy }}'
+ RETURN:
+ _value:
+- description: Returns V(True) if the condition is not "Python truthy", V(False) otherwise.
++ description: Returns C(True) if the condition is not "Python truthy", C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/unreachable.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/unreachable.yml
+@@ -5,7 +5,7 @@ DOCUMENTATION:
+ short_description: Did task end due to the host was unreachable
+ description:
+ - Tests if task was not able to reach the host for execution
+- - This test checks for the existance of a C(unreachable) key in the input dictionary and that it's value is V(True)
++ - This test checks for the existance of a C(unreachable) key in the input dictionary and that it's value is C(True)
+ options:
+ _input:
+ description: registered result from an Ansible task
+@@ -13,9 +13,9 @@ DOCUMENTATION:
+ required: True
+ EXAMPLES: |
+ # test 'status' to know how to respond
+- {{ taskresults is unreachable }}
++ {{ (taskresults is unreachable }}
+
+ RETURN:
+ _value:
+- description: Returns V(True) if the task flagged the host as unreachable, V(False) otherwise.
++ description: Returns C(True) if the task flagged the host as unreachable, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/uri.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/uri.yml
+@@ -26,5 +26,5 @@ EXAMPLES: |
+ {{ 'http://nobody:secret@example.com' is uri(['ftp', 'ftps', 'http', 'https', 'ws', 'wss']) }}
+ RETURN:
+ _value:
+- description: Returns V(false) if the string is not a URI or the schema extracted does not match the supplied list.
++ description: Returns C(false) if the string is not a URI or the schema extracted does not match the supplied list.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/url.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/url.yml
+@@ -25,5 +25,5 @@ EXAMPLES: |
+ {{ 'ftp://admin:secret@example.com/path/to/myfile.yml' is url }}
+ RETURN:
+ _value:
+- description: Returns V(false) if the string is not a URL, V(true) otherwise.
++ description: Returns C(false) if the string is not a URL, C(true) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/urn.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/urn.yml
+@@ -17,5 +17,5 @@ EXAMPLES: |
+ {{ 'mailto://nowone@example.com' is not urn }}
+ RETURN:
+ _value:
+- description: Returns V(true) if the string is a URN and V(false) if it is not.
++ description: Returns C(true) if the string is a URN and C(false) if it is not.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/vault_encrypted.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/vault_encrypted.yml
+@@ -15,5 +15,5 @@ EXAMPLES: |
+ thisistrue: '{{ "$ANSIBLE_VAULT;1.2;AES256;dev...." is ansible_vault }}'
+ RETURN:
+ _value:
+- description: Returns V(True) if the input is a valid ansible vault, V(False) otherwise.
++ description: Returns C(True) if the input is a valid ansible vault, C(False) otherwise.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/version.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/version.yml
+@@ -36,12 +36,12 @@ DOCUMENTATION:
+ - ne
+ default: eq
+ strict:
+- description: Whether to use strict version scheme. Mutually exclusive with O(version_type)
++ description: Whether to use strict version scheme. Mutually exclusive with C(version_type)
+ type: boolean
+ required: False
+ default: False
+ version_type:
+- description: Version scheme to use for comparison. Mutually exclusive with O(strict). See C(notes) for descriptions on the version types.
++ description: Version scheme to use for comparison. Mutually exclusive with C(strict). See C(notes) for descriptions on the version types.
+ type: string
+ required: False
+ choices:
+@@ -52,10 +52,10 @@ DOCUMENTATION:
+ - pep440
+ default: loose
+ notes:
+- - V(loose) - This type corresponds to the Python C(distutils.version.LooseVersion) class. All version formats are valid for this type. The rules for comparison are simple and predictable, but may not always give expected results.
+- - V(strict) - This type corresponds to the Python C(distutils.version.StrictVersion) class. A version number consists of two or three dot-separated numeric components, with an optional "pre-release" tag on the end. The pre-release tag consists of a single letter C(a) or C(b) followed by a number. If the numeric components of two version numbers are equal, then one with a pre-release tag will always be deemed earlier (lesser) than one without.
+- - V(semver)/V(semantic) - This type implements the L(Semantic Version,https://semver.org) scheme for version comparison.
+- - V(pep440) - This type implements the Python L(PEP-440,https://peps.python.org/pep-0440/) versioning rules for version comparison. Added in version 2.14.
++ - C(loose) - This type corresponds to the Python C(distutils.version.LooseVersion) class. All version formats are valid for this type. The rules for comparison are simple and predictable, but may not always give expected results.
++ - C(strict) - This type corresponds to the Python C(distutils.version.StrictVersion) class. A version number consists of two or three dot-separated numeric components, with an optional "pre-release" tag on the end. The pre-release tag consists of a single letter C(a) or C(b) followed by a number. If the numeric components of two version numbers are equal, then one with a pre-release tag will always be deemed earlier (lesser) than one without.
++ - C(semver)/C(semantic) - This type implements the L(Semantic Version,https://semver.org) scheme for version comparison.
++ - C(pep440) - This type implements the Python L(PEP-440,https://peps.python.org/pep-0440/) versioning rules for version comparison. Added in version 2.14.
+ EXAMPLES: |
+ - name: version test examples
+ assert:
+@@ -78,5 +78,5 @@ EXAMPLES: |
+ - "'2.14.0rc1' is version('2.14.0', 'lt', version_type='pep440')"
+ RETURN:
+ _value:
+- description: Returns V(True) or V(False) depending on the outcome of the comparison.
++ description: Returns C(True) or C(False) depending on the outcome of the comparison.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/test/version_compare.yml
++++ ansible-core-2.16.5/lib/ansible/plugins/test/version_compare.yml
+@@ -36,12 +36,12 @@ DOCUMENTATION:
+ - ne
+ default: eq
+ strict:
+- description: Whether to use strict version scheme. Mutually exclusive with O(version_type)
++ description: Whether to use strict version scheme. Mutually exclusive with C(version_type)
+ type: boolean
+ required: False
+ default: False
+ version_type:
+- description: Version scheme to use for comparison. Mutually exclusive with O(strict). See C(notes) for descriptions on the version types.
++ description: Version scheme to use for comparison. Mutually exclusive with C(strict). See C(notes) for descriptions on the version types.
+ type: string
+ required: False
+ choices:
+@@ -52,10 +52,10 @@ DOCUMENTATION:
+ - pep440
+ default: loose
+ notes:
+- - V(loose) - This type corresponds to the Python C(distutils.version.LooseVersion) class. All version formats are valid for this type. The rules for comparison are simple and predictable, but may not always give expected results.
+- - V(strict) - This type corresponds to the Python C(distutils.version.StrictVersion) class. A version number consists of two or three dot-separated numeric components, with an optional "pre-release" tag on the end. The pre-release tag consists of a single letter C(a) or C(b) followed by a number. If the numeric components of two version numbers are equal, then one with a pre-release tag will always be deemed earlier (lesser) than one without.
+- - V(semver)/V(semantic) - This type implements the L(Semantic Version,https://semver.org) scheme for version comparison.
+- - V(pep440) - This type implements the Python L(PEP-440,https://peps.python.org/pep-0440/) versioning rules for version comparison. Added in version 2.14.
++ - C(loose) - This type corresponds to the Python C(distutils.version.LooseVersion) class. All version formats are valid for this type. The rules for comparison are simple and predictable, but may not always give expected results.
++ - C(strict) - This type corresponds to the Python C(distutils.version.StrictVersion) class. A version number consists of two or three dot-separated numeric components, with an optional "pre-release" tag on the end. The pre-release tag consists of a single letter C(a) or C(b) followed by a number. If the numeric components of two version numbers are equal, then one with a pre-release tag will always be deemed earlier (lesser) than one without.
++ - C(semver)/C(semantic) - This type implements the L(Semantic Version,https://semver.org) scheme for version comparison.
++ - C(pep440) - This type implements the Python L(PEP-440,https://peps.python.org/pep-0440/) versioning rules for version comparison. Added in version 2.14.
+ EXAMPLES: |
+ - name: version test examples
+ assert:
+@@ -78,5 +78,5 @@ EXAMPLES: |
+ - "'2.14.0rc1' is version('2.14.0', 'lt', version_type='pep440')"
+ RETURN:
+ _value:
+- description: Returns V(True) or V(False) depending on the outcome of the comparison.
++ description: Returns C(True) or C(False) depending on the outcome of the comparison.
+ type: boolean
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/vars/__init__.py
++++ ansible-core-2.16.5/lib/ansible/plugins/vars/__init__.py
+@@ -30,7 +30,6 @@ class BaseVarsPlugin(AnsiblePlugin):
+ """
+ Loads variables for groups and/or hosts
+ """
+- is_stateless = False
+
+ def __init__(self):
+ """ constructor """
+--- ansible-core-2.16.5.orig/lib/ansible/plugins/vars/host_group_vars.py
++++ ansible-core-2.16.5/lib/ansible/plugins/vars/host_group_vars.py
+@@ -54,30 +54,20 @@ DOCUMENTATION = '''
+ '''
+
+ import os
++from ansible import constants as C
+ from ansible.errors import AnsibleParserError
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.plugins.vars import BaseVarsPlugin
+-from ansible.utils.path import basedir
+-from ansible.inventory.group import InventoryObjectType
++from ansible.inventory.host import Host
++from ansible.inventory.group import Group
+ from ansible.utils.vars import combine_vars
+
+-CANONICAL_PATHS = {} # type: dict[str, str]
+ FOUND = {} # type: dict[str, list[str]]
+-NAK = set() # type: set[str]
+-PATH_CACHE = {} # type: dict[tuple[str, str], str]
+
+
+ class VarsModule(BaseVarsPlugin):
+
+ REQUIRES_ENABLED = True
+- is_stateless = True
+-
+- def load_found_files(self, loader, data, found_files):
+- for found in found_files:
+- new_data = loader.load_from_file(found, cache=True, unsafe=True)
+- if new_data: # ignore empty files
+- data = combine_vars(data, new_data)
+- return data
+
+ def get_vars(self, loader, path, entities, cache=True):
+ ''' parses the inventory file '''
+@@ -85,68 +75,41 @@ class VarsModule(BaseVarsPlugin):
+ if not isinstance(entities, list):
+ entities = [entities]
+
+- # realpath is expensive
+- try:
+- realpath_basedir = CANONICAL_PATHS[path]
+- except KeyError:
+- CANONICAL_PATHS[path] = realpath_basedir = os.path.realpath(basedir(path))
++ super(VarsModule, self).get_vars(loader, path, entities)
+
+ data = {}
+ for entity in entities:
+- try:
+- entity_name = entity.name
+- except AttributeError:
+- raise AnsibleParserError("Supplied entity must be Host or Group, got %s instead" % (type(entity)))
+-
+- try:
+- first_char = entity_name[0]
+- except (TypeError, IndexError, KeyError):
++ if isinstance(entity, Host):
++ subdir = 'host_vars'
++ elif isinstance(entity, Group):
++ subdir = 'group_vars'
++ else:
+ raise AnsibleParserError("Supplied entity must be Host or Group, got %s instead" % (type(entity)))
+
+ # avoid 'chroot' type inventory hostnames /path/to/chroot
+- if first_char != os.path.sep:
++ if not entity.name.startswith(os.path.sep):
+ try:
+ found_files = []
+ # load vars
+- try:
+- entity_type = entity.base_type
+- except AttributeError:
+- raise AnsibleParserError("Supplied entity must be Host or Group, got %s instead" % (type(entity)))
+-
+- if entity_type is InventoryObjectType.HOST:
+- subdir = 'host_vars'
+- elif entity_type is InventoryObjectType.GROUP:
+- subdir = 'group_vars'
+- else:
+- raise AnsibleParserError("Supplied entity must be Host or Group, got %s instead" % (type(entity)))
+-
+- if cache:
+- try:
+- opath = PATH_CACHE[(realpath_basedir, subdir)]
+- except KeyError:
+- opath = PATH_CACHE[(realpath_basedir, subdir)] = os.path.join(realpath_basedir, subdir)
+-
+- if opath in NAK:
+- continue
+- key = '%s.%s' % (entity_name, opath)
+- if key in FOUND:
+- data = self.load_found_files(loader, data, FOUND[key])
+- continue
+- else:
+- opath = PATH_CACHE[(realpath_basedir, subdir)] = os.path.join(realpath_basedir, subdir)
+-
+- if os.path.isdir(opath):
+- self._display.debug("\tprocessing dir %s" % opath)
+- FOUND[key] = found_files = loader.find_vars_files(opath, entity_name)
+- elif not os.path.exists(opath):
+- # cache missing dirs so we don't have to keep looking for things beneath the
+- NAK.add(opath)
++ b_opath = os.path.realpath(to_bytes(os.path.join(self._basedir, subdir)))
++ opath = to_text(b_opath)
++ key = '%s.%s' % (entity.name, opath)
++ if cache and key in FOUND:
++ found_files = FOUND[key]
+ else:
+- self._display.warning("Found %s that is not a directory, skipping: %s" % (subdir, opath))
+- # cache non-directory matches
+- NAK.add(opath)
+-
+- data = self.load_found_files(loader, data, found_files)
++ # no need to do much if path does not exist for basedir
++ if os.path.exists(b_opath):
++ if os.path.isdir(b_opath):
++ self._display.debug("\tprocessing dir %s" % opath)
++ found_files = loader.find_vars_files(opath, entity.name)
++ FOUND[key] = found_files
++ else:
++ self._display.warning("Found %s that is not a directory, skipping: %s" % (subdir, opath))
++
++ for found in found_files:
++ new_data = loader.load_from_file(found, cache=True, unsafe=True)
++ if new_data: # ignore empty files
++ data = combine_vars(data, new_data)
+
+ except Exception as e:
+ raise AnsibleParserError(to_native(e))
+--- ansible-core-2.16.5.orig/lib/ansible/release.py
++++ ansible-core-2.16.5/lib/ansible/release.py
+@@ -19,6 +19,6 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-__version__ = '2.16.5'
++__version__ = '2.14.13'
+ __author__ = 'Ansible, Inc.'
+-__codename__ = "All My Love"
++__codename__ = "C'mon Everybody"
+--- ansible-core-2.16.5.orig/lib/ansible/template/__init__.py
++++ ansible-core-2.16.5/lib/ansible/template/__init__.py
+@@ -45,8 +45,8 @@ from ansible.errors import (
+ AnsibleOptionsError,
+ AnsibleUndefinedVariable,
+ )
+-from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
++from ansible.module_utils.six import string_types, text_type
++from ansible.module_utils._text import to_native, to_text, to_bytes
+ from ansible.module_utils.common.collections import is_sequence
+ from ansible.plugins.loader import filter_loader, lookup_loader, test_loader
+ from ansible.template.native_helpers import ansible_native_concat, ansible_eval_concat, ansible_concat
+@@ -55,7 +55,7 @@ from ansible.template.vars import Ansibl
+ from ansible.utils.display import Display
+ from ansible.utils.listify import listify_lookup_plugin_terms
+ from ansible.utils.native_jinja import NativeJinjaText
+-from ansible.utils.unsafe_proxy import to_unsafe_text, wrap_var, AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText
++from ansible.utils.unsafe_proxy import wrap_var, AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText
+
+ display = Display()
+
+@@ -103,9 +103,9 @@ def generate_ansible_template_vars(path,
+ managed_str = managed_default.format(
+ host=temp_vars['template_host'],
+ uid=temp_vars['template_uid'],
+- file=temp_vars['template_path'].replace('%', '%%'),
++ file=temp_vars['template_path'],
+ )
+- temp_vars['ansible_managed'] = to_unsafe_text(time.strftime(to_native(managed_str), time.localtime(os.path.getmtime(b_path))))
++ temp_vars['ansible_managed'] = to_text(time.strftime(to_native(managed_str), time.localtime(os.path.getmtime(b_path))))
+
+ return temp_vars
+
+@@ -130,7 +130,7 @@ def _escape_backslashes(data, jinja_env)
+ backslashes inside of a jinja2 expression.
+
+ """
+- if '\\' in data and jinja_env.variable_start_string in data:
++ if '\\' in data and '{{' in data:
+ new_data = []
+ d2 = jinja_env.preprocess(data)
+ in_var = False
+@@ -153,39 +153,6 @@ def _escape_backslashes(data, jinja_env)
+ return data
+
+
+-def _create_overlay(data, overrides, jinja_env):
+- if overrides is None:
+- overrides = {}
+-
+- try:
+- has_override_header = data.startswith(JINJA2_OVERRIDE)
+- except (TypeError, AttributeError):
+- has_override_header = False
+-
+- if overrides or has_override_header:
+- overlay = jinja_env.overlay(**overrides)
+- else:
+- overlay = jinja_env
+-
+- # Get jinja env overrides from template
+- if has_override_header:
+- eol = data.find('\n')
+- line = data[len(JINJA2_OVERRIDE):eol]
+- data = data[eol + 1:]
+- for pair in line.split(','):
+- if ':' not in pair:
+- raise AnsibleError("failed to parse jinja2 override '%s'."
+- " Did you use something different from colon as key-value separator?" % pair.strip())
+- (key, val) = pair.split(':', 1)
+- key = key.strip()
+- if hasattr(overlay, key):
+- setattr(overlay, key, ast.literal_eval(val.strip()))
+- else:
+- display.warning(f"Could not find Jinja2 environment setting to override: '{key}'")
+-
+- return data, overlay
+-
+-
+ def is_possibly_template(data, jinja_env):
+ """Determines if a string looks like a template, by seeing if it
+ contains a jinja2 start delimiter. Does not guarantee that the string
+@@ -565,7 +532,7 @@ class AnsibleEnvironment(NativeEnvironme
+ '''
+ context_class = AnsibleContext
+ template_class = AnsibleJ2Template
+- concat = staticmethod(ansible_eval_concat) # type: ignore[assignment]
++ concat = staticmethod(ansible_eval_concat)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+@@ -580,7 +547,7 @@ class AnsibleEnvironment(NativeEnvironme
+
+
+ class AnsibleNativeEnvironment(AnsibleEnvironment):
+- concat = staticmethod(ansible_native_concat) # type: ignore[assignment]
++ concat = staticmethod(ansible_native_concat)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+@@ -592,7 +559,14 @@ class Templar:
+ The main class for templating, with the main entry-point of template().
+ '''
+
+- def __init__(self, loader, variables=None):
++ def __init__(self, loader, shared_loader_obj=None, variables=None):
++ if shared_loader_obj is not None:
++ display.deprecated(
++ "The `shared_loader_obj` option to `Templar` is no longer functional, "
++ "ansible.plugins.loader is used directly instead.",
++ version='2.16',
++ )
++
+ self._loader = loader
+ self._available_variables = {} if variables is None else variables
+
+@@ -606,6 +580,9 @@ class Templar:
+ )
+ self.environment.template_class.environment_class = environment_class
+
++ # jinja2 global is inconsistent across versions, this normalizes them
++ self.environment.globals['dict'] = dict
++
+ # Custom globals
+ self.environment.globals['lookup'] = self._lookup
+ self.environment.globals['query'] = self.environment.globals['q'] = self._query_lookup
+@@ -615,14 +592,11 @@ class Templar:
+ # the current rendering context under which the templar class is working
+ self.cur_context = None
+
+- # this regex is re-compiled each time variable_start_string and variable_end_string are possibly changed
+- self._compile_single_var(self.environment)
++ # FIXME this regex should be re-compiled each time variable_start_string and variable_end_string are changed
++ self.SINGLE_VAR = re.compile(r"^%s\s*(\w*)\s*%s$" % (self.environment.variable_start_string, self.environment.variable_end_string))
+
+ self.jinja2_native = C.DEFAULT_JINJA2_NATIVE
+
+- def _compile_single_var(self, env):
+- self.SINGLE_VAR = re.compile(r"^%s\s*(\w*)\s*%s$" % (env.variable_start_string, env.variable_end_string))
+-
+ def copy_with_new_env(self, environment_class=AnsibleEnvironment, **kwargs):
+ r"""Creates a new copy of Templar with a new environment.
+
+@@ -745,7 +719,7 @@ class Templar:
+ variable = self._convert_bare_variable(variable)
+
+ if isinstance(variable, string_types):
+- if not self.is_possibly_template(variable, overrides):
++ if not self.is_possibly_template(variable):
+ return variable
+
+ # Check to see if the string we are trying to render is just referencing a single
+@@ -770,7 +744,6 @@ class Templar:
+ disable_lookups=disable_lookups,
+ convert_data=convert_data,
+ )
+- self._compile_single_var(self.environment)
+
+ return result
+
+@@ -817,9 +790,8 @@ class Templar:
+
+ templatable = is_template
+
+- def is_possibly_template(self, data, overrides=None):
+- data, env = _create_overlay(data, overrides, self.environment)
+- return is_possibly_template(data, env)
++ def is_possibly_template(self, data):
++ return is_possibly_template(data, self.environment)
+
+ def _convert_bare_variable(self, variable):
+ '''
+@@ -843,7 +815,7 @@ class Templar:
+ def _now_datetime(self, utc=False, fmt=None):
+ '''jinja2 global function to return current datetime, potentially formatted via strftime'''
+ if utc:
+- now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
++ now = datetime.datetime.utcnow()
+ else:
+ now = datetime.datetime.now()
+
+@@ -852,12 +824,12 @@ class Templar:
+
+ return now
+
+- def _query_lookup(self, name, /, *args, **kwargs):
++ def _query_lookup(self, name, *args, **kwargs):
+ ''' wrapper for lookup, force wantlist true'''
+ kwargs['wantlist'] = True
+ return self._lookup(name, *args, **kwargs)
+
+- def _lookup(self, name, /, *args, **kwargs):
++ def _lookup(self, name, *args, **kwargs):
+ instance = lookup_loader.get(name, loader=self._loader, templar=self)
+
+ if instance is None:
+@@ -960,12 +932,31 @@ class Templar:
+ if fail_on_undefined is None:
+ fail_on_undefined = self._fail_on_undefined_errors
+
++ has_template_overrides = data.startswith(JINJA2_OVERRIDE)
++
+ try:
+ # NOTE Creating an overlay that lives only inside do_template means that overrides are not applied
+ # when templating nested variables in AnsibleJ2Vars where Templar.environment is used, not the overlay.
+- data, myenv = _create_overlay(data, overrides, self.environment)
+- # in case delimiters change
+- self._compile_single_var(myenv)
++ # This is historic behavior that is kept for backwards compatibility.
++ if overrides:
++ myenv = self.environment.overlay(overrides)
++ elif has_template_overrides:
++ myenv = self.environment.overlay()
++ else:
++ myenv = self.environment
++
++ # Get jinja env overrides from template
++ if has_template_overrides:
++ eol = data.find('\n')
++ line = data[len(JINJA2_OVERRIDE):eol]
++ data = data[eol + 1:]
++ for pair in line.split(','):
++ if ':' not in pair:
++ raise AnsibleError("failed to parse jinja2 override '%s'."
++ " Did you use something different from colon as key-value separator?" % pair.strip())
++ (key, val) = pair.split(':', 1)
++ key = key.strip()
++ setattr(myenv, key, ast.literal_eval(val.strip()))
+
+ if escape_backslashes:
+ # Allow users to specify backslashes in playbooks as "\\" instead of as "\\\\".
+@@ -973,7 +964,7 @@ class Templar:
+
+ try:
+ t = myenv.from_string(data)
+- except (TemplateSyntaxError, SyntaxError) as e:
++ except TemplateSyntaxError as e:
+ raise AnsibleError("template error while templating string: %s. String: %s" % (to_native(e), to_native(data)), orig_exc=e)
+ except Exception as e:
+ if 'recursion' in to_native(e):
+--- ansible-core-2.16.5.orig/lib/ansible/template/native_helpers.py
++++ ansible-core-2.16.5/lib/ansible/template/native_helpers.py
+@@ -10,7 +10,7 @@ import ast
+ from itertools import islice, chain
+ from types import GeneratorType
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.six import string_types
+ from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
+ from ansible.utils.native_jinja import NativeJinjaText
+@@ -67,7 +67,7 @@ def ansible_eval_concat(nodes):
+ )
+ )
+ )
+- except (TypeError, ValueError, SyntaxError, MemoryError):
++ except (ValueError, SyntaxError, MemoryError):
+ pass
+
+ return out
+@@ -129,7 +129,7 @@ def ansible_native_concat(nodes):
+ # parse the string ourselves without removing leading spaces/tabs.
+ ast.parse(out, mode='eval')
+ )
+- except (TypeError, ValueError, SyntaxError, MemoryError):
++ except (ValueError, SyntaxError, MemoryError):
+ return out
+
+ if isinstance(evaled, string_types):
+--- ansible-core-2.16.5.orig/lib/ansible/template/vars.py
++++ ansible-core-2.16.5/lib/ansible/template/vars.py
+@@ -1,76 +1,128 @@
+ # (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+-# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
++#
++# This file is part of Ansible
++#
++# Ansible is free software: you can redistribute it and/or modify
++# it under the terms of the GNU General Public License as published by
++# the Free Software Foundation, either version 3 of the License, or
++# (at your option) any later version.
++#
++# Ansible is distributed in the hope that it will be useful,
++# but WITHOUT ANY WARRANTY; without even the implied warranty of
++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++# GNU General Public License for more details.
++#
++# You should have received a copy of the GNU General Public License
++# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
++
++# Make coding more python3-ish
++from __future__ import (absolute_import, division, print_function)
++__metaclass__ = type
+
+-from collections import ChainMap
++from collections.abc import Mapping
+
+ from jinja2.utils import missing
+
+ from ansible.errors import AnsibleError, AnsibleUndefinedVariable
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+
+ __all__ = ['AnsibleJ2Vars']
+
+
+-def _process_locals(_l):
+- if _l is None:
+- return {}
+- return {
+- k: v for k, v in _l.items()
+- if v is not missing
+- and k not in {'context', 'environment', 'template'} # NOTE is this really needed?
+- }
++class AnsibleJ2Vars(Mapping):
++ '''
++ Helper class to template all variable content before jinja2 sees it. This is
++ done by hijacking the variable storage that jinja2 uses, and overriding __contains__
++ and __getitem__ to look like a dict. Added bonus is avoiding duplicating the large
++ hashes that inject tends to be.
+
+-
+-class AnsibleJ2Vars(ChainMap):
+- """Helper variable storage class that allows for nested variables templating: `foo: "{{ bar }}"`."""
++ To facilitate using builtin jinja2 things like range, globals are also handled here.
++ '''
+
+ def __init__(self, templar, globals, locals=None):
++ '''
++ Initializes this object with a valid Templar() object, as
++ well as several dictionaries of variables representing
++ different scopes (in jinja2 terminology).
++ '''
++
+ self._templar = templar
+- super().__init__(
+- _process_locals(locals), # first mapping has the highest precedence
+- self._templar.available_variables,
+- globals,
+- )
++ self._globals = globals
++ self._locals = dict()
++ if isinstance(locals, dict):
++ for key, val in locals.items():
++ if val is not missing:
++ if key[:2] == 'l_':
++ self._locals[key[2:]] = val
++ elif key not in ('context', 'environment', 'template'):
++ self._locals[key] = val
++
++ def __contains__(self, k):
++ if k in self._locals:
++ return True
++ if k in self._templar.available_variables:
++ return True
++ if k in self._globals:
++ return True
++ return False
++
++ def __iter__(self):
++ keys = set()
++ keys.update(self._templar.available_variables, self._locals, self._globals)
++ return iter(keys)
++
++ def __len__(self):
++ keys = set()
++ keys.update(self._templar.available_variables, self._locals, self._globals)
++ return len(keys)
+
+ def __getitem__(self, varname):
+- variable = super().__getitem__(varname)
++ if varname in self._locals:
++ return self._locals[varname]
++ if varname in self._templar.available_variables:
++ variable = self._templar.available_variables[varname]
++ elif varname in self._globals:
++ return self._globals[varname]
++ else:
++ raise KeyError("undefined variable: %s" % varname)
+
++ # HostVars is special, return it as-is, as is the special variable
++ # 'vars', which contains the vars structure
+ from ansible.vars.hostvars import HostVars
+- if (varname == "vars" and isinstance(variable, dict)) or isinstance(variable, HostVars) or hasattr(variable, '__UNSAFE__'):
++ if isinstance(variable, dict) and varname == "vars" or isinstance(variable, HostVars) or hasattr(variable, '__UNSAFE__'):
+ return variable
++ else:
++ value = None
++ try:
++ value = self._templar.template(variable)
++ except AnsibleUndefinedVariable as e:
++ # Instead of failing here prematurely, return an Undefined
++ # object which fails only after its first usage allowing us to
++ # do lazy evaluation and passing it into filters/tests that
++ # operate on such objects.
++ return self._templar.environment.undefined(
++ hint=f"{variable}: {e.message}",
++ name=varname,
++ exc=AnsibleUndefinedVariable,
++ )
++ except Exception as e:
++ msg = getattr(e, 'message', None) or to_native(e)
++ raise AnsibleError("An unhandled exception occurred while templating '%s'. "
++ "Error was a %s, original message: %s" % (to_native(variable), type(e), msg))
+
+- try:
+- return self._templar.template(variable)
+- except AnsibleUndefinedVariable as e:
+- # Instead of failing here prematurely, return an Undefined
+- # object which fails only after its first usage allowing us to
+- # do lazy evaluation and passing it into filters/tests that
+- # operate on such objects.
+- return self._templar.environment.undefined(
+- hint=f"{variable}: {e.message}",
+- name=varname,
+- exc=AnsibleUndefinedVariable,
+- )
+- except Exception as e:
+- msg = getattr(e, 'message', None) or to_native(e)
+- raise AnsibleError(
+- f"An unhandled exception occurred while templating '{to_native(variable)}'. "
+- f"Error was a {type(e)}, original message: {msg}"
+- )
++ return value
+
+ def add_locals(self, locals):
+- """If locals are provided, create a copy of self containing those
++ '''
++ If locals are provided, create a copy of self containing those
+ locals in addition to what is already in this variable proxy.
+- """
++ '''
+ if locals is None:
+ return self
+
+- current_locals = self.maps[0]
+- current_globals = self.maps[2]
+-
+ # prior to version 2.9, locals contained all of the vars and not just the current
+ # local vars so this was not necessary for locals to propagate down to nested includes
+- new_locals = current_locals | locals
++ new_locals = self._locals | locals
+
+- return AnsibleJ2Vars(self._templar, current_globals, locals=new_locals)
++ return AnsibleJ2Vars(self._templar, self._globals, locals=new_locals)
+--- ansible-core-2.16.5.orig/lib/ansible/utils/_junit_xml.py
++++ ansible-core-2.16.5/lib/ansible/utils/_junit_xml.py
+@@ -15,7 +15,7 @@ from xml.dom import minidom
+ from xml.etree import ElementTree as ET
+
+
+-@dataclasses.dataclass
++@dataclasses.dataclass # type: ignore[misc] # https://github.com/python/mypy/issues/5374
+ class TestResult(metaclass=abc.ABCMeta):
+ """Base class for the result of a test case."""
+
+--- ansible-core-2.16.5.orig/lib/ansible/utils/cmd_functions.py
++++ ansible-core-2.16.5/lib/ansible/utils/cmd_functions.py
+@@ -24,7 +24,7 @@ import shlex
+ import subprocess
+ import sys
+
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+
+
+ def run_cmd(cmd, live=False, readsize=10):
+--- ansible-core-2.16.5.orig/lib/ansible/utils/collection_loader/_collection_finder.py
++++ ansible-core-2.16.5/lib/ansible/utils/collection_loader/_collection_finder.py
+@@ -7,7 +7,6 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-import itertools
+ import os
+ import os.path
+ import pkgutil
+@@ -40,23 +39,7 @@ except ImportError:
+ reload_module = reload # type: ignore[name-defined] # pylint:disable=undefined-variable
+
+ try:
+- try:
+- # Available on Python >= 3.11
+- # We ignore the import error that will trigger when running mypy with
+- # older Python versions.
+- from importlib.resources.abc import TraversableResources # type: ignore[import]
+- except ImportError:
+- # Used with Python 3.9 and 3.10 only
+- # This member is still available as an alias up until Python 3.14 but
+- # is deprecated as of Python 3.12.
+- from importlib.abc import TraversableResources # deprecated: description='TraversableResources move' python_version='3.10'
+-except ImportError:
+- # Python < 3.9
+- # deprecated: description='TraversableResources fallback' python_version='3.8'
+- TraversableResources = object # type: ignore[assignment,misc]
+-
+-try:
+- from importlib.util import find_spec, spec_from_loader
++ from importlib.util import spec_from_loader
+ except ImportError:
+ pass
+
+@@ -67,11 +50,6 @@ except ImportError:
+ else:
+ HAS_FILE_FINDER = True
+
+-try:
+- import pathlib
+-except ImportError:
+- pass
+-
+ # NB: this supports import sanity test providing a different impl
+ try:
+ from ._collection_meta import _meta_yml_to_dict
+@@ -100,141 +78,6 @@ except AttributeError: # Python 2
+
+
+ PB_EXTENSIONS = ('.yml', '.yaml')
+-SYNTHETIC_PACKAGE_NAME = '<ansible_synthetic_collection_package>'
+-
+-
+-class _AnsibleNSTraversable:
+- """Class that implements the ``importlib.resources.abc.Traversable``
+- interface for the following ``ansible_collections`` namespace packages::
+-
+- * ``ansible_collections``
+- * ``ansible_collections.<namespace>``
+-
+- These namespace packages operate differently from a normal Python
+- namespace package, in that the same namespace can be distributed across
+- multiple directories on the filesystem and still function as a single
+- namespace, such as::
+-
+- * ``/usr/share/ansible/collections/ansible_collections/ansible/posix/``
+- * ``/home/user/.ansible/collections/ansible_collections/ansible/windows/``
+-
+- This class will mimic the behavior of various ``pathlib.Path`` methods,
+- by combining the results of multiple root paths into the output.
+-
+- This class does not do anything to remove duplicate collections from the
+- list, so when traversing either namespace patterns supported by this class,
+- it is possible to have the same collection located in multiple root paths,
+- but precedence rules only use one. When iterating or traversing these
+- package roots, there is the potential to see the same collection in
+- multiple places without indication of which would be used. In such a
+- circumstance, it is best to then call ``importlib.resources.files`` for an
+- individual collection package rather than continuing to traverse from the
+- namespace package.
+-
+- Several methods will raise ``NotImplementedError`` as they do not make
+- sense for these namespace packages.
+- """
+- def __init__(self, *paths):
+- self._paths = [pathlib.Path(p) for p in paths]
+-
+- def __repr__(self):
+- return "_AnsibleNSTraversable('%s')" % "', '".join(map(to_text, self._paths))
+-
+- def iterdir(self):
+- return itertools.chain.from_iterable(p.iterdir() for p in self._paths if p.is_dir())
+-
+- def is_dir(self):
+- return any(p.is_dir() for p in self._paths)
+-
+- def is_file(self):
+- return False
+-
+- def glob(self, pattern):
+- return itertools.chain.from_iterable(p.glob(pattern) for p in self._paths if p.is_dir())
+-
+- def _not_implemented(self, *args, **kwargs):
+- raise NotImplementedError('not usable on namespaces')
+-
+- joinpath = __truediv__ = read_bytes = read_text = _not_implemented
+-
+-
+-class _AnsibleTraversableResources(TraversableResources):
+- """Implements ``importlib.resources.abc.TraversableResources`` for the
+- collection Python loaders.
+-
+- The result of ``files`` will depend on whether a particular collection, or
+- a sub package of a collection was referenced, as opposed to
+- ``ansible_collections`` or a particular namespace. For a collection and
+- its subpackages, a ``pathlib.Path`` instance will be returned, whereas
+- for the higher level namespace packages, ``_AnsibleNSTraversable``
+- will be returned.
+- """
+- def __init__(self, package, loader):
+- self._package = package
+- self._loader = loader
+-
+- def _get_name(self, package):
+- try:
+- # spec
+- return package.name
+- except AttributeError:
+- # module
+- return package.__name__
+-
+- def _get_package(self, package):
+- try:
+- # spec
+- return package.__parent__
+- except AttributeError:
+- # module
+- return package.__package__
+-
+- def _get_path(self, package):
+- try:
+- # spec
+- return package.origin
+- except AttributeError:
+- # module
+- return package.__file__
+-
+- def _is_ansible_ns_package(self, package):
+- origin = getattr(package, 'origin', None)
+- if not origin:
+- return False
+-
+- if origin == SYNTHETIC_PACKAGE_NAME:
+- return True
+-
+- module_filename = os.path.basename(origin)
+- return module_filename in {'__synthetic__', '__init__.py'}
+-
+- def _ensure_package(self, package):
+- if self._is_ansible_ns_package(package):
+- # Short circuit our loaders
+- return
+- if self._get_package(package) != package.__name__:
+- raise TypeError('%r is not a package' % package.__name__)
+-
+- def files(self):
+- package = self._package
+- parts = package.split('.')
+- is_ns = parts[0] == 'ansible_collections' and len(parts) < 3
+-
+- if isinstance(package, string_types):
+- if is_ns:
+- # Don't use ``spec_from_loader`` here, because that will point
+- # to exactly 1 location for a namespace. Use ``find_spec``
+- # to get a list of all locations for the namespace
+- package = find_spec(package)
+- else:
+- package = spec_from_loader(package, self._loader)
+- elif not isinstance(package, ModuleType):
+- raise TypeError('Expected string or module, got %r' % package.__class__.__name__)
+-
+- self._ensure_package(package)
+- if is_ns:
+- return _AnsibleNSTraversable(*package.submodule_search_locations)
+- return pathlib.Path(self._get_path(package)).parent
+
+
+ class _AnsibleCollectionFinder:
+@@ -580,9 +423,6 @@ class _AnsibleCollectionPkgLoaderBase:
+
+ return module_path, has_code, package_path
+
+- def get_resource_reader(self, fullname):
+- return _AnsibleTraversableResources(fullname, self)
+-
+ def exec_module(self, module):
+ # short-circuit redirect; avoid reinitializing existing modules
+ if self._redirect_module:
+@@ -669,7 +509,7 @@ class _AnsibleCollectionPkgLoaderBase:
+ return None
+
+ def _synthetic_filename(self, fullname):
+- return SYNTHETIC_PACKAGE_NAME
++ return '<ansible_synthetic_collection_package>'
+
+ def get_filename(self, fullname):
+ if fullname != self._fullname:
+@@ -908,9 +748,6 @@ class _AnsibleInternalRedirectLoader:
+ if not self._redirect:
+ raise ImportError('not redirected, go ask path_hook')
+
+- def get_resource_reader(self, fullname):
+- return _AnsibleTraversableResources(fullname, self)
+-
+ def exec_module(self, module):
+ # should never see this
+ if not self._redirect:
+--- ansible-core-2.16.5.orig/lib/ansible/utils/display.py
++++ ansible-core-2.16.5/lib/ansible/utils/display.py
+@@ -15,49 +15,34 @@
+ # You should have received a copy of the GNU General Public License
+ # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+-from __future__ import annotations
++from __future__ import (absolute_import, division, print_function)
++__metaclass__ = type
+
+-try:
+- import curses
+-except ImportError:
+- HAS_CURSES = False
+-else:
+- # this will be set to False if curses.setupterm() fails
+- HAS_CURSES = True
+-
+-import collections.abc as c
+-import codecs
+ import ctypes.util
+ import fcntl
+ import getpass
+-import io
+ import logging
+ import os
+ import random
+ import subprocess
+ import sys
+-import termios
+ import textwrap
+ import threading
+ import time
+-import tty
+-import typing as t
+
+-from functools import wraps
+ from struct import unpack, pack
++from termios import TIOCGWINSZ
+
+ from ansible import constants as C
+-from ansible.errors import AnsibleError, AnsibleAssertionError, AnsiblePromptInterrupt, AnsiblePromptNoninteractive
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.errors import AnsibleError, AnsibleAssertionError
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.module_utils.six import text_type
+ from ansible.utils.color import stringc
+ from ansible.utils.multiprocessing import context as multiprocessing_context
+ from ansible.utils.singleton import Singleton
+ from ansible.utils.unsafe_proxy import wrap_var
++from functools import wraps
+
+-if t.TYPE_CHECKING:
+- # avoid circular import at runtime
+- from ansible.executor.task_queue_manager import FinalQueue
+
+ _LIBC = ctypes.cdll.LoadLibrary(ctypes.util.find_library('c'))
+ # Set argtypes, to avoid segfault if the wrong type is provided,
+@@ -67,11 +52,8 @@ _LIBC.wcswidth.argtypes = (ctypes.c_wcha
+ # Max for c_int
+ _MAX_INT = 2 ** (ctypes.sizeof(ctypes.c_int) * 8 - 1) - 1
+
+-MOVE_TO_BOL = b'\r'
+-CLEAR_TO_EOL = b'\x1b[K'
+-
+
+-def get_text_width(text: str) -> int:
++def get_text_width(text):
+ """Function that utilizes ``wcswidth`` or ``wcwidth`` to determine the
+ number of columns used to display a text string.
+
+@@ -122,20 +104,6 @@ def get_text_width(text: str) -> int:
+ return width if width >= 0 else 0
+
+
+-def proxy_display(method):
+-
+- def proxyit(self, *args, **kwargs):
+- if self._final_q:
+- # If _final_q is set, that means we are in a WorkerProcess
+- # and instead of displaying messages directly from the fork
+- # we will proxy them through the queue
+- return self._final_q.send_display(method.__name__, *args, **kwargs)
+- else:
+- return method(self, *args, **kwargs)
+-
+- return proxyit
+-
+-
+ class FilterBlackList(logging.Filter):
+ def __init__(self, blacklist):
+ self.blacklist = [logging.Filter(name) for name in blacklist]
+@@ -196,7 +164,7 @@ b_COW_PATHS = (
+ )
+
+
+-def _synchronize_textiowrapper(tio: t.TextIO, lock: threading.RLock):
++def _synchronize_textiowrapper(tio, lock):
+ # Ensure that a background thread can't hold the internal buffer lock on a file object
+ # during a fork, which causes forked children to hang. We're using display's existing lock for
+ # convenience (and entering the lock before a fork).
+@@ -211,70 +179,15 @@ def _synchronize_textiowrapper(tio: t.Te
+ buffer = tio.buffer
+
+ # monkeypatching the underlying file-like object isn't great, but likely safer than subclassing
+- buffer.write = _wrap_with_lock(buffer.write, lock) # type: ignore[method-assign]
+- buffer.flush = _wrap_with_lock(buffer.flush, lock) # type: ignore[method-assign]
+-
+-
+-def setraw(fd: int, when: int = termios.TCSAFLUSH) -> None:
+- """Put terminal into a raw mode.
+-
+- Copied from ``tty`` from CPython 3.11.0, and modified to not remove OPOST from OFLAG
+-
+- OPOST is kept to prevent an issue with multi line prompts from being corrupted now that display
+- is proxied via the queue from forks. The problem is a race condition, in that we proxy the display
+- over the fork, but before it can be displayed, this plugin will have continued executing, potentially
+- setting stdout and stdin to raw which remove output post processing that commonly converts NL to CRLF
+- """
+- mode = termios.tcgetattr(fd)
+- mode[tty.IFLAG] = mode[tty.IFLAG] & ~(termios.BRKINT | termios.ICRNL | termios.INPCK | termios.ISTRIP | termios.IXON)
+- mode[tty.OFLAG] = mode[tty.OFLAG] & ~(termios.OPOST)
+- mode[tty.CFLAG] = mode[tty.CFLAG] & ~(termios.CSIZE | termios.PARENB)
+- mode[tty.CFLAG] = mode[tty.CFLAG] | termios.CS8
+- mode[tty.LFLAG] = mode[tty.LFLAG] & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG)
+- mode[tty.CC][termios.VMIN] = 1
+- mode[tty.CC][termios.VTIME] = 0
+- termios.tcsetattr(fd, when, mode)
+-
+-
+-def clear_line(stdout: t.BinaryIO) -> None:
+- stdout.write(b'\x1b[%s' % MOVE_TO_BOL)
+- stdout.write(b'\x1b[%s' % CLEAR_TO_EOL)
+-
+-
+-def setup_prompt(stdin_fd: int, stdout_fd: int, seconds: int, echo: bool) -> None:
+- setraw(stdin_fd)
+-
+- # Only set stdout to raw mode if it is a TTY. This is needed when redirecting
+- # stdout to a file since a file cannot be set to raw mode.
+- if os.isatty(stdout_fd):
+- setraw(stdout_fd)
+-
+- if echo:
+- new_settings = termios.tcgetattr(stdin_fd)
+- new_settings[3] = new_settings[3] | termios.ECHO
+- termios.tcsetattr(stdin_fd, termios.TCSANOW, new_settings)
+-
+-
+-def setupterm() -> None:
+- # Nest the try except since curses.error is not available if curses did not import
+- try:
+- curses.setupterm()
+- except (curses.error, TypeError, io.UnsupportedOperation):
+- global HAS_CURSES
+- HAS_CURSES = False
+- else:
+- global MOVE_TO_BOL
+- global CLEAR_TO_EOL
+- # curses.tigetstr() returns None in some circumstances
+- MOVE_TO_BOL = curses.tigetstr('cr') or MOVE_TO_BOL
+- CLEAR_TO_EOL = curses.tigetstr('el') or CLEAR_TO_EOL
++ buffer.write = _wrap_with_lock(buffer.write, lock)
++ buffer.flush = _wrap_with_lock(buffer.flush, lock)
+
+
+ class Display(metaclass=Singleton):
+
+- def __init__(self, verbosity: int = 0) -> None:
++ def __init__(self, verbosity=0):
+
+- self._final_q: FinalQueue | None = None
++ self._final_q = None
+
+ # NB: this lock is used to both prevent intermingled output between threads and to block writes during forks.
+ # Do not change the type of this lock or upgrade to a shared lock (eg multiprocessing.RLock).
+@@ -284,11 +197,11 @@ class Display(metaclass=Singleton):
+ self.verbosity = verbosity
+
+ # list of all deprecation messages to prevent duplicate display
+- self._deprecations: dict[str, int] = {}
+- self._warns: dict[str, int] = {}
+- self._errors: dict[str, int] = {}
++ self._deprecations = {}
++ self._warns = {}
++ self._errors = {}
+
+- self.b_cowsay: bytes | None = None
++ self.b_cowsay = None
+ self.noncow = C.ANSIBLE_COW_SELECTION
+
+ self.set_cowsay_info()
+@@ -299,12 +212,12 @@ class Display(metaclass=Singleton):
+ (out, err) = cmd.communicate()
+ if cmd.returncode:
+ raise Exception
+- self.cows_available: set[str] = {to_text(c) for c in out.split()}
++ self.cows_available = {to_text(c) for c in out.split()} # set comprehension
+ if C.ANSIBLE_COW_ACCEPTLIST and any(C.ANSIBLE_COW_ACCEPTLIST):
+ self.cows_available = set(C.ANSIBLE_COW_ACCEPTLIST).intersection(self.cows_available)
+ except Exception:
+ # could not execute cowsay for some reason
+- self.b_cowsay = None
++ self.b_cowsay = False
+
+ self._set_column_width()
+
+@@ -315,25 +228,13 @@ class Display(metaclass=Singleton):
+ except Exception as ex:
+ self.warning(f"failed to patch stdout/stderr for fork-safety: {ex}")
+
+- codecs.register_error('_replacing_warning_handler', self._replacing_warning_handler)
+ try:
+- sys.stdout.reconfigure(errors='_replacing_warning_handler')
+- sys.stderr.reconfigure(errors='_replacing_warning_handler')
++ sys.stdout.reconfigure(errors='replace')
++ sys.stderr.reconfigure(errors='replace')
+ except Exception as ex:
+- self.warning(f"failed to reconfigure stdout/stderr with custom encoding error handler: {ex}")
+-
+- self.setup_curses = False
++ self.warning(f"failed to reconfigure stdout/stderr with the replace error handler: {ex}")
+
+- def _replacing_warning_handler(self, exception: UnicodeError) -> tuple[str | bytes, int]:
+- # TODO: This should probably be deferred until after the current display is completed
+- # this will require some amount of new functionality
+- self.deprecated(
+- 'Non UTF-8 encoded data replaced with "?" while displaying text to stdout/stderr, this is temporary and will become an error',
+- version='2.18',
+- )
+- return '?', exception.end
+-
+- def set_queue(self, queue: FinalQueue) -> None:
++ def set_queue(self, queue):
+ """Set the _final_q on Display, so that we know to proxy display over the queue
+ instead of directly writing to stdout/stderr from forks
+
+@@ -343,7 +244,7 @@ class Display(metaclass=Singleton):
+ raise RuntimeError('queue cannot be set in parent process')
+ self._final_q = queue
+
+- def set_cowsay_info(self) -> None:
++ def set_cowsay_info(self):
+ if C.ANSIBLE_NOCOWS:
+ return
+
+@@ -354,23 +255,18 @@ class Display(metaclass=Singleton):
+ if os.path.exists(b_cow_path):
+ self.b_cowsay = b_cow_path
+
+- @proxy_display
+- def display(
+- self,
+- msg: str,
+- color: str | None = None,
+- stderr: bool = False,
+- screen_only: bool = False,
+- log_only: bool = False,
+- newline: bool = True,
+- ) -> None:
++ def display(self, msg, color=None, stderr=False, screen_only=False, log_only=False, newline=True):
+ """ Display a message to the user
+
+ Note: msg *must* be a unicode string to prevent UnicodeError tracebacks.
+ """
+
+- if not isinstance(msg, str):
+- raise TypeError(f'Display message must be str, not: {msg.__class__.__name__}')
++ if self._final_q:
++ # If _final_q is set, that means we are in a WorkerProcess
++ # and instead of displaying messages directly from the fork
++ # we will proxy them through the queue
++ return self._final_q.send_display(msg, color=color, stderr=stderr,
++ screen_only=screen_only, log_only=log_only, newline=newline)
+
+ nocolor = msg
+
+@@ -425,32 +321,32 @@ class Display(metaclass=Singleton):
+ # actually log
+ logger.log(lvl, msg2)
+
+- def v(self, msg: str, host: str | None = None) -> None:
++ def v(self, msg, host=None):
+ return self.verbose(msg, host=host, caplevel=0)
+
+- def vv(self, msg: str, host: str | None = None) -> None:
++ def vv(self, msg, host=None):
+ return self.verbose(msg, host=host, caplevel=1)
+
+- def vvv(self, msg: str, host: str | None = None) -> None:
++ def vvv(self, msg, host=None):
+ return self.verbose(msg, host=host, caplevel=2)
+
+- def vvvv(self, msg: str, host: str | None = None) -> None:
++ def vvvv(self, msg, host=None):
+ return self.verbose(msg, host=host, caplevel=3)
+
+- def vvvvv(self, msg: str, host: str | None = None) -> None:
++ def vvvvv(self, msg, host=None):
+ return self.verbose(msg, host=host, caplevel=4)
+
+- def vvvvvv(self, msg: str, host: str | None = None) -> None:
++ def vvvvvv(self, msg, host=None):
+ return self.verbose(msg, host=host, caplevel=5)
+
+- def debug(self, msg: str, host: str | None = None) -> None:
++ def debug(self, msg, host=None):
+ if C.DEFAULT_DEBUG:
+ if host is None:
+ self.display("%6d %0.5f: %s" % (os.getpid(), time.time(), msg), color=C.COLOR_DEBUG)
+ else:
+ self.display("%6d %0.5f [%s]: %s" % (os.getpid(), time.time(), host, msg), color=C.COLOR_DEBUG)
+
+- def verbose(self, msg: str, host: str | None = None, caplevel: int = 2) -> None:
++ def verbose(self, msg, host=None, caplevel=2):
+
+ to_stderr = C.VERBOSE_TO_STDERR
+ if self.verbosity > caplevel:
+@@ -459,14 +355,7 @@ class Display(metaclass=Singleton):
+ else:
+ self.display("<%s> %s" % (host, msg), color=C.COLOR_VERBOSE, stderr=to_stderr)
+
+- def get_deprecation_message(
+- self,
+- msg: str,
+- version: str | None = None,
+- removed: bool = False,
+- date: str | None = None,
+- collection_name: str | None = None,
+- ) -> str:
++ def get_deprecation_message(self, msg, version=None, removed=False, date=None, collection_name=None):
+ ''' used to print out a deprecation message.'''
+ msg = msg.strip()
+ if msg and msg[-1] not in ['!', '?', '.']:
+@@ -501,15 +390,7 @@ class Display(metaclass=Singleton):
+
+ return message_text
+
+- @proxy_display
+- def deprecated(
+- self,
+- msg: str,
+- version: str | None = None,
+- removed: bool = False,
+- date: str | None = None,
+- collection_name: str | None = None,
+- ) -> None:
++ def deprecated(self, msg, version=None, removed=False, date=None, collection_name=None):
+ if not removed and not C.DEPRECATION_WARNINGS:
+ return
+
+@@ -525,8 +406,7 @@ class Display(metaclass=Singleton):
+ self.display(message_text.strip(), color=C.COLOR_DEPRECATE, stderr=True)
+ self._deprecations[message_text] = 1
+
+- @proxy_display
+- def warning(self, msg: str, formatted: bool = False) -> None:
++ def warning(self, msg, formatted=False):
+
+ if not formatted:
+ new_msg = "[WARNING]: %s" % msg
+@@ -539,11 +419,11 @@ class Display(metaclass=Singleton):
+ self.display(new_msg, color=C.COLOR_WARN, stderr=True)
+ self._warns[new_msg] = 1
+
+- def system_warning(self, msg: str) -> None:
++ def system_warning(self, msg):
+ if C.SYSTEM_WARNINGS:
+ self.warning(msg)
+
+- def banner(self, msg: str, color: str | None = None, cows: bool = True) -> None:
++ def banner(self, msg, color=None, cows=True):
+ '''
+ Prints a header-looking line with cowsay or stars with length depending on terminal width (3 minimum)
+ '''
+@@ -566,7 +446,7 @@ class Display(metaclass=Singleton):
+ stars = u"*" * star_len
+ self.display(u"\n%s %s" % (msg, stars), color=color)
+
+- def banner_cowsay(self, msg: str, color: str | None = None) -> None:
++ def banner_cowsay(self, msg, color=None):
+ if u": [" in msg:
+ msg = msg.replace(u"[", u"")
+ if msg.endswith(u"]"):
+@@ -583,7 +463,7 @@ class Display(metaclass=Singleton):
+ (out, err) = cmd.communicate()
+ self.display(u"%s\n" % to_text(out), color=color)
+
+- def error(self, msg: str, wrap_text: bool = True) -> None:
++ def error(self, msg, wrap_text=True):
+ if wrap_text:
+ new_msg = u"\n[ERROR]: %s" % msg
+ wrapped = textwrap.wrap(new_msg, self.columns)
+@@ -595,24 +475,14 @@ class Display(metaclass=Singleton):
+ self._errors[new_msg] = 1
+
+ @staticmethod
+- def prompt(msg: str, private: bool = False) -> str:
++ def prompt(msg, private=False):
+ if private:
+ return getpass.getpass(msg)
+ else:
+ return input(msg)
+
+- def do_var_prompt(
+- self,
+- varname: str,
+- private: bool = True,
+- prompt: str | None = None,
+- encrypt: str | None = None,
+- confirm: bool = False,
+- salt_size: int | None = None,
+- salt: str | None = None,
+- default: str | None = None,
+- unsafe: bool = False,
+- ) -> str:
++ def do_var_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None, unsafe=None):
++
+ result = None
+ if sys.__stdin__.isatty():
+
+@@ -645,7 +515,7 @@ class Display(metaclass=Singleton):
+ if encrypt:
+ # Circular import because encrypt needs a display class
+ from ansible.utils.encrypt import do_encrypt
+- result = do_encrypt(result, encrypt, salt_size=salt_size, salt=salt)
++ result = do_encrypt(result, encrypt, salt_size, salt)
+
+ # handle utf-8 chars
+ result = to_text(result, errors='surrogate_or_strict')
+@@ -654,149 +524,9 @@ class Display(metaclass=Singleton):
+ result = wrap_var(result)
+ return result
+
+- def _set_column_width(self) -> None:
++ def _set_column_width(self):
+ if os.isatty(1):
+- tty_size = unpack('HHHH', fcntl.ioctl(1, termios.TIOCGWINSZ, pack('HHHH', 0, 0, 0, 0)))[1]
++ tty_size = unpack('HHHH', fcntl.ioctl(1, TIOCGWINSZ, pack('HHHH', 0, 0, 0, 0)))[1]
+ else:
+ tty_size = 0
+ self.columns = max(79, tty_size - 1)
+-
+- def prompt_until(
+- self,
+- msg: str,
+- private: bool = False,
+- seconds: int | None = None,
+- interrupt_input: c.Container[bytes] | None = None,
+- complete_input: c.Container[bytes] | None = None,
+- ) -> bytes:
+- if self._final_q:
+- from ansible.executor.process.worker import current_worker
+- self._final_q.send_prompt(
+- worker_id=current_worker.worker_id, prompt=msg, private=private, seconds=seconds,
+- interrupt_input=interrupt_input, complete_input=complete_input
+- )
+- return current_worker.worker_queue.get()
+-
+- if HAS_CURSES and not self.setup_curses:
+- setupterm()
+- self.setup_curses = True
+-
+- if (
+- self._stdin_fd is None
+- or not os.isatty(self._stdin_fd)
+- # Compare the current process group to the process group associated
+- # with terminal of the given file descriptor to determine if the process
+- # is running in the background.
+- or os.getpgrp() != os.tcgetpgrp(self._stdin_fd)
+- ):
+- raise AnsiblePromptNoninteractive('stdin is not interactive')
+-
+- # When seconds/interrupt_input/complete_input are all None, this does mostly the same thing as input/getpass,
+- # but self.prompt may raise a KeyboardInterrupt, which must be caught in the main thread.
+- # If the main thread handled this, it would also need to send a newline to the tty of any hanging pids.
+- # if seconds is None and interrupt_input is None and complete_input is None:
+- # try:
+- # return self.prompt(msg, private=private)
+- # except KeyboardInterrupt:
+- # # can't catch in the results_thread_main daemon thread
+- # raise AnsiblePromptInterrupt('user interrupt')
+-
+- self.display(msg)
+- result = b''
+- with self._lock:
+- original_stdin_settings = termios.tcgetattr(self._stdin_fd)
+- try:
+- setup_prompt(self._stdin_fd, self._stdout_fd, seconds, not private)
+-
+- # flush the buffer to make sure no previous key presses
+- # are read in below
+- termios.tcflush(self._stdin, termios.TCIFLUSH)
+-
+- # read input 1 char at a time until the optional timeout or complete/interrupt condition is met
+- return self._read_non_blocking_stdin(echo=not private, seconds=seconds, interrupt_input=interrupt_input, complete_input=complete_input)
+- finally:
+- # restore the old settings for the duped stdin stdin_fd
+- termios.tcsetattr(self._stdin_fd, termios.TCSADRAIN, original_stdin_settings)
+-
+- def _read_non_blocking_stdin(
+- self,
+- echo: bool = False,
+- seconds: int | None = None,
+- interrupt_input: c.Container[bytes] | None = None,
+- complete_input: c.Container[bytes] | None = None,
+- ) -> bytes:
+- if self._final_q:
+- raise NotImplementedError
+-
+- if seconds is not None:
+- start = time.time()
+- if interrupt_input is None:
+- try:
+- interrupt = termios.tcgetattr(sys.stdin.buffer.fileno())[6][termios.VINTR]
+- except Exception:
+- interrupt = b'\x03' # value for Ctrl+C
+-
+- try:
+- backspace_sequences = [termios.tcgetattr(self._stdin_fd)[6][termios.VERASE]]
+- except Exception:
+- # unsupported/not present, use default
+- backspace_sequences = [b'\x7f', b'\x08']
+-
+- result_string = b''
+- while seconds is None or (time.time() - start < seconds):
+- key_pressed = None
+- try:
+- os.set_blocking(self._stdin_fd, False)
+- while key_pressed is None and (seconds is None or (time.time() - start < seconds)):
+- key_pressed = self._stdin.read(1)
+- # throttle to prevent excess CPU consumption
+- time.sleep(C.DEFAULT_INTERNAL_POLL_INTERVAL)
+- finally:
+- os.set_blocking(self._stdin_fd, True)
+- if key_pressed is None:
+- key_pressed = b''
+-
+- if (interrupt_input is None and key_pressed == interrupt) or (interrupt_input is not None and key_pressed.lower() in interrupt_input):
+- clear_line(self._stdout)
+- raise AnsiblePromptInterrupt('user interrupt')
+- if (complete_input is None and key_pressed in (b'\r', b'\n')) or (complete_input is not None and key_pressed.lower() in complete_input):
+- clear_line(self._stdout)
+- break
+- elif key_pressed in backspace_sequences:
+- clear_line(self._stdout)
+- result_string = result_string[:-1]
+- if echo:
+- self._stdout.write(result_string)
+- self._stdout.flush()
+- else:
+- result_string += key_pressed
+- return result_string
+-
+- @property
+- def _stdin(self) -> t.BinaryIO | None:
+- if self._final_q:
+- raise NotImplementedError
+- try:
+- return sys.stdin.buffer
+- except AttributeError:
+- return None
+-
+- @property
+- def _stdin_fd(self) -> int | None:
+- try:
+- return self._stdin.fileno()
+- except (ValueError, AttributeError):
+- return None
+-
+- @property
+- def _stdout(self) -> t.BinaryIO:
+- if self._final_q:
+- raise NotImplementedError
+- return sys.stdout.buffer
+-
+- @property
+- def _stdout_fd(self) -> int | None:
+- try:
+- return self._stdout.fileno()
+- except (ValueError, AttributeError):
+- return None
+--- ansible-core-2.16.5.orig/lib/ansible/utils/encrypt.py
++++ ansible-core-2.16.5/lib/ansible/utils/encrypt.py
+@@ -4,6 +4,7 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
++import multiprocessing
+ import random
+ import re
+ import string
+@@ -14,7 +15,7 @@ from collections import namedtuple
+ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleAssertionError
+ from ansible.module_utils.six import text_type
+-from ansible.module_utils.common.text.converters import to_text, to_bytes
++from ansible.module_utils._text import to_text, to_bytes
+ from ansible.utils.display import Display
+
+ PASSLIB_E = CRYPT_E = None
+@@ -42,6 +43,8 @@ display = Display()
+
+ __all__ = ['do_encrypt']
+
++_LOCK = multiprocessing.Lock()
++
+ DEFAULT_PASSWORD_LENGTH = 20
+
+
+@@ -102,7 +105,7 @@ class CryptHash(BaseHash):
+ "Python crypt module is deprecated and will be removed from "
+ "Python 3.13. Install the passlib library for continued "
+ "encryption functionality.",
+- version="2.17",
++ version=2.17
+ )
+
+ self.algo_data = self.algorithms[algorithm]
+@@ -125,10 +128,7 @@ class CryptHash(BaseHash):
+ return ret
+
+ def _rounds(self, rounds):
+- if self.algorithm == 'bcrypt':
+- # crypt requires 2 digits for rounds
+- return rounds or self.algo_data.implicit_rounds
+- elif rounds == self.algo_data.implicit_rounds:
++ if rounds == self.algo_data.implicit_rounds:
+ # Passlib does not include the rounds if it is the same as implicit_rounds.
+ # Make crypt lib behave the same, by not explicitly specifying the rounds in that case.
+ return None
+@@ -148,14 +148,12 @@ class CryptHash(BaseHash):
+ saltstring = "$%s" % ident
+
+ if rounds:
+- if self.algorithm == 'bcrypt':
+- saltstring += "$%d" % rounds
+- else:
+- saltstring += "$rounds=%d" % rounds
++ saltstring += "$rounds=%d" % rounds
+
+ saltstring += "$%s" % salt
+
+- # crypt.crypt throws OSError on Python >= 3.9 if it cannot parse saltstring.
++ # crypt.crypt on Python < 3.9 returns None if it cannot parse saltstring
++ # On Python >= 3.9, it throws OSError.
+ try:
+ result = crypt.crypt(secret, saltstring)
+ orig_exc = None
+@@ -163,7 +161,7 @@ class CryptHash(BaseHash):
+ result = None
+ orig_exc = e
+
+- # None as result would be interpreted by some modules (user module)
++ # None as result would be interpreted by the some modules (user module)
+ # as no password at all.
+ if not result:
+ raise AnsibleError(
+@@ -180,7 +178,6 @@ class PasslibHash(BaseHash):
+
+ if not PASSLIB_AVAILABLE:
+ raise AnsibleError("passlib must be installed and usable to hash with '%s'" % algorithm, orig_exc=PASSLIB_E)
+- display.vv("Using passlib to hash input with '%s'" % algorithm)
+
+ try:
+ self.crypt_algo = getattr(passlib.hash, algorithm)
+@@ -267,13 +264,12 @@ class PasslibHash(BaseHash):
+
+
+ def passlib_or_crypt(secret, algorithm, salt=None, salt_size=None, rounds=None, ident=None):
+- display.deprecated("passlib_or_crypt API is deprecated in favor of do_encrypt", version='2.20')
+- return do_encrypt(secret, algorithm, salt=salt, salt_size=salt_size, rounds=rounds, ident=ident)
+-
+-
+-def do_encrypt(result, encrypt, salt_size=None, salt=None, ident=None, rounds=None):
+ if PASSLIB_AVAILABLE:
+- return PasslibHash(encrypt).hash(result, salt=salt, salt_size=salt_size, rounds=rounds, ident=ident)
++ return PasslibHash(algorithm).hash(secret, salt=salt, salt_size=salt_size, rounds=rounds, ident=ident)
+ if HAS_CRYPT:
+- return CryptHash(encrypt).hash(result, salt=salt, salt_size=salt_size, rounds=rounds, ident=ident)
++ return CryptHash(algorithm).hash(secret, salt=salt, salt_size=salt_size, rounds=rounds, ident=ident)
+ raise AnsibleError("Unable to encrypt nor hash, either crypt or passlib must be installed.", orig_exc=CRYPT_E)
++
++
++def do_encrypt(result, encrypt, salt_size=None, salt=None, ident=None):
++ return passlib_or_crypt(result, encrypt, salt_size=salt_size, salt=salt, ident=ident)
+--- ansible-core-2.16.5.orig/lib/ansible/utils/hashing.py
++++ ansible-core-2.16.5/lib/ansible/utils/hashing.py
+@@ -30,7 +30,7 @@ except ImportError:
+ _md5 = None
+
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+
+
+ def secure_hash_s(data, hash_func=sha1):
+--- ansible-core-2.16.5.orig/lib/ansible/utils/jsonrpc.py
++++ ansible-core-2.16.5/lib/ansible/utils/jsonrpc.py
+@@ -8,7 +8,7 @@ import json
+ import pickle
+ import traceback
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.connection import ConnectionError
+ from ansible.module_utils.six import binary_type, text_type
+ from ansible.utils.display import Display
+--- ansible-core-2.16.5.orig/lib/ansible/utils/path.py
++++ ansible-core-2.16.5/lib/ansible/utils/path.py
+@@ -22,7 +22,7 @@ import shutil
+
+ from errno import EEXIST
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+
+
+ __all__ = ['unfrackpath', 'makedirs_safe']
+--- ansible-core-2.16.5.orig/lib/ansible/utils/plugin_docs.py
++++ ansible-core-2.16.5/lib/ansible/utils/plugin_docs.py
+@@ -11,7 +11,7 @@ from ansible import constants as C
+ from ansible.release import __version__ as ansible_version
+ from ansible.errors import AnsibleError, AnsibleParserError, AnsiblePluginNotFound
+ from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.parsing.plugin_docs import read_docstring
+ from ansible.parsing.yaml.loader import AnsibleLoader
+ from ansible.utils.display import Display
+--- ansible-core-2.16.5.orig/lib/ansible/utils/py3compat.py
++++ ansible-core-2.16.5/lib/ansible/utils/py3compat.py
+@@ -17,7 +17,7 @@ import sys
+ from collections.abc import MutableMapping
+
+ from ansible.module_utils.six import PY3
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+
+ __all__ = ('environ',)
+
+--- ansible-core-2.16.5.orig/lib/ansible/utils/shlex.py
++++ ansible-core-2.16.5/lib/ansible/utils/shlex.py
+@@ -20,7 +20,15 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ import shlex
++from ansible.module_utils.six import PY3
++from ansible.module_utils._text import to_bytes, to_text
+
+
+-# shlex.split() wants Unicode (i.e. ``str``) input on Python 3
+-shlex_split = shlex.split
++if PY3:
++ # shlex.split() wants Unicode (i.e. ``str``) input on Python 3
++ shlex_split = shlex.split
++else:
++ # shlex.split() wants bytes (i.e. ``str``) input on Python 2
++ def shlex_split(s, comments=False, posix=True):
++ return map(to_text, shlex.split(to_bytes(s), comments, posix))
++ shlex_split.__doc__ = shlex.split.__doc__
+--- ansible-core-2.16.5.orig/lib/ansible/utils/ssh_functions.py
++++ ansible-core-2.16.5/lib/ansible/utils/ssh_functions.py
+@@ -23,11 +23,8 @@ __metaclass__ = type
+ import subprocess
+
+ from ansible import constants as C
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+ from ansible.module_utils.compat.paramiko import paramiko
+-from ansible.utils.display import Display
+-
+-display = Display()
+
+
+ _HAS_CONTROLPERSIST = {} # type: dict[str, bool]
+@@ -54,11 +51,13 @@ def check_for_controlpersist(ssh_executa
+ return has_cp
+
+
++# TODO: move to 'smart' connection plugin that subclasses to ssh/paramiko as needed.
+ def set_default_transport():
+
+ # deal with 'smart' connection .. one time ..
+ if C.DEFAULT_TRANSPORT == 'smart':
+- display.deprecated("The 'smart' option for connections is deprecated. Set the connection plugin directly instead.", version='2.20')
++ # TODO: check if we can deprecate this as ssh w/o control persist should
++ # not be as common anymore.
+
+ # see if SSH can support ControlPersist if not use paramiko
+ if not check_for_controlpersist('ssh') and paramiko is not None:
+--- ansible-core-2.16.5.orig/lib/ansible/utils/unicode.py
++++ ansible-core-2.16.5/lib/ansible/utils/unicode.py
+@@ -19,7 +19,7 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+
+
+ __all__ = ('unicode_wrap',)
+--- ansible-core-2.16.5.orig/lib/ansible/utils/unsafe_proxy.py
++++ ansible-core-2.16.5/lib/ansible/utils/unsafe_proxy.py
+@@ -53,13 +53,9 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-import sys
+-import types
+-import warnings
+-from sys import intern as _sys_intern
+ from collections.abc import Mapping, Set
+
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.module_utils.common.collections import is_sequence
+ from ansible.utils.native_jinja import NativeJinjaText
+
+@@ -373,20 +369,3 @@ def to_unsafe_text(*args, **kwargs):
+
+ def _is_unsafe(obj):
+ return getattr(obj, '__UNSAFE__', False) is True
+-
+-
+-def _intern(string):
+- """This is a monkey patch for ``sys.intern`` that will strip
+- the unsafe wrapper prior to interning the string.
+-
+- This will not exist in future versions.
+- """
+- if isinstance(string, AnsibleUnsafeText):
+- string = string._strip_unsafe()
+- return _sys_intern(string)
+-
+-
+-if isinstance(sys.intern, types.BuiltinFunctionType):
+- sys.intern = _intern
+-else:
+- warnings.warn("skipped sys.intern patch; appears to have already been patched", RuntimeWarning)
+--- ansible-core-2.16.5.orig/lib/ansible/utils/vars.py
++++ ansible-core-2.16.5/lib/ansible/utils/vars.py
+@@ -29,8 +29,8 @@ from json import dumps
+ from ansible import constants as C
+ from ansible import context
+ from ansible.errors import AnsibleError, AnsibleOptionsError
+-from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils.six import string_types, PY3
++from ansible.module_utils._text import to_native, to_text
+ from ansible.parsing.splitter import parse_kv
+
+
+@@ -109,8 +109,6 @@ def merge_hash(x, y, recursive=True, lis
+ # except performance)
+ if x == {} or x == y:
+ return y.copy()
+- if y == {}:
+- return x
+
+ # in the following we will copy elements from y to x, but
+ # we don't want to modify x, so we create a copy of it
+@@ -183,67 +181,66 @@ def merge_hash(x, y, recursive=True, lis
+
+
+ def load_extra_vars(loader):
++ extra_vars = {}
++ for extra_vars_opt in context.CLIARGS.get('extra_vars', tuple()):
++ data = None
++ extra_vars_opt = to_text(extra_vars_opt, errors='surrogate_or_strict')
++ if extra_vars_opt is None or not extra_vars_opt:
++ continue
+
+- if not getattr(load_extra_vars, 'extra_vars', None):
+- extra_vars = {}
+- for extra_vars_opt in context.CLIARGS.get('extra_vars', tuple()):
+- data = None
+- extra_vars_opt = to_text(extra_vars_opt, errors='surrogate_or_strict')
+- if extra_vars_opt is None or not extra_vars_opt:
+- continue
+-
+- if extra_vars_opt.startswith(u"@"):
+- # Argument is a YAML file (JSON is a subset of YAML)
+- data = loader.load_from_file(extra_vars_opt[1:])
+- elif extra_vars_opt[0] in [u'/', u'.']:
+- raise AnsibleOptionsError("Please prepend extra_vars filename '%s' with '@'" % extra_vars_opt)
+- elif extra_vars_opt[0] in [u'[', u'{']:
+- # Arguments as YAML
+- data = loader.load(extra_vars_opt)
+- else:
+- # Arguments as Key-value
+- data = parse_kv(extra_vars_opt)
+-
+- if isinstance(data, MutableMapping):
+- extra_vars = combine_vars(extra_vars, data)
+- else:
+- raise AnsibleOptionsError("Invalid extra vars data supplied. '%s' could not be made into a dictionary" % extra_vars_opt)
+-
+- setattr(load_extra_vars, 'extra_vars', extra_vars)
++ if extra_vars_opt.startswith(u"@"):
++ # Argument is a YAML file (JSON is a subset of YAML)
++ data = loader.load_from_file(extra_vars_opt[1:])
++ elif extra_vars_opt[0] in [u'/', u'.']:
++ raise AnsibleOptionsError("Please prepend extra_vars filename '%s' with '@'" % extra_vars_opt)
++ elif extra_vars_opt[0] in [u'[', u'{']:
++ # Arguments as YAML
++ data = loader.load(extra_vars_opt)
++ else:
++ # Arguments as Key-value
++ data = parse_kv(extra_vars_opt)
++
++ if isinstance(data, MutableMapping):
++ extra_vars = combine_vars(extra_vars, data)
++ else:
++ raise AnsibleOptionsError("Invalid extra vars data supplied. '%s' could not be made into a dictionary" % extra_vars_opt)
+
+- return load_extra_vars.extra_vars
++ return extra_vars
+
+
+ def load_options_vars(version):
+
+- if not getattr(load_options_vars, 'options_vars', None):
+- if version is None:
+- version = 'Unknown'
+- options_vars = {'ansible_version': version}
+- attrs = {'check': 'check_mode',
+- 'diff': 'diff_mode',
+- 'forks': 'forks',
+- 'inventory': 'inventory_sources',
+- 'skip_tags': 'skip_tags',
+- 'subset': 'limit',
+- 'tags': 'run_tags',
+- 'verbosity': 'verbosity'}
+-
+- for attr, alias in attrs.items():
+- opt = context.CLIARGS.get(attr)
+- if opt is not None:
+- options_vars['ansible_%s' % alias] = opt
+-
+- setattr(load_options_vars, 'options_vars', options_vars)
++ if version is None:
++ version = 'Unknown'
++ options_vars = {'ansible_version': version}
++ attrs = {'check': 'check_mode',
++ 'diff': 'diff_mode',
++ 'forks': 'forks',
++ 'inventory': 'inventory_sources',
++ 'skip_tags': 'skip_tags',
++ 'subset': 'limit',
++ 'tags': 'run_tags',
++ 'verbosity': 'verbosity'}
++
++ for attr, alias in attrs.items():
++ opt = context.CLIARGS.get(attr)
++ if opt is not None:
++ options_vars['ansible_%s' % alias] = opt
+
+- return load_options_vars.options_vars
++ return options_vars
+
+
+ def _isidentifier_PY3(ident):
+ if not isinstance(ident, string_types):
+ return False
+
+- if not ident.isascii():
++ # NOTE Python 3.7 offers str.isascii() so switch over to using it once
++ # we stop supporting 3.5 and 3.6 on the controller
++ try:
++ # Python 2 does not allow non-ascii characters in identifiers so unify
++ # the behavior for Python 3
++ ident.encode('ascii')
++ except UnicodeEncodeError:
+ return False
+
+ if not ident.isidentifier():
+@@ -255,7 +252,26 @@ def _isidentifier_PY3(ident):
+ return True
+
+
+-isidentifier = _isidentifier_PY3
++def _isidentifier_PY2(ident):
++ if not isinstance(ident, string_types):
++ return False
++
++ if not ident:
++ return False
++
++ if C.INVALID_VARIABLE_NAMES.search(ident):
++ return False
++
++ if keyword.iskeyword(ident) or ident in ADDITIONAL_PY2_KEYWORDS:
++ return False
++
++ return True
++
++
++if PY3:
++ isidentifier = _isidentifier_PY3
++else:
++ isidentifier = _isidentifier_PY2
+
+
+ isidentifier.__doc__ = """Determine if string is valid identifier.
+--- ansible-core-2.16.5.orig/lib/ansible/utils/version.py
++++ ansible-core-2.16.5/lib/ansible/utils/version.py
+@@ -9,6 +9,8 @@ import re
+
+ from ansible.module_utils.compat.version import LooseVersion, Version
+
++from ansible.module_utils.six import text_type
++
+
+ # Regular expression taken from
+ # https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
+--- ansible-core-2.16.5.orig/lib/ansible/vars/clean.py
++++ ansible-core-2.16.5/lib/ansible/vars/clean.py
+@@ -13,6 +13,7 @@ from collections.abc import MutableMappi
+ from ansible import constants as C
+ from ansible.errors import AnsibleError
+ from ansible.module_utils import six
++from ansible.module_utils._text import to_text
+ from ansible.plugins.loader import connection_loader
+ from ansible.utils.display import Display
+
+--- ansible-core-2.16.5.orig/lib/ansible/vars/hostvars.py
++++ ansible-core-2.16.5/lib/ansible/vars/hostvars.py
+@@ -137,7 +137,8 @@ class HostVarsVars(Mapping):
+
+ def __getitem__(self, var):
+ templar = Templar(variables=self._vars, loader=self._loader)
+- return templar.template(self._vars[var], fail_on_undefined=False, static_vars=STATIC_VARS)
++ foo = templar.template(self._vars[var], fail_on_undefined=False, static_vars=STATIC_VARS)
++ return foo
+
+ def __contains__(self, var):
+ return (var in self._vars)
+--- ansible-core-2.16.5.orig/lib/ansible/vars/manager.py
++++ ansible-core-2.16.5/lib/ansible/vars/manager.py
+@@ -32,7 +32,7 @@ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleParserError, AnsibleUndefinedVariable, AnsibleFileNotFound, AnsibleAssertionError, AnsibleTemplateError
+ from ansible.inventory.host import Host
+ from ansible.inventory.helpers import sort_groups, get_group_vars
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.six import text_type, string_types
+ from ansible.plugins.loader import lookup_loader
+ from ansible.vars.fact_cache import FactCache
+@@ -139,7 +139,7 @@ class VariableManager:
+ def set_inventory(self, inventory):
+ self._inventory = inventory
+
+- def get_vars(self, play=None, host=None, task=None, include_hostvars=True, include_delegate_to=False, use_cache=True,
++ def get_vars(self, play=None, host=None, task=None, include_hostvars=True, include_delegate_to=True, use_cache=True,
+ _hosts=None, _hosts_all=None, stage='task'):
+ '''
+ Returns the variables, with optional "context" given via the parameters
+@@ -172,6 +172,7 @@ class VariableManager:
+ host=host,
+ task=task,
+ include_hostvars=include_hostvars,
++ include_delegate_to=include_delegate_to,
+ _hosts=_hosts,
+ _hosts_all=_hosts_all,
+ )
+@@ -184,9 +185,6 @@ class VariableManager:
+
+ See notes in the VarsWithSources docstring for caveats and limitations of the source tracking
+ '''
+- if new_data == {}:
+- return data
+-
+ if C.DEFAULT_DEBUG:
+ # Populate var sources dict
+ for key in new_data:
+@@ -199,10 +197,11 @@ class VariableManager:
+ basedirs = [self._loader.get_basedir()]
+
+ if play:
+- # get role defaults (lowest precedence)
+- for role in play.roles:
+- if role.public:
+- all_vars = _combine_and_track(all_vars, role.get_default_vars(), "role '%s' defaults" % role.name)
++ # first we compile any vars specified in defaults/main.yml
++ # for all roles within the specified play
++ for role in play.get_roles():
++ all_vars = _combine_and_track(all_vars, role.get_default_vars(), "role '%s' defaults" % role.name)
++
+ if task:
+ # set basedirs
+ if C.PLAYBOOK_VARS_ROOT == 'all': # should be default
+@@ -216,9 +215,9 @@ class VariableManager:
+ # if we have a task in this context, and that task has a role, make
+ # sure it sees its defaults above any other roles, as we previously
+ # (v1) made sure each task had a copy of its roles default vars
+- # TODO: investigate why we need play or include_role check?
+ if task._role is not None and (play or task.action in C._ACTION_INCLUDE_ROLE):
+- all_vars = _combine_and_track(all_vars, task._role.get_default_vars(dep_chain=task.get_dep_chain()), "role '%s' defaults" % task._role.name)
++ all_vars = _combine_and_track(all_vars, task._role.get_default_vars(dep_chain=task.get_dep_chain()),
++ "role '%s' defaults" % task._role.name)
+
+ if host:
+ # THE 'all' group and the rest of groups for a host, used below
+@@ -384,18 +383,19 @@ class VariableManager:
+ raise AnsibleParserError("Error while reading vars files - please supply a list of file names. "
+ "Got '%s' of type %s" % (vars_files, type(vars_files)))
+
+- # We now merge in all exported vars from all roles in the play (very high precedence)
+- for role in play.roles:
+- if role.public:
+- all_vars = _combine_and_track(all_vars, role.get_vars(include_params=False, only_exports=True), "role '%s' exported vars" % role.name)
++ # By default, we now merge in all vars from all roles in the play,
++ # unless the user has disabled this via a config option
++ if not C.DEFAULT_PRIVATE_ROLE_VARS:
++ for role in play.get_roles():
++ all_vars = _combine_and_track(all_vars, role.get_vars(include_params=False), "role '%s' vars" % role.name)
+
+ # next, we merge in the vars from the role, which will specifically
+ # follow the role dependency chain, and then we merge in the tasks
+ # vars (which will look at parent blocks/task includes)
+ if task:
+ if task._role:
+- all_vars = _combine_and_track(all_vars, task._role.get_vars(task.get_dep_chain(), include_params=False, only_exports=False),
+- "role '%s' all vars" % task._role.name)
++ all_vars = _combine_and_track(all_vars, task._role.get_vars(task.get_dep_chain(), include_params=False),
++ "role '%s' vars" % task._role.name)
+ all_vars = _combine_and_track(all_vars, task.get_vars(), "task vars")
+
+ # next, we merge in the vars cache (include vars) and nonpersistent
+@@ -408,11 +408,12 @@ class VariableManager:
+
+ # next, we merge in role params and task include params
+ if task:
++ if task._role:
++ all_vars = _combine_and_track(all_vars, task._role.get_role_params(task.get_dep_chain()), "role '%s' params" % task._role.name)
++
+ # special case for include tasks, where the include params
+ # may be specified in the vars field for the task, which should
+ # have higher precedence than the vars/np facts above
+- if task._role:
+- all_vars = _combine_and_track(all_vars, task._role.get_role_params(task.get_dep_chain()), "role params")
+ all_vars = _combine_and_track(all_vars, task.get_include_params(), "include params")
+
+ # extra vars
+@@ -443,7 +444,7 @@ class VariableManager:
+ else:
+ return all_vars
+
+- def _get_magic_variables(self, play, host, task, include_hostvars, _hosts=None, _hosts_all=None):
++ def _get_magic_variables(self, play, host, task, include_hostvars, include_delegate_to, _hosts=None, _hosts_all=None):
+ '''
+ Returns a dictionary of so-called "magic" variables in Ansible,
+ which are special variables we set internally for use.
+@@ -455,8 +456,9 @@ class VariableManager:
+ variables['ansible_config_file'] = C.CONFIG_FILE
+
+ if play:
+- # using role_cache as play.roles only has 'public' roles for vars exporting
++ # This is a list of all role names of all dependencies for all roles for this play
+ dependency_role_names = list({d.get_name() for r in play.roles for d in r.get_all_dependencies()})
++ # This is a list of all role names of all roles for this play
+ play_role_names = [r.get_name() for r in play.roles]
+
+ # ansible_role_names includes all role names, dependent or directly referenced by the play
+@@ -468,7 +470,7 @@ class VariableManager:
+ # dependencies that are also explicitly named as roles are included in this list
+ variables['ansible_dependent_role_names'] = dependency_role_names
+
+- # TODO: data tagging!!! DEPRECATED: role_names should be deprecated in favor of ansible_ prefixed ones
++ # DEPRECATED: role_names should be deprecated in favor of ansible_role_names or ansible_play_role_names
+ variables['role_names'] = variables['ansible_play_role_names']
+
+ variables['ansible_play_name'] = play.get_name()
+@@ -514,47 +516,6 @@ class VariableManager:
+
+ return variables
+
+- def get_delegated_vars_and_hostname(self, templar, task, variables):
+- """Get the delegated_vars for an individual task invocation, which may be be in the context
+- of an individual loop iteration.
+-
+- Not used directly be VariableManager, but used primarily within TaskExecutor
+- """
+- delegated_vars = {}
+- delegated_host_name = None
+- if task.delegate_to:
+- delegated_host_name = templar.template(task.delegate_to, fail_on_undefined=False)
+-
+- # no need to do work if omitted
+- if delegated_host_name != self._omit_token:
+-
+- if not delegated_host_name:
+- raise AnsibleError('Empty hostname produced from delegate_to: "%s"' % task.delegate_to)
+-
+- delegated_host = self._inventory.get_host(delegated_host_name)
+- if delegated_host is None:
+- for h in self._inventory.get_hosts(ignore_limits=True, ignore_restrictions=True):
+- # check if the address matches, or if both the delegated_to host
+- # and the current host are in the list of localhost aliases
+- if h.address == delegated_host_name:
+- delegated_host = h
+- break
+- else:
+- delegated_host = Host(name=delegated_host_name)
+-
+- delegated_vars['ansible_delegated_vars'] = {
+- delegated_host_name: self.get_vars(
+- play=task.get_play(),
+- host=delegated_host,
+- task=task,
+- include_delegate_to=False,
+- include_hostvars=True,
+- )
+- }
+- delegated_vars['ansible_delegated_vars'][delegated_host_name]['inventory_hostname'] = variables.get('inventory_hostname')
+-
+- return delegated_vars, delegated_host_name
+-
+ def _get_delegated_vars(self, play, task, existing_variables):
+ # This method has a lot of code copied from ``TaskExecutor._get_loop_items``
+ # if this is failing, and ``TaskExecutor._get_loop_items`` is not
+@@ -566,11 +527,6 @@ class VariableManager:
+ # This "task" is not a Task, so we need to skip it
+ return {}, None
+
+- display.deprecated(
+- 'Getting delegated variables via get_vars is no longer used, and is handled within the TaskExecutor.',
+- version='2.18',
+- )
+-
+ # we unfortunately need to template the delegate_to field here,
+ # as we're fetching vars before post_validate has been called on
+ # the task that has been passed in
+--- ansible-core-2.16.5.orig/lib/ansible/vars/plugins.py
++++ ansible-core-2.16.5/lib/ansible/vars/plugins.py
+@@ -1,48 +1,33 @@
+ # Copyright (c) 2018 Ansible Project
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+-from __future__ import annotations
++# Make coding more python3-ish
++from __future__ import (absolute_import, division, print_function)
++__metaclass__ = type
+
+ import os
+
+-from functools import lru_cache
+-
+ from ansible import constants as C
+ from ansible.errors import AnsibleError
+-from ansible.inventory.group import InventoryObjectType
++from ansible.inventory.host import Host
++from ansible.module_utils._text import to_bytes
+ from ansible.plugins.loader import vars_loader
++from ansible.utils.collection_loader import AnsibleCollectionRef
+ from ansible.utils.display import Display
+ from ansible.utils.vars import combine_vars
+
+ display = Display()
+
+
+-def _prime_vars_loader():
+- # find 3rd party legacy vars plugins once, and look them up by name subsequently
+- list(vars_loader.all(class_only=True))
+- for plugin_name in C.VARIABLE_PLUGINS_ENABLED:
+- if not plugin_name:
+- continue
+- vars_loader.get(plugin_name)
+-
+-
+ def get_plugin_vars(loader, plugin, path, entities):
+
+ data = {}
+ try:
+ data = plugin.get_vars(loader, path, entities)
+ except AttributeError:
+- if hasattr(plugin, 'get_host_vars') or hasattr(plugin, 'get_group_vars'):
+- display.deprecated(
+- f"The vars plugin {plugin.ansible_name} from {plugin._original_path} is relying "
+- "on the deprecated entrypoints 'get_host_vars' and 'get_group_vars'. "
+- "This plugin should be updated to inherit from BaseVarsPlugin and define "
+- "a 'get_vars' method as the main entrypoint instead.",
+- version="2.20",
+- )
+ try:
+ for entity in entities:
+- if entity.base_type is InventoryObjectType.HOST:
++ if isinstance(entity, Host):
+ data |= plugin.get_host_vars(entity.name)
+ else:
+ data |= plugin.get_group_vars(entity.name)
+@@ -54,53 +39,59 @@ def get_plugin_vars(loader, plugin, path
+ return data
+
+
+-# optimized for stateless plugins; non-stateless plugin instances will fall out quickly
+-@lru_cache(maxsize=10)
+-def _plugin_should_run(plugin, stage):
+- # if a plugin-specific setting has not been provided, use the global setting
+- # older/non shipped plugins that don't support the plugin-specific setting should also use the global setting
+- allowed_stages = None
+-
+- try:
+- allowed_stages = plugin.get_option('stage')
+- except (AttributeError, KeyError):
+- pass
+-
+- if allowed_stages:
+- return allowed_stages in ('all', stage)
+-
+- # plugin didn't declare a preference; consult global config
+- config_stage_override = C.RUN_VARS_PLUGINS
+- if config_stage_override == 'demand' and stage == 'inventory':
+- return False
+- elif config_stage_override == 'start' and stage == 'task':
+- return False
+- return True
+-
+-
+ def get_vars_from_path(loader, path, entities, stage):
++
+ data = {}
+- if vars_loader._paths is None:
+- # cache has been reset, reload all()
+- _prime_vars_loader()
+
+- for plugin_name in vars_loader._plugin_instance_cache:
+- if (plugin := vars_loader.get(plugin_name)) is None:
+- continue
++ vars_plugin_list = list(vars_loader.all())
++ for plugin_name in C.VARIABLE_PLUGINS_ENABLED:
++ if AnsibleCollectionRef.is_valid_fqcr(plugin_name):
++ vars_plugin = vars_loader.get(plugin_name)
++ if vars_plugin is None:
++ # Error if there's no play directory or the name is wrong?
++ continue
++ if vars_plugin not in vars_plugin_list:
++ vars_plugin_list.append(vars_plugin)
++
++ for plugin in vars_plugin_list:
++ # legacy plugins always run by default, but they can set REQUIRES_ENABLED=True to opt out.
++
++ builtin_or_legacy = plugin.ansible_name.startswith('ansible.builtin.') or '.' not in plugin.ansible_name
++
++ # builtin is supposed to have REQUIRES_ENABLED=True, the following is for legacy plugins...
++ needs_enabled = not builtin_or_legacy
++ if hasattr(plugin, 'REQUIRES_ENABLED'):
++ needs_enabled = plugin.REQUIRES_ENABLED
++ elif hasattr(plugin, 'REQUIRES_WHITELIST'):
++ display.deprecated("The VarsModule class variable 'REQUIRES_WHITELIST' is deprecated. "
++ "Use 'REQUIRES_ENABLED' instead.", version=2.18)
++ needs_enabled = plugin.REQUIRES_WHITELIST
+
+- collection = '.' in plugin.ansible_name and not plugin.ansible_name.startswith('ansible.builtin.')
++ # A collection plugin was enabled to get to this point because vars_loader.all() does not include collection plugins.
+ # Warn if a collection plugin has REQUIRES_ENABLED because it has no effect.
+- if collection and (hasattr(plugin, 'REQUIRES_ENABLED') or hasattr(plugin, 'REQUIRES_WHITELIST')):
++ if not builtin_or_legacy and (hasattr(plugin, 'REQUIRES_ENABLED') or hasattr(plugin, 'REQUIRES_WHITELIST')):
+ display.warning(
+ "Vars plugins in collections must be enabled to be loaded, REQUIRES_ENABLED is not supported. "
+ "This should be removed from the plugin %s." % plugin.ansible_name
+ )
++ elif builtin_or_legacy and needs_enabled and not plugin.matches_name(C.VARIABLE_PLUGINS_ENABLED):
++ continue
++
++ has_stage = hasattr(plugin, 'get_option') and plugin.has_option('stage')
+
+- if not _plugin_should_run(plugin, stage):
++ # if a plugin-specific setting has not been provided, use the global setting
++ # older/non shipped plugins that don't support the plugin-specific setting should also use the global setting
++ use_global = (has_stage and plugin.get_option('stage') is None) or not has_stage
++
++ if use_global:
++ if C.RUN_VARS_PLUGINS == 'demand' and stage == 'inventory':
++ continue
++ elif C.RUN_VARS_PLUGINS == 'start' and stage == 'task':
++ continue
++ elif has_stage and plugin.get_option('stage') not in ('all', stage):
+ continue
+
+- if (new_vars := get_plugin_vars(loader, plugin, path, entities)) != {}:
+- data = combine_vars(data, new_vars)
++ data = combine_vars(data, get_plugin_vars(loader, plugin, path, entities))
+
+ return data
+
+@@ -114,11 +105,10 @@ def get_vars_from_inventory_sources(load
+ continue
+ if ',' in path and not os.path.exists(path): # skip host lists
+ continue
+- elif not os.path.isdir(path):
++ elif not os.path.isdir(to_bytes(path)):
+ # always pass the directory of the inventory source file
+ path = os.path.dirname(path)
+
+- if (new_vars := get_vars_from_path(loader, path, entities, stage)) != {}:
+- data = combine_vars(data, new_vars)
++ data = combine_vars(data, get_vars_from_path(loader, path, entities, stage))
+
+ return data
+--- ansible-core-2.16.5.orig/pyproject.toml
++++ ansible-core-2.16.5/pyproject.toml
+@@ -1,3 +1,3 @@
+ [build-system]
+-requires = ["setuptools >= 66.1.0"] # minimum setuptools version supporting Python 3.12
++requires = ["setuptools >= 45.2.0"]
+ build-backend = "setuptools.build_meta"
+--- ansible-core-2.16.5.orig/requirements.txt
++++ ansible-core-2.16.5/requirements.txt
+@@ -12,4 +12,4 @@ packaging
+ # NOTE: Ref: https://github.com/sarugaku/resolvelib/issues/69
+ # NOTE: When updating the upper bound, also update the latest version used
+ # NOTE: in the ansible-galaxy-collection test suite.
+-resolvelib >= 0.5.3, < 1.1.0 # dependency resolver used by ansible-galaxy
++resolvelib >= 0.5.3, < 0.9.0 # dependency resolver used by ansible-galaxy
+--- ansible-core-2.16.5.orig/setup.cfg
++++ ansible-core-2.16.5/setup.cfg
+@@ -25,9 +25,9 @@ classifiers =
+ Natural Language :: English
+ Operating System :: POSIX
+ Programming Language :: Python :: 3
++ Programming Language :: Python :: 3.9
+ Programming Language :: Python :: 3.10
+ Programming Language :: Python :: 3.11
+- Programming Language :: Python :: 3.12
+ Programming Language :: Python :: 3 :: Only
+ Topic :: System :: Installation/Setup
+ Topic :: System :: Systems Administration
+@@ -35,7 +35,7 @@ classifiers =
+
+ [options]
+ zip_safe = False
+-python_requires = >=3.10
++python_requires = >=3.9
+ scripts =
+ bin/ansible-test
+
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/MANIFEST.json
++++ ansible-core-2.16.5/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/MANIFEST.json
+@@ -17,7 +17,7 @@
+ "version": "0.1.1231",
+ "readme": "README.md",
+ "license_file": "COPYING",
+- "homepage": ""
++ "homepage": "",
+ },
+ "file_manifest_file": {
+ "format": 1,
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py
+@@ -20,6 +20,7 @@ DOCUMENTATION = '''
+ required: True
+ '''
+
++from ansible.errors import AnsibleParserError
+ from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
+
+
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py
+@@ -32,8 +32,7 @@ RETURN = """
+ version_added: 1.0.0
+ """
+
+-from collections.abc import Sequence
+-
++from ansible.module_utils.common._collections_compat import Sequence
+ from ansible.plugins.lookup import LookupBase
+ from ansible.errors import AnsibleError
+
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/MANIFEST.json
++++ ansible-core-2.16.5/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/MANIFEST.json
+@@ -17,7 +17,7 @@
+ "version": "0.1.1231",
+ "readme": "README.md",
+ "license_file": "COPYING",
+- "homepage": ""
++ "homepage": "",
+ },
+ "file_manifest_file": {
+ "format": 1,
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py
+@@ -19,6 +19,7 @@ DOCUMENTATION = '''
+ required: True
+ '''
+
++from ansible.errors import AnsibleParserError
+ from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
+
+
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py
+@@ -3,17 +3,12 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+
+-DOCUMENTATION = r'''
++DOCUMENTATION = '''
+ ---
+ module: randommodule
+ short_description: A random module
+ description:
+ - A random module.
+- - See O(foo.bar.baz#role:main:foo=bar) for how this is used in the P(foo.bar.baz#role)'s C(main) entrypoint.
+- - See L(the docsite,https://docs.ansible.com/ansible-core/devel/) for more information on ansible-core.
+- - This module is not related to the M(ansible.builtin.copy) module. HORIZONTALLINE You might also be interested
+- in R(ansible_python_interpreter, ansible_python_interpreter).
+- - Sometimes you have M(broken markup) that will result in error messages.
+ author:
+ - Ansible Core Team
+ version_added: 1.0.0
+@@ -23,22 +18,22 @@ deprecated:
+ removed_in: '3.0.0'
+ options:
+ test:
+- description: Some text. Consider not using O(ignore:foo=bar).
++ description: Some text.
+ type: str
+ version_added: 1.2.0
+ sub:
+- description: Suboptions. Contains O(sub.subtest), which can be set to V(123). You can use E(TEST_ENV) to set this.
++ description: Suboptions.
+ type: dict
+ suboptions:
+ subtest:
+- description: A suboption. Not compatible to O(ansible.builtin.copy#module:path=c:\\foo\(1\).txt).
++ description: A suboption.
+ type: int
+ version_added: 1.1.0
+ # The following is the wrong syntax, and should not get processed
+ # by add_collection_to_versions_and_dates()
+ options:
+ subtest2:
+- description: Another suboption. Useful when P(ansible.builtin.shuffle#filter) is used with value V([a,b,\),d\\]).
++ description: Another suboption.
+ type: float
+ version_added: 1.1.0
+ # The following is not supported in modules, and should not get processed
+@@ -70,7 +65,7 @@ seealso:
+ EXAMPLES = '''
+ '''
+
+-RETURN = r'''
++RETURN = '''
+ z_last:
+ description: A last result.
+ type: str
+@@ -80,8 +75,7 @@ z_last:
+ m_middle:
+ description:
+ - This should be in the middle.
+- - Has some more data.
+- - Check out RV(m_middle.suboption) and compare it to RV(a_first=foo) and RV(community.general.foo#lookup:value).
++ - Has some more data
+ type: dict
+ returned: success and 1st of month
+ contains:
+@@ -92,7 +86,7 @@ m_middle:
+ version_added: 1.4.0
+
+ a_first:
+- description: A first result. Use RV(a_first=foo\(bar\\baz\)bam).
++ description: A first result.
+ type: str
+ returned: success
+ '''
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/yolo.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/yolo.yml
+@@ -8,25 +8,6 @@ DOCUMENTATION:
+ description: does not matter
+ type: raw
+ required: true
+- seealso:
+- - module: ansible.builtin.test
+- - module: testns.testcol.fakemodule
+- description: A fake module
+- - plugin: testns.testcol.noop
+- plugin_type: lookup
+- - plugin: testns.testcol.grouped
+- plugin_type: filter
+- description: A grouped filter.
+- - plugin: ansible.builtin.combine
+- plugin_type: filter
+- - plugin: ansible.builtin.file
+- plugin_type: lookup
+- description: Read a file on the controller.
+- - link: https://docs.ansible.com
+- name: Ansible docsite
+- description: See also the Ansible docsite.
+- - ref: foo_bar
+- description: Some foo bar.
+
+ EXAMPLES: |
+ {{ 'anything' is yolo }}
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/MANIFEST.json
++++ ansible-core-2.16.5/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/MANIFEST.json
+@@ -17,7 +17,7 @@
+ "version": "1.2.0",
+ "readme": "README.md",
+ "license_file": "COPYING",
+- "homepage": ""
++ "homepage": "",
+ },
+ "file_manifest_file": {
+ "format": 1,
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-doc/randommodule-text.output
++++ ansible-core-2.16.5/test/integration/targets/ansible-doc/randommodule-text.output
+@@ -1,13 +1,6 @@
+ > TESTNS.TESTCOL.RANDOMMODULE (./collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py)
+
+- A random module. See `foo=bar' (of role foo.bar.baz, main
+- entrypoint) for how this is used in the [foo.bar.baz]'s `main'
+- entrypoint. See the docsite <https://docs.ansible.com/ansible-
+- core/devel/> for more information on ansible-core. This module
+- is not related to the [ansible.builtin.copy] module.
+- ------------- You might also be interested in
+- ansible_python_interpreter. Sometimes you have [broken markup]
+- that will result in error messages.
++ A random module.
+
+ ADDED IN: version 1.0.0 of testns.testcol
+
+@@ -21,8 +14,7 @@ DEPRECATED:
+ OPTIONS (= is mandatory):
+
+ - sub
+- Suboptions. Contains `sub.subtest', which can be set to `123'.
+- You can use `TEST_ENV' to set this.
++ Suboptions.
+ set_via:
+ env:
+ - deprecated:
+@@ -37,8 +29,7 @@ OPTIONS (= is mandatory):
+ OPTIONS:
+
+ - subtest2
+- Another suboption. Useful when [ansible.builtin.shuffle]
+- is used with value `[a,b,),d\]'.
++ Another suboption.
+ default: null
+ type: float
+ added in: version 1.1.0
+@@ -48,15 +39,14 @@ OPTIONS (= is mandatory):
+ SUBOPTIONS:
+
+ - subtest
+- A suboption. Not compatible to `path=c:\foo(1).txt' (of
+- module ansible.builtin.copy).
++ A suboption.
+ default: null
+ type: int
+ added in: version 1.1.0 of testns.testcol
+
+
+ - test
+- Some text. Consider not using `foo=bar'.
++ Some text.
+ default: null
+ type: str
+ added in: version 1.2.0 of testns.testcol
+@@ -103,15 +93,13 @@ EXAMPLES:
+
+ RETURN VALUES:
+ - a_first
+- A first result. Use `a_first=foo(bar\baz)bam'.
++ A first result.
+ returned: success
+ type: str
+
+ - m_middle
+ This should be in the middle.
+- Has some more data.
+- Check out `m_middle.suboption' and compare it to `a_first=foo'
+- and `value' (of lookup plugin community.general.foo).
++ Has some more data
+ returned: success and 1st of month
+ type: dict
+
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-doc/randommodule.output
++++ ansible-core-2.16.5/test/integration/targets/ansible-doc/randommodule.output
+@@ -12,18 +12,14 @@
+ "why": "Test deprecation"
+ },
+ "description": [
+- "A random module.",
+- "See O(foo.bar.baz#role:main:foo=bar) for how this is used in the P(foo.bar.baz#role)'s C(main) entrypoint.",
+- "See L(the docsite,https://docs.ansible.com/ansible-core/devel/) for more information on ansible-core.",
+- "This module is not related to the M(ansible.builtin.copy) module. HORIZONTALLINE You might also be interested in R(ansible_python_interpreter, ansible_python_interpreter).",
+- "Sometimes you have M(broken markup) that will result in error messages."
++ "A random module."
+ ],
+ "filename": "./collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py",
+ "has_action": false,
+ "module": "randommodule",
+ "options": {
+ "sub": {
+- "description": "Suboptions. Contains O(sub.subtest), which can be set to V(123). You can use E(TEST_ENV) to set this.",
++ "description": "Suboptions.",
+ "env": [
+ {
+ "deprecated": {
+@@ -38,14 +34,14 @@
+ ],
+ "options": {
+ "subtest2": {
+- "description": "Another suboption. Useful when P(ansible.builtin.shuffle#filter) is used with value V([a,b,\\),d\\\\]).",
++ "description": "Another suboption.",
+ "type": "float",
+ "version_added": "1.1.0"
+ }
+ },
+ "suboptions": {
+ "subtest": {
+- "description": "A suboption. Not compatible to O(ansible.builtin.copy#module:path=c:\\\\foo\\(1\\).txt).",
++ "description": "A suboption.",
+ "type": "int",
+ "version_added": "1.1.0",
+ "version_added_collection": "testns.testcol"
+@@ -54,7 +50,7 @@
+ "type": "dict"
+ },
+ "test": {
+- "description": "Some text. Consider not using O(ignore:foo=bar).",
++ "description": "Some text.",
+ "type": "str",
+ "version_added": "1.2.0",
+ "version_added_collection": "testns.testcol"
+@@ -107,7 +103,7 @@
+ "metadata": null,
+ "return": {
+ "a_first": {
+- "description": "A first result. Use RV(a_first=foo\\(bar\\\\baz\\)bam).",
++ "description": "A first result.",
+ "returned": "success",
+ "type": "str"
+ },
+@@ -127,8 +123,7 @@
+ },
+ "description": [
+ "This should be in the middle.",
+- "Has some more data.",
+- "Check out RV(m_middle.suboption) and compare it to RV(a_first=foo) and RV(community.general.foo#lookup:value)."
++ "Has some more data"
+ ],
+ "returned": "success and 1st of month",
+ "type": "dict"
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-doc/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/ansible-doc/runme.sh
+@@ -1,74 +1,36 @@
+ #!/usr/bin/env bash
+
+-# always set sane error behaviors, enable execution tracing later if sufficient verbosity requested
+-set -eu
+-
+-verbosity=0
+-
+-# default to silent output for naked grep; -vvv+ will adjust this
+-export GREP_OPTS=-q
+-
+-# shell tracing output is very large from this script; only enable if >= -vvv was passed
+-while getopts :v opt
+-do case "$opt" in
+- v) ((verbosity+=1)) ;;
+- *) ;;
+- esac
+-done
+-
+-if (( verbosity >= 3 ));
+-then
+- set -x;
+- export GREP_OPTS= ;
+-fi
+-
+-echo "running playbook-backed docs tests"
++set -eux
+ ansible-playbook test.yml -i inventory "$@"
+
+ # test keyword docs
+-ansible-doc -t keyword -l | grep $GREP_OPTS 'vars_prompt: list of variables to prompt for.'
+-ansible-doc -t keyword vars_prompt | grep $GREP_OPTS 'description: list of variables to prompt for.'
+-ansible-doc -t keyword asldkfjaslidfhals 2>&1 | grep $GREP_OPTS 'Skipping Invalid keyword'
++ansible-doc -t keyword -l | grep 'vars_prompt: list of variables to prompt for.'
++ansible-doc -t keyword vars_prompt | grep 'description: list of variables to prompt for.'
++ansible-doc -t keyword asldkfjaslidfhals 2>&1 | grep 'Skipping Invalid keyword'
+
+ # collections testing
+ (
+ unset ANSIBLE_PLAYBOOK_DIR
+ cd "$(dirname "$0")"
+
+-
+-echo "test fakemodule docs from collection"
++# test module docs from collection
+ # we use sed to strip the module path from the first line
+ current_out="$(ansible-doc --playbook-dir ./ testns.testcol.fakemodule | sed '1 s/\(^> TESTNS\.TESTCOL\.FAKEMODULE\).*(.*)$/\1/')"
+ expected_out="$(sed '1 s/\(^> TESTNS\.TESTCOL\.FAKEMODULE\).*(.*)$/\1/' fakemodule.output)"
+ test "$current_out" == "$expected_out"
+
+-echo "test randommodule docs from collection"
+ # we use sed to strip the plugin path from the first line, and fix-urls.py to unbreak and replace URLs from stable-X branches
+ current_out="$(ansible-doc --playbook-dir ./ testns.testcol.randommodule | sed '1 s/\(^> TESTNS\.TESTCOL\.RANDOMMODULE\).*(.*)$/\1/' | python fix-urls.py)"
+ expected_out="$(sed '1 s/\(^> TESTNS\.TESTCOL\.RANDOMMODULE\).*(.*)$/\1/' randommodule-text.output)"
+ test "$current_out" == "$expected_out"
+
+-echo "test yolo filter docs from collection"
+-# we use sed to strip the plugin path from the first line, and fix-urls.py to unbreak and replace URLs from stable-X branches
+-current_out="$(ansible-doc --playbook-dir ./ testns.testcol.yolo --type test | sed '1 s/\(^> TESTNS\.TESTCOL\.YOLO\).*(.*)$/\1/' | python fix-urls.py)"
+-expected_out="$(sed '1 s/\(^> TESTNS\.TESTCOL\.YOLO\).*(.*)$/\1/' yolo-text.output)"
+-test "$current_out" == "$expected_out"
+-
+-echo "ensure we do work with valid collection name for list"
+-ansible-doc --list testns.testcol --playbook-dir ./ 2>&1 | grep $GREP_OPTS -v "Invalid collection name"
++# ensure we do work with valid collection name for list
++ansible-doc --list testns.testcol --playbook-dir ./ 2>&1 | grep -v "Invalid collection name"
+
+-echo "ensure we dont break on invalid collection name for list"
+-ansible-doc --list testns.testcol.fakemodule --playbook-dir ./ 2>&1 | grep $GREP_OPTS "Invalid collection name"
++# ensure we dont break on invalid collection name for list
++ansible-doc --list testns.testcol.fakemodule --playbook-dir ./ 2>&1 | grep "Invalid collection name"
+
+-echo "filter list with more than one collection (1/2)"
+-output=$(ansible-doc --list testns.testcol3 testns.testcol4 --playbook-dir ./ 2>&1 | wc -l)
+-test "$output" -eq 2
+-
+-echo "filter list with more than one collection (2/2)"
+-output=$(ansible-doc --list testns.testcol testns.testcol4 --playbook-dir ./ 2>&1 | wc -l)
+-test "$output" -eq 5
+-
+-echo "testing ansible-doc output for various plugin types"
++# test listing diff plugin types from collection
+ for ptype in cache inventory lookup vars filter module
+ do
+ # each plugin type adds 1 from collection
+@@ -88,20 +50,20 @@ do
+ elif [ "${ptype}" == "lookup" ]; then expected_names=("noop");
+ elif [ "${ptype}" == "vars" ]; then expected_names=("noop_vars_plugin"); fi
+ fi
+- echo "testing collection-filtered list for plugin ${ptype}"
++ # ensure we ONLY list from the collection
+ justcol=$(ansible-doc -l -t ${ptype} --playbook-dir ./ testns.testcol|wc -l)
+ test "$justcol" -eq "$expected"
+
+- echo "validate collection plugin name display for plugin ${ptype}"
++ # ensure the right names are displayed
+ list_result=$(ansible-doc -l -t ${ptype} --playbook-dir ./ testns.testcol)
+ metadata_result=$(ansible-doc --metadata-dump --no-fail-on-errors -t ${ptype} --playbook-dir ./ testns.testcol)
+ for name in "${expected_names[@]}"; do
+- echo "${list_result}" | grep $GREP_OPTS "testns.testcol.${name}"
+- echo "${metadata_result}" | grep $GREP_OPTS "testns.testcol.${name}"
++ echo "${list_result}" | grep "testns.testcol.${name}"
++ echo "${metadata_result}" | grep "testns.testcol.${name}"
+ done
+
+- # ensure we get error if passing invalid collection, much less any plugins
+- ansible-doc -l -t ${ptype} bogus.boguscoll 2>&1 | grep $GREP_OPTS "unable to locate collection"
++ # ensure we get error if passinginvalid collection, much less any plugins
++ ansible-doc -l -t ${ptype} testns.testcol 2>&1 | grep "unable to locate collection"
+
+ # TODO: do we want per namespace?
+ # ensure we get 1 plugins when restricting namespace
+@@ -111,28 +73,20 @@ done
+
+ #### test role functionality
+
+-echo "testing role text output"
++# Test role text output
+ # we use sed to strip the role path from the first line
+ current_role_out="$(ansible-doc -t role -r ./roles test_role1 | sed '1 s/\(^> TEST_ROLE1\).*(.*)$/\1/')"
+ expected_role_out="$(sed '1 s/\(^> TEST_ROLE1\).*(.*)$/\1/' fakerole.output)"
+ test "$current_role_out" == "$expected_role_out"
+
+-echo "testing multiple role entrypoints"
+ # Two collection roles are defined, but only 1 has a role arg spec with 2 entry points
+ output=$(ansible-doc -t role -l --playbook-dir . testns.testcol | wc -l)
+ test "$output" -eq 2
+
+-echo "test listing roles with multiple collection filters"
+-# Two collection roles are defined, but only 1 has a role arg spec with 2 entry points
+-output=$(ansible-doc -t role -l --playbook-dir . testns.testcol2 testns.testcol | wc -l)
+-test "$output" -eq 2
+-
+-echo "testing standalone roles"
+ # Include normal roles (no collection filter)
+ output=$(ansible-doc -t role -l --playbook-dir . | wc -l)
+ test "$output" -eq 3
+
+-echo "testing role precedence"
+ # Test that a role in the playbook dir with the same name as a role in the
+ # 'roles' subdir of the playbook dir does not appear (lower precedence).
+ output=$(ansible-doc -t role -l --playbook-dir . | grep -c "test_role1 from roles subdir")
+@@ -140,7 +94,7 @@ test "$output" -eq 1
+ output=$(ansible-doc -t role -l --playbook-dir . | grep -c "test_role1 from playbook dir" || true)
+ test "$output" -eq 0
+
+-echo "testing role entrypoint filter"
++# Test entry point filter
+ current_role_out="$(ansible-doc -t role --playbook-dir . testns.testcol.testrole -e alternate| sed '1 s/\(^> TESTNS\.TESTCOL\.TESTROLE\).*(.*)$/\1/')"
+ expected_role_out="$(sed '1 s/\(^> TESTNS\.TESTCOL\.TESTROLE\).*(.*)$/\1/' fakecollrole.output)"
+ test "$current_role_out" == "$expected_role_out"
+@@ -149,16 +103,10 @@ test "$current_role_out" == "$expected_r
+
+ #### test add_collection_to_versions_and_dates()
+
+-echo "testing json output"
+ current_out="$(ansible-doc --json --playbook-dir ./ testns.testcol.randommodule | sed 's/ *$//' | sed 's/ *"filename": "[^"]*",$//')"
+ expected_out="$(sed 's/ *"filename": "[^"]*",$//' randommodule.output)"
+ test "$current_out" == "$expected_out"
+
+-echo "testing json output 2"
+-current_out="$(ansible-doc --json --playbook-dir ./ testns.testcol.yolo --type test | sed 's/ *$//' | sed 's/ *"filename": "[^"]*",$//')"
+-expected_out="$(sed 's/ *"filename": "[^"]*",$//' yolo.output)"
+-test "$current_out" == "$expected_out"
+-
+ current_out="$(ansible-doc --json --playbook-dir ./ -t cache testns.testcol.notjsonfile | sed 's/ *$//' | sed 's/ *"filename": "[^"]*",$//')"
+ expected_out="$(sed 's/ *"filename": "[^"]*",$//' notjsonfile.output)"
+ test "$current_out" == "$expected_out"
+@@ -171,9 +119,8 @@ current_out="$(ansible-doc --json --play
+ expected_out="$(sed 's/ *"filename": "[^"]*",$//' noop_vars_plugin.output)"
+ test "$current_out" == "$expected_out"
+
+-echo "testing metadata dump"
+ # just ensure it runs
+-ANSIBLE_LIBRARY='./nolibrary' ansible-doc --metadata-dump --playbook-dir /dev/null 1>/dev/null 2>&1
++ANSIBLE_LIBRARY='./nolibrary' ansible-doc --metadata-dump --playbook-dir /dev/null >/dev/null
+
+ # create broken role argument spec
+ mkdir -p broken-docs/collections/ansible_collections/testns/testcol/roles/testrole/meta
+@@ -197,72 +144,71 @@ argument_specs:
+ EOF
+
+ # ensure that --metadata-dump does not fail when --no-fail-on-errors is supplied
+-ANSIBLE_LIBRARY='./nolibrary' ansible-doc --metadata-dump --no-fail-on-errors --playbook-dir broken-docs testns.testcol 1>/dev/null 2>&1
++ANSIBLE_LIBRARY='./nolibrary' ansible-doc --metadata-dump --no-fail-on-errors --playbook-dir broken-docs testns.testcol >/dev/null
+
+ # ensure that --metadata-dump does fail when --no-fail-on-errors is not supplied
+ output=$(ANSIBLE_LIBRARY='./nolibrary' ansible-doc --metadata-dump --playbook-dir broken-docs testns.testcol 2>&1 | grep -c 'ERROR!' || true)
+ test "${output}" -eq 1
+
+-
+-echo "testing legacy plugin listing"
++# ensure we list the 'legacy plugins'
+ [ "$(ansible-doc -M ./library -l ansible.legacy |wc -l)" -gt "0" ]
+
+-echo "testing legacy plugin list via --playbook-dir"
++# playbook dir should work the same
+ [ "$(ansible-doc -l ansible.legacy --playbook-dir ./|wc -l)" -gt "0" ]
+
+-echo "testing undocumented plugin output"
++# see that we show undocumented when missing docs
+ [ "$(ansible-doc -M ./library -l ansible.legacy |grep -c UNDOCUMENTED)" == "6" ]
+
+-echo "testing filtering does not include any 'test_' modules"
++# ensure filtering works and does not include any 'test_' modules
+ [ "$(ansible-doc -M ./library -l ansible.builtin |grep -c test_)" == 0 ]
+ [ "$(ansible-doc --playbook-dir ./ -l ansible.builtin |grep -c test_)" == 0 ]
+
+-echo "testing filtering still shows modules"
++# ensure filtering still shows modules
+ count=$(ANSIBLE_LIBRARY='./nolibrary' ansible-doc -l ansible.builtin |wc -l)
+ [ "${count}" -gt "0" ]
+ [ "$(ansible-doc -M ./library -l ansible.builtin |wc -l)" == "${count}" ]
+ [ "$(ansible-doc --playbook-dir ./ -l ansible.builtin |wc -l)" == "${count}" ]
+
+
+-echo "testing sidecar docs for jinja plugins"
++# produce 'sidecar' docs for test
+ [ "$(ansible-doc -t test --playbook-dir ./ testns.testcol.yolo| wc -l)" -gt "0" ]
+ [ "$(ansible-doc -t filter --playbook-dir ./ donothing| wc -l)" -gt "0" ]
+ [ "$(ansible-doc -t filter --playbook-dir ./ ansible.legacy.donothing| wc -l)" -gt "0" ]
+
+-echo "testing no docs and no sidecar"
+-ansible-doc -t filter --playbook-dir ./ nodocs 2>&1| grep $GREP_OPTS -c 'missing documentation' || true
++# no docs and no sidecar
++ansible-doc -t filter --playbook-dir ./ nodocs 2>&1| grep -c 'missing documentation' || true
+
+-echo "testing sidecar docs for module"
++# produce 'sidecar' docs for module
+ [ "$(ansible-doc -M ./library test_win_module| wc -l)" -gt "0" ]
+ [ "$(ansible-doc --playbook-dir ./ test_win_module| wc -l)" -gt "0" ]
+
+-echo "testing duplicate DOCUMENTATION"
++# test 'double DOCUMENTATION' use
+ [ "$(ansible-doc --playbook-dir ./ double_doc| wc -l)" -gt "0" ]
+
+-echo "testing don't break on module dir"
++# don't break on module dir
+ ansible-doc --list --module-path ./modules > /dev/null
+
+-echo "testing dedupe by fqcn and not base name"
++# ensure we dedupe by fqcn and not base name
+ [ "$(ansible-doc -l -t filter --playbook-dir ./ |grep -c 'b64decode')" -eq "3" ]
+
+-echo "testing no duplicates for plugins that only exist in ansible.builtin when listing ansible.legacy plugins"
++# ensure we don't show duplicates for plugins that only exist in ansible.builtin when listing ansible.legacy plugins
+ [ "$(ansible-doc -l -t filter --playbook-dir ./ |grep -c 'b64encode')" -eq "1" ]
+
+-echo "testing with playbook dir, legacy should override"
+-ansible-doc -t filter split --playbook-dir ./ |grep $GREP_OPTS histerical
++# with playbook dir, legacy should override
++ansible-doc -t filter split --playbook-dir ./ |grep histerical
+
+ pyc_src="$(pwd)/filter_plugins/other.py"
+ pyc_1="$(pwd)/filter_plugins/split.pyc"
+ pyc_2="$(pwd)/library/notaplugin.pyc"
+ trap 'rm -rf "$pyc_1" "$pyc_2"' EXIT
+
+-echo "testing pyc files are not used as adjacent documentation"
++# test pyc files are not used as adjacent documentation
+ python -c "import py_compile; py_compile.compile('$pyc_src', cfile='$pyc_1')"
+-ansible-doc -t filter split --playbook-dir ./ |grep $GREP_OPTS histerical
++ansible-doc -t filter split --playbook-dir ./ |grep histerical
+
+-echo "testing pyc files are not listed as plugins"
++# test pyc files are not listed as plugins
+ python -c "import py_compile; py_compile.compile('$pyc_src', cfile='$pyc_2')"
+ test "$(ansible-doc -l -t module --playbook-dir ./ 2>&1 1>/dev/null |grep -c "notaplugin")" == 0
+
+-echo "testing without playbook dir, builtin should return"
+-ansible-doc -t filter split 2>&1 |grep $GREP_OPTS -v histerical
++# without playbook dir, builtin should return
++ansible-doc -t filter split |grep -v histerical
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection-cli/files/expected.txt
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection-cli/files/expected.txt
+@@ -1,11 +1,6 @@
+ MANIFEST.json
+ FILES.json
+ README.rst
+-GPL
+-LICENSES/
+-LICENSES/MIT.txt
+-.reuse/
+-.reuse/dep5
+ changelogs/
+ docs/
+ playbooks/
+@@ -93,7 +88,6 @@ plugins/test/bar.yml
+ plugins/test/baz.yaml
+ plugins/test/test.py
+ plugins/vars/bar.yml
+-plugins/vars/bar.yml.license
+ plugins/vars/baz.yaml
+ plugins/vars/test.py
+ roles/foo/
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection-cli/files/galaxy.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection-cli/files/galaxy.yml
+@@ -2,7 +2,6 @@ namespace: ns
+ name: col
+ version: 1.0.0
+ readme: README.rst
+-license_file: GPL
+ authors:
+ - Ansible
+ manifest:
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection-cli/files/make_collection_dir.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection-cli/files/make_collection_dir.py
+@@ -5,12 +5,8 @@ paths = [
+ 'ns-col-1.0.0.tar.gz',
+ 'foo.txt',
+ 'README.rst',
+- 'GPL',
+- 'LICENSES/MIT.txt',
+- '.reuse/dep5',
+ 'artifacts/.gitkeep',
+ 'plugins/vars/bar.yml',
+- 'plugins/vars/bar.yml.license',
+ 'plugins/vars/baz.yaml',
+ 'plugins/vars/test.py',
+ 'plugins/vars/docs.md',
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection-scm/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection-scm/tasks/main.yml
+@@ -5,7 +5,7 @@
+
+ - name: Test installing collections from git repositories
+ environment:
+- ANSIBLE_COLLECTIONS_PATH: "{{ galaxy_dir }}/collections"
++ ANSIBLE_COLLECTIONS_PATHS: "{{ galaxy_dir }}/collections"
+ vars:
+ cleanup: True
+ galaxy_dir: "{{ galaxy_dir }}"
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_all.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_all.yml
+@@ -14,8 +14,6 @@
+ command: 'ansible-galaxy collection install {{ artifact_path }} -p {{ alt_install_path }} --no-deps'
+ vars:
+ artifact_path: "{{ galaxy_dir }}/ansible_test-collection_1-1.0.0.tar.gz"
+- environment:
+- ANSIBLE_COLLECTIONS_PATH: ""
+
+ - name: check if the files and folders in build_ignore were respected
+ stat:
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_recursive_scm_dependency.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_recursive_scm_dependency.yml
+@@ -22,12 +22,7 @@
+ lineinfile:
+ path: '{{ scm_path }}/namespace_2/collection_2/galaxy.yml'
+ regexp: '^dependencies'
+- # NOTE: The committish is set to `HEAD` here because Git's default has
+- # NOTE: changed to `main` and it behaves differently in
+- # NOTE: different envs with different Git versions.
+- line: >-
+- dependencies:
+- {'git+file://{{ scm_path }}/namespace_1/.git#collection_1/': 'HEAD'}
++ line: "dependencies: {'git+file://{{ scm_path }}/namespace_1/.git#collection_1/': 'master'}"
+
+ - name: Commit the changes
+ shell: git add ./; git commit -m 'add collection'
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/library/reset_pulp.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/library/reset_pulp.py
+@@ -84,8 +84,7 @@ def invoke_api(module, url, method='GET'
+
+ resp, info = fetch_url(module, url, method=method, data=data, headers=headers)
+ if info['status'] not in status_codes:
+- info['url'] = url
+- module.fail_json(**info)
++ module.fail_json(url=url, **info)
+
+ data = to_text(resp.read())
+ if data:
+@@ -106,7 +105,7 @@ def delete_pulp_distribution(distributio
+
+ def delete_pulp_orphans(module):
+ """ Deletes any orphaned pulp objects. """
+- orphan_uri = module.params['galaxy_ng_server'] + 'pulp/api/v3/orphans/'
++ orphan_uri = module.params['pulp_api'] + '/pulp/api/v3/orphans/'
+ task_info = invoke_api(module, orphan_uri, method='DELETE', status_codes=[202])
+ wait_pulp_task(task_info['task'], module)
+
+@@ -126,39 +125,25 @@ def get_galaxy_namespaces(module):
+ return [n['name'] for n in ns_info['data']]
+
+
+-def get_pulp_distributions(module, distribution):
++def get_pulp_distributions(module):
+ """ Gets a list of all the pulp distributions. """
+- distro_uri = module.params['galaxy_ng_server'] + 'pulp/api/v3/distributions/ansible/ansible/'
+- distro_info = invoke_api(module, distro_uri + '?name=' + distribution)
++ distro_uri = module.params['pulp_api'] + '/pulp/api/v3/distributions/ansible/ansible/'
++ distro_info = invoke_api(module, distro_uri)
+ return [module.params['pulp_api'] + r['pulp_href'] for r in distro_info['results']]
+
+
+-def get_pulp_repositories(module, repository):
++def get_pulp_repositories(module):
+ """ Gets a list of all the pulp repositories. """
+- repo_uri = module.params['galaxy_ng_server'] + 'pulp/api/v3/repositories/ansible/ansible/'
+- repo_info = invoke_api(module, repo_uri + '?name=' + repository)
++ repo_uri = module.params['pulp_api'] + '/pulp/api/v3/repositories/ansible/ansible/'
++ repo_info = invoke_api(module, repo_uri)
+ return [module.params['pulp_api'] + r['pulp_href'] for r in repo_info['results']]
+
+
+-def get_repo_collections(repository, module):
+- collections_uri = module.params['galaxy_ng_server'] + 'v3/plugin/ansible/content/' + repository + '/collections/index/'
+- # status code 500 isn't really expected, an unhandled exception is causing this instead of a 404
+- # See https://issues.redhat.com/browse/AAH-2329
+- info = invoke_api(module, collections_uri + '?limit=100&offset=0', status_codes=[200, 500])
+- if not info:
+- return []
+- return [module.params['pulp_api'] + c['href'] for c in info['data']]
+-
+-
+-def delete_repo_collection(collection, module):
+- task_info = invoke_api(module, collection, method='DELETE', status_codes=[202])
+- wait_pulp_task(task_info['task'], module)
+-
+-
+ def new_galaxy_namespace(name, module):
+ """ Creates a new namespace in Galaxy NG. """
+- ns_uri = module.params['galaxy_ng_server'] + 'v3/namespaces/ '
+- data = {'name': name, 'groups': []}
++ ns_uri = module.params['galaxy_ng_server'] + 'v3/_ui/namespaces/'
++ data = {'name': name, 'groups': [{'name': 'system:partner-engineers', 'object_permissions':
++ ['add_namespace', 'change_namespace', 'upload_to_namespace']}]}
+ ns_info = invoke_api(module, ns_uri, method='POST', data=data, status_codes=[201])
+
+ return ns_info['id']
+@@ -166,17 +151,16 @@ def new_galaxy_namespace(name, module):
+
+ def new_pulp_repository(name, module):
+ """ Creates a new pulp repository. """
+- repo_uri = module.params['galaxy_ng_server'] + 'pulp/api/v3/repositories/ansible/ansible/'
+- # retain_repo_versions to work around https://issues.redhat.com/browse/AAH-2332
+- data = {'name': name, 'retain_repo_versions': '1024'}
++ repo_uri = module.params['pulp_api'] + '/pulp/api/v3/repositories/ansible/ansible/'
++ data = {'name': name}
+ repo_info = invoke_api(module, repo_uri, method='POST', data=data, status_codes=[201])
+
+- return repo_info['pulp_href']
++ return module.params['pulp_api'] + repo_info['pulp_href']
+
+
+ def new_pulp_distribution(name, base_path, repository, module):
+ """ Creates a new pulp distribution for a repository. """
+- distro_uri = module.params['galaxy_ng_server'] + 'pulp/api/v3/distributions/ansible/ansible/'
++ distro_uri = module.params['pulp_api'] + '/pulp/api/v3/distributions/ansible/ansible/'
+ data = {'name': name, 'base_path': base_path, 'repository': repository}
+ task_info = invoke_api(module, distro_uri, method='POST', data=data, status_codes=[202])
+ task_info = wait_pulp_task(task_info['task'], module)
+@@ -210,15 +194,8 @@ def main():
+ )
+ module.params['force_basic_auth'] = True
+
+- # It may be due to the process of cleaning up orphans, but we cannot delete the namespace
+- # while a collection still exists, so this is just a new safety to nuke all collections
+- # first
+- for repository in module.params['repositories']:
+- [delete_repo_collection(c, module) for c in get_repo_collections(repository, module)]
+-
+- for repository in module.params['repositories']:
+- [delete_pulp_distribution(d, module) for d in get_pulp_distributions(module, repository)]
+- [delete_pulp_repository(r, module) for r in get_pulp_repositories(module, repository)]
++ [delete_pulp_distribution(d, module) for d in get_pulp_distributions(module)]
++ [delete_pulp_repository(r, module) for r in get_pulp_repositories(module)]
+ delete_pulp_orphans(module)
+ [delete_galaxy_namespace(n, module) for n in get_galaxy_namespaces(module)]
+
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/library/setup_collections.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/library/setup_collections.py
+@@ -77,7 +77,6 @@ RETURN = '''
+ #
+ '''
+
+-import datetime
+ import os
+ import subprocess
+ import tarfile
+@@ -85,13 +84,13 @@ import tempfile
+ import yaml
+
+ from ansible.module_utils.basic import AnsibleModule
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+ from functools import partial
+ from multiprocessing import dummy as threading
+ from multiprocessing import TimeoutError
+
+
+-COLLECTIONS_BUILD_AND_PUBLISH_TIMEOUT = 180
++COLLECTIONS_BUILD_AND_PUBLISH_TIMEOUT = 120
+
+
+ def publish_collection(module, collection):
+@@ -105,7 +104,6 @@ def publish_collection(module, collectio
+ collection_dir = os.path.join(module.tmpdir, "%s-%s-%s" % (namespace, name, version))
+ b_collection_dir = to_bytes(collection_dir, errors='surrogate_or_strict')
+ os.mkdir(b_collection_dir)
+- os.mkdir(os.path.join(b_collection_dir, b'meta'))
+
+ with open(os.path.join(b_collection_dir, b'README.md'), mode='wb') as fd:
+ fd.write(b"Collection readme")
+@@ -122,8 +120,6 @@ def publish_collection(module, collectio
+ }
+ with open(os.path.join(b_collection_dir, b'galaxy.yml'), mode='wb') as fd:
+ fd.write(to_bytes(yaml.safe_dump(galaxy_meta), errors='surrogate_or_strict'))
+- with open(os.path.join(b_collection_dir, b'meta/runtime.yml'), mode='wb') as fd:
+- fd.write(b'requires_ansible: ">=1.0.0"')
+
+ with tempfile.NamedTemporaryFile(mode='wb') as temp_fd:
+ temp_fd.write(b"data")
+@@ -250,8 +246,7 @@ def run_module():
+ supports_check_mode=False
+ )
+
+- start = datetime.datetime.now()
+- result = dict(changed=True, results=[], start=str(start))
++ result = dict(changed=True, results=[])
+
+ pool = threading.Pool(4)
+ publish_func = partial(publish_collection, module)
+@@ -268,9 +263,7 @@ def run_module():
+ r['build']['rc'] + r['publish']['rc'] for r in result['results']
+ ))
+
+- end = datetime.datetime.now()
+- delta = end - start
+- module.exit_json(failed=failed, end=str(end), delta=str(delta), **result)
++ module.exit_json(failed=failed, **result)
+
+
+ def main():
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/tasks/build.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/tasks/build.yml
+@@ -1,29 +1,4 @@
+ ---
+-- name: create a dangling symbolic link inside collection directory
+- ansible.builtin.file:
+- src: '/non-existent-path/README.md'
+- dest: '{{ galaxy_dir }}/scratch/ansible_test/my_collection/docs/README.md'
+- state: link
+- force: yes
+-
+-- name: build basic collection based on current directory with dangling symlink
+- command: ansible-galaxy collection build {{ galaxy_verbosity }}
+- args:
+- chdir: '{{ galaxy_dir }}/scratch/ansible_test/my_collection'
+- register: fail_symlink_build
+- ignore_errors: yes
+-
+-- name: assert that build fails due to dangling symlink
+- assert:
+- that:
+- - fail_symlink_build.failed
+- - '"Failed to find the target path" in fail_symlink_build.stderr'
+-
+-- name: remove dangling symbolic link
+- ansible.builtin.file:
+- path: '{{ galaxy_dir }}/scratch/ansible_test/my_collection/docs/README.md'
+- state: absent
+-
+ - name: build basic collection based on current directory
+ command: ansible-galaxy collection build {{ galaxy_verbosity }}
+ args:
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/tasks/download.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/tasks/download.yml
+@@ -5,7 +5,7 @@
+ state: directory
+
+ - name: download collection with multiple dependencies with --no-deps
+- command: ansible-galaxy collection download parent_dep.parent_collection:1.0.0 --no-deps -s galaxy_ng {{ galaxy_verbosity }}
++ command: ansible-galaxy collection download parent_dep.parent_collection:1.0.0 --no-deps -s pulp_v2 {{ galaxy_verbosity }}
+ register: download_collection
+ args:
+ chdir: '{{ galaxy_dir }}/download'
+@@ -34,7 +34,7 @@
+ - (download_collection_actual.files[1].path | basename) in ['requirements.yml', 'parent_dep-parent_collection-1.0.0.tar.gz']
+
+ - name: download collection with multiple dependencies
+- command: ansible-galaxy collection download parent_dep.parent_collection:1.0.0 -s galaxy_ng {{ galaxy_verbosity }}
++ command: ansible-galaxy collection download parent_dep.parent_collection:1.0.0 -s pulp_v2 {{ galaxy_verbosity }}
+ register: download_collection
+ args:
+ chdir: '{{ galaxy_dir }}/download'
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/tasks/fail_fast_resolvelib.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/tasks/fail_fast_resolvelib.yml
+@@ -1,5 +1,5 @@
+ # resolvelib>=0.6.0 added an 'incompatibilities' parameter to find_matches
+-# If incompatibilities aren't removed from the viable candidates, this example causes infinite recursion
++# If incompatibilities aren't removed from the viable candidates, this example causes infinite resursion
+ - name: test resolvelib removes incompatibilites in find_matches and errors quickly (prevent infinite recursion)
+ block:
+ - name: create collection dir
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/tasks/init.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/tasks/init.yml
+@@ -5,12 +5,6 @@
+ chdir: '{{ galaxy_dir }}/scratch'
+ register: init_relative
+
+-- name: create required runtime.yml
+- copy:
+- content: |
+- requires_ansible: '>=1.0.0'
+- dest: '{{ galaxy_dir }}/scratch/ansible_test/my_collection/meta/runtime.yml'
+-
+ - name: get result of create default skeleton
+ find:
+ path: '{{ galaxy_dir }}/scratch/ansible_test/my_collection'
+@@ -98,65 +92,6 @@
+ - (init_custom_path_actual.files | map(attribute='path') | list)[2] | basename in ['docs', 'plugins', 'roles', 'meta']
+ - (init_custom_path_actual.files | map(attribute='path') | list)[3] | basename in ['docs', 'plugins', 'roles', 'meta']
+
+-- name: test using a custom skeleton for collection init
+- block:
+- - name: create skeleton directories
+- file:
+- path: "{{ galaxy_dir }}/scratch/skeleton/{{ item }}"
+- state: directory
+- loop:
+- - custom_skeleton
+- - custom_skeleton/plugins
+- - inventory
+-
+- - name: create files
+- file:
+- path: "{{ galaxy_dir }}/scratch/skeleton/{{ item }}"
+- state: touch
+- loop:
+- - inventory/foo.py
+- - galaxy.yml
+-
+- - name: create symlinks
+- file:
+- path: "{{ galaxy_dir }}/scratch/skeleton/{{ item.link }}"
+- src: "{{ galaxy_dir }}/scratch/skeleton/{{ item.source }}"
+- state: link
+- loop:
+- - link: custom_skeleton/plugins/inventory
+- source: inventory
+- - link: custom_skeleton/galaxy.yml
+- source: galaxy.yml
+-
+- - name: initialize a collection using the skeleton
+- command: ansible-galaxy collection init ansible_test.my_collection {{ init_path }} {{ skeleton }}
+- vars:
+- init_path: '--init-path {{ galaxy_dir }}/scratch/skeleton'
+- skeleton: '--collection-skeleton {{ galaxy_dir }}/scratch/skeleton/custom_skeleton'
+-
+- - name: stat expected collection contents
+- stat:
+- path: "{{ galaxy_dir }}/scratch/skeleton/ansible_test/my_collection/{{ item }}"
+- register: stat_result
+- loop:
+- - plugins
+- - plugins/inventory
+- - galaxy.yml
+- - plugins/inventory/foo.py
+-
+- - assert:
+- that:
+- - stat_result.results[0].stat.isdir
+- - stat_result.results[1].stat.islnk
+- - stat_result.results[2].stat.islnk
+- - stat_result.results[3].stat.isreg
+-
+- always:
+- - name: cleanup
+- file:
+- path: "{{ galaxy_dir }}/scratch/skeleton"
+- state: absent
+-
+ - name: create collection for ignored files and folders
+ command: ansible-galaxy collection init ansible_test.ignore
+ args:
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/tasks/install.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/tasks/install.yml
+@@ -165,13 +165,10 @@
+ failed_when:
+ - '"Could not satisfy the following requirements" not in fail_dep_mismatch.stderr'
+ - '" fail_dep2.name:<0.0.5 (dependency of fail_namespace.fail_collection:2.1.2)" not in fail_dep_mismatch.stderr'
+- - 'pre_release_hint not in fail_dep_mismatch.stderr'
+- vars:
+- pre_release_hint: 'Hint: Pre-releases are not installed by default unless the specific version is given. To enable pre-releases, use --pre.'
+
+ - name: Find artifact url for namespace3.name
+ uri:
+- url: '{{ test_api_server }}v3/plugin/ansible/content/primary/collections/index/namespace3/name/versions/1.0.0/'
++ url: '{{ test_server }}{{ vX }}collections/namespace3/name/versions/1.0.0/'
+ user: '{{ pulp_user }}'
+ password: '{{ pulp_password }}'
+ force_basic_auth: true
+@@ -221,7 +218,7 @@
+ state: absent
+
+ - assert:
+- that: expected_error in error
++ that: error == expected_error
+ vars:
+ error: "{{ result.stderr | regex_replace('\\n', ' ') }}"
+ expected_error: >-
+@@ -261,14 +258,12 @@
+ ignore_errors: yes
+ register: result
+
+- - debug:
+- msg: "Actual - {{ error }}"
++ - debug: msg="Actual - {{ error }}"
+
+- - debug:
+- msg: "Expected - {{ expected_error }}"
++ - debug: msg="Expected - {{ expected_error }}"
+
+ - assert:
+- that: expected_error in error
++ that: error == expected_error
+ always:
+ - name: clean up collection skeleton and artifact
+ file:
+@@ -300,7 +295,7 @@
+
+ - name: Find artifact url for namespace4.name
+ uri:
+- url: '{{ test_api_server }}v3/plugin/ansible/content/primary/collections/index/namespace4/name/versions/1.0.0/'
++ url: '{{ test_server }}{{ vX }}collections/namespace4/name/versions/1.0.0/'
+ user: '{{ pulp_user }}'
+ password: '{{ pulp_password }}'
+ force_basic_auth: true
+@@ -330,11 +325,10 @@
+ environment:
+ ANSIBLE_GALAXY_SERVER_LIST: undefined
+
+-# pulp_v2 doesn't require auth
+-- when: v2|default(false)
++- when: not requires_auth
+ block:
+ - name: install a collection with an empty server list - {{ test_id }}
+- command: ansible-galaxy collection install namespace5.name -s '{{ test_server }}' --api-version 2 {{ galaxy_verbosity }}
++ command: ansible-galaxy collection install namespace5.name -s '{{ test_server }}' {{ galaxy_verbosity }}
+ register: install_empty_server_list
+ environment:
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+@@ -577,6 +571,7 @@
+ - namespace8
+ - namespace9
+
++# SIVEL
+ - name: assert invalid signature is not fatal with ansible-galaxy install --ignore-errors - {{ test_id }}
+ assert:
+ that:
+@@ -651,7 +646,6 @@
+ - namespace8
+ - namespace9
+
+-# test --ignore-signature-status-code extends ANSIBLE_GALAXY_IGNORE_SIGNATURE_STATUS_CODES env var
+ - name: install collections with only one valid signature by ignoring the other errors
+ command: ansible-galaxy install -r {{ req_file }} {{ cli_opts }} {{ galaxy_verbosity }} --ignore-signature-status-code FAILURE
+ register: install_req
+@@ -692,60 +686,6 @@
+ vars:
+ install_stderr: "{{ install_req.stderr | regex_replace('\\n', ' ') }}"
+
+-# test --ignore-signature-status-code passed multiple times
+-- name: reinstall collections with only one valid signature by ignoring the other errors
+- command: ansible-galaxy install -r {{ req_file }} {{ cli_opts }} {{ galaxy_verbosity }} {{ ignore_errors }}
+- register: install_req
+- vars:
+- req_file: "{{ galaxy_dir }}/ansible_collections/requirements.yaml"
+- cli_opts: "-s {{ test_name }} --keyring {{ keyring }} --force"
+- keyring: "{{ gpg_homedir }}/pubring.kbx"
+- ignore_errors: "--ignore-signature-status-code BADSIG --ignore-signature-status-code FAILURE"
+- environment:
+- ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+- ANSIBLE_GALAXY_REQUIRED_VALID_SIGNATURE_COUNT: all
+- ANSIBLE_NOCOLOR: True
+- ANSIBLE_FORCE_COLOR: False
+-
+-- name: assert invalid signature is not fatal with ansible-galaxy install - {{ test_name }}
+- assert:
+- that:
+- - install_req is success
+- - '"Installing ''namespace7.name:1.0.0'' to" in install_req.stdout'
+- - '"Signature verification failed for ''namespace7.name'' (return code 1)" not in install_req.stdout'
+- - '"Not installing namespace7.name because GnuPG signature verification failed." not in install_stderr'
+- - '"Installing ''namespace8.name:1.0.0'' to" in install_req.stdout'
+- - '"Installing ''namespace9.name:1.0.0'' to" in install_req.stdout'
+- vars:
+- install_stderr: "{{ install_req.stderr | regex_replace('\\n', ' ') }}"
+-
+-# test --ignore-signature-status-code passed once with a list
+-- name: reinstall collections with only one valid signature by ignoring the other errors
+- command: ansible-galaxy install -r {{ req_file }} {{ cli_opts }} {{ galaxy_verbosity }} {{ ignore_errors }}
+- register: install_req
+- vars:
+- req_file: "{{ galaxy_dir }}/ansible_collections/requirements.yaml"
+- cli_opts: "-s {{ test_name }} --keyring {{ keyring }} --force"
+- keyring: "{{ gpg_homedir }}/pubring.kbx"
+- ignore_errors: "--ignore-signature-status-codes BADSIG FAILURE"
+- environment:
+- ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+- ANSIBLE_GALAXY_REQUIRED_VALID_SIGNATURE_COUNT: all
+- ANSIBLE_NOCOLOR: True
+- ANSIBLE_FORCE_COLOR: False
+-
+-- name: assert invalid signature is not fatal with ansible-galaxy install - {{ test_name }}
+- assert:
+- that:
+- - install_req is success
+- - '"Installing ''namespace7.name:1.0.0'' to" in install_req.stdout'
+- - '"Signature verification failed for ''namespace7.name'' (return code 1)" not in install_req.stdout'
+- - '"Not installing namespace7.name because GnuPG signature verification failed." not in install_stderr'
+- - '"Installing ''namespace8.name:1.0.0'' to" in install_req.stdout'
+- - '"Installing ''namespace9.name:1.0.0'' to" in install_req.stdout'
+- vars:
+- install_stderr: "{{ install_req.stderr | regex_replace('\\n', ' ') }}"
+-
+ - name: clean up collections from last test
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections/{{ collection }}/name'
+@@ -757,45 +697,44 @@
+ - namespace8
+ - namespace9
+
+-- when: not v2|default(false)
+- block:
+- - name: install cache.cache at the current latest version
+- command: ansible-galaxy collection install cache.cache -s '{{ test_name }}' -vvv
+- environment:
+- ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+-
+- - set_fact:
+- cache_version_build: '{{ (cache_version_build | int) + 1 }}'
+-
+- - name: publish update for cache.cache test
+- setup_collections:
+- server: galaxy_ng
+- collections:
+- - namespace: cache
+- name: cache
+- version: 1.0.{{ cache_version_build }}
+-
+- - name: make sure the cache version list is ignored on a collection version change - {{ test_id }}
+- command: ansible-galaxy collection install cache.cache -s '{{ test_name }}' --force -vvv
+- register: install_cached_update
+- environment:
+- ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+-
+- - name: get result of cache version list is ignored on a collection version change - {{ test_id }}
+- slurp:
+- path: '{{ galaxy_dir }}/ansible_collections/cache/cache/MANIFEST.json'
+- register: install_cached_update_actual
+-
+- - name: assert cache version list is ignored on a collection version change - {{ test_id }}
+- assert:
+- that:
+- - '"Installing ''cache.cache:1.0.{{ cache_version_build }}'' to" in install_cached_update.stdout'
+- - (install_cached_update_actual.content | b64decode | from_json).collection_info.version == '1.0.' ~ cache_version_build
++# Uncomment once pulp container is at pulp>=0.5.0
++#- name: install cache.cache at the current latest version
++# command: ansible-galaxy collection install cache.cache -s '{{ test_name }}' -vvv
++# environment:
++# ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
++#
++#- set_fact:
++# cache_version_build: '{{ (cache_version_build | int) + 1 }}'
++#
++#- name: publish update for cache.cache test
++# setup_collections:
++# server: galaxy_ng
++# collections:
++# - namespace: cache
++# name: cache
++# version: 1.0.{{ cache_version_build }}
++#
++#- name: make sure the cache version list is ignored on a collection version change - {{ test_id }}
++# command: ansible-galaxy collection install cache.cache -s '{{ test_name }}' --force -vvv
++# register: install_cached_update
++# environment:
++# ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
++#
++#- name: get result of cache version list is ignored on a collection version change - {{ test_id }}
++# slurp:
++# path: '{{ galaxy_dir }}/ansible_collections/cache/cache/MANIFEST.json'
++# register: install_cached_update_actual
++#
++#- name: assert cache version list is ignored on a collection version change - {{ test_id }}
++# assert:
++# that:
++# - '"Installing ''cache.cache:1.0.{{ cache_version_build }}'' to" in install_cached_update.stdout'
++# - (install_cached_update_actual.content | b64decode | from_json).collection_info.version == '1.0.' ~ cache_version_build
+
+ - name: install collection with symlink - {{ test_id }}
+ command: ansible-galaxy collection install symlink.symlink -s '{{ test_name }}' {{ galaxy_verbosity }}
+ environment:
+- ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
++ ANSIBLE_COLLECTIONS_PATHS: '{{ galaxy_dir }}/ansible_collections'
+ register: install_symlink
+
+ - find:
+@@ -833,56 +772,6 @@
+ - install_symlink_actual.results[5].stat.islnk
+ - install_symlink_actual.results[5].stat.lnk_target == '../REÅDMÈ.md'
+
+-
+-# Testing an install from source to check that symlinks to directories
+-# are preserved (see issue https://github.com/ansible/ansible/issues/78442)
+-- name: symlink_dirs collection install from source test
+- block:
+-
+- - name: create symlink_dirs collection
+- command: ansible-galaxy collection init symlink_dirs.symlink_dirs --init-path "{{ galaxy_dir }}/scratch"
+-
+- - name: create directory in collection
+- file:
+- path: "{{ galaxy_dir }}/scratch/symlink_dirs/symlink_dirs/folderA"
+- state: directory
+-
+- - name: create symlink to folderA
+- file:
+- dest: "{{ galaxy_dir }}/scratch/symlink_dirs/symlink_dirs/folderB"
+- src: ./folderA
+- state: link
+- force: yes
+-
+- - name: install symlink_dirs collection from source
+- command: ansible-galaxy collection install {{ galaxy_dir }}/scratch/symlink_dirs/symlink_dirs/
+- environment:
+- ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
+- register: install_symlink_dirs
+-
+- - name: get result of install collection with symlink_dirs - {{ test_id }}
+- stat:
+- path: '{{ galaxy_dir }}/ansible_collections/symlink_dirs/symlink_dirs/{{ path }}'
+- register: install_symlink_dirs_actual
+- loop_control:
+- loop_var: path
+- loop:
+- - folderA
+- - folderB
+-
+- - name: assert install collection with symlink_dirs - {{ test_id }}
+- assert:
+- that:
+- - '"Installing ''symlink_dirs.symlink_dirs:1.0.0'' to" in install_symlink_dirs.stdout'
+- - install_symlink_dirs_actual.results[0].stat.isdir
+- - install_symlink_dirs_actual.results[1].stat.islnk
+- - install_symlink_dirs_actual.results[1].stat.lnk_target == './folderA'
+- always:
+- - name: clean up symlink_dirs collection directory
+- file:
+- path: "{{ galaxy_dir }}/scratch/symlink_dirs"
+- state: absent
+-
+ - name: remove install directory for the next test because parent_dep.parent_collection was installed - {{ test_id }}
+ file:
+ path: '{{ galaxy_dir }}/ansible_collections'
+@@ -891,7 +780,7 @@
+ - name: install collection and dep compatible with multiple requirements - {{ test_id }}
+ command: ansible-galaxy collection install parent_dep.parent_collection parent_dep2.parent_collection
+ environment:
+- ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
++ ANSIBLE_COLLECTIONS_PATHS: '{{ galaxy_dir }}/ansible_collections'
+ register: install_req
+
+ - name: assert install collections with ansible-galaxy install - {{ test_id }}
+@@ -913,7 +802,7 @@
+ - name: install a collection to the same installation directory - {{ test_id }}
+ command: ansible-galaxy collection install namespace1.name1
+ environment:
+- ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
++ ANSIBLE_COLLECTIONS_PATHS: '{{ galaxy_dir }}/ansible_collections'
+ register: install_req
+
+ - name: assert installed collections with ansible-galaxy install - {{ test_id }}
+@@ -1120,7 +1009,7 @@
+ args:
+ chdir: '{{ galaxy_dir }}/scratch'
+ environment:
+- ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections'
++ ANSIBLE_COLLECTIONS_PATHS: '{{ galaxy_dir }}/ansible_collections'
+ register: install_concrete_pre
+
+ - name: get result of install collections with concrete pre-release dep - {{ test_id }}
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/tasks/install_offline.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/tasks/install_offline.yml
+@@ -25,14 +25,6 @@
+ regexp: "^dependencies:*"
+ line: "dependencies: {'ns.coll2': '>=1.0.0'}"
+
+- - name: create required runtime.yml
+- copy:
+- dest: "{{ galaxy_dir }}/offline/setup/ns/{{ item }}/meta/runtime.yml"
+- content: "requires_ansible: '>=1.0.0'"
+- loop:
+- - coll1
+- - coll2
+-
+ - name: build both collections
+ command: ansible-galaxy collection build {{ init_dir }}/ns/{{ item }}
+ args:
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/tasks/list.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/tasks/list.yml
+@@ -1,4 +1,4 @@
+-- name: initialize dev collection structure
++- name: initialize collection structure
+ command: ansible-galaxy collection init {{ item }} --init-path "{{ galaxy_dir }}/dev/ansible_collections" {{ galaxy_verbosity }}
+ loop:
+ - 'dev.collection1'
+@@ -8,13 +8,6 @@
+ - 'dev.collection5'
+ - 'dev.collection6'
+
+-- name: initialize prod collection structure
+- command: ansible-galaxy collection init {{ item }} --init-path "{{ galaxy_dir }}/prod/ansible_collections" {{ galaxy_verbosity }}
+- loop:
+- - 'prod.collection1'
+- - 'prod.collection2'
+- - 'prod.collection3'
+-
+ - name: replace the default version of the collections
+ lineinfile:
+ path: "{{ galaxy_dir }}/dev/ansible_collections/dev/{{ item.name }}/galaxy.yml"
+@@ -60,13 +53,13 @@
+
+ - assert:
+ that:
+- - "'dev.collection1 *' in list_result.stdout"
++ - "'dev.collection1 *' in list_result.stdout"
+ # Note the version displayed is the 'placeholder' string rather than "*" since it is not falsey
+- - "'dev.collection2 placeholder' in list_result.stdout"
+- - "'dev.collection3 *' in list_result.stdout"
+- - "'dev.collection4 *' in list_result.stdout"
+- - "'dev.collection5 *' in list_result.stdout"
+- - "'dev.collection6 *' in list_result.stdout"
++ - "'dev.collection2 placeholder' in list_result.stdout"
++ - "'dev.collection3 *' in list_result.stdout"
++ - "'dev.collection4 *' in list_result.stdout"
++ - "'dev.collection5 *' in list_result.stdout"
++ - "'dev.collection6 *' in list_result.stdout"
+
+ - name: list collections in human format
+ command: ansible-galaxy collection list --format human
+@@ -76,12 +69,12 @@
+
+ - assert:
+ that:
+- - "'dev.collection1 *' in list_result_human.stdout"
++ - "'dev.collection1 *' in list_result_human.stdout"
+ # Note the version displayed is the 'placeholder' string rather than "*" since it is not falsey
+- - "'dev.collection2 placeholder' in list_result_human.stdout"
+- - "'dev.collection3 *' in list_result_human.stdout"
+- - "'dev.collection5 *' in list_result.stdout"
+- - "'dev.collection6 *' in list_result.stdout"
++ - "'dev.collection2 placeholder' in list_result_human.stdout"
++ - "'dev.collection3 *' in list_result_human.stdout"
++ - "'dev.collection5 *' in list_result.stdout"
++ - "'dev.collection6 *' in list_result.stdout"
+
+ - name: list collections in yaml format
+ command: ansible-galaxy collection list --format yaml
+@@ -91,12 +84,6 @@
+
+ - assert:
+ that:
+- - yaml_result[galaxy_dir ~ '/dev/ansible_collections'] != yaml_result[galaxy_dir ~ '/prod/ansible_collections']
+- vars:
+- yaml_result: '{{ list_result_yaml.stdout | from_yaml }}'
+-
+-- assert:
+- that:
+ - "item.value | length == 6"
+ - "item.value['dev.collection1'].version == '*'"
+ - "item.value['dev.collection2'].version == 'placeholder'"
+@@ -104,7 +91,6 @@
+ - "item.value['dev.collection5'].version == '*'"
+ - "item.value['dev.collection6'].version == '*'"
+ with_dict: "{{ list_result_yaml.stdout | from_yaml }}"
+- when: "'dev' in item.key"
+
+ - name: list collections in json format
+ command: ansible-galaxy collection list --format json
+@@ -121,7 +107,6 @@
+ - "item.value['dev.collection5'].version == '*'"
+ - "item.value['dev.collection6'].version == '*'"
+ with_dict: "{{ list_result_json.stdout | from_json }}"
+- when: "'dev' in item.key"
+
+ - name: list single collection in json format
+ command: "ansible-galaxy collection list {{ item.key }} --format json"
+@@ -152,7 +137,7 @@
+ register: list_result_error
+ ignore_errors: True
+ environment:
+- ANSIBLE_COLLECTIONS_PATH: "i_dont_exist"
++ ANSIBLE_COLLECTIONS_PATH: ""
+
+ - assert:
+ that:
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/tasks/main.yml
+@@ -72,12 +72,13 @@
+ vars:
+ test_name: '{{ item.name }}'
+ test_server: '{{ item.server }}'
+- test_api_server: '{{ item.api_server|default(item.server) }}'
++ vX: '{{ "v3/" if item.v3|default(false) else "v2/" }}'
+ loop:
+ - name: pulp_v2
+- api_server: '{{ galaxy_ng_server }}'
+- server: '{{ pulp_server }}primary/api/'
+- v2: true
++ server: '{{ pulp_server }}published/api/'
++ - name: pulp_v3
++ server: '{{ pulp_server }}published/api/'
++ v3: true
+ - name: galaxy_ng
+ server: '{{ galaxy_ng_server }}'
+ v3: true
+@@ -107,9 +108,8 @@
+ test_id: '{{ item.name }}'
+ test_name: '{{ item.name }}'
+ test_server: '{{ item.server }}'
+- test_api_server: '{{ item.api_server|default(item.server) }}'
++ vX: '{{ "v3/" if item.v3|default(false) else "v2/" }}'
+ requires_auth: '{{ item.requires_auth|default(false) }}'
+- v2: '{{ item.v2|default(false) }}'
+ args:
+ apply:
+ environment:
+@@ -120,9 +120,10 @@
+ v3: true
+ requires_auth: true
+ - name: pulp_v2
+- server: '{{ pulp_server }}primary/api/'
+- api_server: '{{ galaxy_ng_server }}'
+- v2: true
++ server: '{{ pulp_server }}published/api/'
++ - name: pulp_v3
++ server: '{{ pulp_server }}published/api/'
++ v3: true
+
+ - name: test installing and downloading collections with the range of supported resolvelib versions
+ include_tasks: supported_resolvelib.yml
+@@ -134,17 +135,6 @@
+ loop_control:
+ loop_var: resolvelib_version
+
+-- name: test choosing pinned pre-releases anywhere in the dependency tree
+- # This is a regression test for the case when the end-user does not
+- # explicitly allow installing pre-release collection versions, but their
+- # precise pins are still selected if met among the dependencies, either
+- # direct or transitive.
+- include_tasks: pinned_pre_releases_in_deptree.yml
+-
+-- name: test installing prereleases via scm direct requests
+- # In this test suite because the bug relies on the dep having versions on a Galaxy server
+- include_tasks: virtual_direct_requests.yml
+-
+ - name: publish collection with a dep on another server
+ setup_collections:
+ server: secondary
+@@ -186,13 +176,13 @@
+ in install_cross_dep.stdout
+ # pulp_v2 is highest in the list so it will find it there first
+ - >-
+- "'parent_dep.parent_collection:1.0.0' obtained from server galaxy_ng"
++ "'parent_dep.parent_collection:1.0.0' obtained from server pulp_v2"
+ in install_cross_dep.stdout
+ - >-
+- "'child_dep.child_collection:0.9.9' obtained from server galaxy_ng"
++ "'child_dep.child_collection:0.9.9' obtained from server pulp_v2"
+ in install_cross_dep.stdout
+ - >-
+- "'child_dep.child_dep2:1.2.2' obtained from server galaxy_ng"
++ "'child_dep.child_dep2:1.2.2' obtained from server pulp_v2"
+ in install_cross_dep.stdout
+ - (install_cross_dep_actual.results[0].content | b64decode | from_json).collection_info.version == '1.0.0'
+ - (install_cross_dep_actual.results[1].content | b64decode | from_json).collection_info.version == '1.0.0'
+@@ -214,9 +204,10 @@
+ ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}'
+ ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg'
+ vars:
+- test_api_fallback: 'galaxy_ng'
+- test_api_fallback_versions: 'v3, pulp-v3, v1'
+- test_name: 'pulp_v2'
++ test_api_fallback: 'pulp_v2'
++ test_api_fallback_versions: 'v1, v2'
++ test_name: 'galaxy_ng'
++ test_server: '{{ galaxy_ng_server }}'
+
+ - name: run ansible-galaxy collection list tests
+ include_tasks: list.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/tasks/publish.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/tasks/publish.yml
+@@ -5,12 +5,9 @@
+ chdir: '{{ galaxy_dir }}'
+ register: publish_collection
+
+-- name: ensure we can download the published collection - {{ test_name }}
+- command: ansible-galaxy collection install -s {{ test_name }} -p "{{ remote_tmp_dir }}/publish/{{ test_name }}" ansible_test.my_collection==1.0.0 {{ galaxy_verbosity }}
+-
+ - name: get result of publish collection - {{ test_name }}
+ uri:
+- url: '{{ test_api_server }}v3/plugin/ansible/content/primary/collections/index/ansible_test/my_collection/versions/1.0.0/'
++ url: '{{ test_server }}{{ vX }}collections/ansible_test/my_collection/versions/1.0.0/'
+ return_content: yes
+ user: '{{ pulp_user }}'
+ password: '{{ pulp_password }}'
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/tasks/supported_resolvelib.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/tasks/supported_resolvelib.yml
+@@ -20,11 +20,11 @@
+
+ - include_tasks: install.yml
+ vars:
+- test_name: galaxy_ng
++ test_name: pulp_v3
+ test_id: '{{ test_name }} (resolvelib {{ resolvelib_version }})'
+- test_server: '{{ galaxy_ng_server }}'
+- test_api_server: '{{ galaxy_ng_server }}'
+- requires_auth: true
++ test_server: '{{ pulp_server }}published/api/'
++ vX: "v3/"
++ requires_auth: false
+ args:
+ apply:
+ environment:
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/tasks/upgrade.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/tasks/upgrade.yml
+@@ -142,7 +142,7 @@
+ - directory
+
+ - name: install a collection
+- command: ansible-galaxy collection install namespace1.name1==0.0.1 {{ galaxy_verbosity }}
++ command: ansible-galaxy collection install namespace1.name1:0.0.1 {{ galaxy_verbosity }}
+ register: result
+ failed_when:
+ - '"namespace1.name1:0.0.1 was installed successfully" not in result.stdout_lines'
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/tasks/verify.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/tasks/verify.yml
+@@ -3,11 +3,6 @@
+ args:
+ chdir: '{{ galaxy_dir }}/scratch'
+
+-- name: created required runtime.yml
+- copy:
+- content: 'requires_ansible: ">=1.0.0"'
+- dest: '{{ galaxy_dir }}/scratch/ansible_test/verify/meta/runtime.yml'
+-
+ - name: build the collection
+ command: ansible-galaxy collection build scratch/ansible_test/verify
+ args:
+@@ -36,9 +31,6 @@
+ - name: verify the collection against the first valid server
+ command: ansible-galaxy collection verify ansible_test.verify:1.0.0 -vvvv {{ galaxy_verbosity }}
+ register: verify
+- vars:
+- # This sets a specific precedence that the tests are expecting
+- ANSIBLE_GALAXY_SERVER_LIST: offline,secondary,pulp_v2,galaxy_ng
+
+ - assert:
+ that:
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/templates/ansible.cfg.j2
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/templates/ansible.cfg.j2
+@@ -1,22 +1,28 @@
+ [galaxy]
+ # Ensures subsequent unstable reruns don't use the cached information causing another failure
+ cache_dir={{ remote_tmp_dir }}/galaxy_cache
+-server_list=offline,galaxy_ng,secondary,pulp_v2
++server_list=offline,pulp_v2,pulp_v3,galaxy_ng,secondary
+
+ [galaxy_server.offline]
+ url={{ offline_server }}
+
+ [galaxy_server.pulp_v2]
+-url={{ pulp_server }}primary/api/
++url={{ pulp_server }}published/api/
++username={{ pulp_user }}
++password={{ pulp_password }}
++
++[galaxy_server.pulp_v3]
++url={{ pulp_server }}published/api/
++v3=true
+ username={{ pulp_user }}
+ password={{ pulp_password }}
+-api_version=2
+
+ [galaxy_server.galaxy_ng]
+-url={{ galaxy_ng_server }}content/primary/
++url={{ galaxy_ng_server }}
+ token={{ galaxy_ng_token.json.token }}
+
+ [galaxy_server.secondary]
+-url={{ galaxy_ng_server }}content/secondary/
++url={{ pulp_server }}secondary/api/
++v3=true
+ username={{ pulp_user }}
+ password={{ pulp_password }}
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-collection/vars/main.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-collection/vars/main.yml
+@@ -9,20 +9,17 @@ supported_resolvelib_versions:
+ - "0.6.0"
+ - "0.7.0"
+ - "0.8.0"
+- - "0.9.0"
+- - "1.0.1"
+
+ unsupported_resolvelib_versions:
+ - "0.2.0" # Fails on import
+ - "0.5.1"
+
+ pulp_repositories:
+- - primary
++ - published
+ - secondary
+
+ publish_namespaces:
+ - ansible_test
+- - secondary
+
+ collection_list:
+ # Scenario to test out pre-release being ignored unless explicitly set and version pagination.
+@@ -165,41 +162,3 @@ collection_list:
+ name: parent
+ dependencies:
+ namespace1.name1: '*'
+-
+- # non-prerelease is published to test that installing
+- # the pre-release from SCM doesn't accidentally prefer indirect
+- # dependencies from Galaxy
+- - namespace: test_prereleases
+- name: collection2
+- version: 1.0.0
+-
+- - namespace: dev_and_stables_ns
+- name: dev_and_stables_name
+- version: 1.2.3-dev0
+- - namespace: dev_and_stables_ns
+- name: dev_and_stables_name
+- version: 1.2.4
+-
+- - namespace: ns_with_wildcard_dep
+- name: name_with_wildcard_dep
+- version: 5.6.7-beta.3
+- dependencies:
+- dev_and_stables_ns.dev_and_stables_name: >-
+- *
+- - namespace: ns_with_dev_dep
+- name: name_with_dev_dep
+- version: 6.7.8
+- dependencies:
+- dev_and_stables_ns.dev_and_stables_name: 1.2.3-dev0
+-
+- - namespace: rc_meta_ns_with_transitive_dev_dep
+- name: rc_meta_name_with_transitive_dev_dep
+- version: 2.4.5-rc5
+- dependencies:
+- ns_with_dev_dep.name_with_dev_dep: >-
+- *
+- - namespace: meta_ns_with_transitive_wildcard_dep
+- name: meta_name_with_transitive_wildcard_dep
+- version: 4.5.6
+- dependencies:
+- ns_with_wildcard_dep.name_with_wildcard_dep: 5.6.7-beta.3
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-role/files/create-role-archive.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-role/files/create-role-archive.py
+@@ -2,7 +2,6 @@
+ """Create a role archive which overwrites an arbitrary file."""
+
+ import argparse
+-import os
+ import pathlib
+ import tarfile
+ import tempfile
+@@ -19,15 +18,6 @@ def main() -> None:
+ create_archive(args.archive, args.content, args.target)
+
+
+-def generate_files_from_path(path):
+- if os.path.isdir(path):
+- for subpath in os.listdir(path):
+- _path = os.path.join(path, subpath)
+- yield from generate_files_from_path(_path)
+- elif os.path.isfile(path):
+- yield pathlib.Path(path)
+-
+-
+ def create_archive(archive_path: pathlib.Path, content_path: pathlib.Path, target_path: pathlib.Path) -> None:
+ with (
+ tarfile.open(name=archive_path, mode='w') as role_archive,
+@@ -45,15 +35,10 @@ def create_archive(archive_path: pathlib
+ role_archive.add(meta_main_path)
+ role_archive.add(symlink_path)
+
+- for path in generate_files_from_path(content_path):
+- if path == content_path:
+- arcname = str(symlink_path)
+- else:
+- arcname = os.path.join(temp_dir_path, path)
+-
+- content_tarinfo = role_archive.gettarinfo(path, arcname)
+- with path.open('rb') as file_content:
+- role_archive.addfile(content_tarinfo, file_content)
++ content_tarinfo = role_archive.gettarinfo(content_path, str(symlink_path))
++
++ with content_path.open('rb') as content_file:
++ role_archive.addfile(content_tarinfo, content_file)
+
+
+ if __name__ == '__main__':
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-role/tasks/dir-traversal.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-role/tasks/dir-traversal.yml
+@@ -23,9 +23,6 @@
+ command:
+ cmd: ansible-galaxy role install --roles-path '{{ remote_tmp_dir }}/dir-traversal/roles' dangerous.tar
+ chdir: '{{ remote_tmp_dir }}/dir-traversal/source'
+- environment:
+- ANSIBLE_NOCOLOR: True
+- ANSIBLE_FORCE_COLOR: False
+ ignore_errors: true
+ register: galaxy_install_dangerous
+
+@@ -45,86 +42,3 @@
+ - dangerous_overwrite_content.content|default('')|b64decode == ''
+ - not dangerous_overwrite_stat.stat.exists
+ - galaxy_install_dangerous is failed
+- - "'is not a subpath of the role' in (galaxy_install_dangerous.stderr | regex_replace('\n', ' '))"
+-
+-- name: remove tarfile for next test
+- file:
+- path: '{{ item }}'
+- state: absent
+- loop:
+- - '{{ remote_tmp_dir }}/dir-traversal/source/dangerous.tar'
+- - '{{ remote_tmp_dir }}/dir-traversal/roles/dangerous.tar'
+-
+-- name: build dangerous dir traversal role that includes .. in the symlink path
+- script:
+- chdir: '{{ remote_tmp_dir }}/dir-traversal/source'
+- cmd: create-role-archive.py dangerous.tar content.txt {{ remote_tmp_dir }}/dir-traversal/source/../target/target-file-to-overwrite.txt
+- executable: '{{ ansible_playbook_python }}'
+-
+-- name: install dangerous role
+- command:
+- cmd: 'ansible-galaxy role install --roles-path {{ remote_tmp_dir }}/dir-traversal/roles dangerous.tar'
+- chdir: '{{ remote_tmp_dir }}/dir-traversal/source'
+- environment:
+- ANSIBLE_NOCOLOR: True
+- ANSIBLE_FORCE_COLOR: False
+- ignore_errors: true
+- register: galaxy_install_dangerous
+-
+-- name: check for overwritten file
+- stat:
+- path: '{{ remote_tmp_dir }}/dir-traversal/target/target-file-to-overwrite.txt'
+- register: dangerous_overwrite_stat
+-
+-- name: get overwritten content
+- slurp:
+- path: '{{ remote_tmp_dir }}/dir-traversal/target/target-file-to-overwrite.txt'
+- register: dangerous_overwrite_content
+- when: dangerous_overwrite_stat.stat.exists
+-
+-- assert:
+- that:
+- - dangerous_overwrite_content.content|default('')|b64decode == ''
+- - not dangerous_overwrite_stat.stat.exists
+- - galaxy_install_dangerous is failed
+- - "'is not a subpath of the role' in (galaxy_install_dangerous.stderr | regex_replace('\n', ' '))"
+-
+-- name: remove tarfile for next test
+- file:
+- path: '{{ remote_tmp_dir }}/dir-traversal/source/dangerous.tar'
+- state: absent
+-
+-- name: build dangerous dir traversal role that includes .. in the relative symlink path
+- script:
+- chdir: '{{ remote_tmp_dir }}/dir-traversal/source'
+- cmd: create-role-archive.py dangerous_rel.tar content.txt ../context.txt
+-
+-- name: install dangerous role with relative symlink
+- command:
+- cmd: 'ansible-galaxy role install --roles-path {{ remote_tmp_dir }}/dir-traversal/roles dangerous_rel.tar'
+- chdir: '{{ remote_tmp_dir }}/dir-traversal/source'
+- environment:
+- ANSIBLE_NOCOLOR: True
+- ANSIBLE_FORCE_COLOR: False
+- ignore_errors: true
+- register: galaxy_install_dangerous
+-
+-- name: check for symlink outside role
+- stat:
+- path: "{{ remote_tmp_dir | realpath }}/dir-traversal/roles/symlink"
+- register: symlink_outside_role
+-
+-- assert:
+- that:
+- - not symlink_outside_role.stat.exists
+- - galaxy_install_dangerous is failed
+- - "'is not a subpath of the role' in (galaxy_install_dangerous.stderr | regex_replace('\n', ' '))"
+-
+-- name: remove test directories
+- file:
+- path: '{{ remote_tmp_dir }}/dir-traversal/{{ item }}'
+- state: absent
+- loop:
+- - source
+- - target
+- - roles
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy-role/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy-role/tasks/main.yml
+@@ -25,18 +25,10 @@
+ - name: Valid role archive
+ command: "tar cf {{ remote_tmp_dir }}/valid-role.tar {{ remote_tmp_dir }}/role.d"
+
+-- name: Add invalid symlink
+- file:
+- state: link
+- src: "~/invalid"
++- name: Invalid file
++ copy:
++ content: ""
+ dest: "{{ remote_tmp_dir }}/role.d/tasks/~invalid.yml"
+- force: yes
+-
+-- name: Add another invalid symlink
+- file:
+- state: link
+- src: "/"
+- dest: "{{ remote_tmp_dir }}/role.d/tasks/invalid$name.yml"
+
+ - name: Valid requirements file
+ copy:
+@@ -69,4 +61,3 @@
+ command: ansible-galaxy role remove invalid-testrole
+
+ - import_tasks: dir-traversal.yml
+-- import_tasks: valid-role-symlinks.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy/files/testserver.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy/files/testserver.py
+@@ -1,15 +1,20 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-import http.server
+-import socketserver
++import sys
+ import ssl
+
+ if __name__ == '__main__':
+- Handler = http.server.SimpleHTTPRequestHandler
+- context = ssl.SSLContext()
+- context.load_cert_chain(certfile='./cert.pem', keyfile='./key.pem')
+- httpd = socketserver.TCPServer(("", 4443), Handler)
+- httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
++ if sys.version_info[0] >= 3:
++ import http.server
++ import socketserver
++ Handler = http.server.SimpleHTTPRequestHandler
++ httpd = socketserver.TCPServer(("", 4443), Handler)
++ else:
++ import BaseHTTPServer
++ import SimpleHTTPServer
++ Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
++ httpd = BaseHTTPServer.HTTPServer(("", 4443), Handler)
+
++ httpd.socket = ssl.wrap_socket(httpd.socket, certfile='./cert.pem', keyfile='./key.pem', server_side=True)
+ httpd.serve_forever()
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-galaxy/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/ansible-galaxy/runme.sh
+@@ -61,13 +61,10 @@ f_ansible_galaxy_create_role_repo_post()
+ git add .
+ git commit -m "local testing ansible galaxy role"
+
+- # NOTE: `HEAD` is used because the newer Git versions create
+- # NOTE: `main` by default and the older ones differ. We
+- # NOTE: want to avoid hardcoding them.
+ git archive \
+ --format=tar \
+ --prefix="${repo_name}/" \
+- HEAD > "${repo_tar}"
++ master > "${repo_tar}"
+ # Configure basic (insecure) HTTPS-accessible repository
+ galaxy_local_test_role_http_repo="${galaxy_webserver_root}/${galaxy_local_test_role}.git"
+ if [[ ! -d "${galaxy_local_test_role_http_repo}" ]]; then
+@@ -357,7 +354,7 @@ pushd "${galaxy_testdir}"
+ popd # ${galaxy_testdir}
+
+ f_ansible_galaxy_status \
+- "role info non-existent role"
++ "role info non-existant role"
+
+ mkdir -p "${role_testdir}"
+ pushd "${role_testdir}"
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-inventory/files/valid_sample.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-inventory/files/valid_sample.yml
+@@ -4,4 +4,4 @@ all:
+ hosts:
+ something:
+ foo: bar
+- ungrouped: {}
++ ungrouped: {}
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-inventory/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-inventory/tasks/main.yml
+@@ -145,10 +145,3 @@
+ loop_control:
+ loop_var: toml_package
+ when: toml_package is not contains 'tomllib' or (toml_package is contains 'tomllib' and ansible_facts.python.version_info >= [3, 11])
+-
+-
+-- include_tasks: "{{item}}_output.yml"
+- loop:
+- - json
+- - yaml
+- - toml
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-pull/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/ansible-pull/runme.sh
+@@ -36,8 +36,7 @@ function pass_tests {
+ fi
+
+ # test for https://github.com/ansible/ansible/issues/13681
+- # match play default output stats, was matching limit + docker
+- if grep -E '127\.0\.0\.1\s*: ok=' "${temp_log}"; then
++ if grep -E '127\.0\.0\.1.*ok' "${temp_log}"; then
+ cat "${temp_log}"
+ echo "Found host 127.0.0.1 in output. Only localhost should be present."
+ exit 1
+@@ -87,7 +86,5 @@ ANSIBLE_CONFIG='' ansible-pull -d "${pul
+
+ pass_tests_multi
+
+-ANSIBLE_CONFIG='' ansible-pull -d "${pull_dir}" -U "${repo_dir}" conn_secret.yml --connection-password-file "${repo_dir}/secret_connection_password" "$@"
+-
+ # fail if we try do delete /var/tmp
+ ANSIBLE_CONFIG='' ansible-pull -d var/tmp -U "${repo_dir}" --purge "$@"
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-runner/aliases
++++ ansible-core-2.16.5/test/integration/targets/ansible-runner/aliases
+@@ -1,4 +1,5 @@
+ shippable/posix/group5
+ context/controller
++skip/osx
+ skip/macos
+ skip/freebsd
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-runner/files/adhoc_example1.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-runner/files/adhoc_example1.py
+@@ -2,6 +2,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ import json
++import os
+ import sys
+ import ansible_runner
+
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-cloud-openshift/aliases
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-cloud-openshift/aliases
+@@ -1,4 +1,4 @@
+ cloud/openshift
+ shippable/generic/group1
+-disabled # the container crashes when using a non-default network on some docker hosts (such as Ubuntu 20.04)
++disabled # disabled due to requirements conflict: botocore 1.20.6 has requirement urllib3<1.27,>=1.25.4, but you have urllib3 1.24.3.
+ context/controller
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-cloud-openshift/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-cloud-openshift/tasks/main.yml
+@@ -1,13 +1,6 @@
+-- name: Load kubeconfig
+- include_vars: "{{ lookup('env', 'K8S_AUTH_KUBECONFIG') }}"
+-
+-- name: Verify endpoints exist
+- assert:
+- that: clusters
+-
+ - name: Verify endpoints respond
+ uri:
+- url: "{{ item.cluster.server }}"
++ url: "{{ item }}"
+ validate_certs: no
+ with_items:
+- - "{{ clusters }}"
++ - https://openshift-origin:8443/
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-container/runme.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-container/runme.py
+@@ -996,7 +996,7 @@ class AptBootstrapper(Bootstrapper):
+ @classmethod
+ def install_podman(cls) -> bool:
+ """Return True if podman will be installed."""
+- return not (os_release.id == 'ubuntu' and os_release.version_id in {'20.04', '22.04'})
++ return not (os_release.id == 'ubuntu' and os_release.version_id == '20.04')
+
+ @classmethod
+ def install_docker(cls) -> bool:
+@@ -1053,14 +1053,13 @@ class ApkBootstrapper(Bootstrapper):
+ # crun added as podman won't install it as dep if runc is present
+ # but we don't want runc as it fails
+ # The edge `crun` package installed below requires ip6tables, and in
+- # edge, the `iptables` package includes `ip6tables`, but in 3.18 they
+- # are separate. Remove `ip6tables` once we update to 3.19.
++ # edge, the `iptables` package includes `ip6tables`, but in 3.16 they
++ # are separate.
+ packages = ['docker', 'podman', 'openssl', 'crun', 'ip6tables']
+
+ run_command('apk', 'add', *packages)
+- # 3.18 only contains crun 1.8.4, to get 1.9.2 to resolve the run/shm issue, install crun from 3.19
+- # Remove once we update to 3.19
+- run_command('apk', 'upgrade', '-U', '--repository=http://dl-cdn.alpinelinux.org/alpine/v3.19/community', 'crun')
++ # 3.16 only contains crun 1.4.5, to get 1.9.2 to resolve the run/shm issue, install crun from edge
++ run_command('apk', 'upgrade', '-U', '--repository=http://dl-cdn.alpinelinux.org/alpine/edge/community', 'crun')
+ run_command('service', 'docker', 'start')
+ run_command('modprobe', 'tun')
+
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor1.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor1.py
+@@ -16,10 +16,10 @@ RETURN = '''#'''
+
+ from ansible.plugins.lookup import LookupBase
+ # noinspection PyUnresolvedReferences
+-from ansible.plugins import loader # import the loader to verify it works when the collection loader has already been loaded # pylint: disable=unused-import
++from ansible.plugins import loader # import the loader to verify it works when the collection loader has already been loaded
+
+ try:
+- import demo # pylint: disable=unused-import
++ import demo
+ except ImportError:
+ pass
+ else:
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor2.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor2.py
+@@ -16,10 +16,10 @@ RETURN = '''#'''
+
+ from ansible.plugins.lookup import LookupBase
+ # noinspection PyUnresolvedReferences
+-from ansible.plugins import loader # import the loader to verify it works when the collection loader has already been loaded # pylint: disable=unused-import
++from ansible.plugins import loader # import the loader to verify it works when the collection loader has already been loaded
+
+ try:
+- import demo # pylint: disable=unused-import
++ import demo
+ except ImportError:
+ pass
+ else:
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity-import/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity-import/runme.sh
+@@ -1,29 +1,7 @@
+ #!/usr/bin/env bash
+
+-set -eu
+-
+-# Create test scenarios at runtime that do not pass sanity tests.
+-# This avoids the need to create ignore entries for the tests.
+-
+-mkdir -p ansible_collections/ns/col/plugins/lookup
+-
+-(
+- cd ansible_collections/ns/col/plugins/lookup
+-
+- echo "import sys; sys.stdout.write('unwanted stdout')" > stdout.py # stdout: unwanted stdout
+- echo "import sys; sys.stderr.write('unwanted stderr')" > stderr.py # stderr: unwanted stderr
+-)
+-
+ source ../collection/setup.sh
+
+-# Run regular import sanity tests.
+-
+-ansible-test sanity --test import --color --failure-ok --lint --python "${ANSIBLE_TEST_PYTHON_VERSION}" "${@}" 1> actual-stdout.txt 2> actual-stderr.txt
+-diff -u "${TEST_DIR}/expected.txt" actual-stdout.txt
+-grep -f "${TEST_DIR}/expected.txt" actual-stderr.txt
+-
+-# Run import sanity tests which require modifications to the source directory.
+-
+ vendor_dir="$(python -c 'import pathlib, ansible._vendor; print(pathlib.Path(ansible._vendor.__file__).parent)')"
+
+ cleanup() {
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.yaml
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.yaml
+@@ -17,9 +17,6 @@ DOCUMENTATION:
+ default: foo
+ author:
+ - Ansible Core Team
+- seealso:
+- - plugin: ns.col.import_order_lookup
+- plugin_type: lookup
+
+ EXAMPLES: |
+ - name: example for sidecar
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.md
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.md
+@@ -1,4 +1,3 @@
+ README
+ ------
+-
+ This is a simple collection used to test failures with ``ansible-test sanity --test validate-modules``.
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.md
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.md
+@@ -1,4 +1,3 @@
+ README
+ ------
+-
+ This is a simple PowerShell-only collection used to verify that ``ansible-test`` works on a collection.
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity-validate-modules/expected.txt
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity-validate-modules/expected.txt
+@@ -1,26 +1,5 @@
+-plugins/lookup/import_order_lookup.py:5:0: import-before-documentation: Import found before documentation variables. All imports must appear below DOCUMENTATION/EXAMPLES/RETURN.
+-plugins/modules/check_mode_attribute_1.py:0:0: attributes-check-mode: The module does not declare support for check mode, but the check_mode attribute's support value is 'full' and not 'none'
+-plugins/modules/check_mode_attribute_2.py:0:0: attributes-check-mode: The module does not declare support for check mode, but the check_mode attribute's support value is 'partial' and not 'none'
+-plugins/modules/check_mode_attribute_3.py:0:0: attributes-check-mode: The module does declare support for check mode, but the check_mode attribute's support value is 'none'
+-plugins/modules/check_mode_attribute_4.py:0:0: attributes-check-mode-details: The module declares it does not fully support check mode, but has no details on what exactly that means
+-plugins/modules/import_order.py:8:0: import-before-documentation: Import found before documentation variables. All imports must appear below DOCUMENTATION/EXAMPLES/RETURN.
+ plugins/modules/invalid_yaml_syntax.py:0:0: deprecation-mismatch: "meta/runtime.yml" and DOCUMENTATION.deprecation do not agree.
+ plugins/modules/invalid_yaml_syntax.py:0:0: missing-documentation: No DOCUMENTATION provided
+ plugins/modules/invalid_yaml_syntax.py:8:15: documentation-syntax-error: DOCUMENTATION is not valid YAML
+ plugins/modules/invalid_yaml_syntax.py:12:15: invalid-examples: EXAMPLES is not valid YAML
+ plugins/modules/invalid_yaml_syntax.py:16:15: return-syntax-error: RETURN is not valid YAML
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a11.suboptions.b1.description.0: While parsing "V(C\(" at index 1: Unnecessarily escaped "(" @ data['options']['a11']['suboptions']['b1']['description'][0]. Got 'V(C\\(foo\\)).'
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a11.suboptions.b1.description.2: While parsing "P(foo.bar#baz)" at index 1: Plugin name "foo.bar" is not a FQCN @ data['options']['a11']['suboptions']['b1']['description'][2]. Got 'P(foo.bar#baz).'
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a11.suboptions.b1.description.3: While parsing "P(foo.bar.baz)" at index 1: Parameter "foo.bar.baz" is not of the form FQCN#type @ data['options']['a11']['suboptions']['b1']['description'][3]. Got 'P(foo.bar.baz).'
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a11.suboptions.b1.description.4: Directive "P(foo.bar.baz#woof)" must contain a valid plugin type; found "woof" @ data['options']['a11']['suboptions']['b1']['description'][4]. Got 'P(foo.bar.baz#woof).'
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a11.suboptions.b1.description.5: While parsing "E(foo\(" at index 1: Unnecessarily escaped "(" @ data['options']['a11']['suboptions']['b1']['description'][5]. Got 'E(foo\\(bar).'
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a2.description: While parsing "V(C\(" at index 1: Unnecessarily escaped "(" for dictionary value @ data['options']['a2']['description']. Got 'V(C\\(foo\\)).'
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a4.description: While parsing "P(foo.bar#baz)" at index 1: Plugin name "foo.bar" is not a FQCN for dictionary value @ data['options']['a4']['description']. Got 'P(foo.bar#baz).'
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a5.description: While parsing "P(foo.bar.baz)" at index 1: Parameter "foo.bar.baz" is not of the form FQCN#type for dictionary value @ data['options']['a5']['description']. Got 'P(foo.bar.baz).'
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a6.description: Directive "P(foo.bar.baz#woof)" must contain a valid plugin type; found "woof" for dictionary value @ data['options']['a6']['description']. Got 'P(foo.bar.baz#woof).'
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: DOCUMENTATION.options.a7.description: While parsing "E(foo\(" at index 1: Unnecessarily escaped "(" for dictionary value @ data['options']['a7']['description']. Got 'E(foo\\(bar).'
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: Directive "O(bar)" contains a non-existing option "bar"
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: Directive "O(bar=bam)" contains a non-existing option "bar"
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: Directive "O(foo.bar=1)" contains a non-existing option "foo.bar"
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: Directive "RV(bam)" contains a non-existing return value "bam"
+-plugins/modules/semantic_markup.py:0:0: invalid-documentation-markup: Directive "RV(does.not.exist=true)" contains a non-existing return value "does.not.exist"
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity-validate-modules/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity-validate-modules/runme.sh
+@@ -6,17 +6,7 @@ set -eux
+
+ ansible-test sanity --test validate-modules --color --truncate 0 --failure-ok --lint "${@}" 1> actual-stdout.txt 2> actual-stderr.txt
+ diff -u "${TEST_DIR}/expected.txt" actual-stdout.txt
+-grep -F -f "${TEST_DIR}/expected.txt" actual-stderr.txt
+-
+-cd ../col
+-ansible-test sanity --test runtime-metadata
+-
+-cd ../failure
+-if ansible-test sanity --test runtime-metadata 2>&1 | tee out.txt; then
+- echo "runtime-metadata in failure should be invalid"
+- exit 1
+-fi
+-grep out.txt -e 'extra keys not allowed'
++grep -f "${TEST_DIR}/expected.txt" actual-stderr.txt
+
+ cd ../ps_only
+
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.md
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.md
+@@ -1,4 +1,3 @@
+ README
+ ------
+-
+ This is a simple collection used to verify that ``ansible-test`` works on a collection.
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/meta/runtime.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/meta/runtime.yml
+@@ -2,11 +2,4 @@ requires_ansible: '>=2.11' # force ansi
+ plugin_routing:
+ modules:
+ hi:
+- redirect: ns.col2.hello
+- hiya:
+- redirect: ns.col2.package.subdir.hiya
+- module_utils:
+- hi:
+- redirect: ansible_collections.ns.col2.plugins.module_utils
+- hello:
+- redirect: ns.col2.hiya
++ redirect: hello
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/bad.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/bad.py
+@@ -19,9 +19,9 @@ EXAMPLES = '''
+ RETURN = ''' # '''
+
+ from ansible.plugins.lookup import LookupBase
+-from ansible import constants # pylint: disable=unused-import
++from ansible import constants
+
+-import lxml # pylint: disable=unused-import
++import lxml
+
+
+ class LookupModule(LookupBase):
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/world.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/world.py
+@@ -19,7 +19,7 @@ EXAMPLES = '''
+ RETURN = ''' # '''
+
+ from ansible.plugins.lookup import LookupBase
+-from ansible import constants # pylint: disable=unused-import
++from ansible import constants
+
+
+ class LookupModule(LookupBase):
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/modules/bad.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/modules/bad.py
+@@ -19,7 +19,7 @@ EXAMPLES = '''
+ RETURN = ''''''
+
+ from ansible.module_utils.basic import AnsibleModule
+-from ansible import constants # intentionally trigger pylint ansible-bad-module-import error # pylint: disable=unused-import
++from ansible import constants # intentionally trigger pylint ansible-bad-module-import error
+
+
+ def main():
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/random_directory/bad.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/random_directory/bad.py
+@@ -5,4 +5,4 @@ __metaclass__ = type
+
+ # This is not an allowed import, but since this file is in a plugins/ subdirectory that is not checked,
+ # the import sanity test will not complain.
+-import lxml # pylint: disable=unused-import
++import lxml
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py
+@@ -4,12 +4,12 @@ __metaclass__ = type
+ import tempfile
+
+ try:
+- import urllib2 # intentionally trigger pylint ansible-bad-import error # pylint: disable=unused-import
++ import urllib2 # intentionally trigger pylint ansible-bad-import error
+ except ImportError:
+ urllib2 = None
+
+ try:
+- from urllib2 import Request # intentionally trigger pylint ansible-bad-import-from error # pylint: disable=unused-import
++ from urllib2 import Request # intentionally trigger pylint ansible-bad-import-from error
+ except ImportError:
+ Request = None
+
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/sanity/ignore.txt
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/sanity/ignore.txt
+@@ -1,7 +1,6 @@
+ plugins/modules/bad.py import
+ plugins/modules/bad.py pylint:ansible-bad-module-import
+ plugins/lookup/bad.py import
+-plugins/plugin_utils/check_pylint.py pylint:disallowed-name
+ tests/integration/targets/hello/files/bad.py pylint:ansible-bad-function
+ tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import
+ tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import-from
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test-sanity/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/ansible-test-sanity/runme.sh
+@@ -1,11 +1,5 @@
+ #!/usr/bin/env bash
+
+-set -eux
+-
+-ansible-test sanity --color --allow-disabled -e "${@}"
+-
+-set +x
+-
+ source ../collection/setup.sh
+
+ set -x
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-test/venv-pythons.py
++++ ansible-core-2.16.5/test/integration/targets/ansible-test/venv-pythons.py
+@@ -1,7 +1,6 @@
+ #!/usr/bin/env python
+ """Return target Python options for use with ansible-test."""
+
+-import argparse
+ import os
+ import shutil
+ import subprocess
+@@ -11,11 +10,6 @@ from ansible import release
+
+
+ def main():
+- parser = argparse.ArgumentParser()
+- parser.add_argument('--only-versions', action='store_true')
+-
+- options = parser.parse_args()
+-
+ ansible_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(release.__file__))))
+ source_root = os.path.join(ansible_root, 'test', 'lib')
+
+@@ -39,10 +33,6 @@ def main():
+ print(f'{executable} - {"fail" if process.returncode else "pass"}', file=sys.stderr)
+
+ if not process.returncode:
+- if options.only_versions:
+- args.append(python_version)
+- continue
+-
+ args.extend(['--target-python', f'venv/{python_version}'])
+
+ print(' '.join(args))
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-vault/invalid_format/broken-group-vars-tasks.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-vault/invalid_format/broken-group-vars-tasks.yml
+@@ -20,4 +20,4 @@
+ # 3366323866663763660a323766383531396433663861656532373663373134376263383263316261
+ # 3137
+
+-# $ ansible-playbook -i inventory --vault-password-file=vault-secret tasks.yml
++# $ ansible-playbook -i inventory --vault-password-file=vault-secret tasks.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-vault/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/ansible-vault/runme.sh
+@@ -47,18 +47,6 @@ echo $?
+ # view the vault encrypted password file
+ ansible-vault view "$@" --vault-id vault-password encrypted-vault-password
+
+-# check if ansible-vault fails when destination is not writable
+-NOT_WRITABLE_DIR="${MYTMPDIR}/not_writable"
+-TEST_FILE_EDIT4="${NOT_WRITABLE_DIR}/testfile"
+-mkdir "${NOT_WRITABLE_DIR}"
+-touch "${TEST_FILE_EDIT4}"
+-chmod ugo-w "${NOT_WRITABLE_DIR}"
+-ansible-vault encrypt "$@" --vault-password-file vault-password "${TEST_FILE_EDIT4}" < /dev/null > log 2>&1 && :
+-grep "not writable" log && :
+-WRONG_RC=$?
+-echo "rc was $WRONG_RC (1 is expected)"
+-[ $WRONG_RC -eq 1 ]
+-
+ # encrypt with a password from a vault encrypted password file and multiple vault-ids
+ # should fail because we dont know which vault id to use to encrypt with
+ ansible-vault encrypt "$@" --vault-id vault-password --vault-id encrypted-vault-password "${TEST_FILE_ENC_PASSWORD}" && :
+@@ -586,23 +574,3 @@ ansible-playbook realpath.yml "$@" --vau
+
+ # using symlink
+ ansible-playbook symlink.yml "$@" --vault-password-file script/vault-secret.sh 2>&1 |grep "${ER}"
+-
+-### SALT TESTING ###
+-# prep files for encryption
+-for salted in test1 test2 test3
+-do
+- echo 'this is salty' > "salted_${salted}"
+-done
+-
+-# encrypt files
+-ANSIBLE_VAULT_ENCRYPT_SALT=salty ansible-vault encrypt salted_test1 --vault-password-file example1_password "$@"
+-ANSIBLE_VAULT_ENCRYPT_SALT=salty ansible-vault encrypt salted_test2 --vault-password-file example1_password "$@"
+-ansible-vault encrypt salted_test3 --vault-password-file example1_password "$@"
+-
+-# should be the same
+-out=$(diff salted_test1 salted_test2)
+-[ "${out}" == "" ]
+-
+-# shoudl be diff
+-out=$(diff salted_test1 salted_test3 || true)
+-[ "${out}" != "" ]
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-vault/test_vault.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-vault/test_vault.yml
+@@ -1,6 +1,6 @@
+ - hosts: testhost
+ gather_facts: False
+ vars:
+- output_dir: .
++ - output_dir: .
+ roles:
+ - { role: test_vault, tags: test_vault}
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible-vault/test_vaulted_template.yml
++++ ansible-core-2.16.5/test/integration/targets/ansible-vault/test_vaulted_template.yml
+@@ -1,6 +1,6 @@
+ - hosts: testhost
+ gather_facts: False
+ vars:
+- output_dir: .
++ - output_dir: .
+ roles:
+ - { role: test_vaulted_template, tags: test_vaulted_template}
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible/aliases
++++ ansible-core-2.16.5/test/integration/targets/ansible/aliases
+@@ -1,3 +1,2 @@
+ shippable/posix/group3
+ context/controller
+-needs/target/support-callback_plugins
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible/ansible-testé.cfg
++++ ansible-core-2.16.5/test/integration/targets/ansible/ansible-testé.cfg
+@@ -1,3 +1,3 @@
+ [defaults]
+ remote_user = admin
+-collections_path = /tmp/collections
++collections_paths = /tmp/collections
+--- ansible-core-2.16.5.orig/test/integration/targets/ansible/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/ansible/runme.sh
+@@ -14,9 +14,9 @@ ANSIBLE_REMOTE_USER=administrator ansibl
+ ansible-config list | grep 'DEFAULT_REMOTE_USER'
+
+ # Collection
+-ansible-config view -c ./ansible-testé.cfg | grep 'collections_path = /tmp/collections'
++ansible-config view -c ./ansible-testé.cfg | grep 'collections_paths = /tmp/collections'
+ ansible-config dump -c ./ansible-testé.cfg | grep 'COLLECTIONS_PATHS([^)]*) ='
+-ANSIBLE_COLLECTIONS_PATH=/tmp/collections ansible-config dump| grep 'COLLECTIONS_PATHS([^)]*) ='
++ANSIBLE_COLLECTIONS_PATHS=/tmp/collections ansible-config dump| grep 'COLLECTIONS_PATHS([^)]*) ='
+ ansible-config list | grep 'COLLECTIONS_PATHS'
+
+ # 'view' command must fail when config file is missing or has an invalid file extension
+@@ -34,7 +34,7 @@ ansible localhost -m debug -a var=playbo
+ env -u ANSIBLE_PLAYBOOK_DIR ANSIBLE_CONFIG=./playbookdir_cfg.ini ansible localhost -m debug -a var=playbook_dir | grep '"playbook_dir": "/doesnotexist/tmp"'
+
+ # test adhoc callback triggers
+-ANSIBLE_CALLBACK_PLUGINS=../support-callback_plugins/callback_plugins ANSIBLE_STDOUT_CALLBACK=callback_debug ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible --playbook-dir . testhost -i ../../inventory -m ping | grep -E '^v2_' | diff -u adhoc-callback.stdout -
++ANSIBLE_STDOUT_CALLBACK=callback_debug ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible --playbook-dir . testhost -i ../../inventory -m ping | grep -E '^v2_' | diff -u adhoc-callback.stdout -
+
+ # CB_WANTS_IMPLICIT isn't anything in Ansible itself.
+ # Our test cb plugin just accepts it. It lets us avoid copypasting the whole
+--- ansible-core-2.16.5.orig/test/integration/targets/apt/aliases
++++ ansible-core-2.16.5/test/integration/targets/apt/aliases
+@@ -1,5 +1,6 @@
+ shippable/posix/group2
+ destructive
+ skip/freebsd
++skip/osx
+ skip/macos
+ skip/rhel
+--- ansible-core-2.16.5.orig/test/integration/targets/apt/tasks/apt.yml
++++ ansible-core-2.16.5/test/integration/targets/apt/tasks/apt.yml
+@@ -372,7 +372,7 @@
+ - libcaca-dev
+ - libslang2-dev
+
+-# # https://github.com/ansible/ansible/issues/38995
++# https://github.com/ansible/ansible/issues/38995
+ - name: build-dep for a package
+ apt:
+ name: tree
+@@ -524,55 +524,6 @@
+ - "allow_change_held_packages_no_update is not changed"
+ - "allow_change_held_packages_hello_version.stdout == allow_change_held_packages_hello_version_again.stdout"
+
+-# Remove pkg on hold
+-- name: Put hello on hold
+- shell: apt-mark hold hello
+-
+-- name: Get hold list
+- shell: apt-mark showhold
+- register: hello_hold
+-
+-- name: Check that the package hello is on the hold list
+- assert:
+- that:
+- - "'hello' in hello_hold.stdout"
+-
+-- name: Try removing package hello
+- apt:
+- name: hello
+- state: absent
+- register: package_removed
+- ignore_errors: true
+-
+-- name: verify the package is not removed with dpkg
+- shell: dpkg -l hello
+- register: dpkg_result
+-
+-- name: Verify that package was not removed
+- assert:
+- that:
+- - package_removed is failed
+- - dpkg_result is success
+-
+-- name: Try removing package (allow_change_held_packages=yes)
+- apt:
+- name: hello
+- state: absent
+- allow_change_held_packages: yes
+- register: package_removed
+-
+-- name: verify the package is removed with dpkg
+- shell: dpkg -l hello
+- register: dpkg_result
+- ignore_errors: true
+-
+-- name: Verify that package removal was succesfull
+- assert:
+- that:
+- - package_removed is success
+- - dpkg_result is failed
+- - package_removed.changed
+-
+ # Virtual package
+ - name: Install a virtual package
+ apt:
+--- ansible-core-2.16.5.orig/test/integration/targets/apt/tasks/repo.yml
++++ ansible-core-2.16.5/test/integration/targets/apt/tasks/repo.yml
+@@ -400,8 +400,6 @@
+ - { upgrade_type: safe, force_apt_get: True }
+ - { upgrade_type: full, force_apt_get: True }
+
+- - include_tasks: "upgrade_scenarios.yml"
+-
+ - name: Remove aptitude if not originally present
+ apt:
+ pkg: aptitude
+--- ansible-core-2.16.5.orig/test/integration/targets/apt_key/aliases
++++ ansible-core-2.16.5/test/integration/targets/apt_key/aliases
+@@ -1,4 +1,5 @@
+ shippable/posix/group1
+ skip/freebsd
++skip/osx
+ skip/macos
+ skip/rhel
+--- ansible-core-2.16.5.orig/test/integration/targets/apt_key/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/apt_key/tasks/main.yml
+@@ -21,7 +21,7 @@
+
+ - import_tasks: 'apt_key_inline_data.yml'
+ when: ansible_distribution in ('Ubuntu', 'Debian')
+-
++
+ - import_tasks: 'file.yml'
+ when: ansible_distribution in ('Ubuntu', 'Debian')
+
+--- ansible-core-2.16.5.orig/test/integration/targets/apt_repository/aliases
++++ ansible-core-2.16.5/test/integration/targets/apt_repository/aliases
+@@ -1,5 +1,6 @@
+ destructive
+ shippable/posix/group1
+ skip/freebsd
++skip/osx
+ skip/macos
+ skip/rhel
+--- ansible-core-2.16.5.orig/test/integration/targets/apt_repository/tasks/apt.yml
++++ ansible-core-2.16.5/test/integration/targets/apt_repository/tasks/apt.yml
+@@ -152,11 +152,6 @@
+ - 'result.changed'
+ - 'result.state == "present"'
+ - 'result.repo == test_ppa_spec'
+- - '"sources_added" in result'
+- - 'result.sources_added | length == 1'
+- - '"git" in result.sources_added[0]'
+- - '"sources_removed" in result'
+- - 'result.sources_removed | length == 0'
+ - result_cache is not changed
+
+ - name: 'examine apt cache mtime'
+@@ -172,17 +167,6 @@
+ apt_repository: repo='{{test_ppa_spec}}' state=absent
+ register: result
+
+-- assert:
+- that:
+- - 'result.changed'
+- - 'result.state == "absent"'
+- - 'result.repo == test_ppa_spec'
+- - '"sources_added" in result'
+- - 'result.sources_added | length == 0'
+- - '"sources_removed" in result'
+- - 'result.sources_removed | length == 1'
+- - '"git" in result.sources_removed[0]'
+-
+ # When installing a repo with the spec, the key is *NOT* added
+ - name: 'ensure ppa key is absent (expect: pass)'
+ apt_key: id='{{test_ppa_key}}' state=absent
+@@ -240,7 +224,7 @@
+ - assert:
+ that:
+ - result is failed
+- - result.msg.startswith("argument 'repo' is of type <class 'NoneType'> and we were unable to convert to str")
++ - result.msg == 'Please set argument \'repo\' to a non-empty value'
+
+ - name: Test apt_repository with an empty value for repo
+ apt_repository:
+--- ansible-core-2.16.5.orig/test/integration/targets/apt_repository/tasks/mode_cleanup.yaml
++++ ansible-core-2.16.5/test/integration/targets/apt_repository/tasks/mode_cleanup.yaml
+@@ -4,4 +4,4 @@
+ - name: Delete existing repo
+ file:
+ path: "{{ test_repo_path }}"
+- state: absent
++ state: absent
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/argspec/library/argspec.py
++++ ansible-core-2.16.5/test/integration/targets/argspec/library/argspec.py
+@@ -23,10 +23,6 @@ def main():
+ 'type': 'str',
+ 'choices': ['absent', 'present'],
+ },
+- 'default_value': {
+- 'type': 'bool',
+- 'default': True,
+- },
+ 'path': {},
+ 'content': {},
+ 'mapping': {
+@@ -250,7 +246,7 @@ def main():
+ ('state', 'present', ('path', 'content'), True),
+ ),
+ mutually_exclusive=(
+- ('path', 'content', 'default_value',),
++ ('path', 'content'),
+ ),
+ required_one_of=(
+ ('required_one_of_one', 'required_one_of_two'),
+--- ansible-core-2.16.5.orig/test/integration/targets/become/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/become/tasks/main.yml
+@@ -11,8 +11,8 @@
+ ansible_become_user: "{{ become_test_config.user }}"
+ ansible_become_method: "{{ become_test_config.method }}"
+ ansible_become_password: "{{ become_test_config.password | default(None) }}"
+- loop: "{{
+- (become_methods | selectattr('skip', 'undefined') | list)+
++ loop: "{{
++ (become_methods | selectattr('skip', 'undefined') | list)+
+ (become_methods | selectattr('skip', 'defined') | rejectattr('skip') | list)
+ }}"
+ loop_control:
+--- ansible-core-2.16.5.orig/test/integration/targets/blockinfile/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/blockinfile/tasks/main.yml
+@@ -31,7 +31,6 @@
+
+ - import_tasks: add_block_to_existing_file.yml
+ - import_tasks: create_file.yml
+-- import_tasks: create_dir.yml
+ - import_tasks: preserve_line_endings.yml
+ - import_tasks: block_without_trailing_newline.yml
+ - import_tasks: file_without_trailing_newline.yml
+@@ -40,5 +39,3 @@
+ - import_tasks: insertafter.yml
+ - import_tasks: insertbefore.yml
+ - import_tasks: multiline_search.yml
+-- import_tasks: append_newline.yml
+-- import_tasks: prepend_newline.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/blocks/unsafe_failed_task.yml
++++ ansible-core-2.16.5/test/integration/targets/blocks/unsafe_failed_task.yml
+@@ -1,7 +1,7 @@
+ - hosts: localhost
+ gather_facts: false
+ vars:
+- data: {}
++ - data: {}
+ tasks:
+ - block:
+ - name: template error
+--- ansible-core-2.16.5.orig/test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stdout
++++ ansible-core-2.16.5/test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stdout
+@@ -43,7 +43,6 @@ fatal: [testhost]: FAILED! =>
+ TASK [Skipped task] ************************************************************
+ skipping: [testhost] =>
+ changed: false
+- false_condition: false
+ skip_reason: Conditional result was False
+
+ TASK [Task with var in name (foo bar)] *****************************************
+@@ -121,7 +120,6 @@ ok: [testhost] => (item=debug-3) =>
+ msg: debug-3
+ skipping: [testhost] => (item=debug-4) =>
+ ansible_loop_var: item
+- false_condition: item != 4
+ item: 4
+ fatal: [testhost]: FAILED! =>
+ msg: One or more items failed
+@@ -201,11 +199,9 @@ skipping: [testhost] =>
+ TASK [debug] *******************************************************************
+ skipping: [testhost] => (item=1) =>
+ ansible_loop_var: item
+- false_condition: false
+ item: 1
+ skipping: [testhost] => (item=2) =>
+ ansible_loop_var: item
+- false_condition: false
+ item: 2
+ skipping: [testhost] =>
+ msg: All items skipped
+--- ansible-core-2.16.5.orig/test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stdout
++++ ansible-core-2.16.5/test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stdout
+@@ -45,7 +45,6 @@ fatal: [testhost]: FAILED! =>
+ TASK [Skipped task] ************************************************************
+ skipping: [testhost] =>
+ changed: false
+- false_condition: false
+ skip_reason: Conditional result was False
+
+ TASK [Task with var in name (foo bar)] *****************************************
+@@ -127,7 +126,6 @@ ok: [testhost] => (item=debug-3) =>
+ msg: debug-3
+ skipping: [testhost] => (item=debug-4) =>
+ ansible_loop_var: item
+- false_condition: item != 4
+ item: 4
+ fatal: [testhost]: FAILED! =>
+ msg: One or more items failed
+@@ -208,11 +206,9 @@ skipping: [testhost] =>
+ TASK [debug] *******************************************************************
+ skipping: [testhost] => (item=1) =>
+ ansible_loop_var: item
+- false_condition: false
+ item: 1
+ skipping: [testhost] => (item=2) =>
+ ansible_loop_var: item
+- false_condition: false
+ item: 2
+ skipping: [testhost] =>
+ msg: All items skipped
+--- ansible-core-2.16.5.orig/test/integration/targets/check_mode/check_mode.yml
++++ ansible-core-2.16.5/test/integration/targets/check_mode/check_mode.yml
+@@ -1,7 +1,7 @@
+ - name: Test that check works with check_mode specified in roles
+ hosts: testhost
+ vars:
+- output_dir: .
++ - output_dir: .
+ roles:
+ - { role: test_always_run, tags: test_always_run }
+ - { role: test_check_mode, tags: test_check_mode }
+--- ansible-core-2.16.5.orig/test/integration/targets/check_mode/roles/test_check_mode/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/check_mode/roles/test_check_mode/tasks/main.yml
+@@ -25,8 +25,8 @@
+ register: foo
+
+ - name: verify that the file was marked as changed in check mode
+- assert:
+- that:
++ assert:
++ that:
+ - "template_result is changed"
+ - "not foo.stat.exists"
+
+@@ -44,7 +44,7 @@
+ check_mode: no
+
+ - name: verify that the file was not changed
+- assert:
+- that:
++ assert:
++ that:
+ - "checkmode_disabled is changed"
+ - "template_result2 is not changed"
+--- ansible-core-2.16.5.orig/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/connection/localconn.py
++++ ansible-core-2.16.5/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/connection/localconn.py
+@@ -1,7 +1,7 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.plugins.connection import ConnectionBase
+
+ DOCUMENTATION = """
+--- ansible-core-2.16.5.orig/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing.py
++++ ansible-core-2.16.5/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing.py
+@@ -2,7 +2,10 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-from ..module_utils import bogusmu # pylint: disable=relative-beyond-top-level,unused-import
++import json
++import sys
++
++from ..module_utils import bogusmu # pylint: disable=relative-beyond-top-level
+
+
+ def main():
+--- ansible-core-2.16.5.orig/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_collection.py
++++ ansible-core-2.16.5/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_collection.py
+@@ -2,7 +2,10 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-from ..module_utils import missing_redirect_target_collection # pylint: disable=relative-beyond-top-level,unused-import
++import json
++import sys
++
++from ..module_utils import missing_redirect_target_collection # pylint: disable=relative-beyond-top-level
+
+
+ def main():
+--- ansible-core-2.16.5.orig/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_module.py
++++ ansible-core-2.16.5/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_module.py
+@@ -2,7 +2,10 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-from ..module_utils import missing_redirect_target_module # pylint: disable=relative-beyond-top-level,unused-import
++import json
++import sys
++
++from ..module_utils import missing_redirect_target_module # pylint: disable=relative-beyond-top-level
+
+
+ def main():
+--- ansible-core-2.16.5.orig/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/inventory/statichost.py
++++ ansible-core-2.16.5/test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/inventory/statichost.py
+@@ -19,6 +19,7 @@ DOCUMENTATION = '''
+ required: True
+ '''
+
++from ansible.errors import AnsibleParserError
+ from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
+
+
+--- ansible-core-2.16.5.orig/test/integration/targets/collections/test_task_resolved_plugin/callback_plugins/display_resolved_action.py
++++ ansible-core-2.16.5/test/integration/targets/collections/test_task_resolved_plugin/callback_plugins/display_resolved_action.py
+@@ -14,6 +14,7 @@ DOCUMENTATION = '''
+ - Enable in configuration.
+ '''
+
++from ansible import constants as C
+ from ansible.plugins.callback import CallbackBase
+
+
+--- ansible-core-2.16.5.orig/test/integration/targets/command_nonexisting/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/command_nonexisting/tasks/main.yml
+@@ -1,4 +1,4 @@
+ - command: commandthatdoesnotexist --would-be-awkward
+ register: res
+ changed_when: "'changed' in res.stdout"
+- failed_when: "res.stdout != '' or res.stderr != ''"
++ failed_when: "res.stdout != '' or res.stderr != ''"
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/command_shell/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/command_shell/tasks/main.yml
+@@ -284,30 +284,6 @@
+ that:
+ - "command_result6.stdout == '9cd0697c6a9ff6689f0afb9136fa62e0b3fee903'"
+
+-- name: check default var expansion
+- command: /bin/sh -c 'echo "\$TEST"'
+- environment:
+- TEST: z
+- register: command_result7
+-
+-- name: assert vars were expanded
+- assert:
+- that:
+- - command_result7.stdout == '\\z'
+-
+-- name: check disabled var expansion
+- command: /bin/sh -c 'echo "\$TEST"'
+- args:
+- expand_argument_vars: false
+- environment:
+- TEST: z
+- register: command_result8
+-
+-- name: assert vars were not expanded
+- assert:
+- that:
+- - command_result8.stdout == '$TEST'
+-
+ ##
+ ## shell
+ ##
+@@ -570,21 +546,3 @@
+ - command_strip.stderr == 'hello \n '
+ - command_no_strip.stdout== 'hello \n \r\n'
+ - command_no_strip.stderr == 'hello \n \r\n'
+-
+-- name: run shell with expand_argument_vars
+- shell: echo 'hi'
+- args:
+- expand_argument_vars: false
+- register: shell_expand_failure
+- ignore_errors: true
+-
+-- name: assert shell with expand_arguments_vars failed
+- assert:
+- that:
+- - shell_expand_failure is failed
+- - "shell_expand_failure.msg == 'Unsupported parameters for (shell) module: expand_argument_vars'"
+-
+-- name: Run command that backgrounds, to ensure no hang
+- shell: '{{ role_path }}/scripts/yoink.sh &'
+- delegate_to: localhost
+- timeout: 5
+--- ansible-core-2.16.5.orig/test/integration/targets/conditionals/play.yml
++++ ansible-core-2.16.5/test/integration/targets/conditionals/play.yml
+@@ -665,29 +665,3 @@
+ - item
+ loop:
+ - 1 == 1
+-
+- - set_fact:
+- sentinel_file: '{{ lookup("env", "OUTPUT_DIR")}}/LOOKUP_SIDE_EFFECT.txt'
+-
+- - name: ensure sentinel file is absent
+- file:
+- path: '{{ sentinel_file }}'
+- state: absent
+- - name: get an untrusted var that's a valid Jinja expression with a side-effect
+- shell: |
+- echo "lookup('pipe', 'echo bang > \"$SENTINEL_FILE\" && cat \"$SENTINEL_FILE\"')"
+- environment:
+- SENTINEL_FILE: '{{ sentinel_file }}'
+- register: untrusted_expr
+- - name: use a conditional with an inline template that refers to the untrusted expression
+- debug:
+- msg: look at some seemingly innocuous stuff
+- when: '"foo" in {{ untrusted_expr.stdout }}'
+- ignore_errors: true
+- - name: ensure the untrusted expression side-effect has not executed
+- stat:
+- path: '{{ sentinel_file }}'
+- register: sentinel_stat
+- - assert:
+- that:
+- - not sentinel_stat.stat.exists
+--- ansible-core-2.16.5.orig/test/integration/targets/connection_delegation/aliases
++++ ansible-core-2.16.5/test/integration/targets/connection_delegation/aliases
+@@ -1,5 +1,6 @@
+ shippable/posix/group3
+ context/controller
+ skip/freebsd # No sshpass
++skip/osx # No sshpass
+ skip/macos # No sshpass
+ skip/rhel # No sshpass
+--- ansible-core-2.16.5.orig/test/integration/targets/connection_paramiko_ssh/test_connection.inventory
++++ ansible-core-2.16.5/test/integration/targets/connection_paramiko_ssh/test_connection.inventory
+@@ -2,6 +2,6 @@
+ paramiko_ssh-pipelining ansible_ssh_pipelining=true
+ paramiko_ssh-no-pipelining ansible_ssh_pipelining=false
+ [paramiko_ssh:vars]
+-ansible_host={{ 'localhost'|string }}
++ansible_host=localhost
+ ansible_connection=paramiko_ssh
+ ansible_python_interpreter="{{ ansible_playbook_python }}"
+--- ansible-core-2.16.5.orig/test/integration/targets/connection_psrp/tests.yml
++++ ansible-core-2.16.5/test/integration/targets/connection_psrp/tests.yml
+@@ -6,9 +6,6 @@
+ gather_facts: no
+
+ tasks:
+- - name: reboot the host
+- ansible.windows.win_reboot:
+-
+ - name: test complex objects in raw output
+ # until PyYAML is upgraded to 4.x we need to use the \U escape for a unicode codepoint
+ # and enclose in a quote to it translates the \U
+@@ -32,8 +29,15 @@
+ - raw_out.stdout_lines[4] == "winrm"
+ - raw_out.stdout_lines[5] == "string - \U0001F4A9"
+
++ # Become only works on Server 2008 when running with basic auth, skip this host for now as it is too complicated to
++ # override the auth protocol in the tests.
++ - name: check if we running on Server 2008
++ win_shell: '[System.Environment]::OSVersion.Version -ge [Version]"6.1"'
++ register: os_version
++
+ - name: test out become with psrp
+ win_whoami:
++ when: os_version|bool
+ register: whoami_out
+ become: yes
+ become_method: runas
+@@ -43,6 +47,7 @@
+ assert:
+ that:
+ - whoami_out.account.sid == "S-1-5-18"
++ when: os_version|bool
+
+ - name: test out async with psrp
+ win_shell: Start-Sleep -Seconds 2; Write-Output abc
+--- ansible-core-2.16.5.orig/test/integration/targets/connection_winrm/tests.yml
++++ ansible-core-2.16.5/test/integration/targets/connection_winrm/tests.yml
+@@ -6,9 +6,6 @@
+ gather_facts: no
+
+ tasks:
+- - name: reboot the host
+- ansible.windows.win_reboot:
+-
+ - name: setup remote tmp dir
+ import_role:
+ name: ../../setup_remote_tmp_dir
+--- ansible-core-2.16.5.orig/test/integration/targets/copy/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/copy/tasks/main.yml
+@@ -84,7 +84,6 @@
+ - import_tasks: check_mode.yml
+
+ # https://github.com/ansible/ansible/issues/57618
+- # https://github.com/ansible/ansible/issues/79749
+ - name: Test diff contents
+ copy:
+ content: 'Ansible managed\n'
+@@ -96,7 +95,6 @@
+ that:
+ - 'diff_output.diff[0].before == ""'
+ - '"Ansible managed" in diff_output.diff[0].after'
+- - '"file.txt" in diff_output.diff[0].after_header'
+
+ - name: tests with remote_src and non files
+ import_tasks: src_remote_file_is_not_file.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/copy/tasks/tests.yml
++++ ansible-core-2.16.5/test/integration/targets/copy/tasks/tests.yml
+@@ -420,80 +420,6 @@
+ - "stat_results2.stat.mode == '0547'"
+
+ #
+-# test copying an empty dir to a dest dir with remote_src=True
+-#
+-
+-- name: create empty test dir
+- file:
+- path: '{{ remote_dir }}/testcase_empty_dir'
+- state: directory
+-
+-- name: test copying an empty dir to a dir that does not exist (dest ends with slash)
+- copy:
+- src: '{{ remote_dir }}/testcase_empty_dir/'
+- remote_src: yes
+- dest: '{{ remote_dir }}/testcase_empty_dir_dest/'
+- register: copy_result
+-
+-- name: get stat of newly created dir
+- stat:
+- path: '{{ remote_dir }}/testcase_empty_dir_dest'
+- register: stat_result
+-
+-- assert:
+- that:
+- - copy_result.changed
+- - stat_result.stat.exists
+- - stat_result.stat.isdir
+-
+-- name: test no change is made running the task twice
+- copy:
+- src: '{{ remote_dir }}/testcase_empty_dir/'
+- remote_src: yes
+- dest: '{{ remote_dir }}/testcase_empty_dir_dest/'
+- register: copy_result
+- failed_when: copy_result is changed
+-
+-- name: remove to test dest with no trailing slash
+- file:
+- path: '{{ remote_dir }}/testcase_empty_dir_dest/'
+- state: absent
+-
+-- name: test copying an empty dir to a dir that does not exist (both src/dest have no trailing slash)
+- copy:
+- src: '{{ remote_dir }}/testcase_empty_dir'
+- remote_src: yes
+- dest: '{{ remote_dir }}/testcase_empty_dir_dest'
+- register: copy_result
+-
+-- name: get stat of newly created dir
+- stat:
+- path: '{{ remote_dir }}/testcase_empty_dir_dest'
+- register: stat_result
+-
+-- assert:
+- that:
+- - copy_result.changed
+- - stat_result.stat.exists
+- - stat_result.stat.isdir
+-
+-- name: test no change is made running the task twice
+- copy:
+- src: '{{ remote_dir }}/testcase_empty_dir/'
+- remote_src: yes
+- dest: '{{ remote_dir }}/testcase_empty_dir_dest/'
+- register: copy_result
+- failed_when: copy_result is changed
+-
+-- name: clean up src and dest
+- file:
+- path: "{{ item }}"
+- state: absent
+- loop:
+- - '{{ remote_dir }}/testcase_empty_dir'
+- - '{{ remote_dir }}/testcase_empty_dir_dest'
+-
+-#
+ # test recursive copy local_follow=False, no trailing slash
+ #
+
+@@ -2358,81 +2284,3 @@
+ that:
+ - fail_copy_directory_with_enc_file is failed
+ - fail_copy_directory_with_enc_file.msg == 'A vault password or secret must be specified to decrypt {{role_path}}/files-different/vault/vault-file'
+-
+-#
+-# Test for issue 74536: recursively copy all nested directories with remote_src=yes and src='dir/' when dest exists
+-#
+-- vars:
+- src: '{{ remote_dir }}/testcase_74536'
+- block:
+- - name: create source dir with 3 nested subdirs
+- file:
+- path: '{{ src }}/a/b1/c1'
+- state: directory
+-
+- - name: copy the source dir with a trailing slash
+- copy:
+- src: '{{ src }}/'
+- remote_src: yes
+- dest: '{{ src }}_dest/'
+- register: copy_result
+- failed_when: copy_result is not changed
+-
+- - name: remove the source dir to recreate with different subdirs
+- file:
+- path: '{{ src }}'
+- state: absent
+-
+- - name: recreate source dir
+- file:
+- path: "{{ item }}"
+- state: directory
+- loop:
+- - '{{ src }}/a/b1/c2'
+- - '{{ src }}/a/b2/c3'
+-
+- - name: copy the source dir containing new subdirs into the existing dest dir
+- copy:
+- src: '{{ src }}/'
+- remote_src: yes
+- dest: '{{ src }}_dest/'
+- register: copy_result
+-
+- - name: stat each directory that should exist
+- stat:
+- path: '{{ item }}'
+- register: stat_result
+- loop:
+- - '{{ src }}_dest'
+- - '{{ src }}_dest/a'
+- - '{{ src }}_dest/a/b1'
+- - '{{ src }}_dest/a/b2'
+- - '{{ src }}_dest/a/b1/c1'
+- - '{{ src }}_dest/a/b1/c2'
+- - '{{ src }}_dest/a/b2/c3'
+-
+- - debug: msg="{{ stat_result }}"
+-
+- - assert:
+- that:
+- - copy_result is changed
+- # all paths exist
+- - stat_result.results | map(attribute='stat') | map(attribute='exists') | unique == [true]
+- # all paths are dirs
+- - stat_result.results | map(attribute='stat') | map(attribute='isdir') | unique == [true]
+-
+- - name: copy the src again to verify no changes will be made
+- copy:
+- src: '{{ src }}/'
+- remote_src: yes
+- dest: '{{ src }}_dest/'
+- register: copy_result
+- failed_when: copy_result is changed
+-
+- - name: clean up src and dest
+- file:
+- path: '{{ item }}'
+- state: absent
+- loop:
+- - '{{ src }}'
+- - '{{ src }}_dest'
+--- ansible-core-2.16.5.orig/test/integration/targets/cron/aliases
++++ ansible-core-2.16.5/test/integration/targets/cron/aliases
+@@ -1,3 +1,4 @@
+ destructive
+ shippable/posix/group1
++skip/osx
+ skip/macos
+--- ansible-core-2.16.5.orig/test/integration/targets/debconf/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/debconf/tasks/main.yml
+@@ -33,44 +33,4 @@
+ - 'debconf_test0.current is defined'
+ - '"tzdata/Zones/Etc" in debconf_test0.current'
+ - 'debconf_test0.current["tzdata/Zones/Etc"] == "UTC"'
+-
+- - name: install debconf-utils
+- apt:
+- name: debconf-utils
+- state: present
+- register: debconf_utils_deb_install
+-
+- - name: Check if password is set
+- debconf:
+- name: ddclient
+- question: ddclient/password
+- value: "MySecretValue"
+- vtype: password
+- register: debconf_test1
+-
+- - name: validate results for test 1
+- assert:
+- that:
+- - debconf_test1.changed
+-
+- - name: Change password again
+- debconf:
+- name: ddclient
+- question: ddclient/password
+- value: "MySecretValue"
+- vtype: password
+- no_log: yes
+- register: debconf_test2
+-
+- - name: validate results for test 1
+- assert:
+- that:
+- - not debconf_test2.changed
+- always:
+- - name: uninstall debconf-utils
+- apt:
+- name: debconf-utils
+- state: absent
+- when: debconf_utils_deb_install is changed
+-
+- when: ansible_distribution in ('Ubuntu', 'Debian')
+\ No newline at end of file
++ when: ansible_distribution in ('Ubuntu', 'Debian')
+--- ansible-core-2.16.5.orig/test/integration/targets/delegate_to/delegate_local_from_root.yml
++++ ansible-core-2.16.5/test/integration/targets/delegate_to/delegate_local_from_root.yml
+@@ -3,7 +3,7 @@
+ gather_facts: false
+ remote_user: root
+ tasks:
+- - name: ensure we copy w/o errors due to remote user not being overridden
++ - name: ensure we copy w/o errors due to remote user not being overriden
+ copy:
+ src: testfile
+ dest: "{{ playbook_dir }}"
+--- ansible-core-2.16.5.orig/test/integration/targets/delegate_to/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/delegate_to/runme.sh
+@@ -76,7 +76,3 @@ ansible-playbook test_delegate_to_lookup
+ ansible-playbook delegate_local_from_root.yml -i inventory -v "$@" -e 'ansible_user=root'
+ ansible-playbook delegate_with_fact_from_delegate_host.yml "$@"
+ ansible-playbook delegate_facts_loop.yml -i inventory -v "$@"
+-ansible-playbook test_random_delegate_to_with_loop.yml -i inventory -v "$@"
+-
+-# Run playbook multiple times to ensure there are no false-negatives
+-for i in $(seq 0 10); do ansible-playbook test_random_delegate_to_without_loop.yml -i inventory -v "$@"; done;
+--- ansible-core-2.16.5.orig/test/integration/targets/delegate_to/test_delegate_to.yml
++++ ansible-core-2.16.5/test/integration/targets/delegate_to/test_delegate_to.yml
+@@ -1,9 +1,9 @@
+ - hosts: testhost3
+ vars:
+- template_role: ./roles/test_template
+- output_dir: "{{ playbook_dir }}"
+- templated_var: foo
+- templated_dict: { 'hello': 'world' }
++ - template_role: ./roles/test_template
++ - output_dir: "{{ playbook_dir }}"
++ - templated_var: foo
++ - templated_dict: { 'hello': 'world' }
+ tasks:
+ - name: Test no delegate_to
+ setup:
+@@ -57,25 +57,6 @@
+ - name: remove test file
+ file: path={{ output_dir }}/tmp.txt state=absent
+
+- - name: Use omit to thwart delegation
+- ping:
+- delegate_to: "{{ jenkins_install_key_on|default(omit) }}"
+- register: d_omitted
+-
+- - name: Use empty to thwart delegation should fail
+- ping:
+- delegate_to: "{{ jenkins_install_key_on }}"
+- when: jenkins_install_key_on != ""
+- vars:
+- jenkins_install_key_on: ''
+- ignore_errors: true
+- register: d_empty
+-
+- - name: Ensure previous 2 tests actually did what was expected
+- assert:
+- that:
+- - d_omitted is success
+- - d_empty is failed
+
+ - name: verify delegation with per host vars
+ hosts: testhost6
+--- ansible-core-2.16.5.orig/test/integration/targets/dnf/aliases
++++ ansible-core-2.16.5/test/integration/targets/dnf/aliases
+@@ -1,4 +1,6 @@
+ destructive
+ shippable/posix/group1
++skip/power/centos
+ skip/freebsd
++skip/osx
+ skip/macos
+--- ansible-core-2.16.5.orig/test/integration/targets/dnf/tasks/dnf.yml
++++ ansible-core-2.16.5/test/integration/targets/dnf/tasks/dnf.yml
+@@ -224,7 +224,7 @@
+ - assert:
+ that:
+ - dnf_result is success
+- - dnf_result.results|length >= 2
++ - dnf_result.results|length == 2
+ - "dnf_result.results[0].startswith('Removed: ')"
+ - "dnf_result.results[1].startswith('Removed: ')"
+
+@@ -427,10 +427,6 @@
+ - shell: 'dnf -y group install "Custom Group" && dnf -y group remove "Custom Group"'
+ register: shell_dnf_result
+
+-- dnf:
+- name: "@Custom Group"
+- state: absent
+-
+ # GROUP UPGRADE - this will go to the same method as group install
+ # but through group_update - it is its invocation we're testing here
+ # see commit 119c9e5d6eb572c4a4800fbe8136095f9063c37b
+@@ -450,10 +446,6 @@
+ # cleanup until https://github.com/ansible/ansible/issues/27377 is resolved
+ - shell: dnf -y group install "Custom Group" && dnf -y group remove "Custom Group"
+
+-- dnf:
+- name: "@Custom Group"
+- state: absent
+-
+ - name: try to install non existing group
+ dnf:
+ name: "@non-existing-group"
+@@ -559,35 +551,30 @@
+ - "'No package non-existent-rpm available' in dnf_result['failures'][0]"
+ - "'Failed to install some of the specified packages' in dnf_result['msg']"
+
+-- name: ensure sos isn't installed
++- name: use latest to install httpd
+ dnf:
+- name: sos
+- state: absent
+-
+-- name: use latest to install sos
+- dnf:
+- name: sos
++ name: httpd
+ state: latest
+ register: dnf_result
+
+-- name: verify sos was installed
++- name: verify httpd was installed
+ assert:
+ that:
+- - dnf_result is changed
++ - "'changed' in dnf_result"
+
+-- name: uninstall sos
++- name: uninstall httpd
+ dnf:
+- name: sos
++ name: httpd
+ state: removed
+
+-- name: update sos only if it exists
++- name: update httpd only if it exists
+ dnf:
+- name: sos
++ name: httpd
+ state: latest
+ update_only: yes
+ register: dnf_result
+
+-- name: verify sos not installed
++- name: verify httpd not installed
+ assert:
+ that:
+ - "not dnf_result is changed"
+@@ -663,28 +650,6 @@
+ - "dnf_result is not failed"
+
+ - name: verify dnf module outputs
+- assert:
+- that:
+- - "'changed' in dnf_result"
+- - "'results' in dnf_result"
+-
+-# Install RPM from url with update_only
+-- name: install from url with update_only
+- dnf:
+- name: "file://{{ pkg_path }}"
+- state: latest
+- update_only: true
+- disable_gpg_check: true
+- register: dnf_result
+-
+-- name: verify installation
+- assert:
+- that:
+- - "dnf_result is success"
+- - "not dnf_result is changed"
+- - "dnf_result is not failed"
+-
+-- name: verify dnf module outputs
+ assert:
+ that:
+ - "'changed' in dnf_result"
+--- ansible-core-2.16.5.orig/test/integration/targets/dnf/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/dnf/tasks/main.yml
+@@ -61,7 +61,6 @@
+ when:
+ - (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('29', '>=')) or
+ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+- - not dnf5|default(false)
+ tags:
+ - dnf_modularity
+
+@@ -70,6 +69,5 @@
+ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+
+ - include_tasks: cacheonly.yml
+- when:
+- - (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or
+- (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
++ when: (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('23', '>=')) or
++ (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>='))
+--- ansible-core-2.16.5.orig/test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml
++++ ansible-core-2.16.5/test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml
+@@ -240,8 +240,7 @@
+ - name: Do an "upgrade" to an older version of broken-a, allow_downgrade=false
+ dnf:
+ name:
+- #- broken-a-1.2.3-1*
+- - broken-a-1.2.3-1.el7.x86_64
++ - broken-a-1.2.3-1*
+ state: latest
+ allow_downgrade: false
+ check_mode: true
+--- ansible-core-2.16.5.orig/test/integration/targets/dnf/tasks/test_sos_removal.yml
++++ ansible-core-2.16.5/test/integration/targets/dnf/tasks/test_sos_removal.yml
+@@ -15,5 +15,5 @@
+ that:
+ - sos_rm is successful
+ - sos_rm is changed
+- - sos_rm.results|select("contains", "Removed: sos-{{ sos_version }}-{{ sos_release }}")|length > 0
+- - sos_rm.results|length > 0
++ - "'Removed: sos-' ~ sos_version ~ '-' ~ sos_release in sos_rm.results[0]"
++ - sos_rm.results|length == 1
+--- ansible-core-2.16.5.orig/test/integration/targets/dpkg_selections/aliases
++++ ansible-core-2.16.5/test/integration/targets/dpkg_selections/aliases
+@@ -1,5 +1,6 @@
+ shippable/posix/group1
+ destructive
+ skip/freebsd
++skip/osx
+ skip/macos
+ skip/rhel
+--- ansible-core-2.16.5.orig/test/integration/targets/dpkg_selections/tasks/dpkg_selections.yaml
++++ ansible-core-2.16.5/test/integration/targets/dpkg_selections/tasks/dpkg_selections.yaml
+@@ -87,15 +87,3 @@
+ apt:
+ name: hello
+ state: absent
+-
+-- name: Try to select non-existent package
+- dpkg_selections:
+- name: kernel
+- selection: hold
+- ignore_errors: yes
+- register: result
+-
+-- name: Check that module fails for non-existent package
+- assert:
+- that:
+- - "'Failed to find package' in result.msg"
+--- ansible-core-2.16.5.orig/test/integration/targets/egg-info/lookup_plugins/import_pkg_resources.py
++++ ansible-core-2.16.5/test/integration/targets/egg-info/lookup_plugins/import_pkg_resources.py
+@@ -1,7 +1,7 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-import pkg_resources # pylint: disable=unused-import
++import pkg_resources
+
+ from ansible.plugins.lookup import LookupBase
+
+--- ansible-core-2.16.5.orig/test/integration/targets/environment/test_environment.yml
++++ ansible-core-2.16.5/test/integration/targets/environment/test_environment.yml
+@@ -7,8 +7,8 @@
+
+ - hosts: testhost
+ vars:
+- test1:
+- key1: val1
++ - test1:
++ key1: val1
+ environment:
+ PATH: '{{ansible_env.PATH + ":/lola"}}'
+ lola: 'ido'
+@@ -41,9 +41,9 @@
+
+ - hosts: testhost
+ vars:
+- test1:
+- key1: val1
+- test2:
++ - test1:
++ key1: val1
++ - test2:
+ key1: not1
+ other1: val2
+ environment: "{{test1}}"
+--- ansible-core-2.16.5.orig/test/integration/targets/error_from_connection/connection_plugins/dummy.py
++++ ansible-core-2.16.5/test/integration/targets/error_from_connection/connection_plugins/dummy.py
+@@ -11,6 +11,7 @@ DOCUMENTATION = """
+ version_added: "2.0"
+ options: {}
+ """
++import ansible.constants as C
+ from ansible.errors import AnsibleError
+ from ansible.plugins.connection import ConnectionBase
+
+--- ansible-core-2.16.5.orig/test/integration/targets/expect/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/expect/tasks/main.yml
+@@ -148,15 +148,6 @@
+ - "echo_result.stdout_lines[-2] == 'foobar'"
+ - "echo_result.stdout_lines[-1] == 'bar'"
+
+-- name: test timeout is valid as null
+- expect:
+- command: "{{ansible_python_interpreter}} {{test_command_file}}"
+- responses:
+- foo: bar
+- echo: true
+- timeout: null # wait indefinitely
+- timeout: 2 # but shouldn't be waiting long
+-
+ - name: test response list
+ expect:
+ command: "{{ansible_python_interpreter}} {{test_command_file}} foo foo"
+--- ansible-core-2.16.5.orig/test/integration/targets/facts_linux_network/aliases
++++ ansible-core-2.16.5/test/integration/targets/facts_linux_network/aliases
+@@ -1,6 +1,7 @@
+ needs/privileged
+ shippable/posix/group1
+ skip/freebsd
++skip/osx
+ skip/macos
+ context/target
+ destructive
+--- ansible-core-2.16.5.orig/test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml
++++ ansible-core-2.16.5/test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml
+@@ -28,15 +28,6 @@
+ register: failed_fetch_dest_dir
+ ignore_errors: true
+
+-- name: Test unreachable
+- fetch:
+- src: "{{ remote_tmp_dir }}/orig"
+- dest: "{{ output_dir }}"
+- register: unreachable_fetch
+- ignore_unreachable: true
+- vars:
+- ansible_user: wrong
+-
+ - name: Ensure fetch failed
+ assert:
+ that:
+@@ -48,4 +39,3 @@
+ - failed_fetch_no_access.msg is search('file is not readable')
+ - failed_fetch_dest_dir is failed
+ - failed_fetch_dest_dir.msg is search('dest is an existing directory')
+- - unreachable_fetch is unreachable
+--- ansible-core-2.16.5.orig/test/integration/targets/file/tasks/link_rewrite.yml
++++ ansible-core-2.16.5/test/integration/targets/file/tasks/link_rewrite.yml
+@@ -16,11 +16,11 @@
+ dest: "{{ tempdir.path }}/somelink"
+ state: link
+
+-- stat:
++- stat:
+ path: "{{ tempdir.path }}/somelink"
+ register: link
+
+-- stat:
++- stat:
+ path: "{{ tempdir.path }}/somefile"
+ register: file
+
+@@ -32,12 +32,12 @@
+ - file:
+ path: "{{ tempdir.path }}/somelink"
+ mode: 0644
+-
+-- stat:
++
++- stat:
+ path: "{{ tempdir.path }}/somelink"
+ register: link
+
+-- stat:
++- stat:
+ path: "{{ tempdir.path }}/somefile"
+ register: file
+
+--- ansible-core-2.16.5.orig/test/integration/targets/file/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/file/tasks/main.yml
+@@ -779,7 +779,7 @@
+ register: touch_result_in_check_mode_fails_not_existing_group
+
+ - assert:
+- that:
++ that:
+ - touch_result_in_check_mode_not_existing.changed
+ - touch_result_in_check_mode_preserve_access_time.changed
+ - touch_result_in_check_mode_change_only_mode.changed
+--- ansible-core-2.16.5.orig/test/integration/targets/filter_core/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/filter_core/tasks/main.yml
+@@ -454,38 +454,6 @@
+ - password_hash_2 is failed
+ - "'not support' in password_hash_2.msg"
+
+-- name: install passlib if needed
+- pip:
+- name: passlib
+- state: present
+- register: installed_passlib
+-
+-- name: test using passlib with an unsupported hash type
+- set_fact:
+- foo: '{{"hey"|password_hash("msdcc")}}'
+- ignore_errors: yes
+- register: unsupported_hash_type
+-
+-- name: remove passlib if it was installed
+- pip:
+- name: passlib
+- state: absent
+- when: installed_passlib.changed
+-
+-- assert:
+- that:
+- - unsupported_hash_type.msg == msg
+- vars:
+- msg: "msdcc is not in the list of supported passlib algorithms: md5, blowfish, sha256, sha512"
+-
+-- name: test password_hash can work with bcrypt without passlib installed
+- debug:
+- msg: "{{ 'somestring'|password_hash('bcrypt') }}"
+- register: crypt_bcrypt
+- # Some implementations of crypt do not fail outright and return some short value.
+- failed_when: crypt_bcrypt is failed or (crypt_bcrypt.msg|length|int) != 60
+- when: ansible_facts.os_family in ['RedHat', 'Debian']
+-
+ - name: Verify to_uuid throws on weird namespace
+ set_fact:
+ foo: '{{"hey"|to_uuid(namespace=22)}}'
+--- ansible-core-2.16.5.orig/test/integration/targets/filter_encryption/base.yml
++++ ansible-core-2.16.5/test/integration/targets/filter_encryption/base.yml
+@@ -2,7 +2,6 @@
+ gather_facts: true
+ vars:
+ data: secret
+- data2: 'foo: bar\n'
+ dvault: '{{ "secret"|vault("test")}}'
+ password: test
+ s_32: '{{(2**31-1)}}'
+@@ -22,15 +21,6 @@
+ is_64: '{{ "64" in ansible_facts["architecture"] }}'
+ salt: '{{ is_64|bool|ternary(s_64, s_32)|random(seed=inventory_hostname)}}'
+ vaultedstring: '{{ is_64|bool|ternary(vaultedstring_64, vaultedstring_32) }}'
+- # command line vaulted data2
+- vaulted_id: !vault |
+- $ANSIBLE_VAULT;1.2;AES256;test1
+- 36383733336533656264393332663131613335333332346439356164383935656234663631356430
+- 3533353537343834333538356366376233326364613362640a623832636339363966336238393039
+- 35316562626335306534356162623030613566306235623863373036626531346364626166656134
+- 3063376436656635330a363636376131663362633731313964353061663661376638326461393736
+- 3863
+- vaulted_to_id: "{{data2|vault('test1@secret', vault_id='test1')}}"
+
+ tasks:
+ - name: check vaulting
+@@ -45,5 +35,3 @@
+ that:
+ - vaultedstring|unvault(password) == data
+ - vault|unvault(password) == data
+- - vaulted_id|unvault('test1@secret', vault_id='test1')
+- - vaulted_to_id|unvault('test1@secret', vault_id='test1')
+--- ansible-core-2.16.5.orig/test/integration/targets/filter_mathstuff/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/filter_mathstuff/tasks/main.yml
+@@ -64,44 +64,44 @@
+ that:
+ - '[1,2,3]|intersect([4,5,6]) == []'
+ - '[1,2,3]|intersect([3,4,5,6]) == [3]'
+- - '[1,2,3]|intersect([3,2,1]) | sort == [1,2,3]'
+- - '(1,2,3)|intersect((4,5,6)) == []'
+- - '(1,2,3)|intersect((3,4,5,6)) == [3]'
++ - '[1,2,3]|intersect([3,2,1]) == [1,2,3]'
++ - '(1,2,3)|intersect((4,5,6))|list == []'
++ - '(1,2,3)|intersect((3,4,5,6))|list == [3]'
+ - '["a","A","b"]|intersect(["B","c","C"]) == []'
+ - '["a","A","b"]|intersect(["b","B","c","C"]) == ["b"]'
+- - '["a","A","b"]|intersect(["b","A","a"]) | sort(case_sensitive=True) == ["A","a","b"]'
+- - '("a","A","b")|intersect(("B","c","C")) == []'
+- - '("a","A","b")|intersect(("b","B","c","C")) == ["b"]'
++ - '["a","A","b"]|intersect(["b","A","a"]) == ["a","A","b"]'
++ - '("a","A","b")|intersect(("B","c","C"))|list == []'
++ - '("a","A","b")|intersect(("b","B","c","C"))|list == ["b"]'
+
+ - name: Verify difference
+ tags: difference
+ assert:
+ that:
+- - '[1,2,3]|difference([4,5,6]) | sort == [1,2,3]'
+- - '[1,2,3]|difference([3,4,5,6]) | sort == [1,2]'
++ - '[1,2,3]|difference([4,5,6]) == [1,2,3]'
++ - '[1,2,3]|difference([3,4,5,6]) == [1,2]'
+ - '[1,2,3]|difference([3,2,1]) == []'
+- - '(1,2,3)|difference((4,5,6)) | sort == [1,2,3]'
+- - '(1,2,3)|difference((3,4,5,6)) | sort == [1,2]'
+- - '["a","A","b"]|difference(["B","c","C"]) | sort(case_sensitive=True) == ["A","a","b"]'
+- - '["a","A","b"]|difference(["b","B","c","C"]) | sort(case_sensitive=True) == ["A","a"]'
++ - '(1,2,3)|difference((4,5,6))|list == [1,2,3]'
++ - '(1,2,3)|difference((3,4,5,6))|list == [1,2]'
++ - '["a","A","b"]|difference(["B","c","C"]) == ["a","A","b"]'
++ - '["a","A","b"]|difference(["b","B","c","C"]) == ["a","A"]'
+ - '["a","A","b"]|difference(["b","A","a"]) == []'
+- - '("a","A","b")|difference(("B","c","C")) | sort(case_sensitive=True) == ["A","a","b"]'
+- - '("a","A","b")|difference(("b","B","c","C")) | sort(case_sensitive=True) == ["A","a"]'
++ - '("a","A","b")|difference(("B","c","C"))|list|sort(case_sensitive=True) == ["A","a","b"]'
++ - '("a","A","b")|difference(("b","B","c","C"))|list|sort(case_sensitive=True) == ["A","a"]'
+
+ - name: Verify symmetric_difference
+ tags: symmetric_difference
+ assert:
+ that:
+- - '[1,2,3]|symmetric_difference([4,5,6]) | sort == [1,2,3,4,5,6]'
+- - '[1,2,3]|symmetric_difference([3,4,5,6]) | sort == [1,2,4,5,6]'
++ - '[1,2,3]|symmetric_difference([4,5,6]) == [1,2,3,4,5,6]'
++ - '[1,2,3]|symmetric_difference([3,4,5,6]) == [1,2,4,5,6]'
+ - '[1,2,3]|symmetric_difference([3,2,1]) == []'
+- - '(1,2,3)|symmetric_difference((4,5,6)) | sort == [1,2,3,4,5,6]'
+- - '(1,2,3)|symmetric_difference((3,4,5,6)) | sort == [1,2,4,5,6]'
+- - '["a","A","b"]|symmetric_difference(["B","c","C"]) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
+- - '["a","A","b"]|symmetric_difference(["b","B","c","C"]) | sort(case_sensitive=True) == ["A","B","C","a","c"]'
++ - '(1,2,3)|symmetric_difference((4,5,6))|list == [1,2,3,4,5,6]'
++ - '(1,2,3)|symmetric_difference((3,4,5,6))|list == [1,2,4,5,6]'
++ - '["a","A","b"]|symmetric_difference(["B","c","C"]) == ["a","A","b","B","c","C"]'
++ - '["a","A","b"]|symmetric_difference(["b","B","c","C"]) == ["a","A","B","c","C"]'
+ - '["a","A","b"]|symmetric_difference(["b","A","a"]) == []'
+- - '("a","A","b")|symmetric_difference(("B","c","C")) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
+- - '("a","A","b")|symmetric_difference(("b","B","c","C")) | sort(case_sensitive=True) == ["A","B","C","a","c"]'
++ - '("a","A","b")|symmetric_difference(("B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
++ - '("a","A","b")|symmetric_difference(("b","B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","c"]'
+
+ - name: Verify union
+ tags: union
+@@ -112,11 +112,11 @@
+ - '[1,2,3]|union([3,2,1]) == [1,2,3]'
+ - '(1,2,3)|union((4,5,6))|list == [1,2,3,4,5,6]'
+ - '(1,2,3)|union((3,4,5,6))|list == [1,2,3,4,5,6]'
+- - '["a","A","b"]|union(["B","c","C"]) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
+- - '["a","A","b"]|union(["b","B","c","C"]) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
+- - '["a","A","b"]|union(["b","A","a"]) | sort(case_sensitive=True) == ["A","a","b"]'
+- - '("a","A","b")|union(("B","c","C")) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
+- - '("a","A","b")|union(("b","B","c","C")) | sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
++ - '["a","A","b"]|union(["B","c","C"]) == ["a","A","b","B","c","C"]'
++ - '["a","A","b"]|union(["b","B","c","C"]) == ["a","A","b","B","c","C"]'
++ - '["a","A","b"]|union(["b","A","a"]) == ["a","A","b"]'
++ - '("a","A","b")|union(("B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
++ - '("a","A","b")|union(("b","B","c","C"))|list|sort(case_sensitive=True) == ["A","B","C","a","b","c"]'
+
+ - name: Verify min
+ tags: min
+--- ansible-core-2.16.5.orig/test/integration/targets/find/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/find/tasks/main.yml
+@@ -374,6 +374,3 @@
+ - 'remote_tmp_dir_test ~ "/astest/old.txt" in astest_list'
+ - 'remote_tmp_dir_test ~ "/astest/.hidden.txt" in astest_list'
+ - '"checksum" in result.files[0]'
+-
+-- name: Run mode tests
+- import_tasks: mode.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/fork_safe_stdio/aliases
++++ ansible-core-2.16.5/test/integration/targets/fork_safe_stdio/aliases
+@@ -1,3 +1,3 @@
+ shippable/posix/group3
+ context/controller
+-needs/target/test_utils
++skip/macos
+--- ansible-core-2.16.5.orig/test/integration/targets/fork_safe_stdio/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/fork_safe_stdio/runme.sh
+@@ -7,7 +7,7 @@ echo "testing for stdio deadlock on fork
+ # Enable a callback that trips deadlocks on forked-child stdout, time out after 10s; forces running
+ # in a pty, since that tends to be much slower than raw file I/O and thus more likely to trigger the deadlock.
+ # Redirect stdout to /dev/null since it's full of non-printable garbage we don't want to display unless it failed
+-ANSIBLE_CALLBACKS_ENABLED=spewstdio SPEWSTDIO_ENABLED=1 python run-with-pty.py ../test_utils/scripts/timeout.py -- 10 ansible-playbook -i hosts -f 5 test.yml > stdout.txt && RC=$? || RC=$?
++ANSIBLE_CALLBACKS_ENABLED=spewstdio SPEWSTDIO_ENABLED=1 python run-with-pty.py timeout 10s ansible-playbook -i hosts -f 5 test.yml > stdout.txt && RC=$? || RC=$?
+
+ if [ $RC != 0 ]; then
+ echo "failed; likely stdout deadlock. dumping raw output (may be very large)"
+--- ansible-core-2.16.5.orig/test/integration/targets/gathering_facts/library/file_utils.py
++++ ansible-core-2.16.5/test/integration/targets/gathering_facts/library/file_utils.py
+@@ -1,6 +1,9 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
++import json
++import sys
++
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.facts.utils import (
+ get_file_content,
+--- ansible-core-2.16.5.orig/test/integration/targets/gathering_facts/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/gathering_facts/runme.sh
+@@ -25,17 +25,3 @@ ansible-playbook test_module_defaults.y
+ ANSIBLE_FACTS_MODULES='ansible.legacy.setup' ansible-playbook test_module_defaults.yml "$@" --tags custom_fact_module
+
+ ansible-playbook test_module_defaults.yml "$@" --tags networking
+-
+-# test it works by default
+-ANSIBLE_FACTS_MODULES='ansible.legacy.slow' ansible -m gather_facts localhost --playbook-dir ./ "$@"
+-
+-# test that gather_facts will timeout parallel modules that dont support gather_timeout when using gather_Timeout
+-ANSIBLE_FACTS_MODULES='ansible.legacy.slow' ansible -m gather_facts localhost --playbook-dir ./ -a 'gather_timeout=1 parallel=true' "$@" 2>&1 |grep 'Timeout exceeded'
+-
+-# test that gather_facts parallel w/o timing out
+-ANSIBLE_FACTS_MODULES='ansible.legacy.slow' ansible -m gather_facts localhost --playbook-dir ./ -a 'gather_timeout=30 parallel=true' "$@" 2>&1 |grep -v 'Timeout exceeded'
+-
+-
+-# test parallelism
+-ANSIBLE_FACTS_MODULES='dummy1,dummy2,dummy3' ansible -m gather_facts localhost --playbook-dir ./ -a 'gather_timeout=30 parallel=true' "$@" 2>&1
+-rm "${OUTPUT_DIR}/canary.txt"
+--- ansible-core-2.16.5.orig/test/integration/targets/get_url/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/get_url/tasks/main.yml
+@@ -398,8 +398,6 @@
+ port: '{{ http_port }}'
+ state: started
+
+-- include_tasks: hashlib.yml
+-
+ - name: download src with sha1 checksum url in check mode
+ get_url:
+ url: 'http://localhost:{{ http_port }}/27617.txt'
+--- ansible-core-2.16.5.orig/test/integration/targets/get_url/tasks/use_netrc.yml
++++ ansible-core-2.16.5/test/integration/targets/get_url/tasks/use_netrc.yml
+@@ -22,7 +22,7 @@
+ register: response_failed
+
+ - name: Parse token from msg.txt
+- set_fact:
++ set_fact:
+ token: "{{ (response_failed['content'] | b64decode | from_json).token }}"
+
+ - name: assert Test Bearer authorization is failed with netrc
+@@ -48,7 +48,7 @@
+ register: response
+
+ - name: Parse token from msg.txt
+- set_fact:
++ set_fact:
+ token: "{{ (response['content'] | b64decode | from_json).token }}"
+
+ - name: assert Test Bearer authorization is successfull with use_netrc=False
+@@ -64,4 +64,4 @@
+ state: absent
+ with_items:
+ - "{{ remote_tmp_dir }}/netrc"
+- - "{{ remote_tmp_dir }}/msg.txt"
++ - "{{ remote_tmp_dir }}/msg.txt"
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/git/tasks/depth.yml
++++ ansible-core-2.16.5/test/integration/targets/git/tasks/depth.yml
+@@ -95,16 +95,14 @@
+ repo: 'file://{{ repo_dir|expanduser }}/shallow'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+- version: >-
+- {{ git_default_branch }}
++ version: master
+
+ - name: DEPTH | run a second time (now fetch, not clone)
+ git:
+ repo: 'file://{{ repo_dir|expanduser }}/shallow'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+- version: >-
+- {{ git_default_branch }}
++ version: master
+ register: git_fetch
+
+ - name: DEPTH | ensure the fetch succeeded
+@@ -122,8 +120,7 @@
+ repo: 'file://{{ repo_dir|expanduser }}/shallow'
+ dest: '{{ checkout_dir }}'
+ depth: 1
+- version: >-
+- {{ git_default_branch }}
++ version: master
+
+ - name: DEPTH | switch to older branch with depth=1 (uses fetch)
+ git:
+--- ansible-core-2.16.5.orig/test/integration/targets/git/tasks/forcefully-fetch-tag.yml
++++ ansible-core-2.16.5/test/integration/targets/git/tasks/forcefully-fetch-tag.yml
+@@ -11,7 +11,7 @@
+ git add leet;
+ git commit -m uh-oh;
+ git tag -f herewego;
+- git push --tags origin '{{ git_default_branch }}'
++ git push --tags origin master
+ args:
+ chdir: "{{ repo_dir }}/tag_force_push_clone1"
+
+@@ -26,7 +26,7 @@
+ git add leet;
+ git commit -m uh-oh;
+ git tag -f herewego;
+- git push -f --tags origin '{{ git_default_branch }}'
++ git push -f --tags origin master
+ args:
+ chdir: "{{ repo_dir }}/tag_force_push_clone1"
+
+--- ansible-core-2.16.5.orig/test/integration/targets/git/tasks/gpg-verification.yml
++++ ansible-core-2.16.5/test/integration/targets/git/tasks/gpg-verification.yml
+@@ -37,10 +37,8 @@
+ environment:
+ - GNUPGHOME: "{{ git_gpg_gpghome }}"
+ shell: |
+- set -eEu
+-
++ set -e
+ git init
+-
+ touch an_empty_file
+ git add an_empty_file
+ git commit --no-gpg-sign --message "Commit, and don't sign"
+@@ -50,11 +48,11 @@
+ git tag --annotate --message "This is not a signed tag" unsigned_annotated_tag HEAD
+ git commit --allow-empty --gpg-sign --message "Commit, and sign"
+ git tag --sign --message "This is a signed tag" signed_annotated_tag HEAD
+- git checkout -b some_branch/signed_tip '{{ git_default_branch }}'
++ git checkout -b some_branch/signed_tip master
+ git commit --allow-empty --gpg-sign --message "Commit, and sign"
+- git checkout -b another_branch/unsigned_tip '{{ git_default_branch }}'
++ git checkout -b another_branch/unsigned_tip master
+ git commit --allow-empty --no-gpg-sign --message "Commit, and don't sign"
+- git checkout '{{ git_default_branch }}'
++ git checkout master
+ args:
+ chdir: "{{ git_gpg_source }}"
+
+--- ansible-core-2.16.5.orig/test/integration/targets/git/tasks/localmods.yml
++++ ansible-core-2.16.5/test/integration/targets/git/tasks/localmods.yml
+@@ -1,17 +1,6 @@
+ # test for https://github.com/ansible/ansible-modules-core/pull/5505
+ - name: LOCALMODS | prepare old git repo
+- shell: |
+- set -eEu
+-
+- rm -rf localmods
+- mkdir localmods
+- cd localmods
+-
+- git init
+-
+- echo "1" > a
+- git add a
+- git commit -m "1"
++ shell: rm -rf localmods; mkdir localmods; cd localmods; git init; echo "1" > a; git add a; git commit -m "1"
+ args:
+ chdir: "{{repo_dir}}"
+
+@@ -66,18 +55,7 @@
+
+ # localmods and shallow clone
+ - name: LOCALMODS | prepare old git repo
+- shell: |
+- set -eEu
+-
+- rm -rf localmods
+- mkdir localmods
+- cd localmods
+-
+- git init
+-
+- echo "1" > a
+- git add a
+- git commit -m "1"
++ shell: rm -rf localmods; mkdir localmods; cd localmods; git init; echo "1" > a; git add a; git commit -m "1"
+ args:
+ chdir: "{{repo_dir}}"
+
+--- ansible-core-2.16.5.orig/test/integration/targets/git/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/git/tasks/main.yml
+@@ -16,37 +16,27 @@
+ # You should have received a copy of the GNU General Public License
+ # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+-# NOTE: Moving `$HOME` to tmp dir allows this integration test be
+-# NOTE: non-destructive. There is no other way to instruct Git use a custom
+-# NOTE: config path. There are new `$GIT_CONFIG_KEY_{COUNT,KEY,VALUE}` vars
+-# NOTE: for setting specific configuration values but those are only available
+-# NOTE: since Git v2.31 which is why we cannot rely on them yet.
++- import_tasks: setup.yml
++- import_tasks: setup-local-repos.yml
+
+-- block:
+- - import_tasks: setup.yml
+- - import_tasks: setup-local-repos.yml
+-
+- - import_tasks: formats.yml
+- - import_tasks: missing_hostkey.yml
+- - import_tasks: missing_hostkey_acceptnew.yml
+- - import_tasks: no-destination.yml
+- - import_tasks: specific-revision.yml
+- - import_tasks: submodules.yml
+- - import_tasks: change-repo-url.yml
+- - import_tasks: depth.yml
+- - import_tasks: single-branch.yml
+- - import_tasks: checkout-new-tag.yml
+- - include_tasks: gpg-verification.yml
+- when:
++- import_tasks: formats.yml
++- import_tasks: missing_hostkey.yml
++- import_tasks: missing_hostkey_acceptnew.yml
++- import_tasks: no-destination.yml
++- import_tasks: specific-revision.yml
++- import_tasks: submodules.yml
++- import_tasks: change-repo-url.yml
++- import_tasks: depth.yml
++- import_tasks: single-branch.yml
++- import_tasks: checkout-new-tag.yml
++- include_tasks: gpg-verification.yml
++ when:
+ - not gpg_version.stderr
+ - gpg_version.stdout
+ - not (ansible_os_family == 'RedHat' and ansible_distribution_major_version is version('7', '<'))
+- - import_tasks: localmods.yml
+- - import_tasks: reset-origin.yml
+- - import_tasks: ambiguous-ref.yml
+- - import_tasks: archive.yml
+- - import_tasks: separate-git-dir.yml
+- - import_tasks: forcefully-fetch-tag.yml
+- environment:
+- HOME: >-
+- {{ remote_tmp_dir }}
++- import_tasks: localmods.yml
++- import_tasks: reset-origin.yml
++- import_tasks: ambiguous-ref.yml
++- import_tasks: archive.yml
++- import_tasks: separate-git-dir.yml
++- import_tasks: forcefully-fetch-tag.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/git/tasks/missing_hostkey.yml
++++ ansible-core-2.16.5/test/integration/targets/git/tasks/missing_hostkey.yml
+@@ -35,8 +35,7 @@
+ git:
+ repo: '{{ repo_format3 }}'
+ dest: '{{ checkout_dir }}'
+- version: >-
+- {{ git_default_branch }}
++ version: 'master'
+ accept_hostkey: false # should already have been accepted
+ key_file: '{{ github_ssh_private_key }}'
+ ssh_opts: '-o UserKnownHostsFile={{ remote_tmp_dir }}/known_hosts'
+--- ansible-core-2.16.5.orig/test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml
++++ ansible-core-2.16.5/test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml
+@@ -55,8 +55,7 @@
+ git:
+ repo: '{{ repo_format3 }}'
+ dest: '{{ checkout_dir }}'
+- version: >-
+- {{ git_default_branch }}
++ version: 'master'
+ accept_newhostkey: false # should already have been accepted
+ key_file: '{{ github_ssh_private_key }}'
+ ssh_opts: '-o UserKnownHostsFile={{ remote_tmp_dir }}/known_hosts'
+--- ansible-core-2.16.5.orig/test/integration/targets/git/tasks/reset-origin.yml
++++ ansible-core-2.16.5/test/integration/targets/git/tasks/reset-origin.yml
+@@ -12,14 +12,7 @@
+ state: directory
+
+ - name: RESET-ORIGIN | Initialise the repo with a file named origin,see github.com/ansible/ansible/pull/22502
+- shell: |
+- set -eEu
+-
+- git init
+-
+- echo "PR 22502" > origin
+- git add origin
+- git commit -m "PR 22502"
++ shell: git init; echo "PR 22502" > origin; git add origin; git commit -m "PR 22502"
+ args:
+ chdir: "{{ repo_dir }}/origin"
+
+--- ansible-core-2.16.5.orig/test/integration/targets/git/tasks/setup-local-repos.yml
++++ ansible-core-2.16.5/test/integration/targets/git/tasks/setup-local-repos.yml
+@@ -9,32 +9,15 @@
+ - "{{ repo_dir }}/tag_force_push"
+
+ - name: SETUP-LOCAL-REPOS | prepare minimal git repo
+- shell: |
+- set -eEu
+-
+- git init
+-
+- echo "1" > a
+- git add a
+- git commit -m "1"
++ shell: git init; echo "1" > a; git add a; git commit -m "1"
+ args:
+ chdir: "{{ repo_dir }}/minimal"
+
+ - name: SETUP-LOCAL-REPOS | prepare git repo for shallow clone
+ shell: |
+- set -eEu
+-
+- git init
+-
+- echo "1" > a
+- git add a
+- git commit -m "1"
+- git tag earlytag
+- git branch earlybranch
+-
+- echo "2" > a
+- git add a
+- git commit -m "2"
++ git init;
++ echo "1" > a; git add a; git commit -m "1"; git tag earlytag; git branch earlybranch;
++ echo "2" > a; git add a; git commit -m "2";
+ args:
+ chdir: "{{ repo_dir }}/shallow"
+
+@@ -46,10 +29,7 @@
+
+ - name: SETUP-LOCAL-REPOS | prepare tmp git repo with two branches
+ shell: |
+- set -eEu
+-
+ git init
+-
+ echo "1" > a; git add a; git commit -m "1"
+ git checkout -b test_branch; echo "2" > a; git commit -m "2 on branch" a
+ git checkout -b new_branch; echo "3" > a; git commit -m "3 on new branch" a
+@@ -60,9 +40,6 @@
+ # We make the repo here for consistency with the other repos,
+ # but we finish setting it up in forcefully-fetch-tag.yml.
+ - name: SETUP-LOCAL-REPOS | prepare tag_force_push git repo
+- shell: |
+- set -eEu
+-
+- git init --bare
++ shell: git init --bare
+ args:
+ chdir: "{{ repo_dir }}/tag_force_push"
+--- ansible-core-2.16.5.orig/test/integration/targets/git/tasks/setup.yml
++++ ansible-core-2.16.5/test/integration/targets/git/tasks/setup.yml
+@@ -28,44 +28,10 @@
+ register: gpg_version
+
+ - name: SETUP | set git global user.email if not already set
+- shell: git config --global user.email 'noreply@example.com'
++ shell: git config --global user.email || git config --global user.email "noreply@example.com"
+
+ - name: SETUP | set git global user.name if not already set
+- shell: git config --global user.name 'Ansible Test Runner'
+-
+-- name: SETUP | set git global init.defaultBranch
+- shell: >-
+- git config --global init.defaultBranch '{{ git_default_branch }}'
+-
+-- name: SETUP | set git global init.templateDir
+- # NOTE: This custom Git repository template emulates the `init.defaultBranch`
+- # NOTE: setting on Git versions below 2.28.
+- # NOTE: Ref: https://superuser.com/a/1559582.
+- # NOTE: Other workarounds mentioned there, like invoking
+- # NOTE: `git symbolic-ref HEAD refs/heads/main` after each `git init` turned
+- # NOTE: out to have mysterious side effects that break the tests in surprising
+- # NOTE: ways.
+- shell: |
+- set -eEu
+-
+- git config --global \
+- init.templateDir '{{ remote_tmp_dir }}/git-templates/git.git'
+-
+- mkdir -pv '{{ remote_tmp_dir }}/git-templates'
+- set +e
+- GIT_TEMPLATES_DIR=$(\
+- 2>/dev/null \
+- ls -1d \
+- '/Library/Developer/CommandLineTools/usr/share/git-core/templates' \
+- '/usr/local/share/git-core/templates' \
+- '/usr/share/git-core/templates' \
+- )
+- set -e
+- >&2 echo "Found Git's default templates directory: ${GIT_TEMPLATES_DIR}"
+- cp -r "${GIT_TEMPLATES_DIR}" '{{ remote_tmp_dir }}/git-templates/git.git'
+-
+- echo 'ref: refs/heads/{{ git_default_branch }}' \
+- > '{{ remote_tmp_dir }}/git-templates/git.git/HEAD'
++ shell: git config --global user.name || git config --global user.name "Ansible Test Runner"
+
+ - name: SETUP | create repo_dir
+ file:
+--- ansible-core-2.16.5.orig/test/integration/targets/git/tasks/single-branch.yml
++++ ansible-core-2.16.5/test/integration/targets/git/tasks/single-branch.yml
+@@ -52,8 +52,7 @@
+ repo: 'file://{{ repo_dir|expanduser }}/shallow_branches'
+ dest: '{{ checkout_dir }}'
+ single_branch: yes
+- version: >-
+- {{ git_default_branch }}
++ version: master
+ register: single_branch_3
+
+ - name: SINGLE_BRANCH | Clone example git repo using single_branch with version again
+@@ -61,8 +60,7 @@
+ repo: 'file://{{ repo_dir|expanduser }}/shallow_branches'
+ dest: '{{ checkout_dir }}'
+ single_branch: yes
+- version: >-
+- {{ git_default_branch }}
++ version: master
+ register: single_branch_4
+
+ - name: SINGLE_BRANCH | List revisions
+--- ansible-core-2.16.5.orig/test/integration/targets/git/tasks/specific-revision.yml
++++ ansible-core-2.16.5/test/integration/targets/git/tasks/specific-revision.yml
+@@ -162,14 +162,7 @@
+ path: "{{ checkout_dir }}"
+
+ - name: SPECIFIC-REVISION | prepare origina repo
+- shell: |
+- set -eEu
+-
+- git init
+-
+- echo "1" > a
+- git add a
+- git commit -m "1"
++ shell: git init; echo "1" > a; git add a; git commit -m "1"
+ args:
+ chdir: "{{ checkout_dir }}"
+
+@@ -198,14 +191,7 @@
+ force: yes
+
+ - name: SPECIFIC-REVISION | create new commit in original
+- shell: |
+- set -eEu
+-
+- git init
+-
+- echo "2" > b
+- git add b
+- git commit -m "2"
++ shell: git init; echo "2" > b; git add b; git commit -m "2"
+ args:
+ chdir: "{{ checkout_dir }}"
+
+--- ansible-core-2.16.5.orig/test/integration/targets/git/vars/main.yml
++++ ansible-core-2.16.5/test/integration/targets/git/vars/main.yml
+@@ -41,7 +41,6 @@ repo_update_url_2: 'https://github.com/a
+ known_host_files:
+ - "{{ lookup('env','HOME') }}/.ssh/known_hosts"
+ - '/etc/ssh/ssh_known_hosts'
+-git_default_branch: main
+ git_version_supporting_depth: 1.9.1
+ git_version_supporting_ls_remote: 1.7.5
+ git_version_supporting_single_branch: 1.7.10
+--- ansible-core-2.16.5.orig/test/integration/targets/group/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/group/tasks/main.yml
+@@ -16,4 +16,25 @@
+ # You should have received a copy of the GNU General Public License
+ # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+-- import_tasks: tests.yml
++- name: ensure test groups are deleted before the test
++ group:
++ name: '{{ item }}'
++ state: absent
++ loop:
++ - ansibullgroup
++ - ansibullgroup2
++ - ansibullgroup3
++
++- block:
++ - name: run tests
++ include_tasks: tests.yml
++
++ always:
++ - name: remove test groups after test
++ group:
++ name: '{{ item }}'
++ state: absent
++ loop:
++ - ansibullgroup
++ - ansibullgroup2
++ - ansibullgroup3
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/group/tasks/tests.yml
++++ ansible-core-2.16.5/test/integration/targets/group/tasks/tests.yml
+@@ -1,412 +1,343 @@
+ ---
+-- name: ensure test groups are deleted before the test
+- group:
+- name: '{{ item }}'
+- state: absent
+- loop:
+- - ansibullgroup
+- - ansibullgroup2
+- - ansibullgroup3
++##
++## group add
++##
+
+-- block:
+- ##
+- ## group add
+- ##
++- name: create group (check mode)
++ group:
++ name: ansibullgroup
++ state: present
++ register: create_group_check
++ check_mode: True
++
++- name: get result of create group (check mode)
++ script: 'grouplist.sh "{{ ansible_distribution }}"'
++ register: create_group_actual_check
++
++- name: assert create group (check mode)
++ assert:
++ that:
++ - create_group_check is changed
++ - '"ansibullgroup" not in create_group_actual_check.stdout_lines'
+
+- - name: create group (check mode)
+- group:
+- name: ansibullgroup
+- state: present
+- register: create_group_check
+- check_mode: true
++- name: create group
++ group:
++ name: ansibullgroup
++ state: present
++ register: create_group
++
++- name: get result of create group
++ script: 'grouplist.sh "{{ ansible_distribution }}"'
++ register: create_group_actual
++
++- name: assert create group
++ assert:
++ that:
++ - create_group is changed
++ - create_group.gid is defined
++ - '"ansibullgroup" in create_group_actual.stdout_lines'
+
+- - name: get result of create group (check mode)
+- script: 'grouplist.sh "{{ ansible_distribution }}"'
+- register: create_group_actual_check
++- name: create group (idempotent)
++ group:
++ name: ansibullgroup
++ state: present
++ register: create_group_again
++
++- name: assert create group (idempotent)
++ assert:
++ that:
++ - not create_group_again is changed
++
++##
++## group check
++##
+
+- - name: assert create group (check mode)
+- assert:
+- that:
+- - create_group_check is changed
+- - '"ansibullgroup" not in create_group_actual_check.stdout_lines'
++- name: run existing group check tests
++ group:
++ name: "{{ create_group_actual.stdout_lines|random }}"
++ state: present
++ with_sequence: start=1 end=5
++ register: group_test1
++
++- name: validate results for testcase 1
++ assert:
++ that:
++ - group_test1.results is defined
++ - group_test1.results|length == 5
++
++- name: validate change results for testcase 1
++ assert:
++ that:
++ - not group_test1 is changed
++
++##
++## group add with gid
++##
++
++- name: get the next available gid
++ script: gidget.py
++ args:
++ executable: '{{ ansible_python_interpreter }}'
++ register: gid
+
+- - name: create group
+- group:
+- name: ansibullgroup
+- state: present
+- register: create_group
++- name: create a group with a gid (check mode)
++ group:
++ name: ansibullgroup2
++ gid: '{{ gid.stdout_lines[0] }}'
++ state: present
++ register: create_group_gid_check
++ check_mode: True
++
++- name: get result of create a group with a gid (check mode)
++ script: 'grouplist.sh "{{ ansible_distribution }}"'
++ register: create_group_gid_actual_check
++
++- name: assert create group with a gid (check mode)
++ assert:
++ that:
++ - create_group_gid_check is changed
++ - '"ansibullgroup2" not in create_group_gid_actual_check.stdout_lines'
+
+- - name: get result of create group
+- script: 'grouplist.sh "{{ ansible_distribution }}"'
+- register: create_group_actual
++- name: create a group with a gid
++ group:
++ name: ansibullgroup2
++ gid: '{{ gid.stdout_lines[0] }}'
++ state: present
++ register: create_group_gid
++
++- name: get gid of created group
++ command: "{{ ansible_python_interpreter | quote }} -c \"import grp; print(grp.getgrnam('ansibullgroup2').gr_gid)\""
++ register: create_group_gid_actual
++
++- name: assert create group with a gid
++ assert:
++ that:
++ - create_group_gid is changed
++ - create_group_gid.gid | int == gid.stdout_lines[0] | int
++ - create_group_gid_actual.stdout | trim | int == gid.stdout_lines[0] | int
+
+- - name: assert create group
+- assert:
+- that:
+- - create_group is changed
+- - create_group.gid is defined
+- - '"ansibullgroup" in create_group_actual.stdout_lines'
++- name: create a group with a gid (idempotent)
++ group:
++ name: ansibullgroup2
++ gid: '{{ gid.stdout_lines[0] }}'
++ state: present
++ register: create_group_gid_again
++
++- name: assert create group with a gid (idempotent)
++ assert:
++ that:
++ - not create_group_gid_again is changed
++ - create_group_gid_again.gid | int == gid.stdout_lines[0] | int
+
+- - name: create group (idempotent)
++- block:
++ - name: create a group with a non-unique gid
+ group:
+- name: ansibullgroup
++ name: ansibullgroup3
++ gid: '{{ gid.stdout_lines[0] }}'
++ non_unique: true
+ state: present
+- register: create_group_again
++ register: create_group_gid_non_unique
+
+- - name: assert create group (idempotent)
+- assert:
+- that:
+- - not create_group_again is changed
+-
+- ##
+- ## group check
+- ##
+-
+- - name: run existing group check tests
++ - name: validate gid required with non_unique
+ group:
+- name: "{{ create_group_actual.stdout_lines|random }}"
+- state: present
+- with_sequence: start=1 end=5
+- register: group_test1
++ name: foo
++ non_unique: true
++ register: missing_gid
++ ignore_errors: true
+
+- - name: validate results for testcase 1
++ - name: assert create group with a non unique gid
+ assert:
+ that:
+- - group_test1.results is defined
+- - group_test1.results|length == 5
++ - create_group_gid_non_unique is changed
++ - create_group_gid_non_unique.gid | int == gid.stdout_lines[0] | int
++ - missing_gid is failed
++ when: ansible_facts.distribution not in ['MacOSX', 'Alpine']
+
+- - name: validate change results for testcase 1
+- assert:
+- that:
+- - not group_test1 is changed
+-
+- ##
+- ## group add with gid
+- ##
+-
+- - name: get the next available gid
+- script: get_free_gid.py
+- args:
+- executable: '{{ ansible_python_interpreter }}'
+- register: gid
++##
++## group remove
++##
+
+- - name: create a group with a gid (check mode)
+- group:
+- name: ansibullgroup2
+- gid: '{{ gid.stdout_lines[0] }}'
+- state: present
+- register: create_group_gid_check
+- check_mode: true
+-
+- - name: get result of create a group with a gid (check mode)
+- script: 'grouplist.sh "{{ ansible_distribution }}"'
+- register: create_group_gid_actual_check
+-
+- - name: assert create group with a gid (check mode)
+- assert:
+- that:
+- - create_group_gid_check is changed
+- - '"ansibullgroup2" not in create_group_gid_actual_check.stdout_lines'
+-
+- - name: create a group with a gid
+- group:
+- name: ansibullgroup2
+- gid: '{{ gid.stdout_lines[0] }}'
+- state: present
+- register: create_group_gid
++- name: delete group (check mode)
++ group:
++ name: ansibullgroup
++ state: absent
++ register: delete_group_check
++ check_mode: True
+
+- - name: get gid of created group
+- script: "get_gid_for_group.py ansibullgroup2"
+- args:
+- executable: '{{ ansible_python_interpreter }}'
+- register: create_group_gid_actual
++- name: get result of delete group (check mode)
++ script: grouplist.sh "{{ ansible_distribution }}"
++ register: delete_group_actual_check
++
++- name: assert delete group (check mode)
++ assert:
++ that:
++ - delete_group_check is changed
++ - '"ansibullgroup" in delete_group_actual_check.stdout_lines'
+
+- - name: assert create group with a gid
+- assert:
+- that:
+- - create_group_gid is changed
+- - create_group_gid.gid | int == gid.stdout_lines[0] | int
+- - create_group_gid_actual.stdout | trim | int == gid.stdout_lines[0] | int
++- name: delete group
++ group:
++ name: ansibullgroup
++ state: absent
++ register: delete_group
+
+- - name: create a group with a gid (idempotent)
+- group:
+- name: ansibullgroup2
+- gid: '{{ gid.stdout_lines[0] }}'
+- state: present
+- register: create_group_gid_again
++- name: get result of delete group
++ script: grouplist.sh "{{ ansible_distribution }}"
++ register: delete_group_actual
++
++- name: assert delete group
++ assert:
++ that:
++ - delete_group is changed
++ - '"ansibullgroup" not in delete_group_actual.stdout_lines'
+
+- - name: assert create group with a gid (idempotent)
+- assert:
+- that:
+- - not create_group_gid_again is changed
+- - create_group_gid_again.gid | int == gid.stdout_lines[0] | int
++- name: delete group (idempotent)
++ group:
++ name: ansibullgroup
++ state: absent
++ register: delete_group_again
+
+- - block:
+- - name: create a group with a non-unique gid
+- group:
+- name: ansibullgroup3
+- gid: '{{ gid.stdout_lines[0] }}'
+- non_unique: true
+- state: present
+- register: create_group_gid_non_unique
+-
+- - name: validate gid required with non_unique
+- group:
+- name: foo
+- non_unique: true
+- register: missing_gid
+- ignore_errors: true
+-
+- - name: assert create group with a non unique gid
+- assert:
+- that:
+- - create_group_gid_non_unique is changed
+- - create_group_gid_non_unique.gid | int == gid.stdout_lines[0] | int
+- - missing_gid is failed
+- when: ansible_facts.distribution not in ['MacOSX', 'Alpine']
+-
+- ##
+- ## group remove
+- ##
++- name: assert delete group (idempotent)
++ assert:
++ that:
++ - not delete_group_again is changed
++
++- name: Ensure lgroupadd is present
++ action: "{{ ansible_facts.pkg_mgr }}"
++ args:
++ name: libuser
++ state: present
++ when: ansible_facts.system in ['Linux'] and ansible_distribution != 'Alpine' and ansible_os_family != 'Suse'
++ tags:
++ - user_test_local_mode
++
++- name: Ensure lgroupadd is present - Alpine
++ command: apk add -U libuser
++ when: ansible_distribution == 'Alpine'
++ tags:
++ - user_test_local_mode
+
+- - name: delete group (check mode)
++# https://github.com/ansible/ansible/issues/56481
++- block:
++ - name: Test duplicate GID with local=yes
++ group:
++ name: "{{ item }}"
++ gid: 1337
++ local: yes
++ loop:
++ - group1_local_test
++ - group2_local_test
++ ignore_errors: yes
++ register: local_duplicate_gid_result
++
++ - assert:
++ that:
++ - local_duplicate_gid_result['results'][0] is success
++ - local_duplicate_gid_result['results'][1]['msg'] == "GID '1337' already exists with group 'group1_local_test'"
++ always:
++ - name: Cleanup
+ group:
+- name: ansibullgroup
++ name: group1_local_test
+ state: absent
+- register: delete_group_check
+- check_mode: true
++ # only applicable to Linux, limit further to CentOS where 'luseradd' is installed
++ when: ansible_distribution == 'CentOS'
+
+- - name: get result of delete group (check mode)
+- script: 'grouplist.sh "{{ ansible_distribution }}"'
+- register: delete_group_actual_check
++# https://github.com/ansible/ansible/pull/59769
++- block:
++ - name: create a local group with a gid
++ group:
++ name: group1_local_test
++ gid: 1337
++ local: yes
++ state: present
++ register: create_local_group_gid
++
++ - name: get gid of created local group
++ command: "{{ ansible_python_interpreter | quote }} -c \"import grp; print(grp.getgrnam('group1_local_test').gr_gid)\""
++ register: create_local_group_gid_actual
++
++ - name: assert create local group with a gid
++ assert:
++ that:
++ - create_local_group_gid is changed
++ - create_local_group_gid.gid | int == 1337 | int
++ - create_local_group_gid_actual.stdout | trim | int == 1337 | int
++
++ - name: create a local group with a gid (idempotent)
++ group:
++ name: group1_local_test
++ gid: 1337
++ state: present
++ register: create_local_group_gid_again
+
+- - name: assert delete group (check mode)
+- assert:
++ - name: assert create local group with a gid (idempotent)
++ assert:
+ that:
+- - delete_group_check is changed
+- - '"ansibullgroup" in delete_group_actual_check.stdout_lines'
+-
+- - name: delete group
++ - not create_local_group_gid_again is changed
++ - create_local_group_gid_again.gid | int == 1337 | int
++ always:
++ - name: Cleanup create local group with a gid
+ group:
+- name: ansibullgroup
++ name: group1_local_test
+ state: absent
+- register: delete_group
++ # only applicable to Linux, limit further to CentOS where 'luseradd' is installed
++ when: ansible_distribution == 'CentOS'
+
+- - name: get result of delete group
+- script: 'grouplist.sh "{{ ansible_distribution }}"'
+- register: delete_group_actual
++# https://github.com/ansible/ansible/pull/59772
++- block:
++ - name: create group with a gid
++ group:
++ name: group1_test
++ gid: 1337
++ local: no
++ state: present
++ register: create_group_gid
++
++ - name: get gid of created group
++ command: "{{ ansible_python_interpreter | quote }} -c \"import grp; print(grp.getgrnam('group1_test').gr_gid)\""
++ register: create_group_gid_actual
++
++ - name: assert create group with a gid
++ assert:
++ that:
++ - create_group_gid is changed
++ - create_group_gid.gid | int == 1337 | int
++ - create_group_gid_actual.stdout | trim | int == 1337 | int
++
++ - name: create local group with the same gid
++ group:
++ name: group1_test
++ gid: 1337
++ local: yes
++ state: present
++ register: create_local_group_gid
+
+- - name: assert delete group
+- assert:
++ - name: assert create local group with a gid
++ assert:
+ that:
+- - delete_group is changed
+- - '"ansibullgroup" not in delete_group_actual.stdout_lines'
+-
+- - name: delete group (idempotent)
++ - create_local_group_gid.gid | int == 1337 | int
++ always:
++ - name: Cleanup create group with a gid
+ group:
+- name: ansibullgroup
++ name: group1_test
++ local: no
+ state: absent
+- register: delete_group_again
+-
+- - name: assert delete group (idempotent)
+- assert:
+- that:
+- - not delete_group_again is changed
+-
+- - name: Ensure lgroupadd is present
+- action: "{{ ansible_facts.pkg_mgr }}"
+- args:
+- name: libuser
+- state: present
+- when: ansible_facts.system in ['Linux'] and ansible_distribution != 'Alpine' and ansible_os_family != 'Suse'
+- tags:
+- - user_test_local_mode
+-
+- - name: Ensure lgroupadd is present - Alpine
+- command: apk add -U libuser
+- when: ansible_distribution == 'Alpine'
+- tags:
+- - user_test_local_mode
+-
+- # https://github.com/ansible/ansible/issues/56481
+- - block:
+- - name: Test duplicate GID with local=yes
+- group:
+- name: "{{ item }}"
+- gid: 1337
+- local: true
+- loop:
+- - group1_local_test
+- - group2_local_test
+- ignore_errors: true
+- register: local_duplicate_gid_result
+-
+- - assert:
+- that:
+- - local_duplicate_gid_result['results'][0] is success
+- - local_duplicate_gid_result['results'][1]['msg'] == "GID '1337' already exists with group 'group1_local_test'"
+- always:
+- - name: Cleanup
+- group:
+- name: group1_local_test
+- state: absent
+- # only applicable to Linux, limit further to CentOS where 'luseradd' is installed
+- when: ansible_distribution == 'CentOS'
+-
+- # https://github.com/ansible/ansible/pull/59769
+- - block:
+- - name: create a local group with a gid
+- group:
+- name: group1_local_test
+- gid: 1337
+- local: true
+- state: present
+- register: create_local_group_gid
+-
+- - name: get gid of created local group
+- script: "get_gid_for_group.py group1_local_test"
+- args:
+- executable: '{{ ansible_python_interpreter }}'
+- register: create_local_group_gid_actual
+-
+- - name: assert create local group with a gid
+- assert:
+- that:
+- - create_local_group_gid is changed
+- - create_local_group_gid.gid | int == 1337 | int
+- - create_local_group_gid_actual.stdout | trim | int == 1337 | int
+-
+- - name: create a local group with a gid (idempotent)
+- group:
+- name: group1_local_test
+- gid: 1337
+- state: present
+- register: create_local_group_gid_again
+-
+- - name: assert create local group with a gid (idempotent)
+- assert:
+- that:
+- - not create_local_group_gid_again is changed
+- - create_local_group_gid_again.gid | int == 1337 | int
+- always:
+- - name: Cleanup create local group with a gid
+- group:
+- name: group1_local_test
+- state: absent
+- # only applicable to Linux, limit further to CentOS where 'luseradd' is installed
+- when: ansible_distribution == 'CentOS'
+-
+- # https://github.com/ansible/ansible/pull/59772
+- - block:
+- - name: create group with a gid
+- group:
+- name: group1_test
+- gid: 1337
+- local: false
+- state: present
+- register: create_group_gid
+-
+- - name: get gid of created group
+- script: "get_gid_for_group.py group1_test"
+- args:
+- executable: '{{ ansible_python_interpreter }}'
+- register: create_group_gid_actual
+-
+- - name: assert create group with a gid
+- assert:
+- that:
+- - create_group_gid is changed
+- - create_group_gid.gid | int == 1337 | int
+- - create_group_gid_actual.stdout | trim | int == 1337 | int
+-
+- - name: create local group with the same gid
+- group:
+- name: group1_test
+- gid: 1337
+- local: true
+- state: present
+- register: create_local_group_gid
+-
+- - name: assert create local group with a gid
+- assert:
+- that:
+- - create_local_group_gid.gid | int == 1337 | int
+- always:
+- - name: Cleanup create group with a gid
+- group:
+- name: group1_test
+- local: false
+- state: absent
+- - name: Cleanup create local group with the same gid
+- group:
+- name: group1_test
+- local: true
+- state: absent
+- # only applicable to Linux, limit further to CentOS where 'lgroupadd' is installed
+- when: ansible_distribution == 'CentOS'
+-
+- # https://github.com/ansible/ansible/pull/78172
+- - block:
+- - name: Create a group
+- group:
+- name: groupdeltest
+- state: present
+-
+- - name: Create user with primary group of groupdeltest
+- user:
+- name: groupdeluser
+- group: groupdeltest
+- state: present
+-
+- - name: Show we can't delete the group usually
+- group:
+- name: groupdeltest
+- state: absent
+- ignore_errors: true
+- register: failed_delete
+-
+- - name: assert we couldn't delete the group
+- assert:
+- that:
+- - failed_delete is failed
+-
+- - name: force delete the group
+- group:
+- name: groupdeltest
+- force: true
+- state: absent
+-
+- always:
+- - name: Cleanup user
+- user:
+- name: groupdeluser
+- state: absent
+-
+- - name: Cleanup group
+- group:
+- name: groupdeltest
+- state: absent
+- when: ansible_distribution not in ["MacOSX", "Alpine", "FreeBSD"]
+-
+- # create system group
+-
+- - name: remove group
++ - name: Cleanup create local group with the same gid
+ group:
+- name: ansibullgroup
++ name: group1_test
++ local: yes
+ state: absent
++ # only applicable to Linux, limit further to CentOS where 'lgroupadd' is installed
++ when: ansible_distribution == 'CentOS'
+
+- - name: create system group
+- group:
+- name: ansibullgroup
+- state: present
+- system: true
++# create system group
+
+- always:
+- - name: remove test groups after test
+- group:
+- name: '{{ item }}'
+- state: absent
+- loop:
+- - ansibullgroup
+- - ansibullgroup2
+- - ansibullgroup3
++- name: remove group
++ group:
++ name: ansibullgroup
++ state: absent
++
++- name: create system group
++ group:
++ name: ansibullgroup
++ state: present
++ system: yes
+--- ansible-core-2.16.5.orig/test/integration/targets/handlers/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/handlers/runme.sh
+@@ -50,9 +50,6 @@ for strategy in linear free; do
+ [ "$(ansible-playbook test_force_handlers.yml -i inventory.handlers -v "$@" --tags force_false_in_play --force-handlers \
+ | grep -E -o CALLED_HANDLER_. | sort | uniq | xargs)" = "CALLED_HANDLER_B" ]
+
+- # https://github.com/ansible/ansible/pull/80898
+- [ "$(ansible-playbook 80880.yml -i inventory.handlers -vv "$@" 2>&1)" ]
+-
+ unset ANSIBLE_STRATEGY
+
+ done
+@@ -69,9 +66,6 @@ done
+ # Notify handler listen
+ ansible-playbook test_handlers_listen.yml -i inventory.handlers -v "$@"
+
+-# https://github.com/ansible/ansible/issues/82363
+-ansible-playbook test_multiple_handlers_with_recursive_notification.yml -i inventory.handlers -v "$@"
+-
+ # Notify inexistent handlers results in error
+ set +e
+ result="$(ansible-playbook test_handlers_inexistent_notify.yml -i inventory.handlers "$@" 2>&1)"
+@@ -187,24 +181,3 @@ grep out.txt -e "ERROR! Using a block as
+
+ ansible-playbook test_block_as_handler-import.yml "$@" 2>&1 | tee out.txt
+ grep out.txt -e "ERROR! Using a block as a handler is not supported."
+-
+-ansible-playbook test_include_role_handler_once.yml -i inventory.handlers "$@" 2>&1 | tee out.txt
+-[ "$(grep out.txt -ce 'handler ran')" = "1" ]
+-
+-ansible-playbook test_listen_role_dedup.yml "$@" 2>&1 | tee out.txt
+-[ "$(grep out.txt -ce 'a handler from a role')" = "1" ]
+-
+-ansible localhost -m include_role -a "name=r1-dep_chain-vars" "$@"
+-
+-ansible-playbook test_include_tasks_in_include_role.yml "$@" 2>&1 | tee out.txt
+-[ "$(grep out.txt -ce 'handler ran')" = "1" ]
+-
+-ansible-playbook test_run_once.yml -i inventory.handlers "$@" 2>&1 | tee out.txt
+-[ "$(grep out.txt -ce 'handler ran once')" = "1" ]
+-
+-ansible-playbook 82241.yml -i inventory.handlers "$@" 2>&1 | tee out.txt
+-[ "$(grep out.txt -ce 'included_task_from_tasks_dir')" = "1" ]
+-
+-ansible-playbook nested_flush_handlers_failure_force.yml -i inventory.handlers "$@" 2>&1 | tee out.txt
+-[ "$(grep out.txt -ce 'flush_handlers_rescued')" = "1" ]
+-[ "$(grep out.txt -ce 'flush_handlers_always')" = "2" ]
+--- ansible-core-2.16.5.orig/test/integration/targets/include_vars/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/include_vars/tasks/main.yml
+@@ -208,21 +208,6 @@
+ - "config.key2.b == 22"
+ - "config.key3 == 3"
+
+-- name: Include a vars dir with hash variables
+- include_vars:
+- dir: "{{ role_path }}/vars2/hashes/"
+- hash_behaviour: merge
+-
+-- name: Verify that the hash is merged after vars files are accumulated
+- assert:
+- that:
+- - "config | length == 3"
+- - "config.key0 is undefined"
+- - "config.key1 == 1"
+- - "config.key2 | length == 1"
+- - "config.key2.b == 22"
+- - "config.key3 == 3"
+-
+ - include_vars:
+ file: no_auto_unsafe.yml
+ register: baz
+@@ -230,40 +215,3 @@
+ - assert:
+ that:
+ - baz.ansible_facts.foo|type_debug != "AnsibleUnsafeText"
+-
+-- name: setup test following symlinks
+- delegate_to: localhost
+- block:
+- - name: create directory to test following symlinks
+- file:
+- path: "{{ role_path }}/test_symlink"
+- state: directory
+-
+- - name: create symlink to the vars2 dir
+- file:
+- src: "{{ role_path }}/vars2"
+- dest: "{{ role_path }}/test_symlink/symlink"
+- state: link
+-
+-- name: include vars by following the symlink
+- include_vars:
+- dir: "{{ role_path }}/test_symlink"
+- register: follow_sym
+-
+-- assert:
+- that: follow_sym.ansible_included_var_files | sort == [hash1, hash2]
+- vars:
+- hash1: "{{ role_path }}/test_symlink/symlink/hashes/hash1.yml"
+- hash2: "{{ role_path }}/test_symlink/symlink/hashes/hash2.yml"
+-
+-- name: Test include_vars includes everything to the correct depth
+- ansible.builtin.include_vars:
+- dir: "{{ role_path }}/files/test_depth"
+- depth: 3
+- name: test_depth_var
+- register: test_depth
+-
+-- assert:
+- that:
+- - "test_depth.ansible_included_var_files|length == 8"
+- - "test_depth_var.keys()|length == 8"
+--- ansible-core-2.16.5.orig/test/integration/targets/include_vars/vars/services/service_vars.yml
++++ ansible-core-2.16.5/test/integration/targets/include_vars/vars/services/service_vars.yml
+@@ -1,2 +1,2 @@
+ ---
+-service_name: 'my_custom_service'
++service_name: 'my_custom_service'
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/include_vars/vars/services/service_vars_fqcn.yml
++++ ansible-core-2.16.5/test/integration/targets/include_vars/vars/services/service_vars_fqcn.yml
+@@ -1,3 +1,3 @@
+ ---
+ service_name_fqcn: 'my_custom_service'
+-service_name_tmpl_fqcn: '{{ service_name_fqcn }}'
++service_name_tmpl_fqcn: '{{ service_name_fqcn }}'
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/include_when_parent_is_dynamic/tasks.yml
++++ ansible-core-2.16.5/test/integration/targets/include_when_parent_is_dynamic/tasks.yml
+@@ -9,4 +9,4 @@
+
+ # perform an include task which should be static if all of the task's parents are static, otherwise it should be dynamic
+ # this file was loaded using include_tasks, which is dynamic, so this include should also be dynamic
+-- include_tasks: syntax_error.yml
++- include: syntax_error.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/include_when_parent_is_static/tasks.yml
++++ ansible-core-2.16.5/test/integration/targets/include_when_parent_is_static/tasks.yml
+@@ -9,4 +9,4 @@
+
+ # perform an include task which should be static if all of the task's parents are static, otherwise it should be dynamic
+ # this file was loaded using import_tasks, which is static, so this include should also be static
+-- import_tasks: syntax_error.yml
++- include: syntax_error.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/includes/include_on_playbook_should_fail.yml
++++ ansible-core-2.16.5/test/integration/targets/includes/include_on_playbook_should_fail.yml
+@@ -1 +1 @@
+-- include_tasks: test_includes3.yml
++- include: test_includes3.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/includes/roles/test_includes/handlers/main.yml
++++ ansible-core-2.16.5/test/integration/targets/includes/roles/test_includes/handlers/main.yml
+@@ -1 +1 @@
+-- import_tasks: more_handlers.yml
++- include: more_handlers.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/includes/roles/test_includes/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/includes/roles/test_includes/tasks/main.yml
+@@ -17,9 +17,47 @@
+ # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+
++- include: included_task1.yml a=1 b=2 c=3
++
++- name: verify non-variable include params
++ assert:
++ that:
++ - "ca == '1'"
++ - "cb == '2'"
++ - "cc == '3'"
++
++- set_fact:
++ a: 101
++ b: 102
++ c: 103
++
++- include: included_task1.yml a={{a}} b={{b}} c=103
++
++- name: verify variable include params
++ assert:
++ that:
++ - "ca == 101"
++ - "cb == 102"
++ - "cc == 103"
++
++# Test that strings are not turned into numbers
++- set_fact:
++ a: "101"
++ b: "102"
++ c: "103"
++
++- include: included_task1.yml a={{a}} b={{b}} c=103
++
++- name: verify variable include params
++ assert:
++ that:
++ - "ca == '101'"
++ - "cb == '102'"
++ - "cc == '103'"
++
+ # now try long form includes
+
+-- include_tasks: included_task1.yml
++- include: included_task1.yml
+ vars:
+ a: 201
+ b: 202
+--- ansible-core-2.16.5.orig/test/integration/targets/includes/roles/test_includes_free/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/includes/roles/test_includes_free/tasks/main.yml
+@@ -1,9 +1,9 @@
+ - name: this needs to be here
+ debug:
+ msg: "hello"
+-- include_tasks: inner.yml
++- include: inner.yml
+ with_items:
+ - '1'
+-- ansible.builtin.include_tasks: inner_fqcn.yml
++- ansible.builtin.include: inner_fqcn.yml
+ with_items:
+ - '1'
+--- ansible-core-2.16.5.orig/test/integration/targets/includes/roles/test_includes_host_pinned/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/includes/roles/test_includes_host_pinned/tasks/main.yml
+@@ -1,6 +1,6 @@
+ - name: this needs to be here
+ debug:
+ msg: "hello"
+-- include_tasks: inner.yml
++- include: inner.yml
+ with_items:
+ - '1'
+--- ansible-core-2.16.5.orig/test/integration/targets/includes/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/includes/runme.sh
+@@ -10,7 +10,7 @@ echo "EXPECTED ERROR: Ensure we fail if
+ set +e
+ result="$(ansible-playbook -i ../../inventory include_on_playbook_should_fail.yml -v "$@" 2>&1)"
+ set -e
+-grep -q "ERROR! 'include_tasks' is not a valid attribute for a Play" <<< "$result"
++grep -q "ERROR! 'include' is not a valid attribute for a Play" <<< "$result"
+
+ ansible-playbook includes_loop_rescue.yml --extra-vars strategy=linear "$@"
+ ansible-playbook includes_loop_rescue.yml --extra-vars strategy=free "$@"
+--- ansible-core-2.16.5.orig/test/integration/targets/includes/test_includes2.yml
++++ ansible-core-2.16.5/test/integration/targets/includes/test_includes2.yml
+@@ -13,8 +13,8 @@
+ - role: test_includes
+ tags: test_includes
+ tasks:
+- - include_tasks: roles/test_includes/tasks/not_a_role_task.yml
+- - include_tasks: roles/test_includes/tasks/empty.yml
++ - include: roles/test_includes/tasks/not_a_role_task.yml
++ - include: roles/test_includes/tasks/empty.yml
+ - assert:
+ that:
+ - "ca == 33000"
+--- ansible-core-2.16.5.orig/test/integration/targets/includes/test_includes3.yml
++++ ansible-core-2.16.5/test/integration/targets/includes/test_includes3.yml
+@@ -1,6 +1,6 @@
+ - hosts: testhost
+ tasks:
+- - include_tasks: test_includes4.yml
++ - include: test_includes4.yml
+ with_items: ["a"]
+ loop_control:
+ loop_var: r
+--- ansible-core-2.16.5.orig/test/integration/targets/inventory/inventory_plugins/contructed_with_hostvars.py
++++ ansible-core-2.16.5/test/integration/targets/inventory/inventory_plugins/contructed_with_hostvars.py
+@@ -14,7 +14,7 @@ DOCUMENTATION = '''
+ '''
+
+ from ansible.errors import AnsibleParserError
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
+
+
+--- ansible-core-2.16.5.orig/test/integration/targets/inventory_ini/inventory.ini
++++ ansible-core-2.16.5/test/integration/targets/inventory_ini/inventory.ini
+@@ -1,5 +1,3 @@
+-gitlab-runner-01 ansible_host=gitlab-runner-01.internal.example.net ansible_user=root
+-
+ [local]
+ testhost ansible_connection=local ansible_become=no ansible_become_user=ansibletest1
+
+--- ansible-core-2.16.5.orig/test/integration/targets/inventory_ini/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/inventory_ini/runme.sh
+@@ -3,6 +3,3 @@
+ set -eux
+
+ ansible-playbook -v -i inventory.ini test_ansible_become.yml
+-
+-ansible-inventory -v -i inventory.ini --list 2> out
+-test "$(grep -c 'SyntaxWarning' out)" -eq 0
+--- ansible-core-2.16.5.orig/test/integration/targets/iptables/aliases
++++ ansible-core-2.16.5/test/integration/targets/iptables/aliases
+@@ -1,4 +1,5 @@
+ shippable/posix/group2
+ skip/freebsd
++skip/osx
+ skip/macos
+ skip/docker
+--- ansible-core-2.16.5.orig/test/integration/targets/iptables/tasks/chain_management.yml
++++ ansible-core-2.16.5/test/integration/targets/iptables/tasks/chain_management.yml
+@@ -45,26 +45,6 @@
+ - result is not failed
+ - '"FOOBAR-CHAIN" in result.stdout'
+
+-- name: add rule to foobar chain
+- become: true
+- iptables:
+- chain: FOOBAR-CHAIN
+- source: 0.0.0.0
+- destination: 0.0.0.0
+- jump: DROP
+- comment: "FOOBAR-CHAIN RULE"
+-
+-- name: get the state of the iptable rules after rule is added to foobar chain
+- become: true
+- shell: "{{ iptables_bin }} -L"
+- register: result
+-
+-- name: assert rule is present in foobar chain
+- assert:
+- that:
+- - result is not failed
+- - '"FOOBAR-CHAIN RULE" in result.stdout'
+-
+ - name: flush the foobar chain
+ become: true
+ iptables:
+@@ -88,3 +68,4 @@
+ that:
+ - result is not failed
+ - '"FOOBAR-CHAIN" not in result.stdout'
++ - '"FOOBAR-RULE" not in result.stdout'
+--- ansible-core-2.16.5.orig/test/integration/targets/known_hosts/defaults/main.yml
++++ ansible-core-2.16.5/test/integration/targets/known_hosts/defaults/main.yml
+@@ -3,4 +3,4 @@ example_org_rsa_key: >
+ example.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAglyZmHHWskQ9wkh8LYbIqzvg99/oloneH7BaZ02ripJUy/2Zynv4tgUfm9fdXvAb1XXCEuTRnts9FBer87+voU0FPRgx3CfY9Sgr0FspUjnm4lqs53FIab1psddAaS7/F7lrnjl6VqBtPwMRQZG7qlml5uogGJwYJHxX0PGtsdoTJsM=
+
+ example_org_ed25519_key: >
+- example.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIzlnSq5ESxLgW0avvPk3j7zLV59hcAPkxrMNdnZMKP2
++ example.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIzlnSq5ESxLgW0avvPk3j7zLV59hcAPkxrMNdnZMKP2
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/known_hosts/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/known_hosts/tasks/main.yml
+@@ -99,7 +99,7 @@
+ # https://github.com/ansible/ansible/issues/78598
+ # test removing nonexistent host key when the other keys exist for the host
+ - name: remove different key
+- known_hosts:
++ known_hosts:
+ name: example.org
+ key: "{{ example_org_ed25519_key }}"
+ state: absent
+--- ansible-core-2.16.5.orig/test/integration/targets/lookup_config/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/lookup_config/tasks/main.yml
+@@ -42,7 +42,6 @@
+ - name: remote user and port for ssh connection
+ set_fact:
+ ssh_user_and_port: '{{q("config", "remote_user", "port", plugin_type="connection", plugin_name="ssh")}}'
+- ssh_user_and_port_and_origin: '{{q("config", "remote_user", "port", plugin_type="connection", plugin_name="ssh", show_origin=True)}}'
+ vars:
+ ansible_ssh_user: lola
+ ansible_ssh_port: 2022
+@@ -72,5 +71,4 @@
+ - lookup_config_7 is failed
+ - '"Invalid setting" in lookup_config_7.msg'
+ - ssh_user_and_port == ['lola', 2022]
+- - "ssh_user_and_port_and_origin == [['lola', 'var: ansible_ssh_user'], [2022, 'var: ansible_ssh_port']]"
+ - yolo_remote == ["yolo"]
+--- ansible-core-2.16.5.orig/test/integration/targets/lookup_fileglob/issue72873/test.yml
++++ ansible-core-2.16.5/test/integration/targets/lookup_fileglob/issue72873/test.yml
+@@ -5,7 +5,7 @@
+ dir: files
+ tasks:
+ - file: path='{{ dir }}' state=directory
+-
++
+ - file: path='setvars.bat' state=touch # in current directory!
+
+ - file: path='{{ dir }}/{{ item }}' state=touch
+@@ -20,11 +20,11 @@
+
+ - name: Get working order results and sort them
+ set_fact:
+- working: '{{ query("fileglob", "setvars.bat", dir ~ "/*.[ch]") | sort }}'
++ working: '{{ query("fileglob", "setvars.bat", "{{ dir }}/*.[ch]") | sort }}'
+
+ - name: Get broken order results and sort them
+ set_fact:
+- broken: '{{ query("fileglob", dir ~ "/*.[ch]", "setvars.bat") | sort }}'
++ broken: '{{ query("fileglob", "{{ dir }}/*.[ch]", "setvars.bat") | sort }}'
+
+ - assert:
+ that:
+--- ansible-core-2.16.5.orig/test/integration/targets/lookup_first_found/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/lookup_first_found/tasks/main.yml
+@@ -94,56 +94,3 @@
+ - assert:
+ that:
+ - foo is defined
+-
+-# TODO: no 'terms' test
+-- name: test first_found lookup with no terms
+- set_fact:
+- no_terms: "{{ query('first_found', files=['missing1', 'hosts', 'missing2'], paths=['/etc'], errors='ignore') }}"
+-
+-- assert:
+- that: "no_terms|first == '/etc/hosts'"
+-
+-- name: handle templatable dictionary entries
+- block:
+-
+- - name: Load variables specific for OS family
+- assert:
+- that:
+- - "item is file"
+- - "item|basename == 'itworks.yml'"
+- with_first_found:
+- - files:
+- - "{{ansible_id}}-{{ansible_lsb.major_release}}.yml" # invalid var, should be skipped
+- - "{{ansible_lsb.id}}-{{ansible_lsb.major_release}}.yml" # does not exist, but should try
+- - "{{ansible_distribution}}-{{ansible_distribution_major_version}}.yml" # does not exist, but should try
+- - itworks.yml
+- - ishouldnotbefound.yml # this exist, but should not be found
+- paths:
+- - "{{role_path}}/vars"
+-
+- - name: Load variables specific for OS family, but now as list of dicts, same options as above
+- assert:
+- that:
+- - "item is file"
+- - "item|basename == 'itworks.yml'"
+- with_first_found:
+- - files:
+- - "{{ansible_id}}-{{ansible_lsb.major_release}}.yml"
+- paths:
+- - "{{role_path}}/vars"
+- - files:
+- - "{{ansible_lsb.id}}-{{ansible_lsb.major_release}}.yml"
+- paths:
+- - "{{role_path}}/vars"
+- - files:
+- - "{{ansible_distribution}}-{{ansible_distribution_major_version}}.yml"
+- paths:
+- - "{{role_path}}/vars"
+- - files:
+- - itworks.yml
+- paths:
+- - "{{role_path}}/vars"
+- - files:
+- - ishouldnotbefound.yml
+- paths:
+- - "{{role_path}}/vars"
+--- ansible-core-2.16.5.orig/test/integration/targets/lookup_sequence/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/lookup_sequence/tasks/main.yml
+@@ -195,4 +195,4 @@
+ - ansible_failed_task.name == "EXPECTED FAILURE - test bad format string message"
+ - ansible_failed_result.msg == expected
+ vars:
+- expected: "bad formatting string: d"
++ expected: "bad formatting string: d"
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/lookup_together/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/lookup_together/tasks/main.yml
+@@ -26,4 +26,4 @@
+ - assert:
+ that:
+ - ansible_failed_task.name == "EXPECTED FAILURE - test empty list"
+- - ansible_failed_result.msg == "with_together requires at least one element in each list"
++ - ansible_failed_result.msg == "with_together requires at least one element in each list"
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/lookup_url/aliases
++++ ansible-core-2.16.5/test/integration/targets/lookup_url/aliases
+@@ -1,11 +1,4 @@
+ destructive
+ shippable/posix/group3
+ needs/httptester
+-skip/macos # This test crashes Python due to https://wefearchange.org/2018/11/forkmacos.rst.html
+-# Example failure:
+-#
+-# TASK [lookup_url : Test that retrieving a url works] ***************************
+-# objc[15394]: +[__NSCFConstantString initialize] may have been in progress in another thread when fork() was called.
+-# objc[15394]: +[__NSCFConstantString initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in t
+-# he fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug.
+-# ERROR! A worker was found in a dead state
++skip/macos/12.0 # This test crashes Python due to https://wefearchange.org/2018/11/forkmacos.rst.html
+--- ansible-core-2.16.5.orig/test/integration/targets/lookup_url/meta/main.yml
++++ ansible-core-2.16.5/test/integration/targets/lookup_url/meta/main.yml
+@@ -1,2 +1,2 @@
+-dependencies:
++dependencies:
+ - prepare_http_tests
+--- ansible-core-2.16.5.orig/test/integration/targets/lookup_url/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/lookup_url/tasks/main.yml
+@@ -1,6 +1,6 @@
+ - name: Test that retrieving a url works
+ set_fact:
+- web_data: "{{ lookup('url', 'https://' ~ httpbin_host ~ '/get?one') }}"
++ web_data: "{{ lookup('url', 'https://{{ httpbin_host }}/get?one') }}"
+
+ - name: Assert that the url was retrieved
+ assert:
+@@ -9,7 +9,7 @@
+
+ - name: Test that retrieving a url with invalid cert fails
+ set_fact:
+- web_data: "{{ lookup('url', 'https://' ~ badssl_host ~ '/') }}"
++ web_data: "{{ lookup('url', 'https://{{ badssl_host }}/') }}"
+ ignore_errors: True
+ register: url_invalid_cert
+
+@@ -20,12 +20,12 @@
+
+ - name: Test that retrieving a url with invalid cert with validate_certs=False works
+ set_fact:
+- web_data: "{{ lookup('url', 'https://' ~ badssl_host ~ '/', validate_certs=False) }}"
++ web_data: "{{ lookup('url', 'https://{{ badssl_host }}/', validate_certs=False) }}"
+ register: url_no_validate_cert
+
+ - assert:
+ that:
+- - badssl_host_substring in web_data
++ - "'{{ badssl_host_substring }}' in web_data"
+
+ - vars:
+ url: https://{{ httpbin_host }}/get
+@@ -52,27 +52,3 @@
+
+ - name: Test use_netrc=False
+ import_tasks: use_netrc.yml
+-
+-- vars:
+- ansible_lookup_url_agent: ansible-test-lookup-url-agent
+- block:
+- - name: Test user agent
+- set_fact:
+- web_data: "{{ lookup('url', 'https://' ~ httpbin_host ~ '/user-agent') }}"
+-
+- - name: Assert that user agent is set
+- assert:
+- that:
+- - ansible_lookup_url_agent in web_data['user-agent']
+-
+-- vars:
+- ansible_lookup_url_force_basic_auth: yes
+- block:
+- - name: Test force basic auth
+- set_fact:
+- web_data: "{{ lookup('url', 'https://' ~ httpbin_host ~ '/headers', username='abc') }}"
+-
+- - name: Assert that Authorization header is set
+- assert:
+- that:
+- - "'Authorization' in web_data.headers"
+--- ansible-core-2.16.5.orig/test/integration/targets/lookup_url/tasks/use_netrc.yml
++++ ansible-core-2.16.5/test/integration/targets/lookup_url/tasks/use_netrc.yml
+@@ -10,7 +10,7 @@
+
+ - name: test Url lookup with ~/.netrc forced Basic auth
+ set_fact:
+- web_data: "{{ lookup('ansible.builtin.url', 'https://' ~ httpbin_host ~ '/bearer', headers={'Authorization':'Bearer foobar'}) }}"
++ web_data: "{{ lookup('ansible.builtin.url', 'https://{{ httpbin_host }}/bearer', headers={'Authorization':'Bearer foobar'}) }}"
+ ignore_errors: yes
+
+ - name: assert test Url lookup with ~/.netrc forced Basic auth
+@@ -18,11 +18,11 @@
+ that:
+ - "web_data.token.find('v=' ~ 'Zm9vOmJhcg==') == -1"
+ fail_msg: "Was expecting 'foo:bar' in base64, but received: {{ web_data }}"
+- success_msg: "Expected Basic authentication even Bearer headers were sent"
++ success_msg: "Expected Basic authentication even Bearer headers were sent"
+
+ - name: test Url lookup with use_netrc=False
+ set_fact:
+- web_data: "{{ lookup('ansible.builtin.url', 'https://' ~ httpbin_host ~ '/bearer', headers={'Authorization':'Bearer foobar'}, use_netrc='False') }}"
++ web_data: "{{ lookup('ansible.builtin.url', 'https://{{ httpbin_host }}/bearer', headers={'Authorization':'Bearer foobar'}, use_netrc='False') }}"
+
+ - name: assert test Url lookup with netrc=False used Bearer authentication
+ assert:
+@@ -34,4 +34,4 @@
+ - name: Clean up. Removing ~/.netrc
+ file:
+ path: ~/.netrc
+- state: absent
++ state: absent
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/loop-connection/collections/ansible_collections/ns/name/meta/runtime.yml
++++ ansible-core-2.16.5/test/integration/targets/loop-connection/collections/ansible_collections/ns/name/meta/runtime.yml
+@@ -1,4 +1,4 @@
+ plugin_routing:
+ connection:
+ redirected_dummy:
+- redirect: ns.name.dummy
++ redirect: ns.name.dummy
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/loop-connection/main.yml
++++ ansible-core-2.16.5/test/integration/targets/loop-connection/main.yml
+@@ -30,4 +30,4 @@
+ - assert:
+ that:
+ - connected_test.results[0].stderr == "ran - 1"
+- - connected_test.results[1].stderr == "ran - 2"
++ - connected_test.results[1].stderr == "ran - 2"
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/missing_required_lib/library/missing_required_lib.py
++++ ansible-core-2.16.5/test/integration/targets/missing_required_lib/library/missing_required_lib.py
+@@ -8,7 +8,7 @@ __metaclass__ = type
+ from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+
+ try:
+- import ansible_missing_lib # pylint: disable=unused-import
++ import ansible_missing_lib
+ HAS_LIB = True
+ except ImportError as e:
+ HAS_LIB = False
+--- ansible-core-2.16.5.orig/test/integration/targets/module_defaults/action_plugins/debug.py
++++ ansible-core-2.16.5/test/integration/targets/module_defaults/action_plugins/debug.py
+@@ -20,7 +20,7 @@ __metaclass__ = type
+
+ from ansible.errors import AnsibleUndefinedVariable
+ from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.plugins.action import ActionBase
+
+
+--- ansible-core-2.16.5.orig/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py
++++ ansible-core-2.16.5/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py
+@@ -5,6 +5,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ from ansible.plugins.action.normal import ActionModule as ActionBase
++from ansible.utils.vars import merge_hash
+
+
+ class ActionModule(ActionBase):
+--- ansible-core-2.16.5.orig/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py
++++ ansible-core-2.16.5/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py
+@@ -5,6 +5,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ from ansible.plugins.action.normal import ActionModule as ActionBase
++from ansible.utils.vars import merge_hash
+
+
+ class ActionModule(ActionBase):
+--- ansible-core-2.16.5.orig/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py
++++ ansible-core-2.16.5/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py
+@@ -5,6 +5,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ from ansible.plugins.action.normal import ActionModule as ActionBase
++from ansible.utils.vars import merge_hash
+
+
+ class ActionModule(ActionBase):
+--- ansible-core-2.16.5.orig/test/integration/targets/module_no_log/aliases
++++ ansible-core-2.16.5/test/integration/targets/module_no_log/aliases
+@@ -1,4 +1,5 @@
+ shippable/posix/group3
+ context/controller
+ skip/freebsd # not configured to log user.info to /var/log/syslog
++skip/osx # not configured to log user.info to /var/log/syslog
+ skip/macos # not configured to log user.info to /var/log/syslog
+--- ansible-core-2.16.5.orig/test/integration/targets/module_no_log/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/module_no_log/tasks/main.yml
+@@ -59,41 +59,3 @@
+ # 2) the AnsibleModule.log method is not working
+ - good_message in grep.stdout
+ - bad_message not in grep.stdout
+-
+-- name: Ensure we do not obscure what we should not
+- block:
+- - module_that_has_secret:
+- secret: u
+- notsecret: u
+- register: ouch
+- ignore_errors: true
+-
+- - name: no log wont obscure booleans when True, but still hide in msg
+- assert:
+- that:
+- - ouch['changed'] is boolean
+- - "'*' in ouch['msg']"
+-
+- - module_that_has_secret:
+- secret: a
+- notsecret: b
+- register: ouch
+- ignore_errors: true
+-
+- - name: no log wont obscure booleans when False, but still hide in msg
+- assert:
+- that:
+- - ouch['changed'] is boolean
+- - "'*' in ouch['msg']"
+-
+- - module_that_has_secret:
+- secret: True
+- notsecret: False
+- register: ouch
+- ignore_errors: true
+-
+- - name: no log does not hide bool values
+- assert:
+- that:
+- - ouch['changed'] is boolean
+- - "'*' not in ouch['msg']"
+--- ansible-core-2.16.5.orig/test/integration/targets/module_utils/library/test.py
++++ ansible-core-2.16.5/test/integration/targets/module_utils/library/test.py
+@@ -11,8 +11,8 @@ import ansible.module_utils.foo0
+ results['foo0'] = ansible.module_utils.foo0.data
+
+ # Test depthful import with no from
+-import ansible.module_utils.bar0.foo3
+-results['bar0'] = ansible.module_utils.bar0.foo3.data
++import ansible.module_utils.bar0.foo
++results['bar0'] = ansible.module_utils.bar0.foo.data
+
+ # Test import of module_utils/foo1.py
+ from ansible.module_utils import foo1
+@@ -72,12 +72,12 @@ from ansible.module_utils.spam8.ham impo
+ results['spam8'] = (bacon.data, eggs)
+
+ # Test that import of module_utils/qux1/quux.py using as works
+-from ansible.module_utils.qux1 import quux as two
+-results['qux1'] = two.data
++from ansible.module_utils.qux1 import quux as one
++results['qux1'] = one.data
+
+ # Test that importing qux2/quux.py and qux2/quuz.py using as works
+-from ansible.module_utils.qux2 import quux as three, quuz as four
+-results['qux2'] = (three.data, four.data)
++from ansible.module_utils.qux2 import quux as one, quuz as two
++results['qux2'] = (one.data, two.data)
+
+ # Test depth
+ from ansible.module_utils.a.b.c.d.e.f.g.h import data
+--- ansible-core-2.16.5.orig/test/integration/targets/module_utils/library/test_failure.py
++++ ansible-core-2.16.5/test/integration/targets/module_utils/library/test_failure.py
+@@ -6,9 +6,9 @@ results = {}
+ # Test that we are rooted correctly
+ # Following files:
+ # module_utils/yak/zebra/foo.py
+-from ansible.module_utils.zebra import foo4
++from ansible.module_utils.zebra import foo
+
+-results['zebra'] = foo4.data
++results['zebra'] = foo.data
+
+ from ansible.module_utils.basic import AnsibleModule
+ AnsibleModule(argument_spec=dict()).exit_json(**results)
+--- ansible-core-2.16.5.orig/test/integration/targets/module_utils/module_utils_test.yml
++++ ansible-core-2.16.5/test/integration/targets/module_utils/module_utils_test.yml
+@@ -47,7 +47,7 @@
+ assert:
+ that:
+ - result is failed
+- - result['msg'] == "Could not find imported module support code for ansible.modules.test_failure. Looked for (['ansible.module_utils.zebra.foo4', 'ansible.module_utils.zebra'])"
++ - result['msg'] == "Could not find imported module support code for ansible.modules.test_failure. Looked for (['ansible.module_utils.zebra.foo', 'ansible.module_utils.zebra'])"
+
+ - name: Test that alias deprecation works
+ test_alias_deprecation:
+--- ansible-core-2.16.5.orig/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1
++++ ansible-core-2.16.5/test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1
+@@ -87,7 +87,7 @@ Function Assert-DictionaryEqual {
+ }
+
+ Function Exit-Module {
+- # Make sure Exit actually calls exit and not our overridden test behaviour
++ # Make sure Exit actually calls exit and not our overriden test behaviour
+ [Ansible.Basic.AnsibleModule]::Exit = { param([Int32]$rc) exit $rc }
+ Write-Output -InputObject (ConvertTo-Json -InputObject $module.Result -Compress -Depth 99)
+ $module.ExitJson()
+--- ansible-core-2.16.5.orig/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1
++++ ansible-core-2.16.5/test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1
+@@ -328,73 +328,5 @@ finally {
+ }
+ Assert-Equal -actual ([Namespace12.Class12]::GetString()) -expected "b"
+
+-$unsafe_code_fail = @'
+-using System;
+-
+-namespace Namespace13
+-{
+- public class Class13
+- {
+-
+- public static int GetNumber()
+- {
+- int num = 2;
+- int* numPtr = &num;
+-
+- DoubleNumber(numPtr);
+-
+- return num;
+- }
+-
+- private unsafe static void DoubleNumber(int* num)
+- {
+- *num = *num * 3;
+- }
+- }
+-}
+-'@
+-$failed = $false
+-try {
+- Add-CSharpType -Reference $unsafe_code_fail
+-}
+-catch {
+- $failed = $true
+- $actual = $_.Exception.Message.Contains("error CS0227: Unsafe code may only appear if compiling with /unsafe")
+- Assert-Equal -actual $actual -expected $true
+-}
+-Assert-Equal -actual $failed -expected $true
+-
+-$unsafe_code = @'
+-using System;
+-
+-//AllowUnsafe
+-
+-namespace Namespace13
+-{
+- public class Class13
+- {
+- public static int GetNumber()
+- {
+- int num = 2;
+- unsafe
+- {
+- int* numPtr = &num;
+-
+- DoubleNumber(numPtr);
+- }
+-
+- return num;
+- }
+-
+- private unsafe static void DoubleNumber(int* num)
+- {
+- *num = *num * 2;
+- }
+- }
+-}
+-'@
+-Add-CSharpType -Reference $unsafe_code
+-Assert-Equal -actual ([Namespace13.Class13]::GetNumber()) -expected 4
+-
+ $result.res = "success"
+ Exit-Json -obj $result
+--- ansible-core-2.16.5.orig/test/integration/targets/no_log/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/no_log/runme.sh
+@@ -5,7 +5,7 @@ set -eux
+ # This test expects 7 loggable vars and 0 non-loggable ones.
+ # If either mismatches it fails, run the ansible-playbook command to debug.
+ [ "$(ansible-playbook no_log_local.yml -i ../../inventory -vvvvv "$@" | awk \
+-'BEGIN { logme = 0; nolog = 0; } /LOG_ME/ { logme += 1;} /DO_NOT_LOG/ { nolog += 1;} END { printf "%d/%d", logme, nolog; }')" = "27/0" ]
++'BEGIN { logme = 0; nolog = 0; } /LOG_ME/ { logme += 1;} /DO_NOT_LOG/ { nolog += 1;} END { printf "%d/%d", logme, nolog; }')" = "26/0" ]
+
+ # deal with corner cases with no log and loops
+ # no log enabled, should produce 6 censored messages
+@@ -19,8 +19,3 @@ set -eux
+
+ # test invalid data passed to a suboption
+ [ "$(ansible-playbook no_log_suboptions_invalid.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(SUPREME|IDIOM|MOCKUP|EDUCATED|FOOTREST|CRAFTY|FELINE|CRYSTAL|EXPECTANT|AGROUND|GOLIATH|FREEFALL)')" = "0" ]
+-
+-# test variations on ANSIBLE_NO_LOG
+-[ "$(ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "1" ]
+-[ "$(ANSIBLE_NO_LOG=0 ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "1" ]
+-[ "$(ANSIBLE_NO_LOG=1 ansible-playbook no_log_config.yml -i ../../inventory -vvvvv "$@" | grep -Ec 'the output has been hidden')" = "6" ]
+--- ansible-core-2.16.5.orig/test/integration/targets/old_style_cache_plugins/aliases
++++ ansible-core-2.16.5/test/integration/targets/old_style_cache_plugins/aliases
+@@ -2,4 +2,5 @@ destructive
+ needs/root
+ shippable/posix/group5
+ context/controller
++skip/osx
+ skip/macos
+--- ansible-core-2.16.5.orig/test/integration/targets/old_style_cache_plugins/plugins/cache/configurable_redis.py
++++ ansible-core-2.16.5/test/integration/targets/old_style_cache_plugins/plugins/cache/configurable_redis.py
+@@ -44,6 +44,7 @@ DOCUMENTATION = '''
+ import time
+ import json
+
++from ansible import constants as C
+ from ansible.errors import AnsibleError
+ from ansible.parsing.ajson import AnsibleJSONEncoder, AnsibleJSONDecoder
+ from ansible.plugins.cache import BaseCacheModule
+--- ansible-core-2.16.5.orig/test/integration/targets/old_style_cache_plugins/setup_redis_cache.yml
++++ ansible-core-2.16.5/test/integration/targets/old_style_cache_plugins/setup_redis_cache.yml
+@@ -20,9 +20,8 @@
+
+ - name: get the latest stable redis server release
+ get_url:
+- url: https://download.redis.io/redis-stable.tar.gz
++ url: http://download.redis.io/redis-stable.tar.gz
+ dest: ./
+- timeout: 60
+
+ - name: unzip download
+ unarchive:
+--- ansible-core-2.16.5.orig/test/integration/targets/old_style_vars_plugins/deprecation_warning/vars.py
++++ ansible-core-2.16.5/test/integration/targets/old_style_vars_plugins/deprecation_warning/vars.py
+@@ -2,7 +2,7 @@ from ansible.plugins.vars import BaseVar
+
+
+ class VarsModule(BaseVarsPlugin):
+- REQUIRES_WHITELIST = True
++ REQUIRES_WHITELIST = False
+
+ def get_vars(self, loader, path, entities):
+ return {}
+--- ansible-core-2.16.5.orig/test/integration/targets/old_style_vars_plugins/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/old_style_vars_plugins/runme.sh
+@@ -12,39 +12,9 @@ export ANSIBLE_VARS_PLUGINS=./vars_plugi
+ export ANSIBLE_VARS_ENABLED=require_enabled
+ [ "$(ansible-inventory -i localhost, --list --yaml all "$@" | grep -c 'require_enabled')" = "1" ]
+
+-# Test deprecated features
++# Test the deprecated class attribute
+ export ANSIBLE_VARS_PLUGINS=./deprecation_warning
+-WARNING_1="The VarsModule class variable 'REQUIRES_WHITELIST' is deprecated. Use 'REQUIRES_ENABLED' instead."
+-WARNING_2="The vars plugin v2_vars_plugin .* is relying on the deprecated entrypoints 'get_host_vars' and 'get_group_vars'"
++WARNING="The VarsModule class variable 'REQUIRES_WHITELIST' is deprecated. Use 'REQUIRES_ENABLED' instead."
+ ANSIBLE_DEPRECATION_WARNINGS=True ANSIBLE_NOCOLOR=True ANSIBLE_FORCE_COLOR=False \
+- ansible-inventory -i localhost, --list all "$@" 2> err.txt
+-for WARNING in "$WARNING_1" "$WARNING_2"; do
+- ansible localhost -m debug -a "msg={{ lookup('file', 'err.txt') | regex_replace('\n', '') }}" | grep "$WARNING"
+-done
+-
+-# Test how many times vars plugins are loaded for a simple play containing a task
+-# host_group_vars is stateless, so we can load it once and reuse it, every other vars plugin should be instantiated before it runs
+-cat << EOF > "test_task_vars.yml"
+----
+-- hosts: localhost
+- connection: local
+- gather_facts: no
+- tasks:
+- - debug:
+-EOF
+-
+-# hide the debug noise by dumping to a file
+-trap 'rm -rf -- "out.txt"' EXIT
+-
+-ANSIBLE_DEBUG=True ansible-playbook test_task_vars.yml > out.txt
+-[ "$(grep -c "Loading VarsModule 'host_group_vars'" out.txt)" -eq 1 ]
+-[ "$(grep -c "Loading VarsModule 'require_enabled'" out.txt)" -gt 50 ]
+-[ "$(grep -c "Loading VarsModule 'auto_enabled'" out.txt)" -gt 50 ]
+-
+-export ANSIBLE_VARS_ENABLED=ansible.builtin.host_group_vars
+-ANSIBLE_DEBUG=True ansible-playbook test_task_vars.yml > out.txt
+-[ "$(grep -c "Loading VarsModule 'host_group_vars'" out.txt)" -eq 1 ]
+-[ "$(grep -c "Loading VarsModule 'require_enabled'" out.txt)" -lt 3 ]
+-[ "$(grep -c "Loading VarsModule 'auto_enabled'" out.txt)" -gt 50 ]
+-
+-ansible localhost -m include_role -a 'name=a' "$@"
++ ansible-inventory -i localhost, --list all 2> err.txt
++ansible localhost -m debug -a "msg={{ lookup('file', 'err.txt') | regex_replace('\n', '') }}" | grep "$WARNING"
+--- ansible-core-2.16.5.orig/test/integration/targets/omit/75692.yml
++++ ansible-core-2.16.5/test/integration/targets/omit/75692.yml
+@@ -2,10 +2,10 @@
+ hosts: testhost
+ gather_facts: false
+ become: yes
+- # become_user needed at play level for testing this behavior
+ become_user: nobody
+ roles:
+ - name: setup_test_user
++ become: yes
+ become_user: root
+ tasks:
+ - shell: whoami
+--- ansible-core-2.16.5.orig/test/integration/targets/package/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/package/tasks/main.yml
+@@ -239,4 +239,4 @@
+ that:
+ - "result is changed"
+
+- when: ansible_distribution == "Fedora"
++ when: ansible_distribution == "Fedora"
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/package_facts/aliases
++++ ansible-core-2.16.5/test/integration/targets/package_facts/aliases
+@@ -1,2 +1,3 @@
+ shippable/posix/group2
++skip/osx
+ skip/macos
+--- ansible-core-2.16.5.orig/test/integration/targets/parsing/roles/test_good_parsing/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/parsing/roles/test_good_parsing/tasks/main.yml
+@@ -121,10 +121,7 @@
+ - result.cmd == "echo foo --arg=a --arg=b"
+
+ - name: test includes with params
+- include_tasks: test_include.yml
+- vars:
+- fact_name: include_params
+- param: "{{ test_input }}"
++ include: test_include.yml fact_name=include_params param="{{ test_input }}"
+
+ - name: assert the include set the correct fact for the param
+ assert:
+@@ -132,10 +129,7 @@
+ - include_params == test_input
+
+ - name: test includes with quoted params
+- include_tasks: test_include.yml
+- vars:
+- fact_name: double_quoted_param
+- param: "this is a param with double quotes"
++ include: test_include.yml fact_name=double_quoted_param param="this is a param with double quotes"
+
+ - name: assert the include set the correct fact for the double quoted param
+ assert:
+@@ -143,10 +137,7 @@
+ - double_quoted_param == "this is a param with double quotes"
+
+ - name: test includes with single quoted params
+- include_tasks: test_include.yml
+- vars:
+- fact_name: single_quoted_param
+- param: 'this is a param with single quotes'
++ include: test_include.yml fact_name=single_quoted_param param='this is a param with single quotes'
+
+ - name: assert the include set the correct fact for the single quoted param
+ assert:
+@@ -154,7 +145,7 @@
+ - single_quoted_param == "this is a param with single quotes"
+
+ - name: test includes with quoted params in complex args
+- include_tasks: test_include.yml
++ include: test_include.yml
+ vars:
+ fact_name: complex_param
+ param: "this is a param in a complex arg with double quotes"
+@@ -174,7 +165,7 @@
+ - result.msg == "this should be debugged"
+
+ - name: test conditional includes
+- include_tasks: test_include_conditional.yml
++ include: test_include_conditional.yml
+ when: false
+
+ - name: assert the nested include from test_include_conditional was not set
+--- ansible-core-2.16.5.orig/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_conditional.yml
++++ ansible-core-2.16.5/test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_conditional.yml
+@@ -1 +1 @@
+-- include_tasks: test_include_nested.yml
++- include: test_include_nested.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/parsing/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/parsing/runme.sh
+@@ -2,5 +2,5 @@
+
+ set -eux
+
+-ansible-playbook parsing.yml -i ../../inventory "$@" -e "output_dir=${OUTPUT_DIR}"
+-ansible-playbook good_parsing.yml -i ../../inventory "$@"
++ansible-playbook bad_parsing.yml -i ../../inventory -vvv "$@" --tags prepare,common,scenario5
++ansible-playbook good_parsing.yml -i ../../inventory -v "$@"
+--- ansible-core-2.16.5.orig/test/integration/targets/path_lookups/testplay.yml
++++ ansible-core-2.16.5/test/integration/targets/path_lookups/testplay.yml
+@@ -4,11 +4,9 @@
+ pre_tasks:
+ - name: remove {{ remove }}
+ file: path={{ playbook_dir }}/{{ remove }} state=absent
+- tasks:
+- - import_role:
+- name: showfile
+- tasks_from: notmain.yml
+-
++ roles:
++ - showfile
++ post_tasks:
+ - name: from play
+ set_fact: play_result="{{lookup('file', 'testfile')}}"
+
+--- ansible-core-2.16.5.orig/test/integration/targets/pause/test-pause.py
++++ ansible-core-2.16.5/test/integration/targets/pause/test-pause.py
+@@ -168,9 +168,7 @@ pause_test = pexpect.spawn(
+ pause_test.logfile = log_buffer
+ pause_test.expect(r'Pausing for \d+ seconds')
+ pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+-pause_test.send('\n') # test newline does not stop the prompt - waiting for a timeout or ctrl+C
+ pause_test.send('\x03')
+-pause_test.expect("Press 'C' to continue the play or 'A' to abort")
+ pause_test.send('C')
+ pause_test.expect('Task after pause')
+ pause_test.expect(pexpect.EOF)
+@@ -189,7 +187,6 @@ pause_test.logfile = log_buffer
+ pause_test.expect(r'Pausing for \d+ seconds')
+ pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+ pause_test.send('\x03')
+-pause_test.expect("Press 'C' to continue the play or 'A' to abort")
+ pause_test.send('A')
+ pause_test.expect('user requested abort!')
+ pause_test.expect(pexpect.EOF)
+@@ -228,7 +225,6 @@ pause_test.expect(r'Pausing for \d+ seco
+ pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+ pause_test.expect(r"Waiting for two seconds:")
+ pause_test.send('\x03')
+-pause_test.expect("Press 'C' to continue the play or 'A' to abort")
+ pause_test.send('C')
+ pause_test.expect('Task after pause')
+ pause_test.expect(pexpect.EOF)
+@@ -248,7 +244,6 @@ pause_test.expect(r'Pausing for \d+ seco
+ pause_test.expect(r"\(ctrl\+C then 'C' = continue early, ctrl\+C then 'A' = abort\)")
+ pause_test.expect(r"Waiting for two seconds:")
+ pause_test.send('\x03')
+-pause_test.expect("Press 'C' to continue the play or 'A' to abort")
+ pause_test.send('A')
+ pause_test.expect('user requested abort!')
+ pause_test.expect(pexpect.EOF)
+@@ -280,24 +275,6 @@ pause_test.send('\r')
+ pause_test.expect(pexpect.EOF)
+ pause_test.close()
+
+-# Test input is not returned if a timeout is given
+-
+-playbook = 'pause-6.yml'
+-
+-pause_test = pexpect.spawn(
+- 'ansible-playbook',
+- args=[playbook] + args,
+- timeout=10,
+- env=os.environ
+-)
+-
+-pause_test.logfile = log_buffer
+-pause_test.expect(r'Wait for three seconds:')
+-pause_test.send('ignored user input')
+-pause_test.expect('Task after pause')
+-pause_test.expect(pexpect.EOF)
+-pause_test.close()
+-
+
+ # Test that enter presses may not continue the play when a timeout is set.
+
+--- ansible-core-2.16.5.orig/test/integration/targets/pip/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/pip/tasks/main.yml
+@@ -40,9 +40,6 @@
+ extra_args: "-c {{ remote_constraints }}"
+
+ - include_tasks: pip.yml
+-
+- - include_tasks: no_setuptools.yml
+- when: ansible_python.version_info[:2] >= [3, 8]
+ always:
+ - name: platform specific cleanup
+ include_tasks: "{{ cleanup_filename }}"
+--- ansible-core-2.16.5.orig/test/integration/targets/pip/tasks/pip.yml
++++ ansible-core-2.16.5/test/integration/targets/pip/tasks/pip.yml
+@@ -568,28 +568,6 @@
+ that:
+ - "version13 is success"
+
+-- name: Test virtualenv command with venv formatting
+- when: ansible_python.version.major > 2
+- block:
+- - name: Clean up the virtualenv
+- file:
+- state: absent
+- name: "{{ remote_tmp_dir }}/pipenv"
+-
+- # ref: https://github.com/ansible/ansible/issues/76372
+- - name: install using different venv formatting
+- pip:
+- name: "{{ pip_test_package }}"
+- virtualenv: "{{ remote_tmp_dir }}/pipenv"
+- virtualenv_command: "{{ ansible_python_interpreter ~ ' -mvenv' }}"
+- state: present
+- register: version14
+-
+- - name: ensure install using virtualenv_command with venv formatting
+- assert:
+- that:
+- - "version14 is changed"
+-
+ ### test virtualenv_command end ###
+
+ # https://github.com/ansible/ansible/issues/68592
+--- ansible-core-2.16.5.orig/test/integration/targets/pkg_resources/lookup_plugins/check_pkg_resources.py
++++ ansible-core-2.16.5/test/integration/targets/pkg_resources/lookup_plugins/check_pkg_resources.py
+@@ -11,7 +11,7 @@ __metaclass__ = type
+
+ # noinspection PyUnresolvedReferences
+ try:
+- from pkg_resources import Requirement # pylint: disable=unused-import
++ from pkg_resources import Requirement
+ except ImportError:
+ Requirement = None
+
+--- ansible-core-2.16.5.orig/test/integration/targets/plugin_filtering/filter_lookup.yml
++++ ansible-core-2.16.5/test/integration/targets/plugin_filtering/filter_lookup.yml
+@@ -1,6 +1,6 @@
+ ---
+ filter_version: 1.0
+-module_rejectlist:
++module_blacklist:
+ # Specify the name of a lookup plugin here. This should have no effect as
+ # this is only for filtering modules
+ - list
+--- ansible-core-2.16.5.orig/test/integration/targets/plugin_filtering/filter_modules.yml
++++ ansible-core-2.16.5/test/integration/targets/plugin_filtering/filter_modules.yml
+@@ -1,6 +1,6 @@
+ ---
+ filter_version: 1.0
+-module_rejectlist:
++module_blacklist:
+ # A pure action plugin
+ - pause
+ # A hybrid action plugin with module
+--- ansible-core-2.16.5.orig/test/integration/targets/plugin_filtering/filter_ping.yml
++++ ansible-core-2.16.5/test/integration/targets/plugin_filtering/filter_ping.yml
+@@ -1,5 +1,5 @@
+ ---
+ filter_version: 1.0
+-module_rejectlist:
++module_blacklist:
+ # Ping is special
+ - ping
+--- ansible-core-2.16.5.orig/test/integration/targets/plugin_filtering/filter_stat.yml
++++ ansible-core-2.16.5/test/integration/targets/plugin_filtering/filter_stat.yml
+@@ -1,5 +1,5 @@
+ ---
+ filter_version: 1.0
+-module_rejectlist:
++module_blacklist:
+ # Stat is special
+ - stat
+--- ansible-core-2.16.5.orig/test/integration/targets/plugin_filtering/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/plugin_filtering/runme.sh
+@@ -22,11 +22,11 @@ if test $? != 0 ; then
+ fi
+
+ #
+-# Check that if no modules are rejected then Ansible should not through traceback
++# Check that if no modules are blacklisted then Ansible should not through traceback
+ #
+-ANSIBLE_CONFIG=no_rejectlist_module.ini ansible-playbook tempfile.yml -i ../../inventory -vvv "$@"
++ANSIBLE_CONFIG=no_blacklist_module.ini ansible-playbook tempfile.yml -i ../../inventory -vvv "$@"
+ if test $? != 0 ; then
+- echo "### Failed to run tempfile with no modules rejected"
++ echo "### Failed to run tempfile with no modules blacklisted"
+ exit 1
+ fi
+
+@@ -87,7 +87,7 @@ fi
+
+ ANSIBLE_CONFIG=filter_lookup.ini ansible-playbook lookup.yml -i ../../inventory -vvv "$@"
+ if test $? != 0 ; then
+- echo "### Failed to use a lookup plugin when it is incorrectly specified in the *module* reject list"
++ echo "### Failed to use a lookup plugin when it is incorrectly specified in the *module* blacklist"
+ exit 1
+ fi
+
+@@ -107,10 +107,10 @@ ANSIBLE_CONFIG=filter_stat.ini
+ export ANSIBLE_CONFIG
+ CAPTURE=$(ansible-playbook copy.yml -i ../../inventory -vvv "$@" 2>&1)
+ if test $? = 0 ; then
+- echo "### Copy ran even though stat is in the module reject list"
++ echo "### Copy ran even though stat is in the module blacklist"
+ exit 1
+ else
+- echo "$CAPTURE" | grep 'The stat module was specified in the module reject list file,.*, but Ansible will not function without the stat module. Please remove stat from the reject list.'
++ echo "$CAPTURE" | grep 'The stat module was specified in the module blacklist file,.*, but Ansible will not function without the stat module. Please remove stat from the blacklist.'
+ if test $? != 0 ; then
+ echo "### Stat did not give us our custom error message"
+ exit 1
+@@ -124,10 +124,10 @@ ANSIBLE_CONFIG=filter_stat.ini
+ export ANSIBLE_CONFIG
+ CAPTURE=$(ansible-playbook stat.yml -i ../../inventory -vvv "$@" 2>&1)
+ if test $? = 0 ; then
+- echo "### Stat ran even though it is in the module reject list"
++ echo "### Stat ran even though it is in the module blacklist"
+ exit 1
+ else
+- echo "$CAPTURE" | grep 'The stat module was specified in the module reject list file,.*, but Ansible will not function without the stat module. Please remove stat from the reject list.'
++ echo "$CAPTURE" | grep 'The stat module was specified in the module blacklist file,.*, but Ansible will not function without the stat module. Please remove stat from the blacklist.'
+ if test $? != 0 ; then
+ echo "### Stat did not give us our custom error message"
+ exit 1
+--- ansible-core-2.16.5.orig/test/integration/targets/plugin_loader/override/filters.yml
++++ ansible-core-2.16.5/test/integration/targets/plugin_loader/override/filters.yml
+@@ -1,7 +1,7 @@
+ - hosts: testhost
+ gather_facts: false
+ tasks:
+- - name: ensure local 'flag' filter works, 'flatten' is overridden and 'ternary' is still from core
++ - name: ensure local 'flag' filter works, 'flatten' is overriden and 'ternary' is still from core
+ assert:
+ that:
+ - a|flag == 'flagged'
+--- ansible-core-2.16.5.orig/test/integration/targets/plugin_loader/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/plugin_loader/runme.sh
+@@ -34,8 +34,3 @@ done
+
+ # test config loading
+ ansible-playbook use_coll_name.yml -i ../../inventory -e 'ansible_connection=ansible.builtin.ssh' "$@"
+-
+-# test filter loading ignoring duplicate file basename
+-ansible-playbook file_collision/play.yml "$@"
+-
+-ANSIBLE_COLLECTIONS_PATH=$PWD/collections ansible-playbook unsafe_plugin_name.yml "$@"
+--- ansible-core-2.16.5.orig/test/integration/targets/rel_plugin_loading/subdir/inventory_plugins/notyaml.py
++++ ansible-core-2.16.5/test/integration/targets/rel_plugin_loading/subdir/inventory_plugins/notyaml.py
+@@ -64,7 +64,7 @@ from collections.abc import MutableMappi
+
+ from ansible.errors import AnsibleError, AnsibleParserError
+ from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.plugins.inventory import BaseFileInventoryPlugin
+
+ NoneType = type(None)
+--- ansible-core-2.16.5.orig/test/integration/targets/remote_tmp/playbook.yml
++++ ansible-core-2.16.5/test/integration/targets/remote_tmp/playbook.yml
+@@ -30,43 +30,30 @@
+ - name: Test tempdir is removed
+ hosts: testhost
+ gather_facts: false
+- vars:
+- # These tests cannot be run with pipelining as it defeats the purpose of
+- # ensuring remote_tmp is cleaned up. Pipelining is enabled in the test
+- # inventory
+- ansible_pipelining: false
+- # Ensure that the remote_tmp_dir we create allows the unpriv connection user
+- # to create the remote_tmp
+- ansible_become: false
+ tasks:
+ - import_role:
+ name: ../setup_remote_tmp_dir
+
+- - vars:
+- # Isolate the remote_tmp used by these tests
+- ansible_remote_tmp: "{{ remote_tmp_dir }}/remote_tmp"
+- block:
+- - file:
+- state: touch
+- path: "{{ remote_tmp_dir }}/65393"
++ - file:
++ state: touch
++ path: "{{ remote_tmp_dir }}/65393"
+
+- - copy:
+- src: "{{ remote_tmp_dir }}/65393"
+- dest: "{{ remote_tmp_dir }}/65393.2"
+- remote_src: true
++ - copy:
++ src: "{{ remote_tmp_dir }}/65393"
++ dest: "{{ remote_tmp_dir }}/65393.2"
++ remote_src: true
+
+- - find:
+- path: "{{ ansible_remote_tmp }}"
+- use_regex: yes
+- patterns: 'AnsiballZ_.+\.py'
+- recurse: true
+- register: result
++ - find:
++ path: "~/.ansible/tmp"
++ use_regex: yes
++ patterns: 'AnsiballZ_.+\.py'
++ recurse: true
++ register: result
+
+ - debug:
+ var: result
+
+ - assert:
+ that:
+- # Should only be AnsiballZ_find.py because find is actively running
+- - result.files|length == 1
+- - result.files[0].path.endswith('/AnsiballZ_find.py')
++ # Should find nothing since pipelining is used
++ - result.files|length == 0
+--- ansible-core-2.16.5.orig/test/integration/targets/replace/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/replace/tasks/main.yml
+@@ -263,22 +263,3 @@
+ - replace_cat8.stdout_lines[1] == "9.9.9.9"
+ - replace_cat8.stdout_lines[7] == "0.0.0.0"
+ - replace_cat8.stdout_lines[13] == "0.0.0.0"
+-
+-# For Python 3.6 or greater - https://github.com/ansible/ansible/issues/79364
+-- name: Handle bad escape character in regular expression
+- replace:
+- path: /dev/null
+- after: ^
+- before: $
+- regexp: \.
+- replace: '\D'
+- ignore_errors: true
+- register: replace_test9
+- when: ansible_python.version.major == 3 and ansible_python.version.minor > 6
+-
+-- name: Validate the failure
+- assert:
+- that:
+- - replace_test9 is failure
+- - replace_test9.msg.startswith("Unable to process replace")
+- when: ansible_python.version.major == 3 and ansible_python.version.minor > 6
+--- ansible-core-2.16.5.orig/test/integration/targets/roles/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/roles/runme.sh
+@@ -3,47 +3,26 @@
+ set -eux
+
+ # test no dupes when dependencies in b and c point to a in roles:
+-[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags inroles | grep -c '"msg": "A"')" = "1" ]
+-[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags acrossroles | grep -c '"msg": "A"')" = "1" ]
+-[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags intasks | grep -c '"msg": "A"')" = "1" ]
++[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags inroles "$@" | grep -c '"msg": "A"')" = "1" ]
++[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags acrossroles "$@" | grep -c '"msg": "A"')" = "1" ]
++[ "$(ansible-playbook no_dupes.yml -i ../../inventory --tags intasks "$@" | grep -c '"msg": "A"')" = "1" ]
+
+ # but still dupe across plays
+-[ "$(ansible-playbook no_dupes.yml -i ../../inventory | grep -c '"msg": "A"')" = "3" ]
+-
+-# and don't dedupe before the role successfully completes
+-[ "$(ansible-playbook role_complete.yml -i ../../inventory -i fake, --tags conditional_skipped | grep -c '"msg": "A"')" = "1" ]
+-[ "$(ansible-playbook role_complete.yml -i ../../inventory -i fake, --tags conditional_failed | grep -c '"msg": "failed_when task succeeded"')" = "1" ]
+-[ "$(ansible-playbook role_complete.yml -i ../../inventory -i fake, --tags unreachable -vvv | grep -c '"data": "reachable"')" = "1" ]
+-ansible-playbook role_complete.yml -i ../../inventory -i fake, --tags unreachable | grep -e 'ignored=2'
++[ "$(ansible-playbook no_dupes.yml -i ../../inventory "$@" | grep -c '"msg": "A"')" = "3" ]
+
+ # include/import can execute another instance of role
+-[ "$(ansible-playbook allowed_dupes.yml -i ../../inventory --tags importrole | grep -c '"msg": "A"')" = "2" ]
+-[ "$(ansible-playbook allowed_dupes.yml -i ../../inventory --tags includerole | grep -c '"msg": "A"')" = "2" ]
++[ "$(ansible-playbook allowed_dupes.yml -i ../../inventory --tags importrole "$@" | grep -c '"msg": "A"')" = "2" ]
++[ "$(ansible-playbook allowed_dupes.yml -i ../../inventory --tags includerole "$@" | grep -c '"msg": "A"')" = "2" ]
+
+-[ "$(ansible-playbook dupe_inheritance.yml -i ../../inventory | grep -c '"msg": "abc"')" = "3" ]
+
+ # ensure role data is merged correctly
+ ansible-playbook data_integrity.yml -i ../../inventory "$@"
+
+ # ensure role fails when trying to load 'non role' in _from
+-ansible-playbook no_outside.yml -i ../../inventory > role_outside_output.log 2>&1 || true
++ansible-playbook no_outside.yml -i ../../inventory "$@" > role_outside_output.log 2>&1 || true
+ if grep "as it is not inside the expected role path" role_outside_output.log >/dev/null; then
+ echo "Test passed (playbook failed with expected output, output not shown)."
+ else
+ echo "Test failed, expected output from playbook failure is missing, output not shown)."
+ exit 1
+ fi
+-
+-# ensure vars scope is correct
+-ansible-playbook vars_scope.yml -i ../../inventory "$@"
+-
+-# test nested includes get parent roles greater than a depth of 3
+-[ "$(ansible-playbook 47023.yml -i ../../inventory | grep '\<\(Default\|Var\)\>' | grep -c 'is defined')" = "2" ]
+-
+-# ensure import_role called from include_role has the include_role in the dep chain
+-ansible-playbook role_dep_chain.yml -i ../../inventory "$@"
+-
+-# global role privacy setting test, set to private, set to not private, default
+-ANSIBLE_PRIVATE_ROLE_VARS=1 ansible-playbook privacy.yml -e @vars/privacy_vars.yml "$@"
+-ANSIBLE_PRIVATE_ROLE_VARS=0 ansible-playbook privacy.yml -e @vars/privacy_vars.yml "$@"
+-ansible-playbook privacy.yml -e @vars/privacy_vars.yml "$@"
+--- ansible-core-2.16.5.orig/test/integration/targets/roles_arg_spec/roles/c/meta/main.yml
++++ ansible-core-2.16.5/test/integration/targets/roles_arg_spec/roles/c/meta/main.yml
+@@ -2,15 +2,6 @@ argument_specs:
+ main:
+ short_description: Main entry point for role C.
+ options:
+- c_dict:
+- type: "dict"
+- required: true
+ c_int:
+ type: "int"
+ required: true
+- c_list:
+- type: "list"
+- required: true
+- c_raw:
+- type: "raw"
+- required: true
+--- ansible-core-2.16.5.orig/test/integration/targets/roles_arg_spec/test.yml
++++ ansible-core-2.16.5/test/integration/targets/roles_arg_spec/test.yml
+@@ -48,7 +48,6 @@
+ name: a
+ vars:
+ a_int: "{{ INT_VALUE }}"
+- a_str: "import_role"
+
+ - name: "Call role entry point that is defined, but has no spec data"
+ import_role:
+@@ -145,10 +144,7 @@
+ hosts: localhost
+ gather_facts: false
+ vars:
+- c_dict: {}
+ c_int: 1
+- c_list: []
+- c_raw: ~
+ a_str: "some string"
+ a_int: 42
+ tasks:
+@@ -160,125 +156,6 @@
+ include_role:
+ name: c
+
+-- name: "New play to reset vars: Test nested role including/importing role fails with null required options"
+- hosts: localhost
+- gather_facts: false
+- vars:
+- a_main_spec:
+- a_str:
+- required: true
+- type: "str"
+- c_main_spec:
+- c_int:
+- required: true
+- type: "int"
+- c_list:
+- required: true
+- type: "list"
+- c_dict:
+- required: true
+- type: "dict"
+- c_raw:
+- required: true
+- type: "raw"
+- # role c calls a's main and alternate entrypoints
+- a_str: ''
+- c_dict: {}
+- c_int: 0
+- c_list: []
+- c_raw: ~
+- tasks:
+- - name: test type coercion fails on None for required str
+- block:
+- - name: "Test import_role of role C (missing a_str)"
+- import_role:
+- name: c
+- vars:
+- a_str: ~
+- - fail:
+- msg: "Should not get here"
+- rescue:
+- - debug:
+- var: ansible_failed_result
+- - name: "Validate import_role failure"
+- assert:
+- that:
+- # NOTE: a bug here that prevents us from getting ansible_failed_task
+- - ansible_failed_result.argument_errors == [error]
+- - ansible_failed_result.argument_spec_data == a_main_spec
+- vars:
+- error: >-
+- argument 'a_str' is of type <class 'NoneType'> and we were unable to convert to str:
+- 'None' is not a string and conversion is not allowed
+-
+- - name: test type coercion fails on None for required int
+- block:
+- - name: "Test import_role of role C (missing c_int)"
+- import_role:
+- name: c
+- vars:
+- c_int: ~
+- - fail:
+- msg: "Should not get here"
+- rescue:
+- - debug:
+- var: ansible_failed_result
+- - name: "Validate import_role failure"
+- assert:
+- that:
+- # NOTE: a bug here that prevents us from getting ansible_failed_task
+- - ansible_failed_result.argument_errors == [error]
+- - ansible_failed_result.argument_spec_data == c_main_spec
+- vars:
+- error: >-
+- argument 'c_int' is of type <class 'NoneType'> and we were unable to convert to int:
+- <class 'NoneType'> cannot be converted to an int
+-
+- - name: test type coercion fails on None for required list
+- block:
+- - name: "Test import_role of role C (missing c_list)"
+- import_role:
+- name: c
+- vars:
+- c_list: ~
+- - fail:
+- msg: "Should not get here"
+- rescue:
+- - debug:
+- var: ansible_failed_result
+- - name: "Validate import_role failure"
+- assert:
+- that:
+- # NOTE: a bug here that prevents us from getting ansible_failed_task
+- - ansible_failed_result.argument_errors == [error]
+- - ansible_failed_result.argument_spec_data == c_main_spec
+- vars:
+- error: >-
+- argument 'c_list' is of type <class 'NoneType'> and we were unable to convert to list:
+- <class 'NoneType'> cannot be converted to a list
+-
+- - name: test type coercion fails on None for required dict
+- block:
+- - name: "Test import_role of role C (missing c_dict)"
+- import_role:
+- name: c
+- vars:
+- c_dict: ~
+- - fail:
+- msg: "Should not get here"
+- rescue:
+- - debug:
+- var: ansible_failed_result
+- - name: "Validate import_role failure"
+- assert:
+- that:
+- # NOTE: a bug here that prevents us from getting ansible_failed_task
+- - ansible_failed_result.argument_errors == [error]
+- - ansible_failed_result.argument_spec_data == c_main_spec
+- vars:
+- error: >-
+- argument 'c_dict' is of type <class 'NoneType'> and we were unable to convert to dict:
+- <class 'NoneType'> cannot be converted to a dict
+
+ - name: "New play to reset vars: Test nested role including/importing role fails"
+ hosts: localhost
+@@ -293,15 +170,13 @@
+ required: true
+ type: "int"
+
+- c_int: 100
+- c_list: []
+- c_dict: {}
+- c_raw: ~
+ tasks:
+ - block:
+ - name: "Test import_role of role C (missing a_str)"
+ import_role:
+ name: c
++ vars:
++ c_int: 100
+
+ - fail:
+ msg: "Should not get here"
+@@ -326,6 +201,7 @@
+ include_role:
+ name: c
+ vars:
++ c_int: 200
+ a_str: "some string"
+
+ - fail:
+--- ansible-core-2.16.5.orig/test/integration/targets/rpm_key/tasks/rpm_key.yaml
++++ ansible-core-2.16.5/test/integration/targets/rpm_key/tasks/rpm_key.yaml
+@@ -123,32 +123,6 @@
+ assert:
+ that: "'rsa sha1 (md5) pgp md5 OK' in sl_check.stdout or 'digests signatures OK' in sl_check.stdout"
+
+-- name: get keyid
+- shell: "rpm -q gpg-pubkey | head -n 1 | xargs rpm -q --qf %{version}"
+- register: key_id
+-
+-- name: remove GPG key using keyid
+- rpm_key:
+- state: absent
+- key: "{{ key_id.stdout }}"
+- register: remove_keyid
+- failed_when: remove_keyid.changed == false
+-
+-- name: remove GPG key using keyid (idempotent)
+- rpm_key:
+- state: absent
+- key: "{{ key_id.stdout }}"
+- register: key_id_idempotence
+-
+-- name: verify idempotent (key_id)
+- assert:
+- that: "not key_id_idempotence.changed"
+-
+-- name: add very first key on system again
+- rpm_key:
+- state: present
+- key: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/RPM-GPG-KEY-EPEL-7
+-
+ - name: Issue 20325 - Verify fingerprint of key, invalid fingerprint - EXPECTED FAILURE
+ rpm_key:
+ key: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/RPM-GPG-KEY.dag
+--- ansible-core-2.16.5.orig/test/integration/targets/script/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/script/tasks/main.yml
+@@ -37,17 +37,6 @@
+ ## script
+ ##
+
+-- name: Required one of free-form and cmd
+- script:
+- ignore_errors: yes
+- register: script_required
+-
+-- name: assert that the script fails if neither free-form nor cmd is given
+- assert:
+- that:
+- - script_required.failed
+- - "'one of the following' in script_required.msg"
+-
+ - name: execute the test.sh script via command
+ script: test.sh
+ register: script_result0
+--- ansible-core-2.16.5.orig/test/integration/targets/service/aliases
++++ ansible-core-2.16.5/test/integration/targets/service/aliases
+@@ -1,3 +1,4 @@
+ destructive
+ shippable/posix/group1
++skip/osx
+ skip/macos
+--- ansible-core-2.16.5.orig/test/integration/targets/service/files/ansible_test_service.py
++++ ansible-core-2.16.5/test/integration/targets/service/files/ansible_test_service.py
+@@ -9,6 +9,7 @@ __metaclass__ = type
+ import os
+ import resource
+ import signal
++import sys
+ import time
+
+ UMASK = 0
+--- ansible-core-2.16.5.orig/test/integration/targets/service_facts/aliases
++++ ansible-core-2.16.5/test/integration/targets/service_facts/aliases
+@@ -1,3 +1,4 @@
+ shippable/posix/group2
+ skip/freebsd
++skip/osx
+ skip/macos
+--- ansible-core-2.16.5.orig/test/integration/targets/setup_deb_repo/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/setup_deb_repo/tasks/main.yml
+@@ -59,7 +59,6 @@
+ loop:
+ - stable
+ - testing
+- when: install_repo|default(True)|bool is true
+
+ # Need to uncomment the deb-src for the universe component for build-dep state
+ - name: Ensure deb-src for the universe component
+--- ansible-core-2.16.5.orig/test/integration/targets/setup_paramiko/install-Alpine-3-python-3.yml
++++ ansible-core-2.16.5/test/integration/targets/setup_paramiko/install-Alpine-3-python-3.yml
+@@ -1,2 +1,9 @@
++- name: Setup remote constraints
++ include_tasks: setup-remote-constraints.yml
+ - name: Install Paramiko for Python 3 on Alpine
+- command: apk add py3-paramiko
++ pip: # no apk package manager in core, just use pip
++ name: paramiko
++ extra_args: "-c {{ remote_constraints }}"
++ environment:
++ # Not sure why this fixes the test, but it does.
++ SETUPTOOLS_USE_DISTUTILS: stdlib
+--- ansible-core-2.16.5.orig/test/integration/targets/setup_paramiko/uninstall-Alpine-3-python-3.yml
++++ ansible-core-2.16.5/test/integration/targets/setup_paramiko/uninstall-Alpine-3-python-3.yml
+@@ -1,2 +1,4 @@
+ - name: Uninstall Paramiko for Python 3 on Alpine
+- command: apk del py3-paramiko
++ pip:
++ name: paramiko
++ state: absent
+--- ansible-core-2.16.5.orig/test/integration/targets/setup_rpm_repo/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/setup_rpm_repo/tasks/main.yml
+@@ -24,18 +24,9 @@
+ args:
+ name: "{{ rpm_repo_packages }}"
+
+- - name: Install rpmfluff via pip, ensure it is installed with default python as python3-rpm may not exist for other versions
+- block:
+- - action: "{{ ansible_facts.pkg_mgr }}"
+- args:
+- name:
+- - python3-pip
+- - python3
+- state: latest
+-
+- - pip:
+- name: rpmfluff
+- executable: pip3
++ - name: Install rpmfluff via pip
++ pip:
++ name: rpmfluff
+ when: ansible_facts.os_family == 'RedHat' and ansible_distribution_major_version is version('9', '==')
+
+ - set_fact:
+--- ansible-core-2.16.5.orig/test/integration/targets/strategy_linear/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/strategy_linear/runme.sh
+@@ -5,5 +5,3 @@ set -eux
+ ansible-playbook test_include_file_noop.yml -i inventory "$@"
+
+ ansible-playbook task_action_templating.yml -i inventory "$@"
+-
+-ansible-playbook task_templated_run_once.yml -i inventory "$@"
+--- ansible-core-2.16.5.orig/test/integration/targets/subversion/aliases
++++ ansible-core-2.16.5/test/integration/targets/subversion/aliases
+@@ -1,4 +1,6 @@
+ shippable/posix/group2
++skip/osx
+ skip/macos
++skip/rhel/9.0b # svn checkout hangs
+ destructive
+ needs/root
+--- ansible-core-2.16.5.orig/test/integration/targets/systemd/tasks/test_indirect_service.yml
++++ ansible-core-2.16.5/test/integration/targets/systemd/tasks/test_indirect_service.yml
+@@ -34,4 +34,4 @@
+ - assert:
+ that:
+ - systemd_enable_dummy_indirect_1 is changed
+- - systemd_enable_dummy_indirect_2 is not changed
++ - systemd_enable_dummy_indirect_2 is not changed
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/systemd/vars/Debian.yml
++++ ansible-core-2.16.5/test/integration/targets/systemd/vars/Debian.yml
+@@ -1,3 +1,3 @@
+ ssh_service: ssh
+ sleep_bin_path: /bin/sleep
+-indirect_service: dummy
++indirect_service: dummy
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/tags/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/tags/runme.sh
+@@ -73,12 +73,3 @@ ansible-playbook -i ../../inventory ansi
+ ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=untagged --tags untagged "$@"
+ ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=untagged_list --tags untagged,tag3 "$@"
+ ansible-playbook -i ../../inventory ansible_run_tags.yml -e expect=tagged --tags tagged "$@"
+-
+-ansible-playbook test_template_parent_tags.yml "$@" 2>&1 | tee out.txt
+-[ "$(grep out.txt -ce 'Tagged_task')" = "1" ]; rm out.txt
+-
+-ansible-playbook test_template_parent_tags.yml --tags tag1 "$@" 2>&1 | tee out.txt
+-[ "$(grep out.txt -ce 'Tagged_task')" = "1" ]; rm out.txt
+-
+-ansible-playbook test_template_parent_tags.yml --skip-tags tag1 "$@" 2>&1 | tee out.txt
+-[ "$(grep out.txt -ce 'Tagged_task')" = "0" ]; rm out.txt
+--- ansible-core-2.16.5.orig/test/integration/targets/tasks/playbook.yml
++++ ansible-core-2.16.5/test/integration/targets/tasks/playbook.yml
+@@ -6,11 +6,6 @@
+ debug:
+ msg: Hello
+
+- # ensure we properly test for an action name, not a task name when cheking for a meta task
+- - name: "meta"
+- debug:
+- msg: Hello
+-
+ - name: ensure malformed raw_params on arbitrary actions are not ignored
+ debug:
+ garbage {{"with a template"}}
+--- ansible-core-2.16.5.orig/test/integration/targets/tasks/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/tasks/runme.sh
+@@ -1,3 +1,3 @@
+ #!/usr/bin/env bash
+
+-ansible-playbook playbook.yml
+\ No newline at end of file
++ansible-playbook playbook.yml "$@"
+--- ansible-core-2.16.5.orig/test/integration/targets/template/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/template/runme.sh
+@@ -8,10 +8,7 @@ ANSIBLE_ROLES_PATH=../ ansible-playbook
+ ansible testhost -i testhost, -m debug -a 'msg={{ hostvars["localhost"] }}' -e "vars1={{ undef() }}" -e "vars2={{ vars1 }}"
+
+ # Test for https://github.com/ansible/ansible/issues/27262
+-ANSIBLE_CONFIG=ansible_managed.cfg ansible-playbook ansible_managed.yml -i ../../inventory -v "$@"
+-
+-# Test for https://github.com/ansible/ansible/pull/79129
+-ANSIBLE_CONFIG=ansible_managed.cfg ansible-playbook ansible_managed_79129.yml -i ../../inventory -v "$@"
++ansible-playbook ansible_managed.yml -c ansible_managed.cfg -i ../../inventory -v "$@"
+
+ # Test for #42585
+ ANSIBLE_ROLES_PATH=../ ansible-playbook custom_template.yml -i ../../inventory -v "$@"
+@@ -42,7 +39,7 @@ ansible-playbook 72262.yml -v "$@"
+ ansible-playbook unsafe.yml -v "$@"
+
+ # ensure Jinja2 overrides from a template are used
+-ansible-playbook template_overrides.yml -v "$@"
++ansible-playbook in_template_overrides.yml -v "$@"
+
+ ansible-playbook lazy_eval.yml -i ../../inventory -v "$@"
+
+--- ansible-core-2.16.5.orig/test/integration/targets/template/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/template/tasks/main.yml
+@@ -25,7 +25,7 @@
+
+ - name: show jinja2 version
+ debug:
+- msg: "{{ lookup('pipe', ansible_python.executable ~ ' -c \"import jinja2; print(jinja2.__version__)\"') }}"
++ msg: "{{ lookup('pipe', '{{ ansible_python[\"executable\"] }} -c \"import jinja2; print(jinja2.__version__)\"') }}"
+
+ - name: get default group
+ shell: id -gn
+@@ -760,7 +760,7 @@
+ that:
+ - test
+ vars:
+- test: "{{ lookup('file', output_dir ~ '/empty_template.templated')|length == 0 }}"
++ test: "{{ lookup('file', '{{ output_dir }}/empty_template.templated')|length == 0 }}"
+
+ - name: test jinja2 override without colon throws proper error
+ block:
+--- ansible-core-2.16.5.orig/test/integration/targets/template/unsafe.yml
++++ ansible-core-2.16.5/test/integration/targets/template/unsafe.yml
+@@ -3,7 +3,6 @@
+ vars:
+ nottemplated: this should not be seen
+ imunsafe: !unsafe '{{ nottemplated }}'
+- unsafe_set: !unsafe '{{ "test" }}'
+ tasks:
+
+ - set_fact:
+@@ -13,15 +12,11 @@
+ - set_fact:
+ this_always_safe: '{{ imunsafe }}'
+
+- - set_fact:
+- this_unsafe_set: "{{ unsafe_set }}"
+-
+ - name: ensure nothing was templated
+ assert:
+ that:
+ - this_always_safe == imunsafe
+ - imunsafe == this_was_unsafe.strip()
+- - unsafe_set == this_unsafe_set.strip()
+
+
+ - hosts: localhost
+--- ansible-core-2.16.5.orig/test/integration/targets/template_jinja2_non_native/macro_override.yml
++++ ansible-core-2.16.5/test/integration/targets/template_jinja2_non_native/macro_override.yml
+@@ -12,4 +12,4 @@
+ - "'foobar' not in data"
+ - "'\"foo\" \"bar\"' in data"
+ vars:
+- data: "{{ lookup('file', output_dir ~ '/macro_override.out') }}"
++ data: "{{ lookup('file', '{{ output_dir }}/macro_override.out') }}"
+--- ansible-core-2.16.5.orig/test/integration/targets/templating/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/templating/tasks/main.yml
+@@ -33,14 +33,3 @@
+ - result is failed
+ - >-
+ "TemplateSyntaxError: Could not load \"asdf \": 'invalid plugin name: ansible.builtin.asdf '" in result.msg
+-
+-- name: Make sure syntax errors originating from a template being compiled into Python code object result in a failure
+- debug:
+- msg: "{{ lookup('vars', 'v1', default='', default='') }}"
+- ignore_errors: true
+- register: r
+-
+-- assert:
+- that:
+- - r is failed
+- - "'keyword argument repeated' in r.msg"
+--- ansible-core-2.16.5.orig/test/integration/targets/test_core/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/test_core/tasks/main.yml
+@@ -126,16 +126,6 @@
+ hello: world
+ register: executed_task
+
+-- name: Skip me with multiple conditions
+- set_fact:
+- hello: world
+- when:
+- - True == True
+- - foo == 'bar'
+- vars:
+- foo: foo
+- register: skipped_task_multi_condition
+-
+ - name: Try skipped test on non-dictionary
+ set_fact:
+ hello: "{{ 'nope' is skipped }}"
+@@ -146,11 +136,8 @@
+ assert:
+ that:
+ - skipped_task is skipped
+- - skipped_task.false_condition == False
+ - executed_task is not skipped
+ - misuse_of_skipped is failure
+- - skipped_task_multi_condition is skipped
+- - skipped_task_multi_condition.false_condition == "foo == 'bar'"
+
+ - name: Not an async task
+ set_fact:
+--- ansible-core-2.16.5.orig/test/integration/targets/unarchive/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/unarchive/tasks/main.yml
+@@ -20,4 +20,3 @@
+ - import_tasks: test_different_language_var.yml
+ - import_tasks: test_invalid_options.yml
+ - import_tasks: test_ownership_top_folder.yml
+-- import_tasks: test_relative_dest.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/unarchive/tasks/test_different_language_var.yml
++++ ansible-core-2.16.5/test/integration/targets/unarchive/tasks/test_different_language_var.yml
+@@ -2,10 +2,10 @@
+ when: ansible_os_family == 'Debian'
+ block:
+ - name: install fr language pack
+- apt:
++ apt:
+ name: language-pack-fr
+ state: present
+-
++
+ - name: create our unarchive destination
+ file:
+ path: "{{ remote_tmp_dir }}/test-unarchive-nonascii-くらとみ-tar-gz"
+--- ansible-core-2.16.5.orig/test/integration/targets/unarchive/tasks/test_mode.yml
++++ ansible-core-2.16.5/test/integration/targets/unarchive/tasks/test_mode.yml
+@@ -3,29 +3,6 @@
+ path: '{{remote_tmp_dir}}/test-unarchive-tar-gz'
+ state: directory
+
+-- name: test invalid modes
+- unarchive:
+- src: "{{ remote_tmp_dir }}/test-unarchive.tar.gz"
+- dest: "{{ remote_tmp_dir }}/test-unarchive-tar-gz"
+- remote_src: yes
+- mode: "{{ item }}"
+- list_files: True
+- register: unarchive_mode_errors
+- ignore_errors: yes
+- loop:
+- - u=foo
+- - foo=r
+- - ufoo=r
+- - abc=r
+- - ao=r
+- - oa=r
+-
+-- assert:
+- that:
+- - item.failed
+- - "'bad symbolic permission for mode: ' + item.item == item.details"
+- loop: "{{ unarchive_mode_errors.results }}"
+-
+ - name: unarchive and set mode to 0600, directories 0700
+ unarchive:
+ src: "{{ remote_tmp_dir }}/test-unarchive.tar.gz"
+--- ansible-core-2.16.5.orig/test/integration/targets/unsafe_writes/aliases
++++ ansible-core-2.16.5/test/integration/targets/unsafe_writes/aliases
+@@ -1,6 +1,7 @@
+ context/target
+ needs/root
+ skip/freebsd
++skip/osx
+ skip/macos
+ shippable/posix/group2
+ needs/target/setup_remote_tmp_dir
+--- ansible-core-2.16.5.orig/test/integration/targets/until/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/until/tasks/main.yml
+@@ -82,37 +82,3 @@
+ register: counter
+ delay: 0.5
+ until: counter.rc == 0
+-
+-- name: test retries without explicit until, defaults to "until task succeeds"
+- block:
+- - name: EXPECTED FAILURE
+- fail:
+- retries: 3
+- delay: 0.1
+- register: r
+- ignore_errors: true
+-
+- - assert:
+- that:
+- - r.attempts == 3
+-
+- - vars:
+- test_file: "{{ lookup('env', 'OUTPUT_DIR') }}/until_success_test_file"
+- block:
+- - file:
+- name: "{{ test_file }}"
+- state: absent
+-
+- - name: fail on the first invocation, succeed on the second
+- shell: "[ -f {{ test_file }} ] || (touch {{ test_file }} && false)"
+- retries: 5
+- delay: 0.1
+- register: r
+- always:
+- - file:
+- name: "{{ test_file }}"
+- state: absent
+-
+- - assert:
+- that:
+- - r.attempts == 2
+--- ansible-core-2.16.5.orig/test/integration/targets/unvault/main.yml
++++ ansible-core-2.16.5/test/integration/targets/unvault/main.yml
+@@ -1,5 +1,4 @@
+ - hosts: localhost
+- gather_facts: false
+ tasks:
+ - set_fact:
+ unvaulted: "{{ lookup('unvault', 'vault') }}"
+--- ansible-core-2.16.5.orig/test/integration/targets/unvault/runme.sh
++++ ansible-core-2.16.5/test/integration/targets/unvault/runme.sh
+@@ -2,5 +2,5 @@
+
+ set -eux
+
+-# simple run
++
+ ansible-playbook --vault-password-file password main.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/uri/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/uri/tasks/main.yml
+@@ -132,7 +132,7 @@
+ - "result.changed == true"
+
+ - name: "get ca certificate {{ self_signed_host }}"
+- uri:
++ get_url:
+ url: "http://{{ httpbin_host }}/ca2cert.pem"
+ dest: "{{ remote_tmp_dir }}/ca2cert.pem"
+
+@@ -638,18 +638,9 @@
+ - assert:
+ that:
+ - result['set_cookie'] == 'Foo=bar, Baz=qux'
+- # Python 3.10 and earlier sorts cookies in order of most specific (ie. longest) path first
++ # Python sorts cookies in order of most specific (ie. longest) path first
+ # items with the same path are reversed from response order
+ - result['cookies_string'] == 'Baz=qux; Foo=bar'
+- when: ansible_python_version is version('3.11', '<')
+-
+-- assert:
+- that:
+- - result['set_cookie'] == 'Foo=bar, Baz=qux'
+- # Python 3.11 no longer sorts cookies.
+- # See: https://github.com/python/cpython/issues/86232
+- - result['cookies_string'] == 'Foo=bar; Baz=qux'
+- when: ansible_python_version is version('3.11', '>=')
+
+ - name: Write out netrc template
+ template:
+@@ -766,30 +757,6 @@
+ dest: "{{ remote_tmp_dir }}/output"
+ state: absent
+
+-- name: Test download root to dir without content-disposition
+- uri:
+- url: "https://{{ httpbin_host }}/"
+- dest: "{{ remote_tmp_dir }}"
+- register: get_root_no_filename
+-
+-- name: Test downloading to dir without content-disposition
+- uri:
+- url: "https://{{ httpbin_host }}/response-headers"
+- dest: "{{ remote_tmp_dir }}"
+- register: get_dir_no_filename
+-
+-- name: Test downloading to dir with content-disposition
+- uri:
+- url: 'https://{{ httpbin_host }}/response-headers?Content-Disposition=attachment%3B%20filename%3D%22filename.json%22'
+- dest: "{{ remote_tmp_dir }}"
+- register: get_dir_filename
+-
+-- assert:
+- that:
+- - get_root_no_filename.path == remote_tmp_dir ~ "/index.html"
+- - get_dir_no_filename.path == remote_tmp_dir ~ "/response-headers"
+- - get_dir_filename.path == remote_tmp_dir ~ "/filename.json"
+-
+ - name: Test follow_redirects=none
+ import_tasks: redirect-none.yml
+
+--- ansible-core-2.16.5.orig/test/integration/targets/uri/tasks/redirect-none.yml
++++ ansible-core-2.16.5/test/integration/targets/uri/tasks/redirect-none.yml
+@@ -240,7 +240,7 @@
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: none
+ return_content: yes
+- method: HEAD
++ method: GET
+ ignore_errors: yes
+ register: http_308_head
+
+--- ansible-core-2.16.5.orig/test/integration/targets/uri/tasks/redirect-urllib2.yml
++++ ansible-core-2.16.5/test/integration/targets/uri/tasks/redirect-urllib2.yml
+@@ -237,7 +237,7 @@
+ url: https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything
+ follow_redirects: urllib2
+ return_content: yes
+- method: HEAD
++ method: GET
+ ignore_errors: yes
+ register: http_308_head
+
+@@ -250,23 +250,6 @@
+ - http_308_head.redirected == false
+ - http_308_head.status == 308
+ - http_308_head.url == 'https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything'
+- # Python 3.10 and earlier do not support HTTP 308 responses.
+- # See: https://github.com/python/cpython/issues/84501
+- when: ansible_python_version is version('3.11', '<')
+-
+-# NOTE: The HTTP HEAD turns into an HTTP GET
+-- assert:
+- that:
+- - http_308_head is successful
+- - http_308_head.json.data == ''
+- - http_308_head.json.method == 'GET'
+- - http_308_head.json.url == 'https://{{ httpbin_host }}/anything'
+- - http_308_head.redirected == true
+- - http_308_head.status == 200
+- - http_308_head.url == 'https://{{ httpbin_host }}/anything'
+- # Python 3.11 introduced support for HTTP 308 responses.
+- # See: https://github.com/python/cpython/issues/84501
+- when: ansible_python_version is version('3.11', '>=')
+
+ # FIXME: This is fixed in https://github.com/ansible/ansible/pull/36809
+ - name: Test HTTP 308 using GET
+@@ -287,22 +270,6 @@
+ - http_308_get.redirected == false
+ - http_308_get.status == 308
+ - http_308_get.url == 'https://{{ httpbin_host }}/redirect-to?status_code=308&url=https://{{ httpbin_host }}/anything'
+- # Python 3.10 and earlier do not support HTTP 308 responses.
+- # See: https://github.com/python/cpython/issues/84501
+- when: ansible_python_version is version('3.11', '<')
+-
+-- assert:
+- that:
+- - http_308_get is successful
+- - http_308_get.json.data == ''
+- - http_308_get.json.method == 'GET'
+- - http_308_get.json.url == 'https://{{ httpbin_host }}/anything'
+- - http_308_get.redirected == true
+- - http_308_get.status == 200
+- - http_308_get.url == 'https://{{ httpbin_host }}/anything'
+- # Python 3.11 introduced support for HTTP 308 responses.
+- # See: https://github.com/python/cpython/issues/84501
+- when: ansible_python_version is version('3.11', '>=')
+
+ # FIXME: This is fixed in https://github.com/ansible/ansible/pull/36809
+ - name: Test HTTP 308 using POST
+--- ansible-core-2.16.5.orig/test/integration/targets/uri/tasks/return-content.yml
++++ ansible-core-2.16.5/test/integration/targets/uri/tasks/return-content.yml
+@@ -46,4 +46,4 @@
+ assert:
+ that:
+ - result is failed
+- - "'content' not in result"
++ - "'content' not in result"
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/uri/tasks/use_netrc.yml
++++ ansible-core-2.16.5/test/integration/targets/uri/tasks/use_netrc.yml
+@@ -48,4 +48,4 @@
+ - name: Clean up
+ file:
+ dest: "{{ remote_tmp_dir }}/netrc"
+- state: absent
++ state: absent
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/integration/targets/user/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/user/tasks/main.yml
+@@ -31,9 +31,7 @@
+ - import_tasks: test_expires.yml
+ - import_tasks: test_expires_new_account.yml
+ - import_tasks: test_expires_new_account_epoch_negative.yml
+-- import_tasks: test_expires_no_shadow.yml
+ - import_tasks: test_expires_min_max.yml
+-- import_tasks: test_expires_warn.yml
+ - import_tasks: test_shadow_backup.yml
+ - import_tasks: test_ssh_key_passphrase.yml
+ - import_tasks: test_password_lock.yml
+--- ansible-core-2.16.5.orig/test/integration/targets/user/tasks/test_create_user.yml
++++ ansible-core-2.16.5/test/integration/targets/user/tasks/test_create_user.yml
+@@ -65,15 +65,3 @@
+ - "user_test1.results[2]['state'] == 'present'"
+ - "user_test1.results[3]['state'] == 'present'"
+ - "user_test1.results[4]['state'] == 'present'"
+-
+-- name: register user informations
+- when: ansible_facts.system == 'Darwin'
+- command: dscl . -read /Users/ansibulluser
+- register: user_test2
+-
+-- name: validate user defaults for MacOS
+- when: ansible_facts.system == 'Darwin'
+- assert:
+- that:
+- - "'RealName: ansibulluser' in user_test2.stdout_lines "
+- - "'PrimaryGroupID: 20' in user_test2.stdout_lines "
+--- ansible-core-2.16.5.orig/test/integration/targets/user/tasks/test_create_user_home.yml
++++ ansible-core-2.16.5/test/integration/targets/user/tasks/test_create_user_home.yml
+@@ -134,21 +134,3 @@
+ name: randomuser
+ state: absent
+ remove: yes
+-
+-- name: Create user home directory with /dev/null as skeleton, https://github.com/ansible/ansible/issues/75063
+- # create_homedir is mostly used by linux, rest of OSs take care of it themselves via -k option (which fails this task)
+- when: ansible_system == 'Linux'
+- block:
+- - name: "Create user home directory with /dev/null as skeleton"
+- user:
+- name: withskeleton
+- state: present
+- skeleton: "/dev/null"
+- createhome: yes
+- register: create_user_with_skeleton_dev_null
+- always:
+- - name: "Remove test user"
+- user:
+- name: withskeleton
+- state: absent
+- remove: yes
+--- ansible-core-2.16.5.orig/test/integration/targets/user/tasks/test_local.yml
++++ ansible-core-2.16.5/test/integration/targets/user/tasks/test_local.yml
+@@ -86,11 +86,9 @@
+ - testgroup3
+ - testgroup4
+ - testgroup5
+- - testgroup6
+ - local_ansibulluser
+ tags:
+ - user_test_local_mode
+- register: test_groups
+
+ - name: Create local_ansibulluser with groups
+ user:
+@@ -115,18 +113,6 @@
+ tags:
+ - user_test_local_mode
+
+-- name: Append groups for local_ansibulluser (again)
+- user:
+- name: local_ansibulluser
+- state: present
+- local: yes
+- groups: ['testgroup3', 'testgroup4']
+- append: yes
+- register: local_user_test_4_again
+- ignore_errors: yes
+- tags:
+- - user_test_local_mode
+-
+ - name: Test append without groups for local_ansibulluser
+ user:
+ name: local_ansibulluser
+@@ -147,28 +133,6 @@
+ tags:
+ - user_test_local_mode
+
+-- name: Append groups for local_ansibulluser using group id
+- user:
+- name: local_ansibulluser
+- state: present
+- append: yes
+- groups: "{{ test_groups.results[5]['gid'] }}"
+- register: local_user_test_7
+- ignore_errors: yes
+- tags:
+- - user_test_local_mode
+-
+-- name: Append groups for local_ansibulluser using gid (again)
+- user:
+- name: local_ansibulluser
+- state: present
+- append: yes
+- groups: "{{ test_groups.results[5]['gid'] }}"
+- register: local_user_test_7_again
+- ignore_errors: yes
+- tags:
+- - user_test_local_mode
+-
+ # If we don't re-assign, then "Set user expiration" will
+ # fail.
+ - name: Re-assign named group for local_ansibulluser
+@@ -200,7 +164,6 @@
+ - testgroup3
+ - testgroup4
+ - testgroup5
+- - testgroup6
+ - local_ansibulluser
+ tags:
+ - user_test_local_mode
+@@ -212,10 +175,7 @@
+ - local_user_test_2 is not changed
+ - local_user_test_3 is changed
+ - local_user_test_4 is changed
+- - local_user_test_4_again is not changed
+ - local_user_test_6 is changed
+- - local_user_test_7 is changed
+- - local_user_test_7_again is not changed
+ - local_user_test_remove_1 is changed
+ - local_user_test_remove_2 is not changed
+ tags:
+--- ansible-core-2.16.5.orig/test/integration/targets/user/vars/main.yml
++++ ansible-core-2.16.5/test/integration/targets/user/vars/main.yml
+@@ -10,4 +10,4 @@ status_command:
+
+ default_user_group:
+ openSUSE Leap: users
+- MacOSX: staff
++ MacOSX: admin
+--- ansible-core-2.16.5.orig/test/integration/targets/var_blending/roles/test_var_blending/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/var_blending/roles/test_var_blending/tasks/main.yml
+@@ -1,4 +1,4 @@
+-# test code
++# test code
+ # (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
+
+ # This file is part of Ansible
+@@ -22,7 +22,7 @@
+ output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}"
+
+ - name: deploy a template that will use variables at various levels
+- template: src=foo.j2 dest={{output_dir}}/foo.templated
++ template: src=foo.j2 dest={{output_dir}}/foo.templated
+ register: template_result
+
+ - name: copy known good into place
+@@ -33,9 +33,9 @@
+ register: diff_result
+
+ - name: verify templated file matches known good
+- assert:
+- that:
+- - 'diff_result.stdout == ""'
++ assert:
++ that:
++ - 'diff_result.stdout == ""'
+
+ - name: check debug variable with same name as var content
+ debug: var=same_value_as_var_name_var
+--- ansible-core-2.16.5.orig/test/integration/targets/var_precedence/ansible-var-precedence-check.py
++++ ansible-core-2.16.5/test/integration/targets/var_precedence/ansible-var-precedence-check.py
+@@ -14,6 +14,7 @@ import stat
+ import subprocess
+ import tempfile
+ import yaml
++from pprint import pprint
+ from optparse import OptionParser
+ from jinja2 import Environment
+
+@@ -363,9 +364,9 @@ class VarTestMaker(object):
+ block_wrapper = [debug_task, test_task]
+
+ if 'include_params' in self.features:
+- self.tasks.append(dict(name='including tasks', include_tasks='included_tasks.yml', vars=dict(findme='include_params')))
++ self.tasks.append(dict(name='including tasks', include='included_tasks.yml', vars=dict(findme='include_params')))
+ else:
+- self.tasks.append(dict(include_tasks='included_tasks.yml'))
++ self.tasks.append(dict(include='included_tasks.yml'))
+
+ fname = os.path.join(TESTDIR, 'included_tasks.yml')
+ with open(fname, 'w') as f:
+--- ansible-core-2.16.5.orig/test/integration/targets/var_precedence/test_var_precedence.yml
++++ ansible-core-2.16.5/test/integration/targets/var_precedence/test_var_precedence.yml
+@@ -1,18 +1,14 @@
+ ---
+ - hosts: testhost
+ vars:
+- ansible_hostname: "BAD!"
+- vars_var: "vars_var"
+- param_var: "BAD!"
+- vars_files_var: "BAD!"
+- extra_var_override_once_removed: "{{ extra_var_override }}"
+- from_inventory_once_removed: "{{ inven_var | default('BAD!') }}"
++ - ansible_hostname: "BAD!"
++ - vars_var: "vars_var"
++ - param_var: "BAD!"
++ - vars_files_var: "BAD!"
++ - extra_var_override_once_removed: "{{ extra_var_override }}"
++ - from_inventory_once_removed: "{{ inven_var | default('BAD!') }}"
+ vars_files:
+ - vars/test_var_precedence.yml
+- pre_tasks:
+- - name: param vars should also override set_fact
+- set_fact:
+- param_var: "BAD!"
+ roles:
+ - { role: test_var_precedence, param_var: "param_var" }
+ tasks:
+--- ansible-core-2.16.5.orig/test/integration/targets/wait_for/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/wait_for/tasks/main.yml
+@@ -91,7 +91,7 @@
+ wait_for:
+ path: "{{remote_tmp_dir}}/wait_for_keyword"
+ search_regex: completed (?P<foo>\w+) ([0-9]+)
+- timeout: 25
++ timeout: 5
+ register: waitfor
+
+ - name: verify test wait for keyword in file with match groups
+@@ -114,15 +114,6 @@
+ path: "{{remote_tmp_dir}}/utf16.txt"
+ search_regex: completed
+
+-- name: test non mmapable file
+- wait_for:
+- path: "/sys/class/net/lo/carrier"
+- search_regex: "1"
+- timeout: 30
+- when:
+- - ansible_facts['os_family'] not in ['FreeBSD', 'Darwin']
+- - not (ansible_facts['os_family'] in ['RedHat', 'CentOS'] and ansible_facts['distribution_major_version'] is version('7', '<='))
+-
+ - name: test wait for port timeout
+ wait_for:
+ port: 12121
+--- ansible-core-2.16.5.orig/test/integration/targets/win_exec_wrapper/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/win_exec_wrapper/tasks/main.yml
+@@ -272,12 +272,3 @@
+ assert:
+ that:
+ - ps_log_count.stdout | int == 0
+-
+-- name: test module that sets HadErrors with no error records
+- test_rc_1:
+- register: module_had_errors
+-
+-- name: assert test module that sets HadErrors with no error records
+- assert:
+- that:
+- - module_had_errors.rc == 0
+--- ansible-core-2.16.5.orig/test/integration/targets/win_fetch/tasks/main.yml
++++ ansible-core-2.16.5/test/integration/targets/win_fetch/tasks/main.yml
+@@ -215,17 +215,3 @@
+ - fetch_special_file.checksum == '34d4150adc3347f1dd8ce19fdf65b74d971ab602'
+ - fetch_special_file.dest == host_output_dir + "/abc$not var'quote‘"
+ - fetch_special_file_actual.stdout == 'abc'
+-
+-- name: create file with wildcard characters
+- raw: Set-Content -LiteralPath '{{ remote_tmp_dir }}\abc[].txt' -Value 'abc'
+-
+-- name: fetch file with wildcard characters
+- fetch:
+- src: '{{ remote_tmp_dir }}\abc[].txt'
+- dest: '{{ host_output_dir }}/'
+- register: fetch_wildcard_file_nofail
+-
+-- name: assert fetch file with wildcard characters
+- assert:
+- that:
+- - "fetch_wildcard_file_nofail is not failed"
+--- ansible-core-2.16.5.orig/test/integration/targets/win_script/files/test_script_with_args.ps1
++++ ansible-core-2.16.5/test/integration/targets/win_script/files/test_script_with_args.ps1
+@@ -2,5 +2,5 @@
+ # passed to the script.
+
+ foreach ($i in $args) {
+- Write-Host $i
++ Write-Host $i;
+ }
+--- ansible-core-2.16.5.orig/test/integration/targets/win_script/files/test_script_with_errors.ps1
++++ ansible-core-2.16.5/test/integration/targets/win_script/files/test_script_with_errors.ps1
+@@ -2,7 +2,7 @@
+
+ trap {
+ Write-Error -ErrorRecord $_
+- exit 1
++ exit 1;
+ }
+
+ throw "Oh noes I has an error"
+--- ansible-core-2.16.5.orig/test/integration/targets/windows-minimal/library/win_ping_set_attr.ps1
++++ ansible-core-2.16.5/test/integration/targets/windows-minimal/library/win_ping_set_attr.ps1
+@@ -16,16 +16,16 @@
+
+ # POWERSHELL_COMMON
+
+-$params = Parse-Args $args $true
++$params = Parse-Args $args $true;
+
+-$data = Get-Attr $params "data" "pong"
++$data = Get-Attr $params "data" "pong";
+
+ $result = @{
+ changed = $false
+ ping = "pong"
+-}
++};
+
+ # Test that Set-Attr will replace an existing attribute.
+ Set-Attr $result "ping" $data
+
+-Exit-Json $result
++Exit-Json $result;
+--- ansible-core-2.16.5.orig/test/integration/targets/windows-minimal/library/win_ping_strict_mode_error.ps1
++++ ansible-core-2.16.5/test/integration/targets/windows-minimal/library/win_ping_strict_mode_error.ps1
+@@ -16,15 +16,15 @@
+
+ # POWERSHELL_COMMON
+
+-$params = Parse-Args $args $true
++$params = Parse-Args $args $true;
+
+ $params.thisPropertyDoesNotExist
+
+-$data = Get-Attr $params "data" "pong"
++$data = Get-Attr $params "data" "pong";
+
+ $result = @{
+ changed = $false
+ ping = $data
+-}
++};
+
+-Exit-Json $result
++Exit-Json $result;
+--- ansible-core-2.16.5.orig/test/integration/targets/windows-minimal/library/win_ping_syntax_error.ps1
++++ ansible-core-2.16.5/test/integration/targets/windows-minimal/library/win_ping_syntax_error.ps1
+@@ -18,13 +18,13 @@
+
+ $blah = 'I can't quote my strings correctly.'
+
+-$params = Parse-Args $args $true
++$params = Parse-Args $args $true;
+
+-$data = Get-Attr $params "data" "pong"
++$data = Get-Attr $params "data" "pong";
+
+ $result = @{
+ changed = $false
+ ping = $data
+-}
++};
+
+-Exit-Json $result
++Exit-Json $result;
+--- ansible-core-2.16.5.orig/test/integration/targets/windows-minimal/library/win_ping_throw.ps1
++++ ansible-core-2.16.5/test/integration/targets/windows-minimal/library/win_ping_throw.ps1
+@@ -18,13 +18,13 @@
+
+ throw
+
+-$params = Parse-Args $args $true
++$params = Parse-Args $args $true;
+
+-$data = Get-Attr $params "data" "pong"
++$data = Get-Attr $params "data" "pong";
+
+ $result = @{
+ changed = $false
+ ping = $data
+-}
++};
+
+-Exit-Json $result
++Exit-Json $result;
+--- ansible-core-2.16.5.orig/test/integration/targets/windows-minimal/library/win_ping_throw_string.ps1
++++ ansible-core-2.16.5/test/integration/targets/windows-minimal/library/win_ping_throw_string.ps1
+@@ -18,13 +18,13 @@
+
+ throw "no ping for you"
+
+-$params = Parse-Args $args $true
++$params = Parse-Args $args $true;
+
+-$data = Get-Attr $params "data" "pong"
++$data = Get-Attr $params "data" "pong";
+
+ $result = @{
+ changed = $false
+ ping = $data
+-}
++};
+
+-Exit-Json $result
++Exit-Json $result;
+--- ansible-core-2.16.5.orig/test/integration/targets/yum/aliases
++++ ansible-core-2.16.5/test/integration/targets/yum/aliases
+@@ -1,4 +1,5 @@
+ destructive
+ shippable/posix/group1
+ skip/freebsd
++skip/osx
+ skip/macos
+--- ansible-core-2.16.5.orig/test/integration/targets/yum/filter_plugins/filter_list_of_tuples_by_first_param.py
++++ ansible-core-2.16.5/test/integration/targets/yum/filter_plugins/filter_list_of_tuples_by_first_param.py
+@@ -1,6 +1,8 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
++from ansible.errors import AnsibleError, AnsibleFilterError
++
+
+ def filter_list_of_tuples_by_first_param(lst, search, startswith=False):
+ out = []
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/completion/docker.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/completion/docker.txt
+@@ -1,9 +1,9 @@
+-base image=quay.io/ansible/base-test-container:5.10.0 python=3.12,2.7,3.6,3.7,3.8,3.9,3.10,3.11
+-default image=quay.io/ansible/default-test-container:8.12.0 python=3.12,2.7,3.6,3.7,3.8,3.9,3.10,3.11 context=collection
+-default image=quay.io/ansible/ansible-core-test-container:8.12.0 python=3.12,2.7,3.6,3.7,3.8,3.9,3.10,3.11 context=ansible-core
+-alpine3 image=quay.io/ansible/alpine3-test-container:6.3.0 python=3.11 cgroup=none audit=none
+-centos7 image=quay.io/ansible/centos7-test-container:6.3.0 python=2.7 cgroup=v1-only
+-fedora38 image=quay.io/ansible/fedora38-test-container:6.3.0 python=3.11
+-opensuse15 image=quay.io/ansible/opensuse15-test-container:6.3.0 python=3.6
+-ubuntu2004 image=quay.io/ansible/ubuntu2004-test-container:6.3.0 python=3.8
+-ubuntu2204 image=quay.io/ansible/ubuntu2204-test-container:6.3.0 python=3.10
++base image=quay.io/ansible/base-test-container:3.9.0 python=3.11,2.7,3.5,3.6,3.7,3.8,3.9,3.10
++default image=quay.io/ansible/default-test-container:6.13.0 python=3.11,2.7,3.5,3.6,3.7,3.8,3.9,3.10 context=collection
++default image=quay.io/ansible/ansible-core-test-container:6.13.0 python=3.11,2.7,3.5,3.6,3.7,3.8,3.9,3.10 context=ansible-core
++alpine3 image=quay.io/ansible/alpine3-test-container:4.8.0 python=3.10 cgroup=none audit=none
++centos7 image=quay.io/ansible/centos7-test-container:4.8.0 python=2.7 cgroup=v1-only
++fedora36 image=quay.io/ansible/fedora36-test-container:4.8.0 python=3.10
++opensuse15 image=quay.io/ansible/opensuse15-test-container:4.8.0 python=3.6
++ubuntu2004 image=quay.io/ansible/ubuntu2004-test-container:4.8.0 python=3.8
++ubuntu2204 image=quay.io/ansible/ubuntu2204-test-container:4.8.0 python=3.10
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/completion/remote.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/completion/remote.txt
+@@ -1,14 +1,16 @@
+-alpine/3.18 python=3.11 become=doas_sudo provider=aws arch=x86_64
++alpine/3.16 python=3.10 become=doas_sudo provider=aws arch=x86_64
+ alpine become=doas_sudo provider=aws arch=x86_64
+-fedora/38 python=3.11 become=sudo provider=aws arch=x86_64
++fedora/36 python=3.10 become=sudo provider=aws arch=x86_64
+ fedora become=sudo provider=aws arch=x86_64
+-freebsd/13.2 python=3.9,3.11 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64
++freebsd/12.4 python=3.9 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64
++freebsd/13.2 python=3.8,3.7,3.9,3.10 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64
+ freebsd python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64
+-macos/13.2 python=3.11 python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64
++macos/12.0 python=3.10 python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64
+ macos python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64
+ rhel/7.9 python=2.7 become=sudo provider=aws arch=x86_64
+-rhel/8.8 python=3.6,3.11 become=sudo provider=aws arch=x86_64
+-rhel/9.2 python=3.9,3.11 become=sudo provider=aws arch=x86_64
++rhel/8.6 python=3.6,3.8,3.9 become=sudo provider=aws arch=x86_64
++rhel/9.0 python=3.9 become=sudo provider=aws arch=x86_64
+ rhel become=sudo provider=aws arch=x86_64
++ubuntu/20.04 python=3.8,3.9 become=sudo provider=aws arch=x86_64
+ ubuntu/22.04 python=3.10 become=sudo provider=aws arch=x86_64
+ ubuntu become=sudo provider=aws arch=x86_64
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/completion/windows.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/completion/windows.txt
+@@ -1,3 +1,5 @@
++windows/2012 provider=azure arch=x86_64
++windows/2012-R2 provider=azure arch=x86_64
+ windows/2016 provider=aws arch=x86_64
+ windows/2019 provider=aws arch=x86_64
+ windows/2022 provider=aws arch=x86_64
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/ansible-test.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/ansible-test.txt
+@@ -1,5 +1,4 @@
+ # The test-constraints sanity test verifies this file, but changes must be made manually to keep it in up-to-date.
+ virtualenv == 16.7.12 ; python_version < '3'
+-coverage == 7.3.2 ; python_version >= '3.8' and python_version <= '3.12'
+-coverage == 6.5.0 ; python_version >= '3.7' and python_version <= '3.7'
++coverage == 6.5.0 ; python_version >= '3.7' and python_version <= '3.11'
+ coverage == 4.5.4 ; python_version >= '2.6' and python_version <= '3.6'
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/ansible.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/ansible.txt
+@@ -12,4 +12,4 @@ packaging
+ # NOTE: Ref: https://github.com/sarugaku/resolvelib/issues/69
+ # NOTE: When updating the upper bound, also update the latest version used
+ # NOTE: in the ansible-galaxy-collection test suite.
+-resolvelib >= 0.5.3, < 1.1.0 # dependency resolver used by ansible-galaxy
++resolvelib >= 0.5.3, < 0.9.0 # dependency resolver used by ansible-galaxy
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/constraints.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/constraints.txt
+@@ -5,6 +5,7 @@ pywinrm >= 0.3.0 ; python_version < '3.1
+ pywinrm >= 0.4.3 ; python_version >= '3.11' # support for Python 3.11
+ pytest < 5.0.0, >= 4.5.0 ; python_version == '2.7' # pytest 5.0.0 and later will no longer support python 2.7
+ pytest >= 4.5.0 ; python_version > '2.7' # pytest 4.5.0 added support for --strict-markers
++pytest-forked >= 1.0.2 # pytest-forked before 1.0.2 does not work with pytest 4.2.0+
+ ntlm-auth >= 1.3.0 # message encryption support using cryptography
+ requests-ntlm >= 1.1.0 # message encryption support
+ requests-credssp >= 0.1.0 # message encryption support
+@@ -12,4 +13,5 @@ pyparsing < 3.0.0 ; python_version < '3.
+ mock >= 2.0.0 # needed for features backported from Python 3.6 unittest.mock (assert_called, assert_called_once...)
+ pytest-mock >= 1.4.0 # needed for mock_use_standalone_module pytest option
+ setuptools < 45 ; python_version == '2.7' # setuptools 45 and later require python 3.5 or later
++pyspnego >= 0.1.6 ; python_version >= '3.10' # bug in older releases breaks on Python 3.10
+ wheel < 0.38.0 ; python_version < '3.7' # wheel 0.38.0 and later require python 3.7 or later
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.ansible-doc.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.ansible-doc.txt
+@@ -1,5 +1,8 @@
+ # edit "sanity.ansible-doc.in" and generate with: hacking/update-sanity-requirements.py --test ansible-doc
++# pre-build requirement: pyyaml == 6.0
++# pre-build constraint: Cython < 3.0
+ Jinja2==3.1.2
+-MarkupSafe==2.1.3
+-packaging==23.2
+-PyYAML==6.0.1
++MarkupSafe==2.1.1
++packaging==21.3
++pyparsing==3.0.9
++PyYAML==6.0
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.changelog.in
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.changelog.in
+@@ -1,2 +1,3 @@
+-rstcheck < 6 # newer versions have too many dependencies
++rstcheck < 4 # match version used in other sanity tests
+ antsibull-changelog
++docutils < 0.18 # match version required by sphinx in the docs-build sanity test
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.changelog.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.changelog.txt
+@@ -1,9 +1,10 @@
+ # edit "sanity.changelog.in" and generate with: hacking/update-sanity-requirements.py --test changelog
+-antsibull-changelog==0.23.0
+-docutils==0.18.1
+-packaging==23.2
+-PyYAML==6.0.1
+-rstcheck==5.0.0
++# pre-build requirement: pyyaml == 6.0
++# pre-build constraint: Cython < 3.0
++antsibull-changelog==0.16.0
++docutils==0.17.1
++packaging==21.3
++pyparsing==3.0.9
++PyYAML==6.0
++rstcheck==3.5.0
+ semantic-version==2.10.0
+-types-docutils==0.18.3
+-typing_extensions==4.8.0
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.import.plugin.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.import.plugin.txt
+@@ -1,4 +1,6 @@
+ # edit "sanity.import.plugin.in" and generate with: hacking/update-sanity-requirements.py --test import.plugin
++# pre-build requirement: pyyaml == 6.0
++# pre-build constraint: Cython < 3.0
+ Jinja2==3.1.2
+-MarkupSafe==2.1.3
+-PyYAML==6.0.1
++MarkupSafe==2.1.1
++PyYAML==6.0
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.import.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.import.txt
+@@ -1,2 +1,4 @@
+ # edit "sanity.import.in" and generate with: hacking/update-sanity-requirements.py --test import
+-PyYAML==6.0.1
++# pre-build requirement: pyyaml == 6.0
++# pre-build constraint: Cython < 3.0
++PyYAML==6.0
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.integration-aliases.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.integration-aliases.txt
+@@ -1,2 +1,4 @@
+ # edit "sanity.integration-aliases.in" and generate with: hacking/update-sanity-requirements.py --test integration-aliases
+-PyYAML==6.0.1
++# pre-build requirement: pyyaml == 6.0
++# pre-build constraint: Cython < 3.0
++PyYAML==6.0
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.mypy.in
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.mypy.in
+@@ -1,10 +1,10 @@
+-mypy
+-cryptography # type stubs not published separately
+-jinja2 # type stubs not published separately
++mypy[python2] != 0.971 # regression in 0.971 (see https://github.com/python/mypy/pull/13223)
+ packaging # type stubs not published separately
+ types-backports
+-types-paramiko
+-types-pyyaml
++types-jinja2
++types-paramiko < 2.8.14 # newer versions drop support for Python 2.7
++types-pyyaml < 6 # PyYAML 6+ stubs do not support Python 2.7
++types-cryptography < 3.3.16 # newer versions drop support for Python 2.7
+ types-requests
+ types-setuptools
+ types-toml
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.mypy.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.mypy.txt
+@@ -1,18 +1,20 @@
+ # edit "sanity.mypy.in" and generate with: hacking/update-sanity-requirements.py --test mypy
+-cffi==1.16.0
+-cryptography==41.0.4
+-Jinja2==3.1.2
+-MarkupSafe==2.1.3
+-mypy==1.5.1
+-mypy-extensions==1.0.0
+-packaging==23.2
+-pycparser==2.21
++mypy==0.961
++mypy-extensions==0.4.3
++packaging==21.3
++pyparsing==3.0.9
+ tomli==2.0.1
++typed-ast==1.5.4
+ types-backports==0.1.3
+-types-paramiko==3.3.0.0
+-types-PyYAML==6.0.12.12
+-types-requests==2.31.0.7
+-types-setuptools==68.2.0.0
+-types-toml==0.10.8.7
+-typing_extensions==4.8.0
+-urllib3==2.0.6
++types-cryptography==3.3.15
++types-enum34==1.1.8
++types-ipaddress==1.0.8
++types-Jinja2==2.11.9
++types-MarkupSafe==1.1.10
++types-paramiko==2.8.13
++types-PyYAML==5.4.12
++types-requests==2.28.10
++types-setuptools==65.3.0
++types-toml==0.10.8
++types-urllib3==1.26.24
++typing_extensions==4.3.0
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.pep8.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.pep8.txt
+@@ -1,2 +1,2 @@
+ # edit "sanity.pep8.in" and generate with: hacking/update-sanity-requirements.py --test pep8
+-pycodestyle==2.11.0
++pycodestyle==2.9.1
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.pslint.ps1
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.pslint.ps1
+@@ -28,10 +28,8 @@ Function Install-PSModule {
+ }
+ }
+
+-# Versions changes should be made first in ansible-test which is then synced to
+-# the default-test-container over time
+ Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
+-Install-PSModule -Name PSScriptAnalyzer -RequiredVersion 1.21.0
++Install-PSModule -Name PSScriptAnalyzer -RequiredVersion 1.20.0
+
+ if ($IsContainer) {
+ # PSScriptAnalyzer contain lots of json files for the UseCompatibleCommands check. We don't use this rule so by
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.pylint.in
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.pylint.in
+@@ -1,2 +1,2 @@
+-pylint
++pylint == 2.15.5 # currently vetted version
+ pyyaml # needed for collection_detail.py
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.pylint.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.pylint.txt
+@@ -1,11 +1,15 @@
+ # edit "sanity.pylint.in" and generate with: hacking/update-sanity-requirements.py --test pylint
+-astroid==3.0.0
+-dill==0.3.7
+-isort==5.12.0
++# pre-build requirement: pyyaml == 6.0
++# pre-build constraint: Cython < 3.0
++astroid==2.12.12
++dill==0.3.6
++isort==5.10.1
++lazy-object-proxy==1.7.1
+ mccabe==0.7.0
+-platformdirs==3.11.0
+-pylint==3.0.1
+-PyYAML==6.0.1
++platformdirs==2.5.2
++pylint==2.15.5
++PyYAML==6.0
+ tomli==2.0.1
+-tomlkit==0.12.1
+-typing_extensions==4.8.0
++tomlkit==0.11.5
++typing_extensions==4.3.0
++wrapt==1.14.1
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.runtime-metadata.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.runtime-metadata.txt
+@@ -1,3 +1,5 @@
+ # edit "sanity.runtime-metadata.in" and generate with: hacking/update-sanity-requirements.py --test runtime-metadata
+-PyYAML==6.0.1
++# pre-build requirement: pyyaml == 6.0
++# pre-build constraint: Cython < 3.0
++PyYAML==6.0
+ voluptuous==0.13.1
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.validate-modules.in
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.validate-modules.in
+@@ -1,4 +1,3 @@
+ jinja2 # ansible-core requirement
+ pyyaml # needed for collection_detail.py
+ voluptuous
+-antsibull-docs-parser==1.0.0
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.validate-modules.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.validate-modules.txt
+@@ -1,6 +1,7 @@
+ # edit "sanity.validate-modules.in" and generate with: hacking/update-sanity-requirements.py --test validate-modules
+-antsibull-docs-parser==1.0.0
++# pre-build requirement: pyyaml == 6.0
++# pre-build constraint: Cython < 3.0
+ Jinja2==3.1.2
+-MarkupSafe==2.1.3
+-PyYAML==6.0.1
++MarkupSafe==2.1.1
++PyYAML==6.0
+ voluptuous==0.13.1
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/sanity.yamllint.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/sanity.yamllint.txt
+@@ -1,4 +1,6 @@
+ # edit "sanity.yamllint.in" and generate with: hacking/update-sanity-requirements.py --test yamllint
+-pathspec==0.11.2
+-PyYAML==6.0.1
+-yamllint==1.32.0
++# pre-build requirement: pyyaml == 6.0
++# pre-build constraint: Cython < 3.0
++pathspec==0.10.1
++PyYAML==6.0
++yamllint==1.28.0
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_data/requirements/units.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_data/requirements/units.txt
+@@ -2,4 +2,5 @@ mock
+ pytest
+ pytest-mock
+ pytest-xdist
++pytest-forked
+ pyyaml # required by the collection loader (only needed for collections)
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/ci/azp.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/ci/azp.py
+@@ -70,7 +70,7 @@ class AzurePipelines(CIProvider):
+ os.environ['SYSTEM_JOBIDENTIFIER'],
+ )
+ except KeyError as ex:
+- raise MissingEnvironmentVariable(name=ex.args[0]) from None
++ raise MissingEnvironmentVariable(name=ex.args[0])
+
+ return prefix
+
+@@ -121,7 +121,7 @@ class AzurePipelines(CIProvider):
+ task_id=str(uuid.UUID(os.environ['SYSTEM_TASKINSTANCEID'])),
+ )
+ except KeyError as ex:
+- raise MissingEnvironmentVariable(name=ex.args[0]) from None
++ raise MissingEnvironmentVariable(name=ex.args[0])
+
+ self.auth.sign_request(request)
+
+@@ -154,7 +154,7 @@ class AzurePipelinesAuthHelper(Cryptogra
+ try:
+ agent_temp_directory = os.environ['AGENT_TEMPDIRECTORY']
+ except KeyError as ex:
+- raise MissingEnvironmentVariable(name=ex.args[0]) from None
++ raise MissingEnvironmentVariable(name=ex.args[0])
+
+ # the temporary file cannot be deleted because we do not know when the agent has processed it
+ # placing the file in the agent's temp directory allows it to be picked up when the job is running in a container
+@@ -181,7 +181,7 @@ class AzurePipelinesChanges:
+ self.source_branch_name = os.environ['BUILD_SOURCEBRANCHNAME']
+ self.pr_branch_name = os.environ.get('SYSTEM_PULLREQUEST_TARGETBRANCH')
+ except KeyError as ex:
+- raise MissingEnvironmentVariable(name=ex.args[0]) from None
++ raise MissingEnvironmentVariable(name=ex.args[0])
+
+ if self.source_branch.startswith('refs/tags/'):
+ raise ChangeDetectionNotSupported('Change detection is not supported for tags.')
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/cli/environments.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/cli/environments.py
+@@ -146,6 +146,12 @@ def add_global_options(
+ help='install command requirements',
+ )
+
++ global_parser.add_argument(
++ '--no-pip-check',
++ action='store_true',
++ help=argparse.SUPPRESS, # deprecated, kept for now (with a warning) for backwards compatibility
++ )
++
+ add_global_remote(global_parser, controller_mode)
+ add_global_docker(global_parser, controller_mode)
+
+@@ -390,6 +396,7 @@ def add_global_docker(
+ """Add global options for Docker."""
+ if controller_mode != ControllerMode.DELEGATED:
+ parser.set_defaults(
++ docker_no_pull=False,
+ docker_network=None,
+ docker_terminate=None,
+ prime_containers=False,
+@@ -400,6 +407,12 @@ def add_global_docker(
+ return
+
+ parser.add_argument(
++ '--docker-no-pull',
++ action='store_true',
++ help=argparse.SUPPRESS, # deprecated, kept for now (with a warning) for backwards compatibility
++ )
++
++ parser.add_argument(
+ '--docker-network',
+ metavar='NET',
+ help='run using the specified network',
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/coverage/analyze/targets/__init__.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/coverage/analyze/targets/__init__.py
+@@ -57,9 +57,9 @@ def load_report(report: dict[str, t.Any]
+ arc_data: dict[str, dict[str, int]] = report['arcs']
+ line_data: dict[str, dict[int, int]] = report['lines']
+ except KeyError as ex:
+- raise ApplicationError('Document is missing key "%s".' % ex.args) from None
++ raise ApplicationError('Document is missing key "%s".' % ex.args)
+ except TypeError:
+- raise ApplicationError('Document is type "%s" instead of "dict".' % type(report).__name__) from None
++ raise ApplicationError('Document is type "%s" instead of "dict".' % type(report).__name__)
+
+ arcs = dict((path, dict((parse_arc(arc), set(target_sets[index])) for arc, index in data.items())) for path, data in arc_data.items())
+ lines = dict((path, dict((int(line), set(target_sets[index])) for line, index in data.items())) for path, data in line_data.items())
+@@ -72,12 +72,12 @@ def read_report(path: str) -> tuple[list
+ try:
+ report = read_json_file(path)
+ except Exception as ex:
+- raise ApplicationError('File "%s" is not valid JSON: %s' % (path, ex)) from None
++ raise ApplicationError('File "%s" is not valid JSON: %s' % (path, ex))
+
+ try:
+ return load_report(report)
+ except ApplicationError as ex:
+- raise ApplicationError('File "%s" is not an aggregated coverage data file. %s' % (path, ex)) from None
++ raise ApplicationError('File "%s" is not an aggregated coverage data file. %s' % (path, ex))
+
+
+ def write_report(args: CoverageAnalyzeTargetsConfig, report: dict[str, t.Any], path: str) -> None:
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/coverage/combine.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/coverage/combine.py
+@@ -121,7 +121,7 @@ def _command_coverage_combine_python(arg
+ coverage_files = get_python_coverage_files()
+
+ def _default_stub_value(source_paths: list[str]) -> dict[str, set[tuple[int, int]]]:
+- return {path: {(0, 0)} for path in source_paths}
++ return {path: set() for path in source_paths}
+
+ counter = 0
+ sources = _get_coverage_targets(args, walk_compile_targets)
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/integration/cloud/acme.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/integration/cloud/acme.py
+@@ -8,6 +8,7 @@ from ....config import (
+ )
+
+ from ....containers import (
++ CleanupMode,
+ run_support_container,
+ )
+
+@@ -21,6 +22,8 @@ from . import (
+ class ACMEProvider(CloudProvider):
+ """ACME plugin. Sets up cloud resources for tests."""
+
++ DOCKER_SIMULATOR_NAME = 'acme-simulator'
++
+ def __init__(self, args: IntegrationConfig) -> None:
+ super().__init__(args)
+
+@@ -48,18 +51,17 @@ class ACMEProvider(CloudProvider):
+ 14000, # Pebble ACME CA
+ ]
+
+- descriptor = run_support_container(
++ run_support_container(
+ self.args,
+ self.platform,
+ self.image,
+- 'acme-simulator',
++ self.DOCKER_SIMULATOR_NAME,
+ ports,
++ allow_existing=True,
++ cleanup=CleanupMode.YES,
+ )
+
+- if not descriptor:
+- return
+-
+- self._set_cloud_config('acme_host', descriptor.name)
++ self._set_cloud_config('acme_host', self.DOCKER_SIMULATOR_NAME)
+
+ def _setup_static(self) -> None:
+ raise NotImplementedError()
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/integration/cloud/cs.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/integration/cloud/cs.py
+@@ -21,6 +21,7 @@ from ....docker_util import (
+ )
+
+ from ....containers import (
++ CleanupMode,
+ run_support_container,
+ wait_for_file,
+ )
+@@ -35,10 +36,12 @@ from . import (
+ class CsCloudProvider(CloudProvider):
+ """CloudStack cloud provider plugin. Sets up cloud resources before delegation."""
+
++ DOCKER_SIMULATOR_NAME = 'cloudstack-sim'
++
+ def __init__(self, args: IntegrationConfig) -> None:
+ super().__init__(args)
+
+- self.image = os.environ.get('ANSIBLE_CLOUDSTACK_CONTAINER', 'quay.io/ansible/cloudstack-test-container:1.6.1')
++ self.image = os.environ.get('ANSIBLE_CLOUDSTACK_CONTAINER', 'quay.io/ansible/cloudstack-test-container:1.4.0')
+ self.host = ''
+ self.port = 0
+
+@@ -93,8 +96,10 @@ class CsCloudProvider(CloudProvider):
+ self.args,
+ self.platform,
+ self.image,
+- 'cloudstack-sim',
++ self.DOCKER_SIMULATOR_NAME,
+ ports,
++ allow_existing=True,
++ cleanup=CleanupMode.YES,
+ )
+
+ if not descriptor:
+@@ -102,7 +107,7 @@ class CsCloudProvider(CloudProvider):
+
+ # apply work-around for OverlayFS issue
+ # https://github.com/docker/for-linux/issues/72#issuecomment-319904698
+- docker_exec(self.args, descriptor.name, ['find', '/var/lib/mysql', '-type', 'f', '-exec', 'touch', '{}', ';'], capture=True)
++ docker_exec(self.args, self.DOCKER_SIMULATOR_NAME, ['find', '/var/lib/mysql', '-type', 'f', '-exec', 'touch', '{}', ';'], capture=True)
+
+ if self.args.explain:
+ values = dict(
+@@ -110,10 +115,10 @@ class CsCloudProvider(CloudProvider):
+ PORT=str(self.port),
+ )
+ else:
+- credentials = self._get_credentials(descriptor.name)
++ credentials = self._get_credentials(self.DOCKER_SIMULATOR_NAME)
+
+ values = dict(
+- HOST=descriptor.name,
++ HOST=self.DOCKER_SIMULATOR_NAME,
+ PORT=str(self.port),
+ KEY=credentials['apikey'],
+ SECRET=credentials['secretkey'],
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/integration/cloud/galaxy.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/integration/cloud/galaxy.py
+@@ -10,21 +10,12 @@ from ....config import (
+
+ from ....docker_util import (
+ docker_cp_to,
+- docker_exec,
+ )
+
+ from ....containers import (
+ run_support_container,
+ )
+
+-from ....encoding import (
+- to_text,
+-)
+-
+-from ....util import (
+- display,
+-)
+-
+ from . import (
+ CloudEnvironment,
+ CloudEnvironmentConfig,
+@@ -32,59 +23,53 @@ from . import (
+ )
+
+
+-GALAXY_HOST_NAME = 'galaxy-pulp'
+-SETTINGS = {
+- 'PULP_CONTENT_ORIGIN': f'http://{GALAXY_HOST_NAME}',
+- 'PULP_ANSIBLE_API_HOSTNAME': f'http://{GALAXY_HOST_NAME}',
+- 'PULP_GALAXY_API_PATH_PREFIX': '/api/galaxy/',
+- # These paths are unique to the container image which has an nginx location for /pulp/content to route
+- # requests to the content backend
+- 'PULP_ANSIBLE_CONTENT_HOSTNAME': f'http://{GALAXY_HOST_NAME}/pulp/content/api/galaxy/v3/artifacts/collections/',
+- 'PULP_CONTENT_PATH_PREFIX': '/pulp/content/api/galaxy/v3/artifacts/collections/',
+- 'PULP_GALAXY_AUTHENTICATION_CLASSES': [
+- 'rest_framework.authentication.SessionAuthentication',
+- 'rest_framework.authentication.TokenAuthentication',
+- 'rest_framework.authentication.BasicAuthentication',
+- 'django.contrib.auth.backends.ModelBackend',
+- ],
+- # This should probably be false see https://issues.redhat.com/browse/AAH-2328
+- 'PULP_GALAXY_REQUIRE_CONTENT_APPROVAL': 'true',
+- 'PULP_GALAXY_DEPLOYMENT_MODE': 'standalone',
+- 'PULP_GALAXY_AUTO_SIGN_COLLECTIONS': 'false',
+- 'PULP_GALAXY_COLLECTION_SIGNING_SERVICE': 'ansible-default',
+- 'PULP_RH_ENTITLEMENT_REQUIRED': 'insights',
+- 'PULP_TOKEN_AUTH_DISABLED': 'false',
+- 'PULP_TOKEN_SERVER': f'http://{GALAXY_HOST_NAME}/token/',
+- 'PULP_TOKEN_SIGNATURE_ALGORITHM': 'ES256',
+- 'PULP_PUBLIC_KEY_PATH': '/src/galaxy_ng/dev/common/container_auth_public_key.pem',
+- 'PULP_PRIVATE_KEY_PATH': '/src/galaxy_ng/dev/common/container_auth_private_key.pem',
+- 'PULP_ANALYTICS': 'false',
+- 'PULP_GALAXY_ENABLE_UNAUTHENTICATED_COLLECTION_ACCESS': 'true',
+- 'PULP_GALAXY_ENABLE_UNAUTHENTICATED_COLLECTION_DOWNLOAD': 'true',
+- 'PULP_GALAXY_ENABLE_LEGACY_ROLES': 'true',
+- 'PULP_GALAXY_FEATURE_FLAGS__execution_environments': 'false',
+- 'PULP_SOCIAL_AUTH_LOGIN_REDIRECT_URL': '/',
+- 'PULP_GALAXY_FEATURE_FLAGS__ai_deny_index': 'true',
+- 'PULP_DEFAULT_ADMIN_PASSWORD': 'password'
++# We add BasicAuthentication, to make the tasks that deal with
++# direct API access easier to deal with across galaxy_ng and pulp
++SETTINGS = b'''
++CONTENT_ORIGIN = 'http://ansible-ci-pulp:80'
++ANSIBLE_API_HOSTNAME = 'http://ansible-ci-pulp:80'
++ANSIBLE_CONTENT_HOSTNAME = 'http://ansible-ci-pulp:80/pulp/content'
++TOKEN_AUTH_DISABLED = True
++GALAXY_REQUIRE_CONTENT_APPROVAL = False
++GALAXY_AUTHENTICATION_CLASSES = [
++ "rest_framework.authentication.SessionAuthentication",
++ "rest_framework.authentication.TokenAuthentication",
++ "rest_framework.authentication.BasicAuthentication",
++]
++'''
++
++SET_ADMIN_PASSWORD = b'''#!/usr/bin/execlineb -S0
++foreground {
++ redirfd -w 1 /dev/null
++ redirfd -w 2 /dev/null
++ export DJANGO_SETTINGS_MODULE pulpcore.app.settings
++ export PULP_CONTENT_ORIGIN localhost
++ s6-setuidgid postgres
++ if { /usr/local/bin/django-admin reset-admin-password --password password }
++ if { /usr/local/bin/pulpcore-manager create-group system:partner-engineers --users admin }
+ }
++'''
+
++# There are 2 overrides here:
++# 1. Change the gunicorn bind address from 127.0.0.1 to 0.0.0.0 now that Galaxy NG does not allow us to access the
++# Pulp API through it.
++# 2. Grant access allowing us to DELETE a namespace in Galaxy NG. This is as CI deletes and recreates repos and
++# distributions in Pulp which now breaks the namespace in Galaxy NG. Recreating it is the "simple" fix to get it
++# working again.
++# These may not be needed in the future, especially if 1 becomes configurable by an env var but for now they must be
++# done.
++OVERRIDES = b'''#!/usr/bin/execlineb -S0
++foreground {
++ sed -i "0,/\\"127.0.0.1:24817\\"/s//\\"0.0.0.0:24817\\"/" /etc/services.d/pulpcore-api/run
++}
+
+-GALAXY_IMPORTER = b'''
+-[galaxy-importer]
+-ansible_local_tmp=~/.ansible/tmp
+-ansible_test_local_image=false
+-check_required_tags=false
+-check_runtime_yaml=false
+-check_changelog=false
+-infra_osd=false
+-local_image_docker=false
+-log_level_main=INFO
+-require_v1_or_greater=false
+-run_ansible_doc=false
+-run_ansible_lint=false
+-run_ansible_test=false
+-run_flake8=false
+-'''.strip()
++# This sed calls changes the first occurrence to "allow" which is conveniently the delete operation for a namespace.
++# https://github.com/ansible/galaxy_ng/blob/master/galaxy_ng/app/access_control/statements/standalone.py#L9-L11.
++backtick NG_PREFIX { python -c "import galaxy_ng; print(galaxy_ng.__path__[0], end='')" }
++importas ng_prefix NG_PREFIX
++foreground {
++ sed -i "0,/\\"effect\\": \\"deny\\"/s//\\"effect\\": \\"allow\\"/" ${ng_prefix}/app/access_control/statements/standalone.py
++}'''
+
+
+ class GalaxyProvider(CloudProvider):
+@@ -96,9 +81,13 @@ class GalaxyProvider(CloudProvider):
+ def __init__(self, args: IntegrationConfig) -> None:
+ super().__init__(args)
+
+- self.image = os.environ.get(
++ # Cannot use the latest container image as either galaxy_ng 4.2.0rc2 or pulp 0.5.0 has sporatic issues with
++ # dropping published collections in CI. Try running the tests multiple times when updating. Will also need to
++ # comment out the cache tests in 'test/integration/targets/ansible-galaxy-collection/tasks/install.yml' when
++ # the newer update is available.
++ self.pulp = os.environ.get(
+ 'ANSIBLE_PULP_CONTAINER',
+- 'quay.io/pulp/galaxy:4.7.1'
++ 'quay.io/ansible/pulp-galaxy-ng:b79a7be64eff'
+ )
+
+ self.uses_docker = True
+@@ -107,46 +96,48 @@ class GalaxyProvider(CloudProvider):
+ """Setup cloud resource before delegation and reg cleanup callback."""
+ super().setup()
+
+- with tempfile.NamedTemporaryFile(mode='w+') as env_fd:
+- settings = '\n'.join(
+- f'{key}={value}' for key, value in SETTINGS.items()
+- )
+- env_fd.write(settings)
+- env_fd.flush()
+- display.info(f'>>> galaxy_ng Configuration\n{settings}', verbosity=3)
+- descriptor = run_support_container(
+- self.args,
+- self.platform,
+- self.image,
+- GALAXY_HOST_NAME,
+- [
+- 80,
+- ],
+- aliases=[
+- GALAXY_HOST_NAME,
+- ],
+- start=True,
+- options=[
+- '--env-file', env_fd.name,
+- ],
+- )
++ galaxy_port = 80
++ pulp_host = 'ansible-ci-pulp'
++ pulp_port = 24817
++
++ ports = [
++ galaxy_port,
++ pulp_port,
++ ]
++
++ # Create the container, don't run it, we need to inject configs before it starts
++ descriptor = run_support_container(
++ self.args,
++ self.platform,
++ self.pulp,
++ pulp_host,
++ ports,
++ start=False,
++ allow_existing=True,
++ )
+
+ if not descriptor:
+ return
+
+- injected_files = [
+- ('/etc/galaxy-importer/galaxy-importer.cfg', GALAXY_IMPORTER, 'galaxy-importer'),
+- ]
+- for path, content, friendly_name in injected_files:
+- with tempfile.NamedTemporaryFile() as temp_fd:
+- temp_fd.write(content)
+- temp_fd.flush()
+- display.info(f'>>> {friendly_name} Configuration\n{to_text(content)}', verbosity=3)
+- docker_exec(self.args, descriptor.container_id, ['mkdir', '-p', os.path.dirname(path)], True)
+- docker_cp_to(self.args, descriptor.container_id, temp_fd.name, path)
+- docker_exec(self.args, descriptor.container_id, ['chown', 'pulp:pulp', path], True)
++ if not descriptor.running:
++ pulp_id = descriptor.container_id
+
+- self._set_cloud_config('PULP_HOST', GALAXY_HOST_NAME)
++ injected_files = {
++ '/etc/pulp/settings.py': SETTINGS,
++ '/etc/cont-init.d/111-postgres': SET_ADMIN_PASSWORD,
++ '/etc/cont-init.d/000-ansible-test-overrides': OVERRIDES,
++ }
++ for path, content in injected_files.items():
++ with tempfile.NamedTemporaryFile() as temp_fd:
++ temp_fd.write(content)
++ temp_fd.flush()
++ docker_cp_to(self.args, pulp_id, temp_fd.name, path)
++
++ descriptor.start(self.args)
++
++ self._set_cloud_config('PULP_HOST', pulp_host)
++ self._set_cloud_config('PULP_PORT', str(pulp_port))
++ self._set_cloud_config('GALAXY_PORT', str(galaxy_port))
+ self._set_cloud_config('PULP_USER', 'admin')
+ self._set_cloud_config('PULP_PASSWORD', 'password')
+
+@@ -159,19 +150,21 @@ class GalaxyEnvironment(CloudEnvironment
+ pulp_user = str(self._get_cloud_config('PULP_USER'))
+ pulp_password = str(self._get_cloud_config('PULP_PASSWORD'))
+ pulp_host = self._get_cloud_config('PULP_HOST')
++ galaxy_port = self._get_cloud_config('GALAXY_PORT')
++ pulp_port = self._get_cloud_config('PULP_PORT')
+
+ return CloudEnvironmentConfig(
+ ansible_vars=dict(
+ pulp_user=pulp_user,
+ pulp_password=pulp_password,
+- pulp_api=f'http://{pulp_host}',
+- pulp_server=f'http://{pulp_host}/pulp_ansible/galaxy/',
+- galaxy_ng_server=f'http://{pulp_host}/api/galaxy/',
++ pulp_api='http://%s:%s' % (pulp_host, pulp_port),
++ pulp_server='http://%s:%s/pulp_ansible/galaxy/' % (pulp_host, pulp_port),
++ galaxy_ng_server='http://%s:%s/api/galaxy/' % (pulp_host, galaxy_port),
+ ),
+ env_vars=dict(
+ PULP_USER=pulp_user,
+ PULP_PASSWORD=pulp_password,
+- PULP_SERVER=f'http://{pulp_host}/pulp_ansible/galaxy/api/',
+- GALAXY_NG_SERVER=f'http://{pulp_host}/api/galaxy/',
++ PULP_SERVER='http://%s:%s/pulp_ansible/galaxy/api/' % (pulp_host, pulp_port),
++ GALAXY_NG_SERVER='http://%s:%s/api/galaxy/' % (pulp_host, galaxy_port),
+ ),
+ )
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/integration/cloud/httptester.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/integration/cloud/httptester.py
+@@ -13,6 +13,7 @@ from ....config import (
+ )
+
+ from ....containers import (
++ CleanupMode,
+ run_support_container,
+ )
+
+@@ -61,6 +62,8 @@ class HttptesterProvider(CloudProvider):
+ 'http-test-container',
+ ports,
+ aliases=aliases,
++ allow_existing=True,
++ cleanup=CleanupMode.YES,
+ env={
+ KRB5_PASSWORD_ENV: generate_password(),
+ },
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/integration/cloud/nios.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/integration/cloud/nios.py
+@@ -8,6 +8,7 @@ from ....config import (
+ )
+
+ from ....containers import (
++ CleanupMode,
+ run_support_container,
+ )
+
+@@ -21,6 +22,8 @@ from . import (
+ class NiosProvider(CloudProvider):
+ """Nios plugin. Sets up NIOS mock server for tests."""
+
++ DOCKER_SIMULATOR_NAME = 'nios-simulator'
++
+ # Default image to run the nios simulator.
+ #
+ # The simulator must be pinned to a specific version
+@@ -28,7 +31,7 @@ class NiosProvider(CloudProvider):
+ #
+ # It's source source itself resides at:
+ # https://github.com/ansible/nios-test-container
+- DOCKER_IMAGE = 'quay.io/ansible/nios-test-container:2.0.0'
++ DOCKER_IMAGE = 'quay.io/ansible/nios-test-container:1.4.0'
+
+ def __init__(self, args: IntegrationConfig) -> None:
+ super().__init__(args)
+@@ -62,18 +65,17 @@ class NiosProvider(CloudProvider):
+ nios_port,
+ ]
+
+- descriptor = run_support_container(
++ run_support_container(
+ self.args,
+ self.platform,
+ self.image,
+- 'nios-simulator',
++ self.DOCKER_SIMULATOR_NAME,
+ ports,
++ allow_existing=True,
++ cleanup=CleanupMode.YES,
+ )
+
+- if not descriptor:
+- return
+-
+- self._set_cloud_config('NIOS_HOST', descriptor.name)
++ self._set_cloud_config('NIOS_HOST', self.DOCKER_SIMULATOR_NAME)
+
+ def _setup_static(self) -> None:
+ raise NotImplementedError()
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/integration/cloud/openshift.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/integration/cloud/openshift.py
+@@ -16,6 +16,7 @@ from ....config import (
+ )
+
+ from ....containers import (
++ CleanupMode,
+ run_support_container,
+ wait_for_file,
+ )
+@@ -30,6 +31,8 @@ from . import (
+ class OpenShiftCloudProvider(CloudProvider):
+ """OpenShift cloud provider plugin. Sets up cloud resources before delegation."""
+
++ DOCKER_CONTAINER_NAME = 'openshift-origin'
++
+ def __init__(self, args: IntegrationConfig) -> None:
+ super().__init__(args, config_extension='.kubeconfig')
+
+@@ -71,8 +74,10 @@ class OpenShiftCloudProvider(CloudProvid
+ self.args,
+ self.platform,
+ self.image,
+- 'openshift-origin',
++ self.DOCKER_CONTAINER_NAME,
+ ports,
++ allow_existing=True,
++ cleanup=CleanupMode.YES,
+ cmd=cmd,
+ )
+
+@@ -82,7 +87,7 @@ class OpenShiftCloudProvider(CloudProvid
+ if self.args.explain:
+ config = '# Unknown'
+ else:
+- config = self._get_config(descriptor.name, 'https://%s:%s/' % (descriptor.name, port))
++ config = self._get_config(self.DOCKER_CONTAINER_NAME, 'https://%s:%s/' % (self.DOCKER_CONTAINER_NAME, port))
+
+ self._write_config(config)
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/integration/cloud/vcenter.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/integration/cloud/vcenter.py
+@@ -2,6 +2,7 @@
+ from __future__ import annotations
+
+ import configparser
++import os
+
+ from ....util import (
+ ApplicationError,
+@@ -12,6 +13,11 @@ from ....config import (
+ IntegrationConfig,
+ )
+
++from ....containers import (
++ CleanupMode,
++ run_support_container,
++)
++
+ from . import (
+ CloudEnvironment,
+ CloudEnvironmentConfig,
+@@ -22,16 +28,66 @@ from . import (
+ class VcenterProvider(CloudProvider):
+ """VMware vcenter/esx plugin. Sets up cloud resources for tests."""
+
++ DOCKER_SIMULATOR_NAME = 'vcenter-simulator'
++
+ def __init__(self, args: IntegrationConfig) -> None:
+ super().__init__(args)
+
+- self.uses_config = True
++ # The simulator must be pinned to a specific version to guarantee CI passes with the version used.
++ if os.environ.get('ANSIBLE_VCSIM_CONTAINER'):
++ self.image = os.environ.get('ANSIBLE_VCSIM_CONTAINER')
++ else:
++ self.image = 'quay.io/ansible/vcenter-test-container:1.7.0'
++
++ # VMware tests can be run on govcsim or BYO with a static config file.
++ # The simulator is the default if no config is provided.
++ self.vmware_test_platform = os.environ.get('VMWARE_TEST_PLATFORM', 'govcsim')
++
++ if self.vmware_test_platform == 'govcsim':
++ self.uses_docker = True
++ self.uses_config = False
++ elif self.vmware_test_platform == 'static':
++ self.uses_docker = False
++ self.uses_config = True
+
+ def setup(self) -> None:
+ """Setup the cloud resource before delegation and register a cleanup callback."""
+ super().setup()
+
+- if not self._use_static_config():
++ self._set_cloud_config('vmware_test_platform', self.vmware_test_platform)
++
++ if self.vmware_test_platform == 'govcsim':
++ self._setup_dynamic_simulator()
++ self.managed = True
++ elif self.vmware_test_platform == 'static':
++ self._use_static_config()
++ self._setup_static()
++ else:
++ raise ApplicationError('Unknown vmware_test_platform: %s' % self.vmware_test_platform)
++
++ def _setup_dynamic_simulator(self) -> None:
++ """Create a vcenter simulator using docker."""
++ ports = [
++ 443,
++ 8080,
++ 8989,
++ 5000, # control port for flask app in simulator
++ ]
++
++ run_support_container(
++ self.args,
++ self.platform,
++ self.image,
++ self.DOCKER_SIMULATOR_NAME,
++ ports,
++ allow_existing=True,
++ cleanup=CleanupMode.YES,
++ )
++
++ self._set_cloud_config('vcenter_hostname', self.DOCKER_SIMULATOR_NAME)
++
++ def _setup_static(self) -> None:
++ if not os.path.exists(self.config_static_path):
+ raise ApplicationError('Configuration file does not exist: %s' % self.config_static_path)
+
+
+@@ -40,21 +96,37 @@ class VcenterEnvironment(CloudEnvironmen
+
+ def get_environment_config(self) -> CloudEnvironmentConfig:
+ """Return environment configuration for use in the test environment after delegation."""
+- # We may be in a container, so we cannot just reach VMWARE_TEST_PLATFORM,
+- # We do a try/except instead
+- parser = configparser.ConfigParser()
+- parser.read(self.config_path) # static
+-
+- ansible_vars = dict(
+- resource_prefix=self.resource_prefix,
+- )
+- ansible_vars.update(dict(parser.items('DEFAULT', raw=True)))
++ try:
++ # We may be in a container, so we cannot just reach VMWARE_TEST_PLATFORM,
++ # We do a try/except instead
++ parser = configparser.ConfigParser()
++ parser.read(self.config_path) # static
++
++ env_vars = {}
++ ansible_vars = dict(
++ resource_prefix=self.resource_prefix,
++ )
++ ansible_vars.update(dict(parser.items('DEFAULT', raw=True)))
++ except KeyError: # govcsim
++ env_vars = dict(
++ VCENTER_HOSTNAME=str(self._get_cloud_config('vcenter_hostname')),
++ VCENTER_USERNAME='user',
++ VCENTER_PASSWORD='pass',
++ )
++
++ ansible_vars = dict(
++ vcsim=str(self._get_cloud_config('vcenter_hostname')),
++ vcenter_hostname=str(self._get_cloud_config('vcenter_hostname')),
++ vcenter_username='user',
++ vcenter_password='pass',
++ )
+
+ for key, value in ansible_vars.items():
+ if key.endswith('_password'):
+ display.sensitive.add(value)
+
+ return CloudEnvironmentConfig(
++ env_vars=env_vars,
+ ansible_vars=ansible_vars,
+ module_defaults={
+ 'group/vmware': {
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/sanity/__init__.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/sanity/__init__.py
+@@ -127,13 +127,9 @@ TARGET_SANITY_ROOT = os.path.join(ANSIBL
+
+ # NOTE: must match ansible.constants.DOCUMENTABLE_PLUGINS, but with 'module' replaced by 'modules'!
+ DOCUMENTABLE_PLUGINS = (
+- 'become', 'cache', 'callback', 'cliconf', 'connection', 'filter', 'httpapi', 'inventory',
+- 'lookup', 'netconf', 'modules', 'shell', 'strategy', 'test', 'vars',
++ 'become', 'cache', 'callback', 'cliconf', 'connection', 'httpapi', 'inventory', 'lookup', 'netconf', 'modules', 'shell', 'strategy', 'vars'
+ )
+
+-# Plugin types that can have multiple plugins per file, and where filenames not always correspond to plugin names
+-MULTI_FILE_PLUGINS = ('filter', 'test', )
+-
+ created_venvs: list[str] = []
+
+
+@@ -264,7 +260,7 @@ def command_sanity(args: SanityConfig) -
+ virtualenv_python = create_sanity_virtualenv(args, test_profile.python, test.name)
+
+ if virtualenv_python:
+- virtualenv_yaml = args.explain or check_sanity_virtualenv_yaml(virtualenv_python)
++ virtualenv_yaml = check_sanity_virtualenv_yaml(virtualenv_python)
+
+ if test.require_libyaml and not virtualenv_yaml:
+ result = SanitySkipped(test.name)
+@@ -879,7 +875,6 @@ class SanityCodeSmellTest(SanitySingleVe
+ self.__include_directories: bool = self.config.get('include_directories')
+ self.__include_symlinks: bool = self.config.get('include_symlinks')
+ self.__py2_compat: bool = self.config.get('py2_compat', False)
+- self.__error_code: str | None = self.config.get('error_code', None)
+ else:
+ self.output = None
+ self.extensions = []
+@@ -895,7 +890,6 @@ class SanityCodeSmellTest(SanitySingleVe
+ self.__include_directories = False
+ self.__include_symlinks = False
+ self.__py2_compat = False
+- self.__error_code = None
+
+ if self.no_targets:
+ mutually_exclusive = (
+@@ -915,11 +909,6 @@ class SanityCodeSmellTest(SanitySingleVe
+ raise ApplicationError('Sanity test "%s" option "no_targets" is mutually exclusive with options: %s' % (self.name, ', '.join(problems)))
+
+ @property
+- def error_code(self) -> t.Optional[str]:
+- """Error code for ansible-test matching the format used by the underlying test program, or None if the program does not use error codes."""
+- return self.__error_code
+-
+- @property
+ def all_targets(self) -> bool:
+ """True if test targets will not be filtered using includes, excludes, requires or changes. Mutually exclusive with no_targets."""
+ return self.__all_targets
+@@ -1003,8 +992,6 @@ class SanityCodeSmellTest(SanitySingleVe
+ pattern = '^(?P<path>[^:]*):(?P<line>[0-9]+):(?P<column>[0-9]+): (?P<message>.*)$'
+ elif self.output == 'path-message':
+ pattern = '^(?P<path>[^:]*): (?P<message>.*)$'
+- elif self.output == 'path-line-column-code-message':
+- pattern = '^(?P<path>[^:]*):(?P<line>[0-9]+):(?P<column>[0-9]+): (?P<code>[^:]*): (?P<message>.*)$'
+ else:
+ raise ApplicationError('Unsupported output type: %s' % self.output)
+
+@@ -1034,7 +1021,6 @@ class SanityCodeSmellTest(SanitySingleVe
+ path=m['path'],
+ line=int(m.get('line', 0)),
+ column=int(m.get('column', 0)),
+- code=m.get('code'),
+ ) for m in matches]
+
+ messages = settings.process_errors(messages, paths)
+@@ -1180,23 +1166,20 @@ def create_sanity_virtualenv(
+
+ run_pip(args, virtualenv_python, commands, None) # create_sanity_virtualenv()
+
+- if not args.explain:
+- write_text_file(meta_install, virtualenv_install)
++ write_text_file(meta_install, virtualenv_install)
+
+ # false positive: pylint: disable=no-member
+ if any(isinstance(command, PipInstall) and command.has_package('pyyaml') for command in commands):
+- virtualenv_yaml = yamlcheck(virtualenv_python, args.explain)
++ virtualenv_yaml = yamlcheck(virtualenv_python)
+ else:
+ virtualenv_yaml = None
+
+- if not args.explain:
+- write_json_file(meta_yaml, virtualenv_yaml)
++ write_json_file(meta_yaml, virtualenv_yaml)
+
+ created_venvs.append(f'{label}-{python.version}')
+
+- if not args.explain:
+- # touch the marker to keep track of when the virtualenv was last used
+- pathlib.Path(virtualenv_marker).touch()
++ # touch the marker to keep track of when the virtualenv was last used
++ pathlib.Path(virtualenv_marker).touch()
+
+ return virtualenv_python
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/sanity/ansible_doc.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/sanity/ansible_doc.py
+@@ -2,13 +2,11 @@
+ from __future__ import annotations
+
+ import collections
+-import json
+ import os
+ import re
+
+ from . import (
+ DOCUMENTABLE_PLUGINS,
+- MULTI_FILE_PLUGINS,
+ SanitySingleVersion,
+ SanityFailure,
+ SanitySuccess,
+@@ -87,44 +85,6 @@ class AnsibleDocTest(SanitySingleVersion
+ doc_targets[plugin_type].append(plugin_fqcn)
+
+ env = ansible_environment(args, color=False)
+-
+- for doc_type in MULTI_FILE_PLUGINS:
+- if doc_targets.get(doc_type):
+- # List plugins
+- cmd = ['ansible-doc', '-l', '--json', '-t', doc_type]
+- prefix = data_context().content.prefix if data_context().content.collection else 'ansible.builtin.'
+- cmd.append(prefix[:-1])
+- try:
+- stdout, stderr = intercept_python(args, python, cmd, env, capture=True)
+- status = 0
+- except SubprocessError as ex:
+- stdout = ex.stdout
+- stderr = ex.stderr
+- status = ex.status
+-
+- if status:
+- summary = '%s' % SubprocessError(cmd=cmd, status=status, stderr=stderr)
+- return SanityFailure(self.name, summary=summary)
+-
+- if stdout:
+- display.info(stdout.strip(), verbosity=3)
+-
+- if stderr:
+- summary = 'Output on stderr from ansible-doc is considered an error.\n\n%s' % SubprocessError(cmd, stderr=stderr)
+- return SanityFailure(self.name, summary=summary)
+-
+- if args.explain:
+- continue
+-
+- plugin_list_json = json.loads(stdout)
+- doc_targets[doc_type] = []
+- for plugin_name, plugin_value in sorted(plugin_list_json.items()):
+- if plugin_value != 'UNDOCUMENTED':
+- doc_targets[doc_type].append(plugin_name)
+-
+- if not doc_targets[doc_type]:
+- del doc_targets[doc_type]
+-
+ error_messages: list[SanityMessage] = []
+
+ for doc_type in sorted(doc_targets):
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/sanity/import.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/sanity/import.py
+@@ -127,26 +127,20 @@ class ImportTest(SanityMultipleVersion):
+ ('plugin', _get_module_test(False)),
+ ):
+ if import_type == 'plugin' and python.version in REMOTE_ONLY_PYTHON_VERSIONS:
+- # Plugins are not supported on remote-only Python versions.
+- # However, the collection loader is used by the import sanity test and unit tests on remote-only Python versions.
+- # To support this, it is tested as a plugin, but using a venv which installs no requirements.
+- # Filtering of paths relevant to the Python version tested has already been performed by filter_remote_targets.
+- venv_type = 'empty'
+- else:
+- venv_type = import_type
++ continue
+
+ data = '\n'.join([path for path in paths if test(path)])
+
+ if not data and not args.prime_venvs:
+ continue
+
+- virtualenv_python = create_sanity_virtualenv(args, python, f'{self.name}.{venv_type}', coverage=args.coverage, minimize=True)
++ virtualenv_python = create_sanity_virtualenv(args, python, f'{self.name}.{import_type}', coverage=args.coverage, minimize=True)
+
+ if not virtualenv_python:
+ display.warning(f'Skipping sanity test "{self.name}" on Python {python.version} due to missing virtual environment support.')
+ return SanitySkipped(self.name, python.version)
+
+- virtualenv_yaml = args.explain or check_sanity_virtualenv_yaml(virtualenv_python)
++ virtualenv_yaml = check_sanity_virtualenv_yaml(virtualenv_python)
+
+ if virtualenv_yaml is False:
+ display.warning(f'Sanity test "{self.name}" ({import_type}) on Python {python.version} may be slow due to missing libyaml support in PyYAML.')
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/sanity/mypy.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/sanity/mypy.py
+@@ -19,7 +19,6 @@ from . import (
+ from ...constants import (
+ CONTROLLER_PYTHON_VERSIONS,
+ REMOTE_ONLY_PYTHON_VERSIONS,
+- SUPPORTED_PYTHON_VERSIONS,
+ )
+
+ from ...test import (
+@@ -37,7 +36,6 @@ from ...util import (
+ ANSIBLE_TEST_CONTROLLER_ROOT,
+ ApplicationError,
+ is_subdir,
+- str_to_version,
+ )
+
+ from ...util_common import (
+@@ -73,19 +71,9 @@ class MypyTest(SanityMultipleVersion):
+ """Return the given list of test targets, filtered to include only those relevant for the test."""
+ return [target for target in targets if os.path.splitext(target.path)[1] == '.py' and target.path not in self.vendored_paths and (
+ target.path.startswith('lib/ansible/') or target.path.startswith('test/lib/ansible_test/_internal/')
+- or target.path.startswith('packaging/')
+ or target.path.startswith('test/lib/ansible_test/_util/target/sanity/import/'))]
+
+ @property
+- def supported_python_versions(self) -> t.Optional[tuple[str, ...]]:
+- """A tuple of supported Python versions or None if the test does not depend on specific Python versions."""
+- # mypy 0.981 dropped support for Python 2
+- # see: https://mypy-lang.blogspot.com/2022/09/mypy-0981-released.html
+- # cryptography dropped support for Python 3.5 in version 3.3
+- # see: https://cryptography.io/en/latest/changelog/#v3-3
+- return tuple(version for version in SUPPORTED_PYTHON_VERSIONS if str_to_version(version) >= (3, 6))
+-
+- @property
+ def error_code(self) -> t.Optional[str]:
+ """Error code for ansible-test matching the format used by the underlying test program, or None if the program does not use error codes."""
+ return 'ansible-test'
+@@ -117,7 +105,6 @@ class MypyTest(SanityMultipleVersion):
+ MyPyContext('ansible-test', ['test/lib/ansible_test/_internal/'], controller_python_versions),
+ MyPyContext('ansible-core', ['lib/ansible/'], controller_python_versions),
+ MyPyContext('modules', ['lib/ansible/modules/', 'lib/ansible/module_utils/'], remote_only_python_versions),
+- MyPyContext('packaging', ['packaging/'], controller_python_versions),
+ )
+
+ unfiltered_messages: list[SanityMessage] = []
+@@ -170,9 +157,6 @@ class MypyTest(SanityMultipleVersion):
+ # However, it will also report issues on those files, which is not the desired behavior.
+ messages = [message for message in messages if message.path in paths_set]
+
+- if args.explain:
+- return SanitySuccess(self.name, python_version=python.version)
+-
+ results = settings.process_errors(messages, paths)
+
+ if results:
+@@ -255,7 +239,7 @@ class MypyTest(SanityMultipleVersion):
+
+ pattern = r'^(?P<path>[^:]*):(?P<line>[0-9]+):((?P<column>[0-9]+):)? (?P<level>[^:]+): (?P<message>.*)$'
+
+- parsed = parse_to_list_of_dict(pattern, stdout or '')
++ parsed = parse_to_list_of_dict(pattern, stdout)
+
+ messages = [SanityMessage(
+ level=r['level'],
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/sanity/pylint.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/sanity/pylint.py
+@@ -18,11 +18,6 @@ from . import (
+ SANITY_ROOT,
+ )
+
+-from ...constants import (
+- CONTROLLER_PYTHON_VERSIONS,
+- REMOTE_ONLY_PYTHON_VERSIONS,
+-)
+-
+ from ...io import (
+ make_dirs,
+ )
+@@ -43,7 +38,6 @@ from ...util import (
+
+ from ...util_common import (
+ run_command,
+- process_scoped_temporary_file,
+ )
+
+ from ...ansible_util import (
+@@ -87,8 +81,6 @@ class PylintTest(SanitySingleVersion):
+ return [target for target in targets if os.path.splitext(target.path)[1] == '.py' or is_subdir(target.path, 'bin')]
+
+ def test(self, args: SanityConfig, targets: SanityTargets, python: PythonConfig) -> TestResult:
+- min_python_version_db_path = self.create_min_python_db(args, targets.targets)
+-
+ plugin_dir = os.path.join(SANITY_ROOT, 'pylint', 'plugins')
+ plugin_names = sorted(p[0] for p in [
+ os.path.splitext(p) for p in os.listdir(plugin_dir)] if p[1] == '.py' and p[0] != '__init__')
+@@ -171,7 +163,7 @@ class PylintTest(SanitySingleVersion):
+ continue
+
+ context_start = datetime.datetime.now(tz=datetime.timezone.utc)
+- messages += self.pylint(args, context, context_paths, plugin_dir, plugin_names, python, collection_detail, min_python_version_db_path)
++ messages += self.pylint(args, context, context_paths, plugin_dir, plugin_names, python, collection_detail)
+ context_end = datetime.datetime.now(tz=datetime.timezone.utc)
+
+ context_times.append('%s: %d (%s)' % (context, len(context_paths), context_end - context_start))
+@@ -202,22 +194,6 @@ class PylintTest(SanitySingleVersion):
+
+ return SanitySuccess(self.name)
+
+- def create_min_python_db(self, args: SanityConfig, targets: t.Iterable[TestTarget]) -> str:
+- """Create a database of target file paths and their minimum required Python version, returning the path to the database."""
+- target_paths = set(target.path for target in self.filter_remote_targets(list(targets)))
+- controller_min_version = CONTROLLER_PYTHON_VERSIONS[0]
+- target_min_version = REMOTE_ONLY_PYTHON_VERSIONS[0]
+- min_python_versions = {
+- os.path.abspath(target.path): target_min_version if target.path in target_paths else controller_min_version for target in targets
+- }
+-
+- min_python_version_db_path = process_scoped_temporary_file(args)
+-
+- with open(min_python_version_db_path, 'w') as database_file:
+- json.dump(min_python_versions, database_file)
+-
+- return min_python_version_db_path
+-
+ @staticmethod
+ def pylint(
+ args: SanityConfig,
+@@ -227,7 +203,6 @@ class PylintTest(SanitySingleVersion):
+ plugin_names: list[str],
+ python: PythonConfig,
+ collection_detail: CollectionDetail,
+- min_python_version_db_path: str,
+ ) -> list[dict[str, str]]:
+ """Run pylint using the config specified by the context on the specified paths."""
+ rcfile = os.path.join(SANITY_ROOT, 'pylint', 'config', context.split('/')[0] + '.cfg')
+@@ -259,7 +234,6 @@ class PylintTest(SanitySingleVersion):
+ '--rcfile', rcfile,
+ '--output-format', 'json',
+ '--load-plugins', ','.join(sorted(load_plugins)),
+- '--min-python-version-db', min_python_version_db_path,
+ ] + paths # fmt: skip
+
+ if data_context().content.collection:
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py
+@@ -10,7 +10,6 @@ import typing as t
+
+ from . import (
+ DOCUMENTABLE_PLUGINS,
+- MULTI_FILE_PLUGINS,
+ SanitySingleVersion,
+ SanityMessage,
+ SanityFailure,
+@@ -129,10 +128,6 @@ class ValidateModulesTest(SanitySingleVe
+ for target in targets.include:
+ target_per_type[self.get_plugin_type(target)].append(target)
+
+- # Remove plugins that cannot be associated to a single file (test and filter plugins).
+- for plugin_type in MULTI_FILE_PLUGINS:
+- target_per_type.pop(plugin_type, None)
+-
+ cmd = [
+ python.path,
+ os.path.join(SANITY_ROOT, 'validate-modules', 'validate.py'),
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/commands/units/__init__.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/commands/units/__init__.py
+@@ -253,6 +253,7 @@ def command_units(args: UnitsConfig) ->
+
+ cmd = [
+ 'pytest',
++ '--forked',
+ '-r', 'a',
+ '-n', str(args.num_workers) if args.num_workers else 'auto',
+ '--color', 'yes' if args.color else 'no',
+@@ -261,7 +262,6 @@ def command_units(args: UnitsConfig) ->
+ '--junit-xml', os.path.join(ResultType.JUNIT.path, 'python%s-%s-units.xml' % (python.version, test_context)),
+ '--strict-markers', # added in pytest 4.5.0
+ '--rootdir', data_context().content.root,
+- '--confcutdir', data_context().content.root, # avoid permission errors when running from an installed version and using pytest >= 8
+ ] # fmt:skip
+
+ if not data_context().content.collection:
+@@ -275,8 +275,6 @@ def command_units(args: UnitsConfig) ->
+ if data_context().content.collection:
+ plugins.append('ansible_pytest_collections')
+
+- plugins.append('ansible_forked')
+-
+ if plugins:
+ env['PYTHONPATH'] += ':%s' % os.path.join(ANSIBLE_TEST_TARGET_ROOT, 'pytest/plugins')
+ env['PYTEST_PLUGINS'] = ','.join(plugins)
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/config.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/config.py
+@@ -8,6 +8,7 @@ import sys
+ import typing as t
+
+ from .util import (
++ display,
+ verify_sys_executable,
+ version_to_str,
+ type_guard,
+@@ -135,6 +136,12 @@ class EnvironmentConfig(CommonConfig):
+
+ data_context().register_payload_callback(host_callback)
+
++ if args.docker_no_pull:
++ display.warning('The --docker-no-pull option is deprecated and has no effect. It will be removed in a future version of ansible-test.')
++
++ if args.no_pip_check:
++ display.warning('The --no-pip-check option is deprecated and has no effect. It will be removed in a future version of ansible-test.')
++
+ @property
+ def controller(self) -> ControllerHostConfig:
+ """Host configuration for the controller."""
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/containers.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/containers.py
+@@ -3,6 +3,7 @@ from __future__ import annotations
+
+ import collections.abc as c
+ import contextlib
++import enum
+ import json
+ import random
+ import time
+@@ -45,7 +46,6 @@ from .docker_util import (
+ get_docker_container_id,
+ get_docker_host_ip,
+ get_podman_host_ip,
+- get_session_container_name,
+ require_docker,
+ detect_host_properties,
+ )
+@@ -101,6 +101,14 @@ class HostType:
+ managed = 'managed'
+
+
++class CleanupMode(enum.Enum):
++ """How container cleanup should be handled."""
++
++ YES = enum.auto()
++ NO = enum.auto()
++ INFO = enum.auto()
++
++
+ def run_support_container(
+ args: EnvironmentConfig,
+ context: str,
+@@ -109,7 +117,8 @@ def run_support_container(
+ ports: list[int],
+ aliases: t.Optional[list[str]] = None,
+ start: bool = True,
+- cleanup: bool = True,
++ allow_existing: bool = False,
++ cleanup: t.Optional[CleanupMode] = None,
+ cmd: t.Optional[list[str]] = None,
+ env: t.Optional[dict[str, str]] = None,
+ options: t.Optional[list[str]] = None,
+@@ -119,8 +128,6 @@ def run_support_container(
+ Start a container used to support tests, but not run them.
+ Containers created this way will be accessible from tests.
+ """
+- name = get_session_container_name(args, name)
+-
+ if args.prime_containers:
+ docker_pull(args, image)
+ return None
+@@ -158,13 +165,46 @@ def run_support_container(
+
+ options.extend(['--ulimit', 'nofile=%s' % max_open_files])
+
++ support_container_id = None
++
++ if allow_existing:
++ try:
++ container = docker_inspect(args, name)
++ except ContainerNotFoundError:
++ container = None
++
++ if container:
++ support_container_id = container.id
++
++ if not container.running:
++ display.info('Ignoring existing "%s" container which is not running.' % name, verbosity=1)
++ support_container_id = None
++ elif not container.image:
++ display.info('Ignoring existing "%s" container which has the wrong image.' % name, verbosity=1)
++ support_container_id = None
++ elif publish_ports and not all(port and len(port) == 1 for port in [container.get_tcp_port(port) for port in ports]):
++ display.info('Ignoring existing "%s" container which does not have the required published ports.' % name, verbosity=1)
++ support_container_id = None
++
++ if not support_container_id:
++ docker_rm(args, name)
++
+ if args.dev_systemd_debug:
+ options.extend(('--env', 'SYSTEMD_LOG_LEVEL=debug'))
+
+- display.info('Starting new "%s" container.' % name)
+- docker_pull(args, image)
+- support_container_id = run_container(args, image, name, options, create_only=not start, cmd=cmd)
+- running = start
++ if support_container_id:
++ display.info('Using existing "%s" container.' % name)
++ running = True
++ existing = True
++ else:
++ display.info('Starting new "%s" container.' % name)
++ docker_pull(args, image)
++ support_container_id = run_container(args, image, name, options, create_only=not start, cmd=cmd)
++ running = start
++ existing = False
++
++ if cleanup is None:
++ cleanup = CleanupMode.INFO if existing else CleanupMode.YES
+
+ descriptor = ContainerDescriptor(
+ image,
+@@ -175,6 +215,7 @@ def run_support_container(
+ aliases,
+ publish_ports,
+ running,
++ existing,
+ cleanup,
+ env,
+ )
+@@ -653,7 +694,8 @@ class ContainerDescriptor:
+ aliases: list[str],
+ publish_ports: bool,
+ running: bool,
+- cleanup: bool,
++ existing: bool,
++ cleanup: CleanupMode,
+ env: t.Optional[dict[str, str]],
+ ) -> None:
+ self.image = image
+@@ -664,6 +706,7 @@ class ContainerDescriptor:
+ self.aliases = aliases
+ self.publish_ports = publish_ports
+ self.running = running
++ self.existing = existing
+ self.cleanup = cleanup
+ self.env = env
+ self.details: t.Optional[SupportContainer] = None
+@@ -762,8 +805,10 @@ def wait_for_file(
+ def cleanup_containers(args: EnvironmentConfig) -> None:
+ """Clean up containers."""
+ for container in support_containers.values():
+- if container.cleanup:
+- docker_rm(args, container.name)
++ if container.cleanup == CleanupMode.YES:
++ docker_rm(args, container.container_id)
++ elif container.cleanup == CleanupMode.INFO:
++ display.notice(f'Remember to run `{require_docker().command} rm -f {container.name}` when finished testing.')
+
+
+ def create_hosts_entries(context: dict[str, ContainerAccess]) -> list[str]:
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/core_ci.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/core_ci.py
+@@ -28,6 +28,7 @@ from .io import (
+ from .util import (
+ ApplicationError,
+ display,
++ ANSIBLE_TEST_TARGET_ROOT,
+ mutex,
+ )
+
+@@ -291,12 +292,18 @@ class AnsibleCoreCI:
+ """Start instance."""
+ display.info(f'Initializing new {self.label} instance using: {self._uri}', verbosity=1)
+
++ if self.platform == 'windows':
++ winrm_config = read_text_file(os.path.join(ANSIBLE_TEST_TARGET_ROOT, 'setup', 'ConfigureRemotingForAnsible.ps1'))
++ else:
++ winrm_config = None
++
+ data = dict(
+ config=dict(
+ platform=self.platform,
+ version=self.version,
+ architecture=self.arch,
+ public_key=self.ssh_key.pub_contents,
++ winrm_config=winrm_config,
+ )
+ )
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/coverage_util.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/coverage_util.py
+@@ -69,8 +69,7 @@ class CoverageVersion:
+
+ COVERAGE_VERSIONS = (
+ # IMPORTANT: Keep this in sync with the ansible-test.txt requirements file.
+- CoverageVersion('7.3.2', 7, (3, 8), (3, 12)),
+- CoverageVersion('6.5.0', 7, (3, 7), (3, 7)),
++ CoverageVersion('6.5.0', 7, (3, 7), (3, 11)),
+ CoverageVersion('4.5.4', 0, (2, 6), (3, 6)),
+ )
+ """
+@@ -251,9 +250,7 @@ def generate_ansible_coverage_config() -
+ coverage_config = '''
+ [run]
+ branch = True
+-concurrency =
+- multiprocessing
+- thread
++concurrency = multiprocessing
+ parallel = True
+
+ omit =
+@@ -274,9 +271,7 @@ def generate_collection_coverage_config(
+ coverage_config = '''
+ [run]
+ branch = True
+-concurrency =
+- multiprocessing
+- thread
++concurrency = multiprocessing
+ parallel = True
+ disable_warnings =
+ no-data-collected
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/delegation.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/delegation.py
+@@ -328,6 +328,7 @@ def filter_options(
+ ) -> c.Iterable[str]:
+ """Return an iterable that filters out unwanted CLI options and injects new ones as requested."""
+ replace: list[tuple[str, int, t.Optional[t.Union[bool, str, list[str]]]]] = [
++ ('--docker-no-pull', 0, False),
+ ('--truncate', 1, str(args.truncate)),
+ ('--color', 1, 'yes' if args.color else 'no'),
+ ('--redact', 0, False),
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/diff.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/diff.py
+@@ -143,7 +143,7 @@ class DiffParser:
+ traceback.format_exc(),
+ )
+
+- raise ApplicationError(message.strip()) from None
++ raise ApplicationError(message.strip())
+
+ self.previous_line = self.line
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/docker_util.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/docker_util.py
+@@ -300,7 +300,7 @@ def detect_host_properties(args: CommonC
+ options = ['--volume', '/sys/fs/cgroup:/probe:ro']
+ cmd = ['sh', '-c', ' && echo "-" && '.join(multi_line_commands)]
+
+- stdout = run_utility_container(args, 'ansible-test-probe', cmd, options)[0]
++ stdout = run_utility_container(args, f'ansible-test-probe-{args.session_name}', cmd, options)[0]
+
+ if args.explain:
+ return ContainerHostProperties(
+@@ -336,7 +336,7 @@ def detect_host_properties(args: CommonC
+ cmd = ['sh', '-c', 'ulimit -Hn']
+
+ try:
+- stdout = run_utility_container(args, 'ansible-test-ulimit', cmd, options)[0]
++ stdout = run_utility_container(args, f'ansible-test-ulimit-{args.session_name}', cmd, options)[0]
+ except SubprocessError as ex:
+ display.warning(str(ex))
+ else:
+@@ -402,11 +402,6 @@ def detect_host_properties(args: CommonC
+ return properties
+
+
+-def get_session_container_name(args: CommonConfig, name: str) -> str:
+- """Return the given container name with the current test session name applied to it."""
+- return f'{name}-{args.session_name}'
+-
+-
+ def run_utility_container(
+ args: CommonConfig,
+ name: str,
+@@ -415,8 +410,6 @@ def run_utility_container(
+ data: t.Optional[str] = None,
+ ) -> tuple[t.Optional[str], t.Optional[str]]:
+ """Run the specified command using the ansible-test utility container, returning stdout and stderr."""
+- name = get_session_container_name(args, name)
+-
+ options = options + [
+ '--name', name,
+ '--rm',
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/host_profiles.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/host_profiles.py
+@@ -99,6 +99,7 @@ from .ansible_util import (
+ )
+
+ from .containers import (
++ CleanupMode,
+ HostType,
+ get_container_database,
+ run_support_container,
+@@ -446,7 +447,7 @@ class DockerProfile(ControllerHostProfil
+ @property
+ def label(self) -> str:
+ """Label to apply to resources related to this profile."""
+- return f'{"controller" if self.controller else "target"}'
++ return f'{"controller" if self.controller else "target"}-{self.args.session_name}'
+
+ def provision(self) -> None:
+ """Provision the host before delegation."""
+@@ -461,7 +462,7 @@ class DockerProfile(ControllerHostProfil
+ ports=[22],
+ publish_ports=not self.controller, # connections to the controller over SSH are not required
+ options=init_config.options,
+- cleanup=False,
++ cleanup=CleanupMode.NO,
+ cmd=self.build_init_command(init_config, init_probe),
+ )
+
+@@ -806,7 +807,6 @@ class DockerProfile(ControllerHostProfil
+ - Avoid hanging indefinitely or for an unreasonably long time.
+
+ NOTE: The container must have a POSIX-compliant default shell "sh" with a non-builtin "sleep" command.
+- The "sleep" command is invoked through "env" to avoid using a shell builtin "sleep" (if present).
+ """
+ command = ''
+
+@@ -814,7 +814,7 @@ class DockerProfile(ControllerHostProfil
+ command += f'{init_config.command} && '
+
+ if sleep or init_config.command_privileged:
+- command += 'env sleep 60 ; '
++ command += 'sleep 60 ; '
+
+ if not command:
+ return None
+@@ -838,7 +838,7 @@ class DockerProfile(ControllerHostProfil
+ """Check the cgroup v1 systemd hierarchy to verify it is writeable for our container."""
+ probe_script = (read_text_file(os.path.join(ANSIBLE_TEST_TARGET_ROOT, 'setup', 'check_systemd_cgroup_v1.sh'))
+ .replace('@MARKER@', self.MARKER)
+- .replace('@LABEL@', f'{self.label}-{self.args.session_name}'))
++ .replace('@LABEL@', self.label))
+
+ cmd = ['sh']
+
+@@ -853,7 +853,7 @@ class DockerProfile(ControllerHostProfil
+
+ def create_systemd_cgroup_v1(self) -> str:
+ """Create a unique ansible-test cgroup in the v1 systemd hierarchy and return its path."""
+- self.cgroup_path = f'/sys/fs/cgroup/systemd/ansible-test-{self.label}-{self.args.session_name}'
++ self.cgroup_path = f'/sys/fs/cgroup/systemd/ansible-test-{self.label}'
+
+ # Privileged mode is required to create the cgroup directories on some hosts, such as Fedora 36 and RHEL 9.0.
+ # The mkdir command will fail with "Permission denied" otherwise.
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/http.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/http.py
+@@ -126,7 +126,7 @@ class HttpResponse:
+ try:
+ return json.loads(self.response)
+ except ValueError:
+- raise HttpError(self.status_code, 'Cannot parse response to %s %s as JSON:\n%s' % (self.method, self.url, self.response)) from None
++ raise HttpError(self.status_code, 'Cannot parse response to %s %s as JSON:\n%s' % (self.method, self.url, self.response))
+
+
+ class HttpError(ApplicationError):
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/junit_xml.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/junit_xml.py
+@@ -15,7 +15,7 @@ from xml.dom import minidom
+ from xml.etree import ElementTree as ET
+
+
+-@dataclasses.dataclass
++@dataclasses.dataclass # type: ignore[misc] # https://github.com/python/mypy/issues/5374
+ class TestResult(metaclass=abc.ABCMeta):
+ """Base class for the result of a test case."""
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/pypi_proxy.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/pypi_proxy.py
+@@ -76,7 +76,7 @@ def run_pypi_proxy(args: EnvironmentConf
+ args=args,
+ context='__pypi_proxy__',
+ image=image,
+- name='pypi-test-container',
++ name=f'pypi-test-container-{args.session_name}',
+ ports=[port],
+ )
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/python_requirements.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/python_requirements.py
+@@ -297,7 +297,7 @@ def run_pip(
+ connection.run([python.path], data=script, capture=True)
+ except SubprocessError as ex:
+ if 'pip is unavailable:' in ex.stdout + ex.stderr:
+- raise PipUnavailableError(python) from None
++ raise PipUnavailableError(python)
+
+ raise
+
+@@ -441,8 +441,8 @@ def get_venv_packages(python: PythonConf
+ # See: https://github.com/ansible/base-test-container/blob/main/files/installer.py
+
+ default_packages = dict(
+- pip='23.1.2',
+- setuptools='67.7.2',
++ pip='21.3.1',
++ setuptools='60.8.2',
+ wheel='0.37.1',
+ )
+
+@@ -452,6 +452,11 @@ def get_venv_packages(python: PythonConf
+ setuptools='44.1.1', # 45.0.0 requires Python 3.5+
+ wheel=None,
+ ),
++ '3.5': dict(
++ pip='20.3.4', # 21.0 requires Python 3.6+
++ setuptools='50.3.2', # 51.0.0 requires Python 3.6+
++ wheel=None,
++ ),
+ '3.6': dict(
+ pip='21.3.1', # 22.0 requires Python 3.7+
+ setuptools='59.6.0', # 59.7.0 requires Python 3.7+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/util.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/util.py
+@@ -31,6 +31,11 @@ from termios import TIOCGWINSZ
+ # CAUTION: Avoid third-party imports in this module whenever possible.
+ # Any third-party imports occurring here will result in an error if they are vendored by ansible-core.
+
++try:
++ from typing_extensions import TypeGuard # TypeGuard was added in Python 3.10
++except ImportError:
++ TypeGuard = None
++
+ from .locale_util import (
+ LOCALE_WARNING,
+ CONFIGURED_LOCALE,
+@@ -431,7 +436,7 @@ def raw_command(
+ display.info(f'{description}: {escaped_cmd}', verbosity=cmd_verbosity, truncate=True)
+ display.info('Working directory: %s' % cwd, verbosity=2)
+
+- program = find_executable(cmd[0], cwd=cwd, path=env['PATH'], required=False)
++ program = find_executable(cmd[0], cwd=cwd, path=env['PATH'], required='warning')
+
+ if program:
+ display.info('Program found: %s' % program, verbosity=2)
+@@ -1150,7 +1155,7 @@ def verify_sys_executable(path: str) ->
+ return expected_executable
+
+
+-def type_guard(sequence: c.Sequence[t.Any], guard_type: t.Type[C]) -> t.TypeGuard[c.Sequence[C]]:
++def type_guard(sequence: c.Sequence[t.Any], guard_type: t.Type[C]) -> TypeGuard[c.Sequence[C]]:
+ """
+ Raises an exception if any item in the given sequence does not match the specified guard type.
+ Use with assert so that type checkers are aware of the type guard.
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_internal/util_common.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_internal/util_common.py
+@@ -88,7 +88,7 @@ class ExitHandler:
+
+ try:
+ func(*args, **kwargs)
+- except BaseException as ex: # pylint: disable=broad-exception-caught
++ except BaseException as ex: # pylint: disable=broad-except
+ last_exception = ex
+ display.fatal(f'Exit handler failed: {ex}')
+
+@@ -498,14 +498,9 @@ def run_command(
+ )
+
+
+-def yamlcheck(python: PythonConfig, explain: bool = False) -> t.Optional[bool]:
++def yamlcheck(python: PythonConfig) -> t.Optional[bool]:
+ """Return True if PyYAML has libyaml support, False if it does not and None if it was not found."""
+- stdout = raw_command([python.path, os.path.join(ANSIBLE_TEST_TARGET_TOOLS_ROOT, 'yamlcheck.py')], capture=True, explain=explain)[0]
+-
+- if explain:
+- return None
+-
+- result = json.loads(stdout)
++ result = json.loads(raw_command([python.path, os.path.join(ANSIBLE_TEST_TARGET_TOOLS_ROOT, 'yamlcheck.py')], capture=True)[0])
+
+ if not result['yaml']:
+ return None
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/code-smell/no-get-exception.json
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/code-smell/no-get-exception.json
+@@ -2,10 +2,6 @@
+ "extensions": [
+ ".py"
+ ],
+- "prefixes": [
+- "lib/ansible/",
+- "plugins/"
+- ],
+ "ignore_self": true,
+ "output": "path-line-column-message"
+ }
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/code-smell/replace-urlopen.json
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/code-smell/replace-urlopen.json
+@@ -2,10 +2,6 @@
+ "extensions": [
+ ".py"
+ ],
+- "prefixes": [
+- "lib/ansible/",
+- "plugins/"
+- ],
+ "ignore_self": true,
+ "output": "path-line-column-message"
+ }
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py
+@@ -16,19 +16,9 @@ from voluptuous.humanize import humanize
+
+ from ansible.module_utils.compat.version import StrictVersion, LooseVersion
+ from ansible.module_utils.six import string_types
+-from ansible.utils.collection_loader import AnsibleCollectionRef
+ from ansible.utils.version import SemanticVersion
+
+
+-def fqcr(value):
+- """Validate a FQCR."""
+- if not isinstance(value, string_types):
+- raise Invalid('Must be a string that is a FQCR')
+- if not AnsibleCollectionRef.is_valid_fqcr(value):
+- raise Invalid('Must be a FQCR')
+- return value
+-
+-
+ def isodate(value, check_deprecation_date=False, is_tombstone=False):
+ """Validate a datetime.date or ISO 8601 date string."""
+ # datetime.date objects come from YAML dates, these are ok
+@@ -136,15 +126,12 @@ def validate_metadata_file(path, is_ansi
+ with open(path, 'r', encoding='utf-8') as f_path:
+ routing = yaml.safe_load(f_path)
+ except yaml.error.MarkedYAMLError as ex:
+- print('%s:%d:%d: YAML load failed: %s' % (
+- path,
+- ex.context_mark.line + 1 if ex.context_mark else 0,
+- ex.context_mark.column + 1 if ex.context_mark else 0,
+- re.sub(r'\s+', ' ', str(ex)),
+- ))
++ print('%s:%d:%d: YAML load failed: %s' % (path, ex.context_mark.line +
++ 1, ex.context_mark.column + 1, re.sub(r'\s+', ' ', str(ex))))
+ return
+ except Exception as ex: # pylint: disable=broad-except
+- print('%s:%d:%d: YAML load failed: %s' % (path, 0, 0, re.sub(r'\s+', ' ', str(ex))))
++ print('%s:%d:%d: YAML load failed: %s' %
++ (path, 0, 0, re.sub(r'\s+', ' ', str(ex))))
+ return
+
+ if is_ansible:
+@@ -197,37 +184,17 @@ def validate_metadata_file(path, is_ansi
+ avoid_additional_data
+ )
+
+- plugins_routing_common_schema = Schema({
+- ('deprecation'): Any(deprecation_schema),
+- ('tombstone'): Any(tombstoning_schema),
+- ('redirect'): fqcr,
+- }, extra=PREVENT_EXTRA)
+-
+- plugin_routing_schema = Any(plugins_routing_common_schema)
+-
+- # Adjusted schema for modules only
+- plugin_routing_schema_modules = Any(
+- plugins_routing_common_schema.extend({
+- ('action_plugin'): fqcr}
+- )
+- )
+-
+- # Adjusted schema for module_utils
+- plugin_routing_schema_mu = Any(
+- plugins_routing_common_schema.extend({
+- ('redirect'): Any(*string_types)}
+- ),
++ plugin_routing_schema = Any(
++ Schema({
++ ('deprecation'): Any(deprecation_schema),
++ ('tombstone'): Any(tombstoning_schema),
++ ('redirect'): Any(*string_types),
++ }, extra=PREVENT_EXTRA),
+ )
+
+ list_dict_plugin_routing_schema = [{str_type: plugin_routing_schema}
+ for str_type in string_types]
+
+- list_dict_plugin_routing_schema_mu = [{str_type: plugin_routing_schema_mu}
+- for str_type in string_types]
+-
+- list_dict_plugin_routing_schema_modules = [{str_type: plugin_routing_schema_modules}
+- for str_type in string_types]
+-
+ plugin_schema = Schema({
+ ('action'): Any(None, *list_dict_plugin_routing_schema),
+ ('become'): Any(None, *list_dict_plugin_routing_schema),
+@@ -240,8 +207,8 @@ def validate_metadata_file(path, is_ansi
+ ('httpapi'): Any(None, *list_dict_plugin_routing_schema),
+ ('inventory'): Any(None, *list_dict_plugin_routing_schema),
+ ('lookup'): Any(None, *list_dict_plugin_routing_schema),
+- ('module_utils'): Any(None, *list_dict_plugin_routing_schema_mu),
+- ('modules'): Any(None, *list_dict_plugin_routing_schema_modules),
++ ('module_utils'): Any(None, *list_dict_plugin_routing_schema),
++ ('modules'): Any(None, *list_dict_plugin_routing_schema),
+ ('netconf'): Any(None, *list_dict_plugin_routing_schema),
+ ('shell'): Any(None, *list_dict_plugin_routing_schema),
+ ('strategy'): Any(None, *list_dict_plugin_routing_schema),
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/code-smell/use-compat-six.json
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/code-smell/use-compat-six.json
+@@ -2,9 +2,5 @@
+ "extensions": [
+ ".py"
+ ],
+- "prefixes": [
+- "lib/ansible/",
+- "plugins/"
+- ],
+ "output": "path-line-column-message"
+ }
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-core.ini
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-core.ini
+@@ -34,9 +34,6 @@ ignore_missing_imports = True
+ [mypy-md5.*]
+ ignore_missing_imports = True
+
+-[mypy-imp.*]
+-ignore_missing_imports = True
+-
+ [mypy-scp.*]
+ ignore_missing_imports = True
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-test.ini
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-test.ini
+@@ -6,10 +6,10 @@
+ # There are ~350 errors reported in ansible-test when strict optional checking is enabled.
+ # Until the number of occurrences are greatly reduced, it's better to disable strict checking.
+ strict_optional = False
+-# There are ~13 type-abstract errors reported in ansible-test.
+-# This is due to assumptions mypy makes about Type and abstract types.
+-# See: https://discuss.python.org/t/add-abstracttype-to-the-typing-module/21996/13
+-disable_error_code = type-abstract
++# There are ~25 errors reported in ansible-test under the 'misc' code.
++# The majority of those errors are "Only concrete class can be given", which is due to a limitation of mypy.
++# See: https://github.com/python/mypy/issues/5374
++disable_error_code = misc
+
+ [mypy-argcomplete]
+ ignore_missing_imports = True
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/pep8/current-ignore.txt
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/pep8/current-ignore.txt
+@@ -2,8 +2,3 @@ E402
+ W503
+ W504
+ E741
+-
+-# The E203 rule is not PEP 8 compliant.
+-# Unfortunately this means it also conflicts with the output from `black`.
+-# See: https://github.com/PyCQA/pycodestyle/issues/373
+-E203
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/pslint/settings.psd1
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/pslint/settings.psd1
+@@ -4,9 +4,6 @@
+ Enable = $true
+ MaximumLineLength = 160
+ }
+- PSAvoidSemicolonsAsLineTerminators = @{
+- Enable = $true
+- }
+ PSPlaceOpenBrace = @{
+ Enable = $true
+ OnSameLine = $true
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg
+@@ -10,7 +10,6 @@ disable=
+ raise-missing-from, # Python 2.x does not support raise from
+ super-with-arguments, # Python 2.x does not support super without arguments
+ redundant-u-string-prefix, # Python 2.x support still required
+- broad-exception-raised, # many exceptions with no need for a custom type
+ too-few-public-methods,
+ too-many-arguments,
+ too-many-branches,
+@@ -20,7 +19,6 @@ disable=
+ too-many-nested-blocks,
+ too-many-return-statements,
+ too-many-statements,
+- use-dict-literal, # ignoring as a common style issue
+ useless-return, # complains about returning None when the return type is optional
+
+ [BASIC]
+@@ -57,5 +55,3 @@ preferred-modules =
+ # Listing them here makes it possible to enable the import-error check.
+ ignored-modules =
+ py,
+- pytest,
+- _pytest.runner,
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg
+@@ -7,7 +7,7 @@ disable=
+ deprecated-module, # results vary by Python version
+ duplicate-code, # consistent results require running with --jobs 1 and testing all files
+ import-outside-toplevel, # common pattern in ansible related code
+- broad-exception-raised, # many exceptions with no need for a custom type
++ raise-missing-from, # Python 2.x does not support raise from
+ too-few-public-methods,
+ too-many-public-methods,
+ too-many-arguments,
+@@ -18,7 +18,6 @@ disable=
+ too-many-nested-blocks,
+ too-many-return-statements,
+ too-many-statements,
+- use-dict-literal, # ignoring as a common style issue
+ unspecified-encoding, # always run with UTF-8 encoding enforced
+ useless-return, # complains about returning None when the return type is optional
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg
+@@ -17,7 +17,6 @@ disable=
+ too-many-nested-blocks,
+ too-many-return-statements,
+ too-many-statements,
+- use-dict-literal, # ignoring as a common style issue
+ unspecified-encoding, # always run with UTF-8 encoding enforced
+ useless-return, # complains about returning None when the return type is optional
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/pylint/config/collection.cfg
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/pylint/config/collection.cfg
+@@ -9,8 +9,7 @@ disable=
+ attribute-defined-outside-init,
+ bad-indentation,
+ bad-mcs-classmethod-argument,
+- broad-exception-caught,
+- broad-exception-raised,
++ broad-except,
+ c-extension-no-member,
+ cell-var-from-loop,
+ chained-comparison,
+@@ -30,7 +29,6 @@ disable=
+ consider-using-max-builtin,
+ consider-using-min-builtin,
+ cyclic-import, # consistent results require running with --jobs 1 and testing all files
+- deprecated-comment, # custom plugin only used by ansible-core, not collections
+ deprecated-method, # results vary by Python version
+ deprecated-module, # results vary by Python version
+ duplicate-code, # consistent results require running with --jobs 1 and testing all files
+@@ -97,6 +95,8 @@ disable=
+ too-many-public-methods,
+ too-many-return-statements,
+ too-many-statements,
++ trailing-comma-tuple,
++ trailing-comma-tuple,
+ try-except-raise,
+ unbalanced-tuple-unpacking,
+ undefined-loop-variable,
+@@ -110,9 +110,10 @@ disable=
+ unsupported-delete-operation,
+ unsupported-membership-test,
+ unused-argument,
++ unused-import,
+ unused-variable,
+ unspecified-encoding, # always run with UTF-8 encoding enforced
+- use-dict-literal, # ignoring as a common style issue
++ use-dict-literal, # many occurrences
+ use-list-literal, # many occurrences
+ use-implicit-booleaness-not-comparison, # many occurrences
+ useless-object-inheritance,
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/pylint/config/default.cfg
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/pylint/config/default.cfg
+@@ -10,8 +10,7 @@ disable=
+ attribute-defined-outside-init,
+ bad-indentation,
+ bad-mcs-classmethod-argument,
+- broad-exception-caught,
+- broad-exception-raised,
++ broad-except,
+ c-extension-no-member,
+ cell-var-from-loop,
+ chained-comparison,
+@@ -62,6 +61,8 @@ disable=
+ not-a-mapping,
+ not-an-iterable,
+ not-callable,
++ pointless-statement,
++ pointless-string-statement,
+ possibly-unused-variable,
+ protected-access,
+ raise-missing-from, # Python 2.x does not support raise from
+@@ -90,6 +91,8 @@ disable=
+ too-many-public-methods,
+ too-many-return-statements,
+ too-many-statements,
++ trailing-comma-tuple,
++ trailing-comma-tuple,
+ try-except-raise,
+ unbalanced-tuple-unpacking,
+ undefined-loop-variable,
+@@ -102,9 +105,10 @@ disable=
+ unsupported-delete-operation,
+ unsupported-membership-test,
+ unused-argument,
++ unused-import,
+ unused-variable,
+ unspecified-encoding, # always run with UTF-8 encoding enforced
+- use-dict-literal, # ignoring as a common style issue
++ use-dict-literal, # many occurrences
+ use-list-literal, # many occurrences
+ use-implicit-booleaness-not-comparison, # many occurrences
+ useless-object-inheritance,
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py
+@@ -5,31 +5,14 @@
+ from __future__ import annotations
+
+ import datetime
+-import functools
+-import json
+ import re
+-import shlex
+ import typing as t
+-from tokenize import COMMENT, TokenInfo
+
+ import astroid
+
+-# support pylint 2.x and 3.x -- remove when supporting only 3.x
+-try:
+- from pylint.interfaces import IAstroidChecker, ITokenChecker
+-except ImportError:
+- class IAstroidChecker:
+- """Backwards compatibility for 2.x / 3.x support."""
+-
+- class ITokenChecker:
+- """Backwards compatibility for 2.x / 3.x support."""
+-
+-try:
+- from pylint.checkers.utils import check_messages
+-except ImportError:
+- from pylint.checkers.utils import only_required_for_messages as check_messages
+-
+-from pylint.checkers import BaseChecker, BaseTokenChecker
++from pylint.interfaces import IAstroidChecker
++from pylint.checkers import BaseChecker
++from pylint.checkers.utils import check_messages
+
+ from ansible.module_utils.compat.version import LooseVersion
+ from ansible.module_utils.six import string_types
+@@ -112,7 +95,7 @@ ANSIBLE_VERSION = LooseVersion('.'.join(
+
+
+ def _get_expr_name(node):
+- """Function to get either ``attrname`` or ``name`` from ``node.func.expr``
++ """Funciton to get either ``attrname`` or ``name`` from ``node.func.expr``
+
+ Created specifically for the case of ``display.deprecated`` or ``self._display.deprecated``
+ """
+@@ -123,17 +106,6 @@ def _get_expr_name(node):
+ return node.func.expr.name
+
+
+-def _get_func_name(node):
+- """Function to get either ``attrname`` or ``name`` from ``node.func``
+-
+- Created specifically for the case of ``from ansible.module_utils.common.warnings import deprecate``
+- """
+- try:
+- return node.func.attrname
+- except AttributeError:
+- return node.func.name
+-
+-
+ def parse_isodate(value):
+ """Parse an ISO 8601 date string."""
+ msg = 'Expected ISO 8601 date string (YYYY-MM-DD)'
+@@ -146,7 +118,7 @@ def parse_isodate(value):
+ try:
+ return datetime.datetime.strptime(value, '%Y-%m-%d').date()
+ except ValueError:
+- raise ValueError(msg) from None
++ raise ValueError(msg)
+
+
+ class AnsibleDeprecatedChecker(BaseChecker):
+@@ -188,8 +160,6 @@ class AnsibleDeprecatedChecker(BaseCheck
+ self.add_message('ansible-deprecated-date', node=node, args=(date,))
+
+ def _check_version(self, node, version, collection_name):
+- if collection_name is None:
+- collection_name = 'ansible.builtin'
+ if not isinstance(version, (str, float)):
+ if collection_name == 'ansible.builtin':
+ symbol = 'ansible-invalid-deprecated-version'
+@@ -227,17 +197,12 @@ class AnsibleDeprecatedChecker(BaseCheck
+ @property
+ def collection_name(self) -> t.Optional[str]:
+ """Return the collection name, or None if ansible-core is being tested."""
+- return self.linter.config.collection_name
++ return self.config.collection_name
+
+ @property
+ def collection_version(self) -> t.Optional[SemanticVersion]:
+ """Return the collection version, or None if ansible-core is being tested."""
+- if self.linter.config.collection_version is None:
+- return None
+- sem_ver = SemanticVersion(self.linter.config.collection_version)
+- # Ignore pre-release for version comparison to catch issues before the final release is cut.
+- sem_ver.prerelease = ()
+- return sem_ver
++ return SemanticVersion(self.config.collection_version) if self.config.collection_version is not None else None
+
+ @check_messages(*(MSGS.keys()))
+ def visit_call(self, node):
+@@ -246,9 +211,8 @@ class AnsibleDeprecatedChecker(BaseCheck
+ date = None
+ collection_name = None
+ try:
+- funcname = _get_func_name(node)
+- if (funcname == 'deprecated' and 'display' in _get_expr_name(node) or
+- funcname == 'deprecate'):
++ if (node.func.attrname == 'deprecated' and 'display' in _get_expr_name(node) or
++ node.func.attrname == 'deprecate' and _get_expr_name(node)):
+ if node.keywords:
+ for keyword in node.keywords:
+ if len(node.keywords) == 1 and keyword.arg is None:
+@@ -294,137 +258,6 @@ class AnsibleDeprecatedChecker(BaseCheck
+ pass
+
+
+-class AnsibleDeprecatedCommentChecker(BaseTokenChecker):
+- """Checks for ``# deprecated:`` comments to ensure that the ``version``
+- has not passed or met the time for removal
+- """
+-
+- __implements__ = (ITokenChecker,)
+-
+- name = 'deprecated-comment'
+- msgs = {
+- 'E9601': ("Deprecated core version (%r) found: %s",
+- "ansible-deprecated-version-comment",
+- "Used when a '# deprecated:' comment specifies a version "
+- "less than or equal to the current version of Ansible",
+- {'minversion': (2, 6)}),
+- 'E9602': ("Deprecated comment contains invalid keys %r",
+- "ansible-deprecated-version-comment-invalid-key",
+- "Used when a '#deprecated:' comment specifies invalid data",
+- {'minversion': (2, 6)}),
+- 'E9603': ("Deprecated comment missing version",
+- "ansible-deprecated-version-comment-missing-version",
+- "Used when a '#deprecated:' comment specifies invalid data",
+- {'minversion': (2, 6)}),
+- 'E9604': ("Deprecated python version (%r) found: %s",
+- "ansible-deprecated-python-version-comment",
+- "Used when a '#deprecated:' comment specifies a python version "
+- "less than or equal to the minimum python version",
+- {'minversion': (2, 6)}),
+- 'E9605': ("Deprecated comment contains invalid version %r: %s",
+- "ansible-deprecated-version-comment-invalid-version",
+- "Used when a '#deprecated:' comment specifies an invalid version",
+- {'minversion': (2, 6)}),
+- }
+-
+- options = (
+- ('min-python-version-db', {
+- 'default': None,
+- 'type': 'string',
+- 'metavar': '<path>',
+- 'help': 'The path to the DB mapping paths to minimum Python versions.',
+- }),
+- )
+-
+- def process_tokens(self, tokens: list[TokenInfo]) -> None:
+- for token in tokens:
+- if token.type == COMMENT:
+- self._process_comment(token)
+-
+- def _deprecated_string_to_dict(self, token: TokenInfo, string: str) -> dict[str, str]:
+- valid_keys = {'description', 'core_version', 'python_version'}
+- data = dict.fromkeys(valid_keys)
+- for opt in shlex.split(string):
+- if '=' not in opt:
+- data[opt] = None
+- continue
+- key, _sep, value = opt.partition('=')
+- data[key] = value
+- if not any((data['core_version'], data['python_version'])):
+- self.add_message(
+- 'ansible-deprecated-version-comment-missing-version',
+- line=token.start[0],
+- col_offset=token.start[1],
+- )
+- bad = set(data).difference(valid_keys)
+- if bad:
+- self.add_message(
+- 'ansible-deprecated-version-comment-invalid-key',
+- line=token.start[0],
+- col_offset=token.start[1],
+- args=(','.join(bad),)
+- )
+- return data
+-
+- @functools.cached_property
+- def _min_python_version_db(self) -> dict[str, str]:
+- """A dictionary of absolute file paths and their minimum required Python version."""
+- with open(self.linter.config.min_python_version_db) as db_file:
+- return json.load(db_file)
+-
+- def _process_python_version(self, token: TokenInfo, data: dict[str, str]) -> None:
+- current_file = self.linter.current_file
+- check_version = self._min_python_version_db[current_file]
+-
+- try:
+- if LooseVersion(data['python_version']) < LooseVersion(check_version):
+- self.add_message(
+- 'ansible-deprecated-python-version-comment',
+- line=token.start[0],
+- col_offset=token.start[1],
+- args=(
+- data['python_version'],
+- data['description'] or 'description not provided',
+- ),
+- )
+- except (ValueError, TypeError) as exc:
+- self.add_message(
+- 'ansible-deprecated-version-comment-invalid-version',
+- line=token.start[0],
+- col_offset=token.start[1],
+- args=(data['python_version'], exc)
+- )
+-
+- def _process_core_version(self, token: TokenInfo, data: dict[str, str]) -> None:
+- try:
+- if ANSIBLE_VERSION >= LooseVersion(data['core_version']):
+- self.add_message(
+- 'ansible-deprecated-version-comment',
+- line=token.start[0],
+- col_offset=token.start[1],
+- args=(
+- data['core_version'],
+- data['description'] or 'description not provided',
+- )
+- )
+- except (ValueError, TypeError) as exc:
+- self.add_message(
+- 'ansible-deprecated-version-comment-invalid-version',
+- line=token.start[0],
+- col_offset=token.start[1],
+- args=(data['core_version'], exc)
+- )
+-
+- def _process_comment(self, token: TokenInfo) -> None:
+- if token.string.startswith('# deprecated:'):
+- data = self._deprecated_string_to_dict(token, token.string[13:].strip())
+- if data['core_version']:
+- self._process_core_version(token, data)
+- if data['python_version']:
+- self._process_python_version(token, data)
+-
+-
+ def register(linter):
+ """required method to auto register this checker """
+ linter.register_checker(AnsibleDeprecatedChecker(linter))
+- linter.register_checker(AnsibleDeprecatedCommentChecker(linter))
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/string_format.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/string_format.py
+@@ -5,26 +5,23 @@
+ from __future__ import annotations
+
+ import astroid
+-
+-# support pylint 2.x and 3.x -- remove when supporting only 3.x
+-try:
+- from pylint.interfaces import IAstroidChecker
+-except ImportError:
+- class IAstroidChecker:
+- """Backwards compatibility for 2.x / 3.x support."""
+-
+-try:
+- from pylint.checkers.utils import check_messages
+-except ImportError:
+- from pylint.checkers.utils import only_required_for_messages as check_messages
+-
++from pylint.interfaces import IAstroidChecker
+ from pylint.checkers import BaseChecker
+ from pylint.checkers import utils
++from pylint.checkers.utils import check_messages
++try:
++ from pylint.checkers.utils import parse_format_method_string
++except ImportError:
++ # noinspection PyUnresolvedReferences
++ from pylint.checkers.strings import parse_format_method_string
+
+ MSGS = {
+- 'E9305': ("disabled", # kept for backwards compatibility with inline ignores, remove after 2.14 is EOL
++ 'E9305': ("Format string contains automatic field numbering "
++ "specification",
+ "ansible-format-automatic-specification",
+- "disabled"),
++ "Used when a PEP 3101 format string contains automatic "
++ "field numbering (e.g. '{}').",
++ {'minversion': (2, 6)}),
+ 'E9390': ("bytes object has no .format attribute",
+ "ansible-no-format-on-bytestring",
+ "Used when a bytestring was used as a PEP 3101 format string "
+@@ -67,6 +64,20 @@ class AnsibleStringFormatChecker(BaseChe
+ if isinstance(strnode.value, bytes):
+ self.add_message('ansible-no-format-on-bytestring', node=node)
+ return
++ if not isinstance(strnode.value, str):
++ return
++
++ if node.starargs or node.kwargs:
++ return
++ try:
++ num_args = parse_format_method_string(strnode.value)[1]
++ except utils.IncompleteFormatString:
++ return
++
++ if num_args:
++ self.add_message('ansible-format-automatic-specification',
++ node=node)
++ return
+
+
+ def register(linter):
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py
+@@ -6,14 +6,8 @@ import typing as t
+
+ import astroid
+
+-# support pylint 2.x and 3.x -- remove when supporting only 3.x
+-try:
+- from pylint.interfaces import IAstroidChecker
+-except ImportError:
+- class IAstroidChecker:
+- """Backwards compatibility for 2.x / 3.x support."""
+-
+ from pylint.checkers import BaseChecker
++from pylint.interfaces import IAstroidChecker
+
+ ANSIBLE_TEST_MODULES_PATH = os.environ['ANSIBLE_TEST_MODULES_PATH']
+ ANSIBLE_TEST_MODULE_UTILS_PATH = os.environ['ANSIBLE_TEST_MODULE_UTILS_PATH']
+@@ -100,7 +94,10 @@ class AnsibleUnwantedChecker(BaseChecker
+ )),
+
+ # see https://docs.python.org/3/library/collections.abc.html
+- collections=UnwantedEntry('ansible.module_utils.six.moves.collections_abc',
++ collections=UnwantedEntry('ansible.module_utils.common._collections_compat',
++ ignore_paths=(
++ '/lib/ansible/module_utils/common/_collections_compat.py',
++ ),
+ names=(
+ 'MappingView',
+ 'ItemsView',
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py
+@@ -33,9 +33,6 @@ from collections.abc import Mapping
+ from contextlib import contextmanager
+ from fnmatch import fnmatch
+
+-from antsibull_docs_parser import dom
+-from antsibull_docs_parser.parser import parse, Context
+-
+ import yaml
+
+ from voluptuous.humanize import humanize_error
+@@ -66,7 +63,6 @@ setup_collection_loader()
+
+ from ansible import __version__ as ansible_version
+ from ansible.executor.module_common import REPLACER_WINDOWS, NEW_STYLE_PYTHON_MODULE_RE
+-from ansible.module_utils.common.collections import is_iterable
+ from ansible.module_utils.common.parameters import DEFAULT_TYPE_VALIDATORS
+ from ansible.module_utils.compat.version import StrictVersion, LooseVersion
+ from ansible.module_utils.basic import to_bytes
+@@ -78,13 +74,9 @@ from ansible.utils.version import Semant
+
+ from .module_args import AnsibleModuleImportError, AnsibleModuleNotInitialized, get_argument_spec
+
+-from .schema import (
+- ansible_module_kwargs_schema,
+- doc_schema,
+- return_schema,
+-)
++from .schema import ansible_module_kwargs_schema, doc_schema, return_schema
+
+-from .utils import CaptureStd, NoArgsAnsibleModule, compare_unordered_lists, parse_yaml, parse_isodate
++from .utils import CaptureStd, NoArgsAnsibleModule, compare_unordered_lists, is_empty, parse_yaml, parse_isodate
+
+
+ if PY3:
+@@ -305,6 +297,8 @@ class ModuleValidator(Validator):
+ # win_dsc is a dynamic arg spec, the docs won't ever match
+ PS_ARG_VALIDATE_REJECTLIST = frozenset(('win_dsc.ps1', ))
+
++ ACCEPTLIST_FUTURE_IMPORTS = frozenset(('absolute_import', 'division', 'print_function'))
++
+ def __init__(self, path, git_cache: GitCache, analyze_arg_spec=False, collection=None, collection_version=None,
+ reporter=None, routing=None, plugin_type='module'):
+ super(ModuleValidator, self).__init__(reporter=reporter or Reporter())
+@@ -407,10 +401,13 @@ class ModuleValidator(Validator):
+ if isinstance(child, ast.Expr) and isinstance(child.value, ast.Constant) and isinstance(child.value.value, str):
+ continue
+
+- # allow __future__ imports (the specific allowed imports are checked by other sanity tests)
++ # allowed from __future__ imports
+ if isinstance(child, ast.ImportFrom) and child.module == '__future__':
+- continue
+-
++ for future_import in child.names:
++ if future_import.name not in self.ACCEPTLIST_FUTURE_IMPORTS:
++ break
++ else:
++ continue
+ return False
+ return True
+ except AttributeError:
+@@ -639,21 +636,29 @@ class ModuleValidator(Validator):
+ )
+
+ def _ensure_imports_below_docs(self, doc_info, first_callable):
+- doc_line_numbers = [lineno for lineno in (doc_info[key]['lineno'] for key in doc_info) if lineno > 0]
+-
+- min_doc_line = min(doc_line_numbers) if doc_line_numbers else None
++ min_doc_line = min(doc_info[key]['lineno'] for key in doc_info)
+ max_doc_line = max(doc_info[key]['end_lineno'] for key in doc_info)
+
+ import_lines = []
+
+ for child in self.ast.body:
+ if isinstance(child, (ast.Import, ast.ImportFrom)):
+- # allow __future__ imports (the specific allowed imports are checked by other sanity tests)
+ if isinstance(child, ast.ImportFrom) and child.module == '__future__':
+- continue
+-
++ # allowed from __future__ imports
++ for future_import in child.names:
++ if future_import.name not in self.ACCEPTLIST_FUTURE_IMPORTS:
++ self.reporter.error(
++ path=self.object_path,
++ code='illegal-future-imports',
++ msg=('Only the following from __future__ imports are allowed: %s'
++ % ', '.join(self.ACCEPTLIST_FUTURE_IMPORTS)),
++ line=child.lineno
++ )
++ break
++ else: # for-else. If we didn't find a problem nad break out of the loop, then this is a legal import
++ continue
+ import_lines.append(child.lineno)
+- if min_doc_line and child.lineno < min_doc_line:
++ if child.lineno < min_doc_line:
+ self.reporter.error(
+ path=self.object_path,
+ code='import-before-documentation',
+@@ -670,7 +675,7 @@ class ModuleValidator(Validator):
+ for grandchild in bodies:
+ if isinstance(grandchild, (ast.Import, ast.ImportFrom)):
+ import_lines.append(grandchild.lineno)
+- if min_doc_line and grandchild.lineno < min_doc_line:
++ if grandchild.lineno < min_doc_line:
+ self.reporter.error(
+ path=self.object_path,
+ code='import-before-documentation',
+@@ -808,22 +813,22 @@ class ModuleValidator(Validator):
+ continue
+
+ if grandchild.id == 'DOCUMENTATION':
+- docs['DOCUMENTATION']['value'] = child.value.value
++ docs['DOCUMENTATION']['value'] = child.value.s
+ docs['DOCUMENTATION']['lineno'] = child.lineno
+ docs['DOCUMENTATION']['end_lineno'] = (
+- child.lineno + len(child.value.value.splitlines())
++ child.lineno + len(child.value.s.splitlines())
+ )
+ elif grandchild.id == 'EXAMPLES':
+- docs['EXAMPLES']['value'] = child.value.value
++ docs['EXAMPLES']['value'] = child.value.s
+ docs['EXAMPLES']['lineno'] = child.lineno
+ docs['EXAMPLES']['end_lineno'] = (
+- child.lineno + len(child.value.value.splitlines())
++ child.lineno + len(child.value.s.splitlines())
+ )
+ elif grandchild.id == 'RETURN':
+- docs['RETURN']['value'] = child.value.value
++ docs['RETURN']['value'] = child.value.s
+ docs['RETURN']['lineno'] = child.lineno
+ docs['RETURN']['end_lineno'] = (
+- child.lineno + len(child.value.value.splitlines())
++ child.lineno + len(child.value.s.splitlines())
+ )
+
+ return docs
+@@ -1036,8 +1041,6 @@ class ModuleValidator(Validator):
+ 'invalid-documentation',
+ )
+
+- self._validate_all_semantic_markup(doc, returns)
+-
+ if not self.collection:
+ existing_doc = self._check_for_new_args(doc)
+ self._check_version_added(doc, existing_doc)
+@@ -1163,113 +1166,6 @@ class ModuleValidator(Validator):
+
+ return doc_info, doc
+
+- def _check_sem_option(self, part: dom.OptionNamePart, current_plugin: dom.PluginIdentifier) -> None:
+- if part.plugin is None or part.plugin != current_plugin:
+- return
+- if part.entrypoint is not None:
+- return
+- if tuple(part.link) not in self._all_options:
+- self.reporter.error(
+- path=self.object_path,
+- code='invalid-documentation-markup',
+- msg='Directive "%s" contains a non-existing option "%s"' % (part.source, part.name)
+- )
+-
+- def _check_sem_return_value(self, part: dom.ReturnValuePart, current_plugin: dom.PluginIdentifier) -> None:
+- if part.plugin is None or part.plugin != current_plugin:
+- return
+- if part.entrypoint is not None:
+- return
+- if tuple(part.link) not in self._all_return_values:
+- self.reporter.error(
+- path=self.object_path,
+- code='invalid-documentation-markup',
+- msg='Directive "%s" contains a non-existing return value "%s"' % (part.source, part.name)
+- )
+-
+- def _validate_semantic_markup(self, object) -> None:
+- # Make sure we operate on strings
+- if is_iterable(object):
+- for entry in object:
+- self._validate_semantic_markup(entry)
+- return
+- if not isinstance(object, string_types):
+- return
+-
+- if self.collection:
+- fqcn = f'{self.collection_name}.{self.name}'
+- else:
+- fqcn = f'ansible.builtin.{self.name}'
+- current_plugin = dom.PluginIdentifier(fqcn=fqcn, type=self.plugin_type)
+- for par in parse(object, Context(current_plugin=current_plugin), errors='message', add_source=True):
+- for part in par:
+- # Errors are already covered during schema validation, we only check for option and
+- # return value references
+- if part.type == dom.PartType.OPTION_NAME:
+- self._check_sem_option(part, current_plugin)
+- if part.type == dom.PartType.RETURN_VALUE:
+- self._check_sem_return_value(part, current_plugin)
+-
+- def _validate_semantic_markup_collect(self, destination, sub_key, data, all_paths):
+- if not isinstance(data, dict):
+- return
+- for key, value in data.items():
+- if not isinstance(value, dict):
+- continue
+- keys = {key}
+- if is_iterable(value.get('aliases')):
+- keys.update(value['aliases'])
+- new_paths = [path + [key] for path in all_paths for key in keys]
+- destination.update([tuple(path) for path in new_paths])
+- self._validate_semantic_markup_collect(destination, sub_key, value.get(sub_key), new_paths)
+-
+- def _validate_semantic_markup_options(self, options):
+- if not isinstance(options, dict):
+- return
+- for key, value in options.items():
+- self._validate_semantic_markup(value.get('description'))
+- self._validate_semantic_markup_options(value.get('suboptions'))
+-
+- def _validate_semantic_markup_return_values(self, return_vars):
+- if not isinstance(return_vars, dict):
+- return
+- for key, value in return_vars.items():
+- self._validate_semantic_markup(value.get('description'))
+- self._validate_semantic_markup(value.get('returned'))
+- self._validate_semantic_markup_return_values(value.get('contains'))
+-
+- def _validate_all_semantic_markup(self, docs, return_docs):
+- if not isinstance(docs, dict):
+- docs = {}
+- if not isinstance(return_docs, dict):
+- return_docs = {}
+-
+- self._all_options = set()
+- self._all_return_values = set()
+- self._validate_semantic_markup_collect(self._all_options, 'suboptions', docs.get('options'), [[]])
+- self._validate_semantic_markup_collect(self._all_return_values, 'contains', return_docs, [[]])
+-
+- for string_keys in ('short_description', 'description', 'notes', 'requirements', 'todo'):
+- self._validate_semantic_markup(docs.get(string_keys))
+-
+- if is_iterable(docs.get('seealso')):
+- for entry in docs.get('seealso'):
+- if isinstance(entry, dict):
+- self._validate_semantic_markup(entry.get('description'))
+-
+- if isinstance(docs.get('attributes'), dict):
+- for entry in docs.get('attributes').values():
+- if isinstance(entry, dict):
+- for key in ('description', 'details'):
+- self._validate_semantic_markup(entry.get(key))
+-
+- if isinstance(docs.get('deprecated'), dict):
+- for key in ('why', 'alternative'):
+- self._validate_semantic_markup(docs.get('deprecated').get(key))
+-
+- self._validate_semantic_markup_options(docs.get('options'))
+- self._validate_semantic_markup_return_values(return_docs)
+-
+ def _check_version_added(self, doc, existing_doc):
+ version_added_raw = doc.get('version_added')
+ try:
+@@ -1337,31 +1233,6 @@ class ModuleValidator(Validator):
+
+ self._validate_argument_spec(docs, spec, kwargs)
+
+- if isinstance(docs, Mapping) and isinstance(docs.get('attributes'), Mapping):
+- if isinstance(docs['attributes'].get('check_mode'), Mapping):
+- support_value = docs['attributes']['check_mode'].get('support')
+- if not kwargs.get('supports_check_mode', False):
+- if support_value != 'none':
+- self.reporter.error(
+- path=self.object_path,
+- code='attributes-check-mode',
+- msg="The module does not declare support for check mode, but the check_mode attribute's"
+- " support value is '%s' and not 'none'" % support_value
+- )
+- else:
+- if support_value not in ('full', 'partial', 'N/A'):
+- self.reporter.error(
+- path=self.object_path,
+- code='attributes-check-mode',
+- msg="The module does declare support for check mode, but the check_mode attribute's support value is '%s'" % support_value
+- )
+- if support_value in ('partial', 'N/A') and docs['attributes']['check_mode'].get('details') in (None, '', []):
+- self.reporter.error(
+- path=self.object_path,
+- code='attributes-check-mode-details',
+- msg="The module declares it does not fully support check mode, but has no details on what exactly that means"
+- )
+-
+ def _validate_list_of_module_args(self, name, terms, spec, context):
+ if terms is None:
+ return
+@@ -1877,7 +1748,7 @@ class ModuleValidator(Validator):
+ )
+
+ arg_default = None
+- if 'default' in data and data['default'] is not None:
++ if 'default' in data and not is_empty(data['default']):
+ try:
+ with CaptureStd():
+ arg_default = _type_checker(data['default'])
+@@ -1918,7 +1789,7 @@ class ModuleValidator(Validator):
+
+ try:
+ doc_default = None
+- if 'default' in doc_options_arg and doc_options_arg['default'] is not None:
++ if 'default' in doc_options_arg and not is_empty(doc_options_arg['default']):
+ with CaptureStd():
+ doc_default = _type_checker(doc_options_arg['default'])
+ except (Exception, SystemExit):
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py
+@@ -29,7 +29,7 @@ from contextlib import contextmanager
+ from ansible.executor.powershell.module_manifest import PSModuleDepFinder
+ from ansible.module_utils.basic import FILE_COMMON_ARGUMENTS, AnsibleModule
+ from ansible.module_utils.six import reraise
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+
+ from .utils import CaptureStd, find_executable, get_module_name_from_filename
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py
+@@ -11,8 +11,7 @@ from ansible.module_utils.compat.version
+ from functools import partial
+ from urllib.parse import urlparse
+
+-from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Invalid, Length, MultipleInvalid, Required, Schema, Self, ValueInvalid, Exclusive
+-from ansible.constants import DOCUMENTABLE_PLUGINS
++from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Invalid, Length, Required, Schema, Self, ValueInvalid, Exclusive
+ from ansible.module_utils.six import string_types
+ from ansible.module_utils.common.collections import is_iterable
+ from ansible.module_utils.parsing.convert_bool import boolean
+@@ -20,9 +19,6 @@ from ansible.parsing.quoting import unqu
+ from ansible.utils.version import SemanticVersion
+ from ansible.release import __version__
+
+-from antsibull_docs_parser import dom
+-from antsibull_docs_parser.parser import parse, Context
+-
+ from .utils import parse_isodate
+
+ list_string_types = list(string_types)
+@@ -84,8 +80,26 @@ def date(error_code=None):
+ return Any(isodate, error_code=error_code)
+
+
+-# Roles can also be referenced by semantic markup
+-_VALID_PLUGIN_TYPES = set(DOCUMENTABLE_PLUGINS + ('role', ))
++_MODULE = re.compile(r"\bM\(([^)]+)\)")
++_LINK = re.compile(r"\bL\(([^)]+)\)")
++_URL = re.compile(r"\bU\(([^)]+)\)")
++_REF = re.compile(r"\bR\(([^)]+)\)")
++
++
++def _check_module_link(directive, content):
++ if not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(content):
++ raise _add_ansible_error_code(
++ Invalid('Directive "%s" must contain a FQCN' % directive), 'invalid-documentation-markup')
++
++
++def _check_link(directive, content):
++ if ',' not in content:
++ raise _add_ansible_error_code(
++ Invalid('Directive "%s" must contain a comma' % directive), 'invalid-documentation-markup')
++ idx = content.rindex(',')
++ title = content[:idx]
++ url = content[idx + 1:].lstrip(' ')
++ _check_url(directive, url)
+
+
+ def _check_url(directive, content):
+@@ -93,10 +107,15 @@ def _check_url(directive, content):
+ parsed_url = urlparse(content)
+ if parsed_url.scheme not in ('', 'http', 'https'):
+ raise ValueError('Schema must be HTTP, HTTPS, or not specified')
+- return []
+- except ValueError:
+- return [_add_ansible_error_code(
+- Invalid('Directive %s must contain a valid URL' % directive), 'invalid-documentation-markup')]
++ except ValueError as exc:
++ raise _add_ansible_error_code(
++ Invalid('Directive "%s" must contain an URL' % directive), 'invalid-documentation-markup')
++
++
++def _check_ref(directive, content):
++ if ',' not in content:
++ raise _add_ansible_error_code(
++ Invalid('Directive "%s" must contain a comma' % directive), 'invalid-documentation-markup')
+
+
+ def doc_string(v):
+@@ -104,55 +123,25 @@ def doc_string(v):
+ if not isinstance(v, string_types):
+ raise _add_ansible_error_code(
+ Invalid('Must be a string'), 'invalid-documentation')
+- errors = []
+- for par in parse(v, Context(), errors='message', strict=True, add_source=True):
+- for part in par:
+- if part.type == dom.PartType.ERROR:
+- errors.append(_add_ansible_error_code(Invalid(part.message), 'invalid-documentation-markup'))
+- if part.type == dom.PartType.URL:
+- errors.extend(_check_url('U()', part.url))
+- if part.type == dom.PartType.LINK:
+- errors.extend(_check_url('L()', part.url))
+- if part.type == dom.PartType.MODULE:
+- if not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(part.fqcn):
+- errors.append(_add_ansible_error_code(Invalid(
+- 'Directive "%s" must contain a FQCN; found "%s"' % (part.source, part.fqcn)),
+- 'invalid-documentation-markup'))
+- if part.type == dom.PartType.PLUGIN:
+- if not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(part.plugin.fqcn):
+- errors.append(_add_ansible_error_code(Invalid(
+- 'Directive "%s" must contain a FQCN; found "%s"' % (part.source, part.plugin.fqcn)),
+- 'invalid-documentation-markup'))
+- if part.plugin.type not in _VALID_PLUGIN_TYPES:
+- errors.append(_add_ansible_error_code(Invalid(
+- 'Directive "%s" must contain a valid plugin type; found "%s"' % (part.source, part.plugin.type)),
+- 'invalid-documentation-markup'))
+- if part.type == dom.PartType.OPTION_NAME:
+- if part.plugin is not None and not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(part.plugin.fqcn):
+- errors.append(_add_ansible_error_code(Invalid(
+- 'Directive "%s" must contain a FQCN; found "%s"' % (part.source, part.plugin.fqcn)),
+- 'invalid-documentation-markup'))
+- if part.plugin is not None and part.plugin.type not in _VALID_PLUGIN_TYPES:
+- errors.append(_add_ansible_error_code(Invalid(
+- 'Directive "%s" must contain a valid plugin type; found "%s"' % (part.source, part.plugin.type)),
+- 'invalid-documentation-markup'))
+- if part.type == dom.PartType.RETURN_VALUE:
+- if part.plugin is not None and not FULLY_QUALIFIED_COLLECTION_RESOURCE_RE.match(part.plugin.fqcn):
+- errors.append(_add_ansible_error_code(Invalid(
+- 'Directive "%s" must contain a FQCN; found "%s"' % (part.source, part.plugin.fqcn)),
+- 'invalid-documentation-markup'))
+- if part.plugin is not None and part.plugin.type not in _VALID_PLUGIN_TYPES:
+- errors.append(_add_ansible_error_code(Invalid(
+- 'Directive "%s" must contain a valid plugin type; found "%s"' % (part.source, part.plugin.type)),
+- 'invalid-documentation-markup'))
+- if len(errors) == 1:
+- raise errors[0]
+- if errors:
+- raise MultipleInvalid(errors)
++ for m in _MODULE.finditer(v):
++ _check_module_link(m.group(0), m.group(1))
++ for m in _LINK.finditer(v):
++ _check_link(m.group(0), m.group(1))
++ for m in _URL.finditer(v):
++ _check_url(m.group(0), m.group(1))
++ for m in _REF.finditer(v):
++ _check_ref(m.group(0), m.group(1))
+ return v
+
+
+-doc_string_or_strings = Any(doc_string, [doc_string])
++def doc_string_or_strings(v):
++ """Match a documentation string, or list of strings."""
++ if isinstance(v, string_types):
++ return doc_string(v)
++ if isinstance(v, (list, tuple)):
++ return [doc_string(vv) for vv in v]
++ raise _add_ansible_error_code(
++ Invalid('Must be a string or list of strings'), 'invalid-documentation')
+
+
+ def is_callable(v):
+@@ -184,11 +173,6 @@ seealso_schema = Schema(
+ 'description': doc_string,
+ },
+ {
+- Required('plugin'): Any(*string_types),
+- Required('plugin_type'): Any(*DOCUMENTABLE_PLUGINS),
+- 'description': doc_string,
+- },
+- {
+ Required('ref'): Any(*string_types),
+ Required('description'): doc_string,
+ },
+@@ -810,7 +794,7 @@ def author(value):
+
+ def doc_schema(module_name, for_collection=False, deprecated_module=False, plugin_type='module'):
+
+- if module_name.startswith('_') and not for_collection:
++ if module_name.startswith('_'):
+ module_name = module_name[1:]
+ deprecated_module = True
+ if for_collection is False and plugin_type == 'connection' and module_name == 'paramiko_ssh':
+@@ -880,6 +864,9 @@ def doc_schema(module_name, for_collecti
+ 'action_group': add_default_attributes({
+ Required('membership'): list_string_types,
+ }),
++ 'forced_action_plugin': add_default_attributes({
++ Required('action_plugin'): any_string_types,
++ }),
+ 'platform': add_default_attributes({
+ Required('platforms'): Any(list_string_types, *string_types)
+ }),
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py
+@@ -28,7 +28,7 @@ from io import BytesIO, TextIOWrapper
+ import yaml
+ import yaml.reader
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.common.yaml import SafeLoader
+ from ansible.module_utils.six import string_types
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/sanity/yamllint/yamllinter.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/sanity/yamllint/yamllinter.py
+@@ -181,15 +181,15 @@ class YamlChecker:
+ if doc_types and target.id not in doc_types:
+ continue
+
+- fmt_match = fmt_re.match(statement.value.value.lstrip())
++ fmt_match = fmt_re.match(statement.value.s.lstrip())
+ fmt = 'yaml'
+ if fmt_match:
+ fmt = fmt_match.group(1)
+
+ docs[target.id] = dict(
+- yaml=statement.value.value,
++ yaml=statement.value.s,
+ lineno=statement.lineno,
+- end_lineno=statement.lineno + len(statement.value.value.splitlines()),
++ end_lineno=statement.lineno + len(statement.value.s.splitlines()),
+ fmt=fmt.lower(),
+ )
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/controller/tools/collection_detail.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/controller/tools/collection_detail.py
+@@ -50,7 +50,7 @@ def read_manifest_json(collection_path):
+ )
+ validate_version(result['version'])
+ except Exception as ex: # pylint: disable=broad-except
+- raise Exception('{0}: {1}'.format(os.path.basename(manifest_path), ex)) from None
++ raise Exception('{0}: {1}'.format(os.path.basename(manifest_path), ex))
+
+ return result
+
+@@ -71,7 +71,7 @@ def read_galaxy_yml(collection_path):
+ )
+ validate_version(result['version'])
+ except Exception as ex: # pylint: disable=broad-except
+- raise Exception('{0}: {1}'.format(os.path.basename(galaxy_path), ex)) from None
++ raise Exception('{0}: {1}'.format(os.path.basename(galaxy_path), ex))
+
+ return result
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/target/common/constants.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/target/common/constants.py
+@@ -7,14 +7,14 @@ __metaclass__ = type
+
+ REMOTE_ONLY_PYTHON_VERSIONS = (
+ '2.7',
++ '3.5',
+ '3.6',
+ '3.7',
+ '3.8',
+- '3.9',
+ )
+
+ CONTROLLER_PYTHON_VERSIONS = (
++ '3.9',
+ '3.10',
+ '3.11',
+- '3.12',
+ )
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/target/pytest/plugins/ansible_pytest_collections.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/target/pytest/plugins/ansible_pytest_collections.py
+@@ -32,50 +32,6 @@ def collection_pypkgpath(self):
+ raise Exception('File "%s" not found in collection path "%s".' % (self.strpath, ANSIBLE_COLLECTIONS_PATH))
+
+
+-def enable_assertion_rewriting_hook(): # type: () -> None
+- """
+- Enable pytest's AssertionRewritingHook on Python 3.x.
+- This is necessary because the Ansible collection loader intercepts imports before the pytest provided loader ever sees them.
+- """
+- import sys
+-
+- if sys.version_info[0] == 2:
+- return # Python 2.x is not supported
+-
+- hook_name = '_pytest.assertion.rewrite.AssertionRewritingHook'
+- hooks = [hook for hook in sys.meta_path if hook.__class__.__module__ + '.' + hook.__class__.__qualname__ == hook_name]
+-
+- if len(hooks) != 1:
+- raise Exception('Found {} instance(s) of "{}" in sys.meta_path.'.format(len(hooks), hook_name))
+-
+- assertion_rewriting_hook = hooks[0]
+-
+- # This is based on `_AnsibleCollectionPkgLoaderBase.exec_module` from `ansible/utils/collection_loader/_collection_finder.py`.
+- def exec_module(self, module):
+- # short-circuit redirect; avoid reinitializing existing modules
+- if self._redirect_module: # pylint: disable=protected-access
+- return
+-
+- # execute the module's code in its namespace
+- code_obj = self.get_code(self._fullname) # pylint: disable=protected-access
+-
+- if code_obj is not None: # things like NS packages that can't have code on disk will return None
+- # This logic is loosely based on `AssertionRewritingHook._should_rewrite` from pytest.
+- # See: https://github.com/pytest-dev/pytest/blob/779a87aada33af444f14841a04344016a087669e/src/_pytest/assertion/rewrite.py#L209
+- should_rewrite = self._package_to_load == 'conftest' or self._package_to_load.startswith('test_') # pylint: disable=protected-access
+-
+- if should_rewrite:
+- # noinspection PyUnresolvedReferences
+- assertion_rewriting_hook.exec_module(module)
+- else:
+- exec(code_obj, module.__dict__) # pylint: disable=exec-used
+-
+- # noinspection PyProtectedMember
+- from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionPkgLoaderBase
+-
+- _AnsibleCollectionPkgLoaderBase.exec_module = exec_module
+-
+-
+ def pytest_configure():
+ """Configure this pytest plugin."""
+ try:
+@@ -84,8 +40,6 @@ def pytest_configure():
+ except AttributeError:
+ pytest_configure.executed = True
+
+- enable_assertion_rewriting_hook()
+-
+ # noinspection PyProtectedMember
+ from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionFinder
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/target/sanity/import/importer.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/target/sanity/import/importer.py
+@@ -552,11 +552,13 @@ def main():
+ "Python 2 is no longer supported by the Python core team. Support for it is now deprecated in cryptography,"
+ " and will be removed in the next release.")
+
+- # ansible.utils.unsafe_proxy attempts patching sys.intern generating a warning if it was already patched
+- warnings.filterwarnings(
+- "ignore",
+- "skipped sys.intern patch; appears to have already been patched"
+- )
++ if sys.version_info[:2] == (3, 5):
++ warnings.filterwarnings(
++ "ignore",
++ "Python 3.5 support will be dropped in the next release ofcryptography. Please upgrade your Python.")
++ warnings.filterwarnings(
++ "ignore",
++ "Python 3.5 support will be dropped in the next release of cryptography. Please upgrade your Python.")
+
+ try:
+ yield
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/target/setup/bootstrap.sh
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/target/setup/bootstrap.sh
+@@ -53,7 +53,7 @@ install_pip() {
+ pip_bootstrap_url="https://ci-files.testing.ansible.com/ansible-test/get-pip-20.3.4.py"
+ ;;
+ *)
+- pip_bootstrap_url="https://ci-files.testing.ansible.com/ansible-test/get-pip-23.1.2.py"
++ pip_bootstrap_url="https://ci-files.testing.ansible.com/ansible-test/get-pip-21.3.1.py"
+ ;;
+ esac
+
+@@ -111,15 +111,6 @@ bootstrap_remote_alpine()
+ echo "Failed to install packages. Sleeping before trying again..."
+ sleep 10
+ done
+-
+- # Upgrade the `libexpat` package to ensure that an upgraded Python (`pyexpat`) continues to work.
+- while true; do
+- # shellcheck disable=SC2086
+- apk upgrade -q libexpat \
+- && break
+- echo "Failed to upgrade libexpat. Sleeping before trying again..."
+- sleep 10
+- done
+ }
+
+ bootstrap_remote_fedora()
+@@ -172,6 +163,8 @@ bootstrap_remote_freebsd()
+ # Declare platform/python version combinations which do not have supporting OS packages available.
+ # For these combinations ansible-test will use pip to install the requirements instead.
+ case "${platform_version}/${python_version}" in
++ "12.4/3.9")
++ ;;
+ *)
+ jinja2_pkg="" # not available
+ cryptography_pkg="" # not available
+@@ -268,7 +261,7 @@ bootstrap_remote_rhel_8()
+ if [ "${python_version}" = "3.6" ]; then
+ py_pkg_prefix="python3"
+ else
+- py_pkg_prefix="python${python_version}"
++ py_pkg_prefix="python${python_package_version}"
+ fi
+
+ packages="
+@@ -276,14 +269,6 @@ bootstrap_remote_rhel_8()
+ ${py_pkg_prefix}-devel
+ "
+
+- # pip isn't included in the Python devel package under Python 3.11
+- if [ "${python_version}" != "3.6" ]; then
+- packages="
+- ${packages}
+- ${py_pkg_prefix}-pip
+- "
+- fi
+-
+ # Jinja2 is not installed with an OS package since the provided version is too old.
+ # Instead, ansible-test will install it using pip.
+ if [ "${controller}" ]; then
+@@ -293,19 +278,9 @@ bootstrap_remote_rhel_8()
+ "
+ fi
+
+- # Python 3.11 isn't a module like the earlier versions
+- if [ "${python_version}" = "3.6" ]; then
+- while true; do
+- # shellcheck disable=SC2086
+- yum module install -q -y "python${python_package_version}" \
+- && break
+- echo "Failed to install packages. Sleeping before trying again..."
+- sleep 10
+- done
+- fi
+-
+ while true; do
+ # shellcheck disable=SC2086
++ yum module install -q -y "python${python_package_version}" && \
+ yum install -q -y ${packages} \
+ && break
+ echo "Failed to install packages. Sleeping before trying again..."
+@@ -317,34 +292,22 @@ bootstrap_remote_rhel_8()
+
+ bootstrap_remote_rhel_9()
+ {
+- if [ "${python_version}" = "3.9" ]; then
+- py_pkg_prefix="python3"
+- else
+- py_pkg_prefix="python${python_version}"
+- fi
++ py_pkg_prefix="python3"
+
+ packages="
+ gcc
+ ${py_pkg_prefix}-devel
+ "
+
+- # pip is not included in the Python devel package under Python 3.11
+- if [ "${python_version}" != "3.9" ]; then
+- packages="
+- ${packages}
+- ${py_pkg_prefix}-pip
+- "
+- fi
+-
+ # Jinja2 is not installed with an OS package since the provided version is too old.
+ # Instead, ansible-test will install it using pip.
+- # packaging and resolvelib are missing for Python 3.11 (and possible later) so we just
+- # skip them and let ansible-test install them from PyPI.
+ if [ "${controller}" ]; then
+ packages="
+ ${packages}
+ ${py_pkg_prefix}-cryptography
++ ${py_pkg_prefix}-packaging
+ ${py_pkg_prefix}-pyyaml
++ ${py_pkg_prefix}-resolvelib
+ "
+ fi
+
+@@ -424,6 +387,14 @@ bootstrap_remote_ubuntu()
+ echo "Failed to install packages. Sleeping before trying again..."
+ sleep 10
+ done
++
++ if [ "${controller}" ]; then
++ if [ "${platform_version}/${python_version}" = "20.04/3.9" ]; then
++ # Install pyyaml using pip so libyaml support is available on Python 3.9.
++ # The OS package install (which is installed by default) only has a .so file for Python 3.8.
++ pip_install "--upgrade pyyaml"
++ fi
++ fi
+ }
+
+ bootstrap_docker()
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/_util/target/setup/quiet_pip.py
++++ ansible-core-2.16.5/test/lib/ansible_test/_util/target/setup/quiet_pip.py
+@@ -27,6 +27,10 @@ WARNING_MESSAGE_FILTERS = (
+ # pip 21.0 will drop support for Python 2.7 in January 2021.
+ # More details about Python 2 support in pip, can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support
+ 'DEPRECATION: Python 2.7 reached the end of its life ',
++
++ # DEPRECATION: Python 3.5 reached the end of its life on September 13th, 2020. Please upgrade your Python as Python 3.5 is no longer maintained.
++ # pip 21.0 will drop support for Python 3.5 in January 2021. pip 21.0 will remove support for this functionality.
++ 'DEPRECATION: Python 3.5 reached the end of its life ',
+ )
+
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/config/cloud-config-aws.ini.template
++++ ansible-core-2.16.5/test/lib/ansible_test/config/cloud-config-aws.ini.template
+@@ -6,9 +6,7 @@
+ # 2) Using the automatically provisioned AWS credentials in ansible-test.
+ #
+ # If you do not want to use the automatically provisioned temporary AWS credentials,
+-# fill in the @VAR placeholders below and save this file without the .template extension,
+-# into the tests/integration directory of the collection you're testing.
+-# If you need to omit optional fields like security_token, comment out that line.
++# fill in the @VAR placeholders below and save this file without the .template extension.
+ # This will cause ansible-test to use the given configuration instead of temporary credentials.
+ #
+ # NOTE: Automatic provisioning of AWS credentials requires an ansible-core-ci API key.
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/config/cloud-config-azure.ini.template
++++ ansible-core-2.16.5/test/lib/ansible_test/config/cloud-config-azure.ini.template
+@@ -6,8 +6,7 @@
+ # 2) Using the automatically provisioned Azure credentials in ansible-test.
+ #
+ # If you do not want to use the automatically provisioned temporary Azure credentials,
+-# fill in the values below and save this file without the .template extension,
+-# into the tests/integration directory of the collection you're testing.
++# fill in the values below and save this file without the .template extension.
+ # This will cause ansible-test to use the given configuration instead of temporary credentials.
+ #
+ # NOTE: Automatic provisioning of Azure credentials requires an ansible-core-ci API key in ~/.ansible-core-ci.key
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/config/cloud-config-cloudscale.ini.template
++++ ansible-core-2.16.5/test/lib/ansible_test/config/cloud-config-cloudscale.ini.template
+@@ -4,8 +4,6 @@
+ #
+ # 1) Running integration tests without using ansible-test.
+ #
+-# Fill in the value below and save this file without the .template extension,
+-# into the tests/integration directory of the collection you're testing.
+
+ [default]
+ cloudscale_api_token = @API_TOKEN
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/config/cloud-config-cs.ini.template
++++ ansible-core-2.16.5/test/lib/ansible_test/config/cloud-config-cs.ini.template
+@@ -6,8 +6,7 @@
+ # 2) Using the automatically provisioned cloudstack-sim docker container in ansible-test.
+ #
+ # If you do not want to use the automatically provided CloudStack simulator,
+-# fill in the @VAR placeholders below and save this file without the .template extension,
+-# into the tests/integration directory of the collection you're testing.
++# fill in the @VAR placeholders below and save this file without the .template extension.
+ # This will cause ansible-test to use the given configuration and not launch the simulator.
+ #
+ # It is recommended that you DO NOT use this template unless you cannot use the simulator.
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/config/cloud-config-gcp.ini.template
++++ ansible-core-2.16.5/test/lib/ansible_test/config/cloud-config-gcp.ini.template
+@@ -6,8 +6,7 @@
+ # 2) Using the automatically provisioned cloudstack-sim docker container in ansible-test.
+ #
+ # If you do not want to use the automatically provided GCP simulator,
+-# fill in the @VAR placeholders below and save this file without the .template extension,
+-# into the tests/integration directory of the collection you're testing.
++# fill in the @VAR placeholders below and save this file without the .template extension.
+ # This will cause ansible-test to use the given configuration and not launch the simulator.
+ #
+ # It is recommended that you DO NOT use this template unless you cannot use the simulator.
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/config/cloud-config-hcloud.ini.template
++++ ansible-core-2.16.5/test/lib/ansible_test/config/cloud-config-hcloud.ini.template
+@@ -6,8 +6,7 @@
+ # 2) Using the automatically provisioned Hetzner Cloud credentials in ansible-test.
+ #
+ # If you do not want to use the automatically provisioned temporary Hetzner Cloud credentials,
+-# fill in the @VAR placeholders below and save this file without the .template extension,
+-# into the tests/integration directory of the collection you're testing.
++# fill in the @VAR placeholders below and save this file without the .template extension.
+ # This will cause ansible-test to use the given configuration instead of temporary credentials.
+ #
+ # NOTE: Automatic provisioning of Hetzner Cloud credentials requires an ansible-core-ci API key.
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/config/cloud-config-opennebula.ini.template
++++ ansible-core-2.16.5/test/lib/ansible_test/config/cloud-config-opennebula.ini.template
+@@ -6,8 +6,7 @@
+ # 2) Running integration tests against previously recorded XMLRPC fixtures
+ #
+ # If you want to test against a Live OpenNebula platform,
+-# fill in the values below and save this file without the .template extension,
+-# into the tests/integration directory of the collection you're testing.
++# fill in the values below and save this file without the .template extension.
+ # This will cause ansible-test to use the given configuration.
+ #
+ # If you run with @FIXTURES enabled (true) then you can decide if you want to
+@@ -18,4 +17,4 @@ opennebula_url: @URL
+ opennebula_username: @USERNAME
+ opennebula_password: @PASSWORD
+ opennebula_test_fixture: @FIXTURES
+-opennebula_test_fixture_replay: @REPLAY
++opennebula_test_fixture_replay: @REPLAY
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/config/cloud-config-openshift.kubeconfig.template
++++ ansible-core-2.16.5/test/lib/ansible_test/config/cloud-config-openshift.kubeconfig.template
+@@ -6,8 +6,7 @@
+ # 2) Using the automatically provisioned openshift-origin docker container in ansible-test.
+ #
+ # If you do not want to use the automatically provided OpenShift container,
+-# place your kubeconfig file next into the tests/integration directory of the collection you're testing,
+-# with the same name is this file, but without the .template extension.
++# place your kubeconfig file next to this file, with the same name, but without the .template extension.
+ # This will cause ansible-test to use the given configuration and not launch the automatically provided container.
+ #
+ # It is recommended that you DO NOT use this template unless you cannot use the automatically provided container.
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/config/cloud-config-scaleway.ini.template
++++ ansible-core-2.16.5/test/lib/ansible_test/config/cloud-config-scaleway.ini.template
+@@ -5,8 +5,7 @@
+ # 1) Running integration tests without using ansible-test.
+ #
+ # If you want to test against the Vultr public API,
+-# fill in the values below and save this file without the .template extension,
+-# into the tests/integration directory of the collection you're testing.
++# fill in the values below and save this file without the .template extension.
+ # This will cause ansible-test to use the given configuration.
+
+ [default]
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/config/cloud-config-vcenter.ini.template
++++ ansible-core-2.16.5/test/lib/ansible_test/config/cloud-config-vcenter.ini.template
+@@ -6,8 +6,7 @@
+ # 2) Using the automatically provisioned VMware credentials in ansible-test.
+ #
+ # If you do not want to use the automatically provisioned temporary VMware credentials,
+-# fill in the @VAR placeholders below and save this file without the .template extension,
+-# into the tests/integration directory of the collection you're testing.
++# fill in the @VAR placeholders below and save this file without the .template extension.
+ # This will cause ansible-test to use the given configuration instead of temporary credentials.
+ #
+ # NOTE: Automatic provisioning of VMware credentials requires an ansible-core-ci API key.
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/config/cloud-config-vultr.ini.template
++++ ansible-core-2.16.5/test/lib/ansible_test/config/cloud-config-vultr.ini.template
+@@ -5,8 +5,7 @@
+ # 1) Running integration tests without using ansible-test.
+ #
+ # If you want to test against the Vultr public API,
+-# fill in the values below and save this file without the .template extension,
+-# into the tests/integration directory of the collection you're testing.
++# fill in the values below and save this file without the .template extension.
+ # This will cause ansible-test to use the given configuration.
+
+ [default]
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/config/inventory.networking.template
++++ ansible-core-2.16.5/test/lib/ansible_test/config/inventory.networking.template
+@@ -6,8 +6,7 @@
+ # 2) Using the `--platform` option to provision temporary network instances on EC2.
+ #
+ # If you do not want to use the automatically provisioned temporary network instances,
+-# fill in the @VAR placeholders below and save this file without the .template extension,
+-# into the tests/integration directory of the collection you're testing.
++# fill in the @VAR placeholders below and save this file without the .template extension.
+ #
+ # NOTE: Automatic provisioning of network instances on EC2 requires an ansible-core-ci API key.
+
+--- ansible-core-2.16.5.orig/test/lib/ansible_test/config/inventory.winrm.template
++++ ansible-core-2.16.5/test/lib/ansible_test/config/inventory.winrm.template
+@@ -6,8 +6,7 @@
+ # 1) Using the `--windows` option to provision temporary Windows instances on EC2.
+ #
+ # If you do not want to use the automatically provisioned temporary Windows instances,
+-# fill in the @VAR placeholders below and save this file without the .template extension,
+-# into the tests/integration directory of the collection you're testing.
++# fill in the @VAR placeholders below and save this file without the .template extension.
+ #
+ # NOTE: Automatic provisioning of Windows instances on EC2 requires an ansible-core-ci API key.
+ #
+--- ansible-core-2.16.5.orig/test/sanity/code-smell/ansible-requirements.py
++++ ansible-core-2.16.5/test/sanity/code-smell/ansible-requirements.py
+@@ -1,6 +1,7 @@
+ from __future__ import annotations
+
+ import re
++import sys
+
+
+ def read_file(path):
+--- ansible-core-2.16.5.orig/test/sanity/code-smell/deprecated-config.requirements.in
++++ ansible-core-2.16.5/test/sanity/code-smell/deprecated-config.requirements.in
+@@ -1,2 +1,2 @@
+-jinja2
++jinja2 # ansible-core requirement
+ pyyaml
+--- ansible-core-2.16.5.orig/test/sanity/code-smell/deprecated-config.requirements.txt
++++ ansible-core-2.16.5/test/sanity/code-smell/deprecated-config.requirements.txt
+@@ -1,4 +1,6 @@
+ # edit "deprecated-config.requirements.in" and generate with: hacking/update-sanity-requirements.py --test deprecated-config
++# pre-build requirement: pyyaml == 6.0
++# pre-build constraint: Cython < 3.0
+ Jinja2==3.1.2
+-MarkupSafe==2.1.3
+-PyYAML==6.0.1
++MarkupSafe==2.1.1
++PyYAML==6.0
+--- ansible-core-2.16.5.orig/test/sanity/code-smell/obsolete-files.json
++++ ansible-core-2.16.5/test/sanity/code-smell/obsolete-files.json
+@@ -1,8 +1,6 @@
+ {
+ "include_symlinks": true,
+ "prefixes": [
+- "docs/",
+- "examples/",
+ "test/runner/",
+ "test/sanity/ansible-doc/",
+ "test/sanity/compile/",
+--- ansible-core-2.16.5.orig/test/sanity/code-smell/package-data.requirements.in
++++ ansible-core-2.16.5/test/sanity/code-smell/package-data.requirements.in
+@@ -1,8 +1,8 @@
+ build # required to build sdist
+ wheel # required to build wheel
+ jinja2
+-pyyaml
+-resolvelib < 1.1.0
+-rstcheck < 6 # newer versions have too many dependencies
++pyyaml # ansible-core requirement
++resolvelib < 0.9.0
++rstcheck < 4 # match version used in other sanity tests
+ antsibull-changelog
+-setuptools == 66.1.0 # minimum supported setuptools
++setuptools == 45.2.0 # minimum supported setuptools
+--- ansible-core-2.16.5.orig/test/sanity/code-smell/package-data.requirements.txt
++++ ansible-core-2.16.5/test/sanity/code-smell/package-data.requirements.txt
+@@ -1,17 +1,18 @@
+ # edit "package-data.requirements.in" and generate with: hacking/update-sanity-requirements.py --test package-data
+-antsibull-changelog==0.23.0
+-build==1.0.3
+-docutils==0.18.1
++# pre-build requirement: pyyaml == 6.0
++# pre-build constraint: Cython < 3.0
++antsibull-changelog==0.16.0
++build==0.10.0
++docutils==0.17.1
+ Jinja2==3.1.2
+-MarkupSafe==2.1.3
+-packaging==23.2
++MarkupSafe==2.1.1
++packaging==21.3
+ pyproject_hooks==1.0.0
+-PyYAML==6.0.1
+-resolvelib==1.0.1
+-rstcheck==5.0.0
++pyparsing==3.0.9
++PyYAML==6.0
++resolvelib==0.8.1
++rstcheck==3.5.0
+ semantic-version==2.10.0
+-setuptools==66.1.0
++setuptools==45.2.0
+ tomli==2.0.1
+-types-docutils==0.18.3
+-typing_extensions==4.8.0
+-wheel==0.41.2
++wheel==0.41.0
+--- ansible-core-2.16.5.orig/test/sanity/code-smell/release-names.py
++++ ansible-core-2.16.5/test/sanity/code-smell/release-names.py
+@@ -22,7 +22,7 @@ Test that the release name is present in
+
+ from __future__ import annotations
+
+-import pathlib
++from yaml import safe_load
+
+ from ansible.release import __codename__
+
+@@ -30,7 +30,8 @@ from ansible.release import __codename__
+ def main():
+ """Entrypoint to the script"""
+
+- releases = pathlib.Path('.github/RELEASE_NAMES.txt').read_text().splitlines()
++ with open('.github/RELEASE_NAMES.yml') as f:
++ releases = safe_load(f.read())
+
+ # Why this format? The file's sole purpose is to be read by a human when they need to know
+ # which release names have already been used. So:
+@@ -40,7 +41,7 @@ def main():
+ if __codename__ == name:
+ break
+ else:
+- print(f'.github/RELEASE_NAMES.txt: Current codename {__codename__!r} not present in the file')
++ print('.github/RELEASE_NAMES.yml: Current codename was not present in the file')
+
+
+ if __name__ == '__main__':
+--- ansible-core-2.16.5.orig/test/sanity/code-smell/test-constraints.py
++++ ansible-core-2.16.5/test/sanity/code-smell/test-constraints.py
+@@ -65,6 +65,12 @@ def main():
+ # keeping constraints for tests other than sanity tests in one file helps avoid conflicts
+ print('%s:%d:%d: put the constraint (%s%s) in `%s`' % (path, lineno, 1, name, raw_constraints, constraints_path))
+
++ for name, requirements in frozen_sanity.items():
++ if len(set(req[3].group('constraints').strip() for req in requirements)) != 1:
++ for req in requirements:
++ print('%s:%d:%d: sanity constraint (%s) does not match others for package `%s`' % (
++ req[0], req[1], req[3].start('constraints') + 1, req[3].group('constraints'), name))
++
+
+ def check_ansible_test(path: str, requirements: list[tuple[int, str, re.Match]]) -> None:
+ sys.path.insert(0, str(pathlib.Path(__file__).parent.parent.parent.joinpath('lib')))
+--- ansible-core-2.16.5.orig/test/sanity/code-smell/update-bundled.requirements.txt
++++ ansible-core-2.16.5/test/sanity/code-smell/update-bundled.requirements.txt
+@@ -1,2 +1,3 @@
+ # edit "update-bundled.requirements.in" and generate with: hacking/update-sanity-requirements.py --test update-bundled
+-packaging==23.2
++packaging==21.3
++pyparsing==3.0.9
+--- ansible-core-2.16.5.orig/test/sanity/ignore.txt
++++ ansible-core-2.16.5/test/sanity/ignore.txt
+@@ -1,21 +1,16 @@
++.azure-pipelines/scripts/publish-codecov.py replace-urlopen
+ lib/ansible/cli/scripts/ansible_connection_cli_stub.py shebang
+ lib/ansible/config/base.yml no-unwanted-files
++lib/ansible/executor/playbook_executor.py pylint:disallowed-name
+ lib/ansible/executor/powershell/async_watchdog.ps1 pslint:PSCustomUseLiteralPath
+ lib/ansible/executor/powershell/async_wrapper.ps1 pslint:PSCustomUseLiteralPath
+ lib/ansible/executor/powershell/exec_wrapper.ps1 pslint:PSCustomUseLiteralPath
+-lib/ansible/galaxy/collection/__init__.py mypy-3.10:attr-defined # inline ignore has no effect
+-lib/ansible/galaxy/collection/__init__.py mypy-3.11:attr-defined # inline ignore has no effect
+-lib/ansible/galaxy/collection/__init__.py mypy-3.12:attr-defined # inline ignore has no effect
+-lib/ansible/galaxy/collection/gpg.py mypy-3.10:arg-type
+-lib/ansible/galaxy/collection/gpg.py mypy-3.11:arg-type
+-lib/ansible/galaxy/collection/gpg.py mypy-3.12:arg-type
+-lib/ansible/parsing/yaml/constructor.py mypy-3.10:type-var # too many occurrences to ignore inline
+-lib/ansible/parsing/yaml/constructor.py mypy-3.11:type-var # too many occurrences to ignore inline
+-lib/ansible/parsing/yaml/constructor.py mypy-3.12:type-var # too many occurrences to ignore inline
++lib/ansible/executor/task_queue_manager.py pylint:disallowed-name
+ lib/ansible/keyword_desc.yml no-unwanted-files
+ lib/ansible/modules/apt.py validate-modules:parameter-invalid
+ lib/ansible/modules/apt_repository.py validate-modules:parameter-invalid
+ lib/ansible/modules/assemble.py validate-modules:nonexistent-parameter-documented
++lib/ansible/modules/async_status.py use-argspec-type-path
+ lib/ansible/modules/async_status.py validate-modules!skip
+ lib/ansible/modules/async_wrapper.py ansible-doc!skip # not an actual module
+ lib/ansible/modules/async_wrapper.py pylint:ansible-bad-function # ignore, required
+@@ -26,48 +21,61 @@ lib/ansible/modules/command.py validate-
+ lib/ansible/modules/command.py validate-modules:doc-missing-type
+ lib/ansible/modules/command.py validate-modules:nonexistent-parameter-documented
+ lib/ansible/modules/command.py validate-modules:undocumented-parameter
++lib/ansible/modules/copy.py pylint:disallowed-name
+ lib/ansible/modules/copy.py validate-modules:doc-default-does-not-match-spec
+ lib/ansible/modules/copy.py validate-modules:nonexistent-parameter-documented
+ lib/ansible/modules/copy.py validate-modules:undocumented-parameter
++lib/ansible/modules/dnf.py validate-modules:doc-required-mismatch
+ lib/ansible/modules/dnf.py validate-modules:parameter-invalid
+-lib/ansible/modules/dnf5.py validate-modules:parameter-invalid
+ lib/ansible/modules/file.py validate-modules:undocumented-parameter
+ lib/ansible/modules/find.py use-argspec-type-path # fix needed
++lib/ansible/modules/git.py pylint:disallowed-name
+ lib/ansible/modules/git.py use-argspec-type-path
++lib/ansible/modules/git.py validate-modules:doc-missing-type
+ lib/ansible/modules/git.py validate-modules:doc-required-mismatch
++lib/ansible/modules/iptables.py pylint:disallowed-name
+ lib/ansible/modules/lineinfile.py validate-modules:doc-choices-do-not-match-spec
+ lib/ansible/modules/lineinfile.py validate-modules:doc-default-does-not-match-spec
+ lib/ansible/modules/lineinfile.py validate-modules:nonexistent-parameter-documented
+ lib/ansible/modules/package_facts.py validate-modules:doc-choices-do-not-match-spec
++lib/ansible/modules/pip.py pylint:disallowed-name
+ lib/ansible/modules/replace.py validate-modules:nonexistent-parameter-documented
+-lib/ansible/modules/replace.py pylint:used-before-assignment # false positive detection by pylint
+ lib/ansible/modules/service.py validate-modules:nonexistent-parameter-documented
+ lib/ansible/modules/service.py validate-modules:use-run-command-not-popen
++lib/ansible/modules/stat.py validate-modules:doc-default-does-not-match-spec # get_md5 is undocumented
+ lib/ansible/modules/stat.py validate-modules:parameter-invalid
++lib/ansible/modules/stat.py validate-modules:parameter-type-not-in-doc
++lib/ansible/modules/stat.py validate-modules:undocumented-parameter
+ lib/ansible/modules/systemd_service.py validate-modules:parameter-invalid
++lib/ansible/modules/systemd_service.py validate-modules:return-syntax-error
++lib/ansible/modules/sysvinit.py validate-modules:return-syntax-error
+ lib/ansible/modules/uri.py validate-modules:doc-required-mismatch
+ lib/ansible/modules/user.py validate-modules:doc-default-does-not-match-spec
+ lib/ansible/modules/user.py validate-modules:use-run-command-not-popen
++lib/ansible/modules/yum.py pylint:disallowed-name
+ lib/ansible/modules/yum.py validate-modules:parameter-invalid
+-lib/ansible/module_utils/basic.py pylint:unused-import # deferring resolution to allow enabling the rule now
++lib/ansible/modules/yum_repository.py validate-modules:doc-default-does-not-match-spec
++lib/ansible/modules/yum_repository.py validate-modules:parameter-type-not-in-doc
++lib/ansible/modules/yum_repository.py validate-modules:undocumented-parameter
+ lib/ansible/module_utils/compat/_selectors2.py future-import-boilerplate # ignore bundled
+ lib/ansible/module_utils/compat/_selectors2.py metaclass-boilerplate # ignore bundled
++lib/ansible/module_utils/compat/_selectors2.py pylint:disallowed-name
+ lib/ansible/module_utils/compat/selinux.py import-2.7!skip # pass/fail depends on presence of libselinux.so
++lib/ansible/module_utils/compat/selinux.py import-3.5!skip # pass/fail depends on presence of libselinux.so
+ lib/ansible/module_utils/compat/selinux.py import-3.6!skip # pass/fail depends on presence of libselinux.so
+ lib/ansible/module_utils/compat/selinux.py import-3.7!skip # pass/fail depends on presence of libselinux.so
+ lib/ansible/module_utils/compat/selinux.py import-3.8!skip # pass/fail depends on presence of libselinux.so
+ lib/ansible/module_utils/compat/selinux.py import-3.9!skip # pass/fail depends on presence of libselinux.so
+ lib/ansible/module_utils/compat/selinux.py import-3.10!skip # pass/fail depends on presence of libselinux.so
+ lib/ansible/module_utils/compat/selinux.py import-3.11!skip # pass/fail depends on presence of libselinux.so
+-lib/ansible/module_utils/compat/selinux.py import-3.12!skip # pass/fail depends on presence of libselinux.so
+ lib/ansible/module_utils/distro/_distro.py future-import-boilerplate # ignore bundled
+ lib/ansible/module_utils/distro/_distro.py metaclass-boilerplate # ignore bundled
+ lib/ansible/module_utils/distro/_distro.py no-assert
+-lib/ansible/module_utils/distro/_distro.py pep8!skip # bundled code we don't want to modify
+-lib/ansible/module_utils/distro/_distro.py pylint:undefined-variable # ignore bundled
+ lib/ansible/module_utils/distro/_distro.py pylint:using-constant-test # bundled code we don't want to modify
++lib/ansible/module_utils/distro/_distro.py pep8!skip # bundled code we don't want to modify
+ lib/ansible/module_utils/distro/__init__.py empty-init # breaks namespacing, bundled, do not override
+ lib/ansible/module_utils/facts/__init__.py empty-init # breaks namespacing, deprecate and eventually remove
++lib/ansible/module_utils/facts/network/linux.py pylint:disallowed-name
+ lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 pslint:PSUseApprovedVerbs
+ lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 pslint:PSProvideCommentHelp # need to agree on best format for comment location
+ lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 pslint:PSUseApprovedVerbs
+@@ -85,23 +93,33 @@ lib/ansible/module_utils/six/__init__.py
+ lib/ansible/module_utils/six/__init__.py no-dict-iterkeys
+ lib/ansible/module_utils/six/__init__.py no-dict-itervalues
+ lib/ansible/module_utils/six/__init__.py pylint:self-assigning-variable
+-lib/ansible/module_utils/six/__init__.py pylint:trailing-comma-tuple
+ lib/ansible/module_utils/six/__init__.py replace-urlopen
++lib/ansible/module_utils/urls.py pylint:arguments-renamed
++lib/ansible/module_utils/urls.py pylint:disallowed-name
+ lib/ansible/module_utils/urls.py replace-urlopen
++lib/ansible/parsing/vault/__init__.py pylint:disallowed-name
+ lib/ansible/parsing/yaml/objects.py pylint:arguments-renamed
++lib/ansible/playbook/base.py pylint:disallowed-name
+ lib/ansible/playbook/collectionsearch.py required-and-default-attributes # https://github.com/ansible/ansible/issues/61460
++lib/ansible/playbook/helpers.py pylint:disallowed-name
++lib/ansible/playbook/playbook_include.py pylint:arguments-renamed
+ lib/ansible/playbook/role/include.py pylint:arguments-renamed
+ lib/ansible/plugins/action/normal.py action-plugin-docs # default action plugin for modules without a dedicated action plugin
+ lib/ansible/plugins/cache/base.py ansible-doc!skip # not a plugin, but a stub for backwards compatibility
+ lib/ansible/plugins/callback/__init__.py pylint:arguments-renamed
+ lib/ansible/plugins/inventory/advanced_host_list.py pylint:arguments-renamed
+ lib/ansible/plugins/inventory/host_list.py pylint:arguments-renamed
++lib/ansible/plugins/lookup/random_choice.py pylint:arguments-renamed
++lib/ansible/plugins/lookup/sequence.py pylint:disallowed-name
++lib/ansible/plugins/shell/cmd.py pylint:arguments-renamed
++lib/ansible/plugins/strategy/__init__.py pylint:disallowed-name
++lib/ansible/plugins/strategy/linear.py pylint:disallowed-name
+ lib/ansible/utils/collection_loader/_collection_finder.py pylint:deprecated-class
+ lib/ansible/utils/collection_loader/_collection_meta.py pylint:deprecated-class
++lib/ansible/vars/hostvars.py pylint:disallowed-name
+ test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py pylint:ansible-bad-function # ignore, required for testing
+ test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import-from # ignore, required for testing
+ test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import # ignore, required for testing
+-test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/plugin_utils/check_pylint.py pylint:disallowed-name # ignore, required for testing
+ test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/modules/hello.py pylint:relative-beyond-top-level
+ test/integration/targets/ansible-test-units/ansible_collections/ns/col/plugins/modules/hello.py pylint:relative-beyond-top-level
+ test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py pylint:relative-beyond-top-level
+@@ -114,10 +132,8 @@ test/integration/targets/collections_rel
+ test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util2.py pylint:relative-beyond-top-level
+ test/integration/targets/fork_safe_stdio/vendored_pty.py pep8!skip # vendored code
+ test/integration/targets/gathering_facts/library/bogus_facts shebang
+-test/integration/targets/gathering_facts/library/dummy1 shebang
+ test/integration/targets/gathering_facts/library/facts_one shebang
+ test/integration/targets/gathering_facts/library/facts_two shebang
+-test/integration/targets/gathering_facts/library/slow shebang
+ test/integration/targets/incidental_win_reboot/templates/post_reboot.ps1 pslint!skip
+ test/integration/targets/json_cleanup/library/bad_json shebang
+ test/integration/targets/lookup_csvfile/files/crlf.csv line-endings
+@@ -127,6 +143,11 @@ test/integration/targets/module_preceden
+ test/integration/targets/module_precedence/roles_with_extension/foo/library/a.ini shebang
+ test/integration/targets/module_precedence/roles_with_extension/foo/library/ping.ini shebang
+ test/integration/targets/module_utils/library/test.py future-import-boilerplate # allow testing of Python 2.x implicit relative imports
++test/integration/targets/module_utils/module_utils/bar0/foo.py pylint:disallowed-name
++test/integration/targets/module_utils/module_utils/foo.py pylint:disallowed-name
++test/integration/targets/module_utils/module_utils/sub/bar/bar.py pylint:disallowed-name
++test/integration/targets/module_utils/module_utils/sub/bar/__init__.py pylint:disallowed-name
++test/integration/targets/module_utils/module_utils/yak/zebra/foo.py pylint:disallowed-name
+ test/integration/targets/old_style_modules_posix/library/helloworld.sh shebang
+ test/integration/targets/template/files/encoding_1252_utf-8.expected no-smart-quotes
+ test/integration/targets/template/files/encoding_1252_windows-1252.expected no-smart-quotes
+@@ -144,9 +165,28 @@ test/integration/targets/win_script/file
+ test/integration/targets/win_script/files/test_script_with_args.ps1 pslint:PSAvoidUsingWriteHost # Keep
+ test/integration/targets/win_script/files/test_script_with_splatting.ps1 pslint:PSAvoidUsingWriteHost # Keep
+ test/lib/ansible_test/_data/requirements/sanity.pslint.ps1 pslint:PSCustomUseLiteralPath # Uses wildcards on purpose
++test/lib/ansible_test/_util/target/setup/ConfigureRemotingForAnsible.ps1 pslint:PSCustomUseLiteralPath
++test/lib/ansible_test/_util/target/setup/requirements.py replace-urlopen
++test/support/integration/plugins/modules/timezone.py pylint:disallowed-name
++test/support/integration/plugins/module_utils/compat/ipaddress.py future-import-boilerplate
++test/support/integration/plugins/module_utils/compat/ipaddress.py metaclass-boilerplate
++test/support/integration/plugins/module_utils/compat/ipaddress.py no-unicode-literals
++test/support/integration/plugins/module_utils/network/common/utils.py future-import-boilerplate
++test/support/integration/plugins/module_utils/network/common/utils.py metaclass-boilerplate
++test/support/integration/plugins/module_utils/network/common/utils.py pylint:use-a-generator
++test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/netconf/netconf.py pylint:used-before-assignment
++test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/filter/network.py pylint:consider-using-dict-comprehension
+ test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/compat/ipaddress.py no-unicode-literals
++test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/compat/ipaddress.py pep8:E203
++test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py pylint:unnecessary-comprehension
++test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py pylint:use-a-generator
++test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/netconf/default.py pylint:unnecessary-comprehension
+ test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/cliconf/ios.py pylint:arguments-renamed
++test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py pep8:E501
+ test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/cliconf/vyos.py pylint:arguments-renamed
++test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py pep8:E231
++test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py pylint:disallowed-name
++test/support/windows-integration/plugins/action/win_copy.py pylint:used-before-assignment
+ test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/module_utils/WebRequest.psm1 pslint!skip
+ test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_uri.ps1 pslint!skip
+ test/support/windows-integration/plugins/modules/async_status.ps1 pslint!skip
+@@ -167,11 +207,19 @@ test/support/windows-integration/plugins
+ test/support/windows-integration/plugins/modules/win_user.ps1 pslint!skip
+ test/support/windows-integration/plugins/modules/win_wait_for.ps1 pslint!skip
+ test/support/windows-integration/plugins/modules/win_whoami.ps1 pslint!skip
++test/units/executor/test_play_iterator.py pylint:disallowed-name
++test/units/modules/test_apt.py pylint:disallowed-name
+ test/units/module_utils/basic/test_deprecate_warn.py pylint:ansible-deprecated-no-version
+ test/units/module_utils/basic/test_deprecate_warn.py pylint:ansible-deprecated-version
+-test/units/module_utils/common/warnings/test_deprecate.py pylint:ansible-deprecated-no-version # testing Display.deprecated call without a version or date
+-test/units/module_utils/common/warnings/test_deprecate.py pylint:ansible-deprecated-version # testing Deprecated version found in call to Display.deprecated or AnsibleModule.deprecate
++test/units/module_utils/basic/test_run_command.py pylint:disallowed-name
+ test/units/module_utils/urls/fixtures/multipart.txt line-endings # Fixture for HTTP tests that use CRLF
++test/units/module_utils/urls/test_fetch_url.py replace-urlopen
++test/units/module_utils/urls/test_gzip.py replace-urlopen
++test/units/module_utils/urls/test_Request.py replace-urlopen
++test/units/parsing/vault/test_vault.py pylint:disallowed-name
++test/units/playbook/role/test_role.py pylint:disallowed-name
++test/units/plugins/test_plugins.py pylint:disallowed-name
++test/units/template/test_templar.py pylint:disallowed-name
+ test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py pylint:relative-beyond-top-level
+ test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/modules/__init__.py empty-init # testing that collections don't need inits
+ test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/ansible/__init__.py empty-init # testing that collections don't need inits
+@@ -179,26 +227,3 @@ test/units/utils/collection_loader/fixtu
+ test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/__init__.py empty-init # testing that collections don't need inits
+ test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/testcoll/__init__.py empty-init # testing that collections don't need inits
+ test/units/utils/collection_loader/test_collection_loader.py pylint:undefined-variable # magic runtime local var splatting
+-.github/CONTRIBUTING.md pymarkdown:line-length
+-hacking/backport/README.md pymarkdown:no-bare-urls
+-hacking/ticket_stubs/bug_internal_api.md pymarkdown:no-bare-urls
+-hacking/ticket_stubs/bug_wrong_repo.md pymarkdown:no-bare-urls
+-hacking/ticket_stubs/collections.md pymarkdown:line-length
+-hacking/ticket_stubs/collections.md pymarkdown:no-bare-urls
+-hacking/ticket_stubs/guide_newbie_about_gh_and_contributing_to_ansible.md pymarkdown:no-bare-urls
+-hacking/ticket_stubs/no_thanks.md pymarkdown:line-length
+-hacking/ticket_stubs/no_thanks.md pymarkdown:no-bare-urls
+-hacking/ticket_stubs/pr_duplicate.md pymarkdown:no-bare-urls
+-hacking/ticket_stubs/pr_merged.md pymarkdown:no-bare-urls
+-hacking/ticket_stubs/proposal.md pymarkdown:no-bare-urls
+-hacking/ticket_stubs/question_not_bug.md pymarkdown:no-bare-urls
+-hacking/ticket_stubs/resolved.md pymarkdown:no-bare-urls
+-hacking/ticket_stubs/wider_discussion.md pymarkdown:no-bare-urls
+-lib/ansible/galaxy/data/apb/README.md pymarkdown:line-length
+-lib/ansible/galaxy/data/container/README.md pymarkdown:line-length
+-lib/ansible/galaxy/data/default/role/README.md pymarkdown:line-length
+-lib/ansible/galaxy/data/network/README.md pymarkdown:line-length
+-README.md pymarkdown:line-length
+-test/integration/targets/ansible-vault/invalid_format/README.md pymarkdown:no-bare-urls
+-test/support/README.md pymarkdown:no-bare-urls
+-test/units/cli/test_data/role_skeleton/README.md pymarkdown:line-length
+--- ansible-core-2.16.5.orig/test/support/README.md
++++ ansible-core-2.16.5/test/support/README.md
+@@ -1,4 +1,4 @@
+-# IMPORTANT
++# IMPORTANT!
+
+ Files under this directory are not actual plugins and modules used by Ansible
+ and as such should **not be modified**. They are used for testing purposes
+--- ansible-core-2.16.5.orig/test/support/integration/plugins/modules/sefcontext.py
++++ ansible-core-2.16.5/test/support/integration/plugins/modules/sefcontext.py
+@@ -105,11 +105,13 @@ RETURN = r'''
+ # Default return values
+ '''
+
++import os
++import subprocess
+ import traceback
+
+ from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+ from ansible.module_utils.common.respawn import has_respawned, probe_interpreters_for_module, respawn_module
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+ SELINUX_IMP_ERR = None
+ try:
+--- ansible-core-2.16.5.orig/test/support/integration/plugins/modules/timezone.py
++++ ansible-core-2.16.5/test/support/integration/plugins/modules/timezone.py
+@@ -121,7 +121,7 @@ class Timezone(object):
+ # running in the global zone where changing the timezone has no effect.
+ zonename_cmd = module.get_bin_path('zonename')
+ if zonename_cmd is not None:
+- (rc, stdout, stderr) = module.run_command(zonename_cmd)
++ (rc, stdout, _) = module.run_command(zonename_cmd)
+ if rc == 0 and stdout.strip() == 'global':
+ module.fail_json(msg='Adjusting timezone is not supported in Global Zone')
+
+@@ -731,7 +731,7 @@ class BSDTimezone(Timezone):
+ # Strategy 3:
+ # (If /etc/localtime is not symlinked)
+ # Check all files in /usr/share/zoneinfo and return first non-link match.
+- for dname, dirs, fnames in sorted(os.walk(zoneinfo_dir)):
++ for dname, _, fnames in sorted(os.walk(zoneinfo_dir)):
+ for fname in sorted(fnames):
+ zoneinfo_file = os.path.join(dname, fname)
+ if not os.path.islink(zoneinfo_file) and filecmp.cmp(zoneinfo_file, localtime_file):
+--- ansible-core-2.16.5.orig/test/support/integration/plugins/modules/zypper.py
++++ ansible-core-2.16.5/test/support/integration/plugins/modules/zypper.py
+@@ -41,7 +41,7 @@ options:
+ - Package name C(name) or package specifier or a list of either.
+ - Can include a version like C(name=1.0), C(name>3.4) or C(name<=2.7). If a version is given, C(oldpackage) is implied and zypper is allowed to
+ update the package within the version range given.
+- - You can also pass a url or a local path to an rpm file.
++ - You can also pass a url or a local path to a rpm file.
+ - When using state=latest, this can be '*', which updates all installed packages.
+ required: true
+ aliases: [ 'pkg' ]
+@@ -202,7 +202,8 @@ EXAMPLES = '''
+ import xml
+ import re
+ from xml.dom.minidom import parseString as parseXML
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils.six import iteritems
++from ansible.module_utils._text import to_native
+
+ # import module snippets
+ from ansible.module_utils.basic import AnsibleModule
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/net_get.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/net_get.py
+@@ -24,7 +24,7 @@ import uuid
+ import hashlib
+
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_text, to_bytes
++from ansible.module_utils._text import to_text, to_bytes
+ from ansible.module_utils.connection import Connection, ConnectionError
+ from ansible.plugins.action import ActionBase
+ from ansible.module_utils.six.moves.urllib.parse import urlsplit
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/net_put.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/net_put.py
+@@ -23,7 +23,7 @@ import uuid
+ import hashlib
+
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_text, to_bytes
++from ansible.module_utils._text import to_text, to_bytes
+ from ansible.module_utils.connection import Connection, ConnectionError
+ from ansible.plugins.action import ActionBase
+ from ansible.module_utils.six.moves.urllib.parse import urlsplit
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/network.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/action/network.py
+@@ -25,7 +25,7 @@ import time
+ import re
+
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_text, to_bytes
++from ansible.module_utils._text import to_text, to_bytes
+ from ansible.module_utils.six.moves.urllib.parse import urlsplit
+ from ansible.plugins.action.normal import ActionModule as _ActionModule
+ from ansible.utils.display import Display
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/connection/network_cli.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/connection/network_cli.py
+@@ -302,7 +302,7 @@ from functools import wraps
+ from io import BytesIO
+
+ from ansible.errors import AnsibleConnectionFailure, AnsibleError
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.module_utils.basic import missing_required_lib
+ from ansible.module_utils.six import PY3
+ from ansible.module_utils.six.moves import cPickle
+@@ -1310,6 +1310,7 @@ class Connection(NetworkConnectionBase):
+ remote host before triggering timeout exception
+ :return: None
+ """
++ """Fetch file over scp/sftp from remote device"""
+ ssh = self.ssh_type_conn._connect_uncached()
+ if self.ssh_type == "libssh":
+ self.ssh_type_conn.fetch_file(source, destination, proto=proto)
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/connection/persistent.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/connection/persistent.py
+@@ -29,7 +29,7 @@ options:
+ """
+ from ansible.executor.task_executor import start_connection
+ from ansible.plugins.connection import ConnectionBase
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.connection import Connection as SocketConnection
+ from ansible.utils.display import Display
+
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py
+@@ -29,7 +29,7 @@ import re
+ import hashlib
+
+ from ansible.module_utils.six.moves import zip
+-from ansible.module_utils.common.text.converters import to_bytes, to_native
++from ansible.module_utils._text import to_bytes, to_native
+
+ DEFAULT_COMMENT_TOKENS = ["#", "!", "/*", "*/", "echo"]
+
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py
+@@ -79,7 +79,7 @@ class FactsBase(object):
+ self._module.fail_json(
+ msg="Subset must be one of [%s], got %s"
+ % (
+- ", ".join(sorted(list(valid_subsets))),
++ ", ".join(sorted([item for item in valid_subsets])),
+ subset,
+ )
+ )
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py
+@@ -27,7 +27,7 @@
+ #
+ import sys
+
+-from ansible.module_utils.common.text.converters import to_text, to_bytes
++from ansible.module_utils._text import to_text, to_bytes
+ from ansible.module_utils.connection import Connection, ConnectionError
+
+ try:
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py
+@@ -28,7 +28,7 @@
+ import traceback
+ import json
+
+-from ansible.module_utils.common.text.converters import to_text, to_native
++from ansible.module_utils._text import to_text, to_native
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.basic import env_fallback
+ from ansible.module_utils.connection import Connection, ConnectionError
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py
+@@ -36,12 +36,26 @@ import json
+
+ from itertools import chain
+
+-from ansible.module_utils.common.text.converters import to_text, to_bytes
+-from ansible.module_utils.six.moves.collections_abc import Mapping
++from ansible.module_utils._text import to_text, to_bytes
++from ansible.module_utils.common._collections_compat import Mapping
+ from ansible.module_utils.six import iteritems, string_types
+ from ansible.module_utils import basic
+ from ansible.module_utils.parsing.convert_bool import boolean
+
++# Backwards compatibility for 3rd party modules
++# TODO(pabelanger): With move to ansible.netcommon, we should clean this code
++# up and have modules import directly themself.
++from ansible.module_utils.common.network import ( # noqa: F401
++ to_bits,
++ is_netmask,
++ is_masklen,
++ to_netmask,
++ to_masklen,
++ to_subnet,
++ to_ipv6_network,
++ VALID_MASKS,
++)
++
+ try:
+ from jinja2 import Environment, StrictUndefined
+ from jinja2.exceptions import UndefinedError
+@@ -593,7 +607,7 @@ def remove_empties(cfg_dict):
+ elif (
+ isinstance(val, list)
+ and val
+- and all(isinstance(x, dict) for x in val)
++ and all([isinstance(x, dict) for x in val])
+ ):
+ child_val = [remove_empties(x) for x in val]
+ if child_val:
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_config.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_config.py
+@@ -206,7 +206,7 @@ import json
+
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.connection import Connection
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+
+
+ def validate_args(module, device_operations):
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/cliconf/ios.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/cliconf/ios.py
+@@ -38,7 +38,7 @@ import json
+ from collections.abc import Mapping
+
+ from ansible.errors import AnsibleConnectionFailure
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.six import iteritems
+ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import (
+ NetworkConfig,
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/module_utils/network/ios/ios.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/module_utils/network/ios/ios.py
+@@ -27,7 +27,7 @@
+ #
+ import json
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.basic import env_fallback
+ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
+ to_list,
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_command.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_command.py
+@@ -134,7 +134,7 @@ failed_conditions:
+ """
+ import time
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import (
+ Conditional,
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py
+@@ -34,8 +34,7 @@ extends_documentation_fragment:
+ - cisco.ios.ios
+ notes:
+ - Tested against IOS 15.6
+-- Abbreviated commands are NOT idempotent,
+- see L(Network FAQ,../network/user_guide/faq.html#why-do-the-config-modules-always-return-changed-true-with-abbreviated-commands).
++- Abbreviated commands are NOT idempotent, see L(Network FAQ,../network/user_guide/faq.html#why-do-the-config-modules-always-return-changed-true-with-abbreviated-commands).
+ options:
+ lines:
+ description:
+@@ -327,7 +326,7 @@ time:
+ """
+ import json
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.connection import ConnectionError
+ from ansible_collections.cisco.ios.plugins.module_utils.network.ios.ios import (
+ run_commands,
+@@ -576,7 +575,6 @@ def main():
+ )
+
+ if running_config.sha1 != base_config.sha1:
+- before, after = "", ""
+ if module.params["diff_against"] == "intended":
+ before = running_config
+ after = base_config
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/terminal/ios.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/terminal/ios.py
+@@ -24,7 +24,7 @@ import json
+ import re
+
+ from ansible.errors import AnsibleConnectionFailure
+-from ansible.module_utils.common.text.converters import to_text, to_bytes
++from ansible.module_utils._text import to_text, to_bytes
+ from ansible.plugins.terminal import TerminalBase
+ from ansible.utils.display import Display
+
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/cliconf/vyos.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/cliconf/vyos.py
+@@ -37,7 +37,7 @@ import json
+ from collections.abc import Mapping
+
+ from ansible.errors import AnsibleConnectionFailure
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import (
+ NetworkConfig,
+ )
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/module_utils/network/vyos/vyos.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/module_utils/network/vyos/vyos.py
+@@ -27,7 +27,7 @@
+ #
+ import json
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.basic import env_fallback
+ from ansible.module_utils.connection import Connection, ConnectionError
+
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py
+@@ -133,7 +133,7 @@ warnings:
+ """
+ import time
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import (
+ Conditional,
+@@ -192,7 +192,7 @@ def main():
+ interval = module.params["interval"]
+ match = module.params["match"]
+
+- for dummy in range(retries):
++ for _ in range(retries):
+ responses = run_commands(module, commands)
+
+ for item in list(conditionals):
+@@ -213,7 +213,7 @@ def main():
+ module.fail_json(msg=msg, failed_conditions=failed_conditions)
+
+ result.update(
+- {"stdout": responses, "stdout_lines": list(to_lines(responses)), }
++ {"stdout": responses, "stdout_lines": list(to_lines(responses)),}
+ )
+
+ module.exit_json(**result)
+--- ansible-core-2.16.5.orig/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_config.py
++++ ansible-core-2.16.5/test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_config.py
+@@ -178,7 +178,7 @@ time:
+ """
+ import re
+
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.module_utils.basic import AnsibleModule
+ from ansible.module_utils.connection import ConnectionError
+ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import (
+--- ansible-core-2.16.5.orig/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/action/win_copy.py
++++ ansible-core-2.16.5/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/action/win_copy.py
+@@ -18,7 +18,7 @@ import zipfile
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleFileNotFound
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.plugins.action import ActionBase
+ from ansible.utils.hashing import checksum
+@@ -439,7 +439,7 @@ class ActionModule(ActionBase):
+ source_full = self._loader.get_real_file(source, decrypt=decrypt)
+ except AnsibleFileNotFound as e:
+ result['failed'] = True
+- result['msg'] = "could not find src=%s, %s" % (source, to_text(e))
++ result['msg'] = "could not find src=%s, %s" % (source_full, to_text(e))
+ return result
+
+ original_basename = os.path.basename(source)
+--- ansible-core-2.16.5.orig/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_stat.ps1
++++ ansible-core-2.16.5/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_stat.ps1
+@@ -95,7 +95,7 @@ If ($null -ne $info) {
+ isreadonly = ($attributes -contains "ReadOnly")
+ isreg = $false
+ isshared = $false
+- nlink = 1 # Number of links to the file (hard links), overridden below if islnk
++ nlink = 1 # Number of links to the file (hard links), overriden below if islnk
+ # lnk_target = islnk or isjunction Target of the symlink. Note that relative paths remain relative
+ # lnk_source = islnk os isjunction Target of the symlink normalized for the remote filesystem
+ hlnk_targets = @()
+--- ansible-core-2.16.5.orig/test/support/windows-integration/plugins/action/win_copy.py
++++ ansible-core-2.16.5/test/support/windows-integration/plugins/action/win_copy.py
+@@ -18,7 +18,7 @@ import zipfile
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleFileNotFound
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.parsing.convert_bool import boolean
+ from ansible.plugins.action import ActionBase
+ from ansible.utils.hashing import checksum
+@@ -439,7 +439,7 @@ class ActionModule(ActionBase):
+ source_full = self._loader.get_real_file(source, decrypt=decrypt)
+ except AnsibleFileNotFound as e:
+ result['failed'] = True
+- result['msg'] = "could not find src=%s, %s" % (source, to_text(e))
++ result['msg'] = "could not find src=%s, %s" % (source_full, to_text(e))
+ return result
+
+ original_basename = os.path.basename(source)
+--- ansible-core-2.16.5.orig/test/support/windows-integration/plugins/action/win_reboot.py
++++ ansible-core-2.16.5/test/support/windows-integration/plugins/action/win_reboot.py
+@@ -4,9 +4,10 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-from datetime import datetime, timezone
++from datetime import datetime
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.errors import AnsibleError
++from ansible.module_utils._text import to_native
+ from ansible.plugins.action import ActionBase
+ from ansible.plugins.action.reboot import ActionModule as RebootActionModule
+ from ansible.utils.display import Display
+@@ -64,7 +65,7 @@ class ActionModule(RebootActionModule, A
+
+ result = {}
+ reboot_result = self._low_level_execute_command(reboot_command, sudoable=self.DEFAULT_SUDOABLE)
+- result['start'] = datetime.now(timezone.utc)
++ result['start'] = datetime.utcnow()
+
+ # Test for "A system shutdown has already been scheduled. (1190)" and handle it gracefully
+ stdout = reboot_result['stdout']
+--- ansible-core-2.16.5.orig/test/support/windows-integration/plugins/modules/win_stat.ps1
++++ ansible-core-2.16.5/test/support/windows-integration/plugins/modules/win_stat.ps1
+@@ -95,7 +95,7 @@ If ($null -ne $info) {
+ isreadonly = ($attributes -contains "ReadOnly")
+ isreg = $false
+ isshared = $false
+- nlink = 1 # Number of links to the file (hard links), overridden below if islnk
++ nlink = 1 # Number of links to the file (hard links), overriden below if islnk
+ # lnk_target = islnk or isjunction Target of the symlink. Note that relative paths remain relative
+ # lnk_source = islnk os isjunction Target of the symlink normalized for the remote filesystem
+ hlnk_targets = @()
+--- ansible-core-2.16.5.orig/test/units/_vendor/test_vendor.py
++++ ansible-core-2.16.5/test/units/_vendor/test_vendor.py
+@@ -1,22 +1,27 @@
+ # (c) 2020 Ansible Project
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
++from __future__ import (absolute_import, division, print_function)
++__metaclass__ = type
++
+ import os
+ import pkgutil
+ import pytest
+ import sys
+
+-from unittest.mock import patch
++from unittest.mock import MagicMock, NonCallableMagicMock, patch
+
+
+ def reset_internal_vendor_package():
+ import ansible
+ ansible_vendor_path = os.path.join(os.path.dirname(ansible.__file__), '_vendor')
+
+- list(map(sys.path.remove, [path for path in sys.path if path == ansible_vendor_path]))
++ if ansible_vendor_path in sys.path:
++ sys.path.remove(ansible_vendor_path)
+
+ for pkg in ['ansible._vendor', 'ansible']:
+- sys.modules.pop(pkg, None)
++ if pkg in sys.modules:
++ del sys.modules[pkg]
+
+
+ def test_package_path_masking():
+@@ -45,10 +50,16 @@ def test_vendored(vendored_pkg_names=Non
+ import ansible
+ ansible_vendor_path = os.path.join(os.path.dirname(ansible.__file__), '_vendor')
+ assert sys.path[0] == ansible_vendor_path
++
++ if ansible_vendor_path in previous_path:
++ previous_path.remove(ansible_vendor_path)
++
+ assert sys.path[1:] == previous_path
+
+
+ def test_vendored_conflict():
+ with pytest.warns(UserWarning) as w:
++ import pkgutil
++ import sys
+ test_vendored(vendored_pkg_names=['sys', 'pkgutil']) # pass a real package we know is already loaded
+- assert any(list('pkgutil, sys' in str(msg.message) for msg in w)) # ensure both conflicting modules are listed and sorted
++ assert any('pkgutil, sys' in str(msg.message) for msg in w) # ensure both conflicting modules are listed and sorted
+--- ansible-core-2.16.5.orig/test/units/cli/arguments/test_optparse_helpers.py
++++ ansible-core-2.16.5/test/units/cli/arguments/test_optparse_helpers.py
+@@ -14,7 +14,10 @@ from ansible.cli.arguments import option
+ from ansible import __path__ as ansible_path
+ from ansible.release import __version__ as ansible_version
+
+-cpath = C.DEFAULT_MODULE_PATH
++if C.DEFAULT_MODULE_PATH is None:
++ cpath = u'Default w/o overrides'
++else:
++ cpath = C.DEFAULT_MODULE_PATH
+
+ FAKE_PROG = u'ansible-cli-test'
+ VERSION_OUTPUT = opt_help.version(prog=FAKE_PROG)
+--- ansible-core-2.16.5.orig/test/units/cli/galaxy/test_execute_list_collection.py
++++ ansible-core-2.16.5/test/units/cli/galaxy/test_execute_list_collection.py
+@@ -5,29 +5,37 @@
+ from __future__ import absolute_import, division, print_function
+ __metaclass__ = type
+
+-import pathlib
+-
+ import pytest
+
+-from ansible import constants as C
+ from ansible import context
+ from ansible.cli.galaxy import GalaxyCLI
+ from ansible.errors import AnsibleError, AnsibleOptionsError
+ from ansible.galaxy import collection
+ from ansible.galaxy.dependency_resolution.dataclasses import Requirement
+-from ansible.module_utils.common.text.converters import to_native
+-from ansible.plugins.loader import init_plugin_loader
++from ansible.module_utils._text import to_native
++
++
++def path_exists(path):
++ if to_native(path) == '/root/.ansible/collections/ansible_collections/sandwiches/ham':
++ return False
++ elif to_native(path) == '/usr/share/ansible/collections/ansible_collections/sandwiches/reuben':
++ return False
++ elif to_native(path) == 'nope':
++ return False
++ else:
++ return True
+
+
+ def isdir(path):
+ if to_native(path) == 'nope':
+ return False
+- return True
++ else:
++ return True
+
+
+ def cliargs(collections_paths=None, collection_name=None):
+ if collections_paths is None:
+- collections_paths = ['/root/.ansible/collections', '/usr/share/ansible/collections']
++ collections_paths = ['~/root/.ansible/collections', '/usr/share/ansible/collections']
+
+ context.CLIARGS._store = {
+ 'collections_path': collections_paths,
+@@ -38,61 +46,95 @@ def cliargs(collections_paths=None, coll
+
+
+ @pytest.fixture
+-def mock_from_path(mocker, monkeypatch):
+- collection_args = {
+- '/usr/share/ansible/collections/ansible_collections/sandwiches/pbj': (
++def mock_collection_objects(mocker):
++ mocker.patch('ansible.cli.galaxy.GalaxyCLI._resolve_path', side_effect=['/root/.ansible/collections', '/usr/share/ansible/collections'])
++ mocker.patch('ansible.cli.galaxy.validate_collection_path',
++ side_effect=['/root/.ansible/collections/ansible_collections', '/usr/share/ansible/collections/ansible_collections'])
++
++ collection_args_1 = (
++ (
+ 'sandwiches.pbj',
+- '1.0.0',
+- '/usr/share/ansible/collections/ansible_collections/sandwiches/pbj',
++ '1.5.0',
++ None,
+ 'dir',
+ None,
+ ),
+- '/usr/share/ansible/collections/ansible_collections/sandwiches/ham': (
+- 'sandwiches.ham',
+- '1.0.0',
+- '/usr/share/ansible/collections/ansible_collections/sandwiches/ham',
++ (
++ 'sandwiches.reuben',
++ '2.5.0',
++ None,
+ 'dir',
+ None,
+ ),
+- '/root/.ansible/collections/ansible_collections/sandwiches/pbj': (
++ )
++
++ collection_args_2 = (
++ (
+ 'sandwiches.pbj',
+- '1.5.0',
+- '/root/.ansible/collections/ansible_collections/sandwiches/pbj',
++ '1.0.0',
++ None,
+ 'dir',
+ None,
+ ),
+- '/root/.ansible/collections/ansible_collections/sandwiches/reuben': (
+- 'sandwiches.reuben',
+- '2.5.0',
+- '/root/.ansible/collections/ansible_collections/sandwiches/reuben',
++ (
++ 'sandwiches.ham',
++ '1.0.0',
++ None,
+ 'dir',
+ None,
+ ),
+- }
++ )
+
+- def dispatch_requirement(path, am):
+- return Requirement(*collection_args[to_native(path)])
++ collections_path_1 = [Requirement(*cargs) for cargs in collection_args_1]
++ collections_path_2 = [Requirement(*cargs) for cargs in collection_args_2]
+
+- files_mock = mocker.MagicMock()
+- mocker.patch('ansible.galaxy.collection.files', return_value=files_mock)
+- files_mock.glob.return_value = []
++ mocker.patch('ansible.cli.galaxy.find_existing_collections', side_effect=[collections_path_1, collections_path_2])
+
+- mocker.patch.object(pathlib.Path, 'is_dir', return_value=True)
+- for path, args in collection_args.items():
+- files_mock.glob.return_value.append(pathlib.Path(args[2]))
+
+- mocker.patch('ansible.galaxy.collection.Candidate.from_dir_path_as_unknown', side_effect=dispatch_requirement)
++@pytest.fixture
++def mock_from_path(mocker):
++ def _from_path(collection_name='pbj'):
++ collection_args = {
++ 'sandwiches.pbj': (
++ (
++ 'sandwiches.pbj',
++ '1.5.0',
++ None,
++ 'dir',
++ None,
++ ),
++ (
++ 'sandwiches.pbj',
++ '1.0.0',
++ None,
++ 'dir',
++ None,
++ ),
++ ),
++ 'sandwiches.ham': (
++ (
++ 'sandwiches.ham',
++ '1.0.0',
++ None,
++ 'dir',
++ None,
++ ),
++ ),
++ }
++
++ from_path_objects = [Requirement(*args) for args in collection_args[collection_name]]
++ mocker.patch('ansible.cli.galaxy.Requirement.from_dir_path_as_unknown', side_effect=from_path_objects)
+
+- monkeypatch.setattr(C, 'COLLECTIONS_PATHS', ['/root/.ansible/collections', '/usr/share/ansible/collections'])
++ return _from_path
+
+
+-def test_execute_list_collection_all(mocker, capsys, mock_from_path, tmp_path_factory):
++def test_execute_list_collection_all(mocker, capsys, mock_collection_objects, tmp_path_factory):
+ """Test listing all collections from multiple paths"""
+
+ cliargs()
+- init_plugin_loader()
+
+ mocker.patch('os.path.exists', return_value=True)
++ mocker.patch('os.path.isdir', return_value=True)
+ gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list'])
+ tmp_path = tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections')
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
+@@ -110,20 +152,21 @@ def test_execute_list_collection_all(moc
+ assert out_lines[5] == 'sandwiches.reuben 2.5.0 '
+ assert out_lines[6] == ''
+ assert out_lines[7] == '# /usr/share/ansible/collections/ansible_collections'
+- assert out_lines[8] == 'Collection Version'
+- assert out_lines[9] == '----------------- -------'
+- assert out_lines[10] == 'sandwiches.ham 1.0.0 '
+- assert out_lines[11] == 'sandwiches.pbj 1.0.0 '
++ assert out_lines[8] == 'Collection Version'
++ assert out_lines[9] == '-------------- -------'
++ assert out_lines[10] == 'sandwiches.ham 1.0.0 '
++ assert out_lines[11] == 'sandwiches.pbj 1.0.0 '
+
+
+-def test_execute_list_collection_specific(mocker, capsys, mock_from_path, tmp_path_factory):
++def test_execute_list_collection_specific(mocker, capsys, mock_collection_objects, mock_from_path, tmp_path_factory):
+ """Test listing a specific collection"""
+
+ collection_name = 'sandwiches.ham'
++ mock_from_path(collection_name)
+
+ cliargs(collection_name=collection_name)
+- init_plugin_loader()
+-
++ mocker.patch('os.path.exists', path_exists)
++ mocker.patch('os.path.isdir', return_value=True)
+ mocker.patch('ansible.galaxy.collection.validate_collection_name', collection_name)
+ mocker.patch('ansible.cli.galaxy._get_collection_widths', return_value=(14, 5))
+
+@@ -143,14 +186,15 @@ def test_execute_list_collection_specifi
+ assert out_lines[4] == 'sandwiches.ham 1.0.0 '
+
+
+-def test_execute_list_collection_specific_duplicate(mocker, capsys, mock_from_path, tmp_path_factory):
++def test_execute_list_collection_specific_duplicate(mocker, capsys, mock_collection_objects, mock_from_path, tmp_path_factory):
+ """Test listing a specific collection that exists at multiple paths"""
+
+ collection_name = 'sandwiches.pbj'
++ mock_from_path(collection_name)
+
+ cliargs(collection_name=collection_name)
+- init_plugin_loader()
+-
++ mocker.patch('os.path.exists', path_exists)
++ mocker.patch('os.path.isdir', return_value=True)
+ mocker.patch('ansible.galaxy.collection.validate_collection_name', collection_name)
+
+ gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list', collection_name])
+@@ -177,8 +221,6 @@ def test_execute_list_collection_specifi
+ def test_execute_list_collection_specific_invalid_fqcn(mocker, tmp_path_factory):
+ """Test an invalid fully qualified collection name (FQCN)"""
+
+- init_plugin_loader()
+-
+ collection_name = 'no.good.name'
+
+ cliargs(collection_name=collection_name)
+@@ -196,7 +238,6 @@ def test_execute_list_collection_no_vali
+ """Test listing collections when no valid paths are given"""
+
+ cliargs()
+- init_plugin_loader()
+
+ mocker.patch('os.path.exists', return_value=True)
+ mocker.patch('os.path.isdir', return_value=False)
+@@ -216,14 +257,13 @@ def test_execute_list_collection_no_vali
+ assert 'exists, but it\nis not a directory.' in err
+
+
+-def test_execute_list_collection_one_invalid_path(mocker, capsys, mock_from_path, tmp_path_factory):
++def test_execute_list_collection_one_invalid_path(mocker, capsys, mock_collection_objects, tmp_path_factory):
+ """Test listing all collections when one invalid path is given"""
+
+- cliargs(collections_paths=['nope'])
+- init_plugin_loader()
+-
++ cliargs()
+ mocker.patch('os.path.exists', return_value=True)
+ mocker.patch('os.path.isdir', isdir)
++ mocker.patch('ansible.cli.galaxy.GalaxyCLI._resolve_path', side_effect=['/root/.ansible/collections', 'nope'])
+ mocker.patch('ansible.utils.color.ANSIBLE_COLOR', False)
+
+ gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list', '-p', 'nope'])
+--- ansible-core-2.16.5.orig/test/units/cli/test_adhoc.py
++++ ansible-core-2.16.5/test/units/cli/test_adhoc.py
+@@ -93,15 +93,19 @@ def test_run_no_extra_vars():
+ assert exec_info.value.code == 2
+
+
+-def test_ansible_version(capsys):
++def test_ansible_version(capsys, mocker):
+ adhoc_cli = AdHocCLI(args=['/bin/ansible', '--version'])
+ with pytest.raises(SystemExit):
+ adhoc_cli.run()
+ version = capsys.readouterr()
+- version_lines = version.out.splitlines()
++ try:
++ version_lines = version.out.splitlines()
++ except AttributeError:
++ # Python 2.6 does return a named tuple, so get the first item
++ version_lines = version[0].splitlines()
+
+ assert len(version_lines) == 9, 'Incorrect number of lines in "ansible --version" output'
+- assert re.match(r'ansible \[core [0-9.a-z]+\]', version_lines[0]), 'Incorrect ansible version line in "ansible --version" output'
++ assert re.match(r'ansible \[core [0-9.a-z]+\]$', version_lines[0]), 'Incorrect ansible version line in "ansible --version" output'
+ assert re.match(' config file = .*$', version_lines[1]), 'Incorrect config file line in "ansible --version" output'
+ assert re.match(' configured module search path = .*$', version_lines[2]), 'Incorrect module search path in "ansible --version" output'
+ assert re.match(' ansible python module location = .*$', version_lines[3]), 'Incorrect python module location in "ansible --version" output'
+--- ansible-core-2.16.5.orig/test/units/cli/test_data/collection_skeleton/README.md
++++ ansible-core-2.16.5/test/units/cli/test_data/collection_skeleton/README.md
+@@ -1 +1 @@
+-A readme
++A readme
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/units/cli/test_data/collection_skeleton/docs/My Collection.md
++++ ansible-core-2.16.5/test/units/cli/test_data/collection_skeleton/docs/My Collection.md
+@@ -1 +1 @@
+-Welcome to my test collection doc for {{ namespace }}.
++Welcome to my test collection doc for {{ namespace }}.
+\ No newline at end of file
+--- ansible-core-2.16.5.orig/test/units/cli/test_doc.py
++++ ansible-core-2.16.5/test/units/cli/test_doc.py
+@@ -5,7 +5,7 @@ __metaclass__ = type
+ import pytest
+
+ from ansible.cli.doc import DocCLI, RoleMixin
+-from ansible.plugins.loader import module_loader, init_plugin_loader
++from ansible.plugins.loader import module_loader
+
+
+ TTY_IFY_DATA = {
+@@ -118,7 +118,6 @@ def test_builtin_modules_list():
+ args = ['ansible-doc', '-l', 'ansible.builtin', '-t', 'module']
+ obj = DocCLI(args=args)
+ obj.parse()
+- init_plugin_loader()
+ result = obj._list_plugins('module', module_loader)
+ assert len(result) > 0
+
+--- ansible-core-2.16.5.orig/test/units/cli/test_galaxy.py
++++ ansible-core-2.16.5/test/units/cli/test_galaxy.py
+@@ -20,8 +20,6 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-import contextlib
+-
+ import ansible
+ from io import BytesIO
+ import json
+@@ -39,7 +37,7 @@ from ansible.cli.galaxy import GalaxyCLI
+ from ansible.galaxy import collection
+ from ansible.galaxy.api import GalaxyAPI
+ from ansible.errors import AnsibleError
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.utils import context_objects as co
+ from ansible.utils.display import Display
+ from units.compat import unittest
+@@ -62,7 +60,8 @@ class TestGalaxy(unittest.TestCase):
+ cls.temp_dir = tempfile.mkdtemp(prefix='ansible-test_galaxy-')
+ os.chdir(cls.temp_dir)
+
+- shutil.rmtree("./delete_me", ignore_errors=True)
++ if os.path.exists("./delete_me"):
++ shutil.rmtree("./delete_me")
+
+ # creating framework for a role
+ gc = GalaxyCLI(args=["ansible-galaxy", "init", "--offline", "delete_me"])
+@@ -72,7 +71,8 @@ class TestGalaxy(unittest.TestCase):
+
+ # making a temp dir for role installation
+ cls.role_path = os.path.join(tempfile.mkdtemp(), "roles")
+- os.makedirs(cls.role_path)
++ if not os.path.isdir(cls.role_path):
++ os.makedirs(cls.role_path)
+
+ # creating a tar file name for class data
+ cls.role_tar = './delete_me.tar.gz'
+@@ -80,29 +80,37 @@ class TestGalaxy(unittest.TestCase):
+
+ # creating a temp file with installation requirements
+ cls.role_req = './delete_me_requirements.yml'
+- with open(cls.role_req, "w") as fd:
+- fd.write("- 'src': '%s'\n 'name': '%s'\n 'path': '%s'" % (cls.role_tar, cls.role_name, cls.role_path))
++ fd = open(cls.role_req, "w")
++ fd.write("- 'src': '%s'\n 'name': '%s'\n 'path': '%s'" % (cls.role_tar, cls.role_name, cls.role_path))
++ fd.close()
+
+ @classmethod
+ def makeTar(cls, output_file, source_dir):
+ ''' used for making a tarfile from a role directory '''
+ # adding directory into a tar file
+- with tarfile.open(output_file, "w:gz") as tar:
++ try:
++ tar = tarfile.open(output_file, "w:gz")
+ tar.add(source_dir, arcname=os.path.basename(source_dir))
++ except AttributeError: # tarfile obj. has no attribute __exit__ prior to python 2. 7
++ pass
++ finally: # ensuring closure of tarfile obj
++ tar.close()
+
+ @classmethod
+ def tearDownClass(cls):
+ '''After tests are finished removes things created in setUpClass'''
+ # deleting the temp role directory
+- shutil.rmtree(cls.role_dir, ignore_errors=True)
+- with contextlib.suppress(FileNotFoundError):
++ if os.path.exists(cls.role_dir):
++ shutil.rmtree(cls.role_dir)
++ if os.path.exists(cls.role_req):
+ os.remove(cls.role_req)
+- with contextlib.suppress(FileNotFoundError):
++ if os.path.exists(cls.role_tar):
+ os.remove(cls.role_tar)
+- shutil.rmtree(cls.role_path, ignore_errors=True)
++ if os.path.isdir(cls.role_path):
++ shutil.rmtree(cls.role_path)
+
+ os.chdir('/')
+- shutil.rmtree(cls.temp_dir, ignore_errors=True)
++ shutil.rmtree(cls.temp_dir)
+
+ def setUp(self):
+ # Reset the stored command line args
+@@ -129,7 +137,8 @@ class TestGalaxy(unittest.TestCase):
+ role_info = {'name': 'some_role_name',
+ 'galaxy_info': galaxy_info}
+ display_result = gc._display_role_info(role_info)
+- self.assertNotEqual(display_result.find('\n\tgalaxy_info:'), -1, 'Expected galaxy_info to be indented once')
++ if display_result.find('\n\tgalaxy_info:') == -1:
++ self.fail('Expected galaxy_info to be indented once')
+
+ def test_run(self):
+ ''' verifies that the GalaxyCLI object's api is created and that execute() is called. '''
+@@ -167,9 +176,7 @@ class TestGalaxy(unittest.TestCase):
+ with patch.object(ansible.utils.display.Display, "display", return_value=None) as mocked_display:
+ # testing that error expected is raised
+ self.assertRaises(AnsibleError, gc.run)
+- assert mocked_display.call_count == 2
+- assert mocked_display.mock_calls[0].args[0] == "Starting galaxy role install process"
+- assert "fake_role_name was NOT installed successfully" in mocked_display.mock_calls[1].args[0]
++ self.assertTrue(mocked_display.called_once_with("- downloading role 'fake_role_name', owned by "))
+
+ def test_exit_without_ignore_with_flag(self):
+ ''' tests that GalaxyCLI exits without the error specified if the --ignore-errors flag is used '''
+@@ -177,9 +184,7 @@ class TestGalaxy(unittest.TestCase):
+ gc = GalaxyCLI(args=["ansible-galaxy", "install", "--server=None", "fake_role_name", "--ignore-errors"])
+ with patch.object(ansible.utils.display.Display, "display", return_value=None) as mocked_display:
+ gc.run()
+- assert mocked_display.call_count == 2
+- assert mocked_display.mock_calls[0].args[0] == "Starting galaxy role install process"
+- assert "fake_role_name was NOT installed successfully" in mocked_display.mock_calls[1].args[0]
++ self.assertTrue(mocked_display.called_once_with("- downloading role 'fake_role_name', owned by "))
+
+ def test_parse_no_action(self):
+ ''' testing the options parser when no action is given '''
+@@ -272,6 +277,8 @@ class ValidRoleTests(object):
+
+ # Make temp directory for testing
+ cls.test_dir = tempfile.mkdtemp()
++ if not os.path.isdir(cls.test_dir):
++ os.makedirs(cls.test_dir)
+
+ cls.role_dir = os.path.join(cls.test_dir, role_name)
+ cls.role_name = role_name
+@@ -290,8 +297,9 @@ class ValidRoleTests(object):
+ cls.role_skeleton_path = gc.galaxy.default_role_skeleton_path
+
+ @classmethod
+- def tearDownRole(cls):
+- shutil.rmtree(cls.test_dir, ignore_errors=True)
++ def tearDownClass(cls):
++ if os.path.isdir(cls.test_dir):
++ shutil.rmtree(cls.test_dir)
+
+ def test_metadata(self):
+ with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf:
+@@ -341,10 +349,6 @@ class TestGalaxyInitDefault(unittest.Tes
+ def setUpClass(cls):
+ cls.setUpRole(role_name='delete_me')
+
+- @classmethod
+- def tearDownClass(cls):
+- cls.tearDownRole()
+-
+ def test_metadata_contents(self):
+ with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf:
+ metadata = yaml.safe_load(mf)
+@@ -357,10 +361,6 @@ class TestGalaxyInitAPB(unittest.TestCas
+ def setUpClass(cls):
+ cls.setUpRole('delete_me_apb', galaxy_args=['--type=apb'])
+
+- @classmethod
+- def tearDownClass(cls):
+- cls.tearDownRole()
+-
+ def test_metadata_apb_tag(self):
+ with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf:
+ metadata = yaml.safe_load(mf)
+@@ -391,10 +391,6 @@ class TestGalaxyInitContainer(unittest.T
+ def setUpClass(cls):
+ cls.setUpRole('delete_me_container', galaxy_args=['--type=container'])
+
+- @classmethod
+- def tearDownClass(cls):
+- cls.tearDownRole()
+-
+ def test_metadata_container_tag(self):
+ with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf:
+ metadata = yaml.safe_load(mf)
+@@ -426,10 +422,6 @@ class TestGalaxyInitSkeleton(unittest.Te
+ role_skeleton_path = os.path.join(os.path.split(__file__)[0], 'test_data', 'role_skeleton')
+ cls.setUpRole('delete_me_skeleton', skeleton_path=role_skeleton_path, use_explicit_type=True)
+
+- @classmethod
+- def tearDownClass(cls):
+- cls.tearDownRole()
+-
+ def test_empty_files_dir(self):
+ files_dir = os.path.join(self.role_dir, 'files')
+ self.assertTrue(os.path.isdir(files_dir))
+@@ -771,20 +763,6 @@ def test_collection_install_with_names(c
+ assert mock_install.call_args[0][6] is False # force_deps
+
+
+-def test_collection_install_with_invalid_requirements_format(collection_install):
+- output_dir = collection_install[2]
+-
+- requirements_file = os.path.join(output_dir, 'requirements.yml')
+- with open(requirements_file, 'wb') as req_obj:
+- req_obj.write(b'"invalid"')
+-
+- galaxy_args = ['ansible-galaxy', 'collection', 'install', '--requirements-file', requirements_file,
+- '--collections-path', output_dir]
+-
+- with pytest.raises(AnsibleError, match="Expecting requirements yaml to be a list or dictionary but got str"):
+- GalaxyCLI(args=galaxy_args).run()
+-
+-
+ def test_collection_install_with_requirements_file(collection_install):
+ mock_install, mock_warning, output_dir = collection_install
+
+@@ -1264,7 +1242,12 @@ def test_install_implicit_role_with_coll
+ assert len(mock_role_install.call_args[0][0]) == 1
+ assert str(mock_role_install.call_args[0][0][0]) == 'namespace.name'
+
+- assert not any(list('contains collections which will be ignored' in mock_call[1][0] for mock_call in mock_display.mock_calls))
++ found = False
++ for mock_call in mock_display.mock_calls:
++ if 'contains collections which will be ignored' in mock_call[1][0]:
++ found = True
++ break
++ assert not found
+
+
+ @pytest.mark.parametrize('requirements_file', ['''
+@@ -1291,7 +1274,12 @@ def test_install_explicit_role_with_coll
+ assert len(mock_role_install.call_args[0][0]) == 1
+ assert str(mock_role_install.call_args[0][0][0]) == 'namespace.name'
+
+- assert any(list('contains collections which will be ignored' in mock_call[1][0] for mock_call in mock_display.mock_calls))
++ found = False
++ for mock_call in mock_display.mock_calls:
++ if 'contains collections which will be ignored' in mock_call[1][0]:
++ found = True
++ break
++ assert found
+
+
+ @pytest.mark.parametrize('requirements_file', ['''
+@@ -1318,7 +1306,12 @@ def test_install_role_with_collections_a
+ assert len(mock_role_install.call_args[0][0]) == 1
+ assert str(mock_role_install.call_args[0][0][0]) == 'namespace.name'
+
+- assert any(list('contains collections which will be ignored' in mock_call[1][0] for mock_call in mock_display.mock_calls))
++ found = False
++ for mock_call in mock_display.mock_calls:
++ if 'contains collections which will be ignored' in mock_call[1][0]:
++ found = True
++ break
++ assert found
+
+
+ @pytest.mark.parametrize('requirements_file', ['''
+@@ -1345,4 +1338,9 @@ def test_install_collection_with_roles(r
+
+ assert mock_role_install.call_count == 0
+
+- assert any(list('contains roles which will be ignored' in mock_call[1][0] for mock_call in mock_display.mock_calls))
++ found = False
++ for mock_call in mock_display.mock_calls:
++ if 'contains roles which will be ignored' in mock_call[1][0]:
++ found = True
++ break
++ assert found
+--- ansible-core-2.16.5.orig/test/units/cli/test_vault.py
++++ ansible-core-2.16.5/test/units/cli/test_vault.py
+@@ -29,7 +29,7 @@ from units.mock.vault_helper import Text
+
+ from ansible import context, errors
+ from ansible.cli.vault import VaultCLI
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.utils import context_objects as co
+
+
+@@ -171,28 +171,7 @@ class TestVaultCli(unittest.TestCase):
+ mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+ cli = VaultCLI(args=['ansible-vault', 'create', '/dev/null/foo'])
+ cli.parse()
+- self.assertRaisesRegex(errors.AnsibleOptionsError,
+- "not a tty, editor cannot be opened",
+- cli.run)
+-
+- @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+- @patch('ansible.cli.vault.VaultEditor')
+- def test_create_skip_tty_check(self, mock_vault_editor, mock_setup_vault_secrets):
+- mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+- cli = VaultCLI(args=['ansible-vault', 'create', '--skip-tty-check', '/dev/null/foo'])
+- cli.parse()
+- cli.run()
+-
+- @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+- @patch('ansible.cli.vault.VaultEditor')
+- def test_create_with_tty(self, mock_vault_editor, mock_setup_vault_secrets):
+- mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+- self.tty_stdout_patcher = patch('ansible.cli.sys.stdout.isatty', return_value=True)
+- self.tty_stdout_patcher.start()
+- cli = VaultCLI(args=['ansible-vault', 'create', '/dev/null/foo'])
+- cli.parse()
+ cli.run()
+- self.tty_stdout_patcher.stop()
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ @patch('ansible.cli.vault.VaultEditor')
+--- ansible-core-2.16.5.orig/test/units/compat/mock.py
++++ ansible-core-2.16.5/test/units/compat/mock.py
+@@ -6,7 +6,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ try:
+- from unittest.mock import ( # pylint: disable=unused-import
++ from unittest.mock import (
+ call,
+ patch,
+ mock_open,
+--- ansible-core-2.16.5.orig/test/units/config/manager/test_find_ini_config_file.py
++++ ansible-core-2.16.5/test/units/config/manager/test_find_ini_config_file.py
+@@ -13,7 +13,7 @@ import stat
+ import pytest
+
+ from ansible.config.manager import find_ini_config_file
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+
+ real_exists = os.path.exists
+ real_isdir = os.path.isdir
+@@ -28,17 +28,22 @@ cfg_in_homedir = os.path.expanduser('~/.
+
+
+ @pytest.fixture
+-def setup_env(request, monkeypatch):
++def setup_env(request):
+ cur_config = os.environ.get('ANSIBLE_CONFIG', None)
+ cfg_path = request.param[0]
+
+ if cfg_path is None and cur_config:
+- monkeypatch.delenv('ANSIBLE_CONFIG')
++ del os.environ['ANSIBLE_CONFIG']
+ else:
+- monkeypatch.setenv('ANSIBLE_CONFIG', request.param[0])
++ os.environ['ANSIBLE_CONFIG'] = request.param[0]
+
+ yield
+
++ if cur_config is None and cfg_path:
++ del os.environ['ANSIBLE_CONFIG']
++ else:
++ os.environ['ANSIBLE_CONFIG'] = cur_config
++
+
+ @pytest.fixture
+ def setup_existing_files(request, monkeypatch):
+@@ -49,8 +54,10 @@ def setup_existing_files(request, monkey
+ return False
+
+ def _os_access(path, access):
+- assert to_text(path) in (request.param[0])
+- return True
++ if to_text(path) in (request.param[0]):
++ return True
++ else:
++ return False
+
+ # Enable user and system dirs so that we know cwd takes precedence
+ monkeypatch.setattr("os.path.exists", _os_path_exists)
+@@ -155,11 +162,13 @@ class TestFindIniFile:
+ real_stat = os.stat
+
+ def _os_stat(path):
+- assert path == working_dir
+- from posix import stat_result
+- stat_info = list(real_stat(path))
+- stat_info[stat.ST_MODE] |= stat.S_IWOTH
+- return stat_result(stat_info)
++ if path == working_dir:
++ from posix import stat_result
++ stat_info = list(real_stat(path))
++ stat_info[stat.ST_MODE] |= stat.S_IWOTH
++ return stat_result(stat_info)
++ else:
++ return real_stat(path)
+
+ monkeypatch.setattr('os.stat', _os_stat)
+
+@@ -178,11 +187,13 @@ class TestFindIniFile:
+ real_stat = os.stat
+
+ def _os_stat(path):
+- assert path == working_dir
+- from posix import stat_result
+- stat_info = list(real_stat(path))
+- stat_info[stat.ST_MODE] |= stat.S_IWOTH
+- return stat_result(stat_info)
++ if path == working_dir:
++ from posix import stat_result
++ stat_info = list(real_stat(path))
++ stat_info[stat.ST_MODE] |= stat.S_IWOTH
++ return stat_result(stat_info)
++ else:
++ return real_stat(path)
+
+ monkeypatch.setattr('os.stat', _os_stat)
+
+@@ -204,14 +215,14 @@ class TestFindIniFile:
+ real_stat = os.stat
+
+ def _os_stat(path):
+- if path != working_dir:
++ if path == working_dir:
++ from posix import stat_result
++ stat_info = list(real_stat(path))
++ stat_info[stat.ST_MODE] |= stat.S_IWOTH
++ return stat_result(stat_info)
++ else:
+ return real_stat(path)
+
+- from posix import stat_result
+- stat_info = list(real_stat(path))
+- stat_info[stat.ST_MODE] |= stat.S_IWOTH
+- return stat_result(stat_info)
+-
+ monkeypatch.setattr('os.stat', _os_stat)
+
+ warnings = set()
+@@ -229,11 +240,13 @@ class TestFindIniFile:
+ real_stat = os.stat
+
+ def _os_stat(path):
+- assert path == working_dir
+- from posix import stat_result
+- stat_info = list(real_stat(path))
+- stat_info[stat.ST_MODE] |= stat.S_IWOTH
+- return stat_result(stat_info)
++ if path == working_dir:
++ from posix import stat_result
++ stat_info = list(real_stat(path))
++ stat_info[stat.ST_MODE] |= stat.S_IWOTH
++ return stat_result(stat_info)
++ else:
++ return real_stat(path)
+
+ monkeypatch.setattr('os.stat', _os_stat)
+
+--- ansible-core-2.16.5.orig/test/units/config/test_manager.py
++++ ansible-core-2.16.5/test/units/config/test_manager.py
+@@ -10,7 +10,7 @@ import os
+ import os.path
+ import pytest
+
+-from ansible.config.manager import ConfigManager, ensure_type, resolve_path, get_config_type
++from ansible.config.manager import ConfigManager, Setting, ensure_type, resolve_path, get_config_type
+ from ansible.errors import AnsibleOptionsError, AnsibleError
+ from ansible.module_utils.six import integer_types, string_types
+ from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
+@@ -18,7 +18,6 @@ from ansible.parsing.yaml.objects import
+ curdir = os.path.dirname(__file__)
+ cfg_file = os.path.join(curdir, 'test.cfg')
+ cfg_file2 = os.path.join(curdir, 'test2.cfg')
+-cfg_file3 = os.path.join(curdir, 'test3.cfg')
+
+ ensure_test_data = [
+ ('a,b', 'list', list),
+@@ -66,15 +65,6 @@ ensure_test_data = [
+ ('None', 'none', type(None))
+ ]
+
+-ensure_unquoting_test_data = [
+- ('"value"', '"value"', 'str', 'env'),
+- ('"value"', '"value"', 'str', 'yaml'),
+- ('"value"', 'value', 'str', 'ini'),
+- ('\'value\'', 'value', 'str', 'ini'),
+- ('\'\'value\'\'', '\'value\'', 'str', 'ini'),
+- ('""value""', '"value"', 'str', 'ini')
+-]
+-
+
+ class TestConfigManager:
+ @classmethod
+@@ -89,11 +79,6 @@ class TestConfigManager:
+ def test_ensure_type(self, value, expected_type, python_type):
+ assert isinstance(ensure_type(value, expected_type), python_type)
+
+- @pytest.mark.parametrize("value, expected_value, value_type, origin", ensure_unquoting_test_data)
+- def test_ensure_type_unquoting(self, value, expected_value, value_type, origin):
+- actual_value = ensure_type(value, value_type, origin)
+- assert actual_value == expected_value
+-
+ def test_resolve_path(self):
+ assert os.path.join(curdir, 'test.yml') == resolve_path('./test.yml', cfg_file)
+
+@@ -157,16 +142,3 @@ class TestConfigManager:
+
+ actual_value = ensure_type(vault_var, value_type)
+ assert actual_value == "vault text"
+-
+-
+-@pytest.mark.parametrize(("key", "expected_value"), (
+- ("COLOR_UNREACHABLE", "bright red"),
+- ("COLOR_VERBOSE", "rgb013"),
+- ("COLOR_DEBUG", "gray10")))
+-def test_256color_support(key, expected_value):
+- # GIVEN: a config file containing 256-color values with default definitions
+- manager = ConfigManager(cfg_file3)
+- # WHEN: get config values
+- actual_value = manager.get_config_value(key)
+- # THEN: no error
+- assert actual_value == expected_value
+--- ansible-core-2.16.5.orig/test/units/executor/module_common/test_modify_module.py
++++ ansible-core-2.16.5/test/units/executor/module_common/test_modify_module.py
+@@ -8,6 +8,9 @@ __metaclass__ = type
+ import pytest
+
+ from ansible.executor.module_common import modify_module
++from ansible.module_utils.six import PY2
++
++from test_module_common import templar
+
+
+ FAKE_OLD_MODULE = b'''#!/usr/bin/python
+@@ -19,7 +22,10 @@ print('{"result": "%s"}' % sys.executabl
+ @pytest.fixture
+ def fake_old_module_open(mocker):
+ m = mocker.mock_open(read_data=FAKE_OLD_MODULE)
+- mocker.patch('builtins.open', m)
++ if PY2:
++ mocker.patch('__builtin__.open', m)
++ else:
++ mocker.patch('builtins.open', m)
+
+ # this test no longer makes sense, since a Python module will always either have interpreter discovery run or
+ # an explicit interpreter passed (so we'll never default to the module shebang)
+--- ansible-core-2.16.5.orig/test/units/executor/module_common/test_module_common.py
++++ ansible-core-2.16.5/test/units/executor/module_common/test_module_common.py
+@@ -27,6 +27,7 @@ import ansible.errors
+
+ from ansible.executor import module_common as amc
+ from ansible.executor.interpreter_discovery import InterpreterDiscoveryRequiredError
++from ansible.module_utils.six import PY2
+
+
+ class TestStripComments:
+@@ -43,16 +44,15 @@ class TestStripComments:
+ assert amc._strip_comments(all_comments) == u""
+
+ def test_all_whitespace(self):
+- all_whitespace = (
+- '\n'
+- ' \n'
+- '\n'
+- ' \n'
+- '\t\t\r\n'
+- '\n'
+- ' '
+- )
+-
++ # Note: Do not remove the spaces on the blank lines below. They're
++ # test data to show that the lines get removed despite having spaces
++ # on them
++ all_whitespace = u"""
++
++
++
++\t\t\r\n
++ """ # nopep8
+ assert amc._strip_comments(all_whitespace) == u""
+
+ def test_somewhat_normal(self):
+@@ -80,16 +80,31 @@ class TestSlurp:
+ def test_slurp_file(self, mocker):
+ mocker.patch('os.path.exists', side_effect=lambda x: True)
+ m = mocker.mock_open(read_data='This is a test')
+- mocker.patch('builtins.open', m)
++ if PY2:
++ mocker.patch('__builtin__.open', m)
++ else:
++ mocker.patch('builtins.open', m)
+ assert amc._slurp('some_file') == 'This is a test'
+
+ def test_slurp_file_with_newlines(self, mocker):
+ mocker.patch('os.path.exists', side_effect=lambda x: True)
+ m = mocker.mock_open(read_data='#!/usr/bin/python\ndef test(args):\nprint("hi")\n')
+- mocker.patch('builtins.open', m)
++ if PY2:
++ mocker.patch('__builtin__.open', m)
++ else:
++ mocker.patch('builtins.open', m)
+ assert amc._slurp('some_file') == '#!/usr/bin/python\ndef test(args):\nprint("hi")\n'
+
+
++@pytest.fixture
++def templar():
++ class FakeTemplar:
++ def template(self, template_string, *args, **kwargs):
++ return template_string
++
++ return FakeTemplar()
++
++
+ class TestGetShebang:
+ """Note: We may want to change the API of this function in the future. It isn't a great API"""
+ def test_no_interpreter_set(self, templar):
+--- ansible-core-2.16.5.orig/test/units/executor/module_common/test_recursive_finder.py
++++ ansible-core-2.16.5/test/units/executor/module_common/test_recursive_finder.py
+@@ -29,7 +29,7 @@ from io import BytesIO
+ import ansible.errors
+
+ from ansible.executor.module_common import recursive_finder
+-from ansible.plugins.loader import init_plugin_loader
++
+
+ # These are the modules that are brought in by module_utils/basic.py This may need to be updated
+ # when basic.py gains new imports
+@@ -42,6 +42,7 @@ MODULE_UTILS_BASIC_FILES = frozenset(('a
+ 'ansible/module_utils/basic.py',
+ 'ansible/module_utils/six/__init__.py',
+ 'ansible/module_utils/_text.py',
++ 'ansible/module_utils/common/_collections_compat.py',
+ 'ansible/module_utils/common/_json_compat.py',
+ 'ansible/module_utils/common/collections.py',
+ 'ansible/module_utils/common/parameters.py',
+@@ -78,8 +79,6 @@ ANSIBLE_LIB = os.path.join(os.path.dirna
+
+ @pytest.fixture
+ def finder_containers():
+- init_plugin_loader()
+-
+ FinderContainers = namedtuple('FinderContainers', ['zf'])
+
+ zipoutput = BytesIO()
+--- ansible-core-2.16.5.orig/test/units/executor/test_interpreter_discovery.py
++++ ansible-core-2.16.5/test/units/executor/test_interpreter_discovery.py
+@@ -9,7 +9,7 @@ __metaclass__ = type
+ from unittest.mock import MagicMock
+
+ from ansible.executor.interpreter_discovery import discover_interpreter
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+
+ mock_ubuntu_platform_res = to_text(
+ r'{"osrelease_content": "NAME=\"Ubuntu\"\nVERSION=\"16.04.5 LTS (Xenial Xerus)\"\nID=ubuntu\nID_LIKE=debian\n'
+@@ -20,7 +20,7 @@ mock_ubuntu_platform_res = to_text(
+
+
+ def test_discovery_interpreter_linux_auto_legacy():
+- res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3\nENDFOUND'
++ res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3.5\n/usr/bin/python3\nENDFOUND'
+
+ mock_action = MagicMock()
+ mock_action._low_level_execute_command.side_effect = [{'stdout': res1}, {'stdout': mock_ubuntu_platform_res}]
+@@ -35,7 +35,7 @@ def test_discovery_interpreter_linux_aut
+
+
+ def test_discovery_interpreter_linux_auto_legacy_silent():
+- res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3\nENDFOUND'
++ res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3.5\n/usr/bin/python3\nENDFOUND'
+
+ mock_action = MagicMock()
+ mock_action._low_level_execute_command.side_effect = [{'stdout': res1}, {'stdout': mock_ubuntu_platform_res}]
+@@ -47,7 +47,7 @@ def test_discovery_interpreter_linux_aut
+
+
+ def test_discovery_interpreter_linux_auto():
+- res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3\nENDFOUND'
++ res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3.5\n/usr/bin/python3\nENDFOUND'
+
+ mock_action = MagicMock()
+ mock_action._low_level_execute_command.side_effect = [{'stdout': res1}, {'stdout': mock_ubuntu_platform_res}]
+--- ansible-core-2.16.5.orig/test/units/executor/test_play_iterator.py
++++ ansible-core-2.16.5/test/units/executor/test_play_iterator.py
+@@ -25,7 +25,6 @@ from unittest.mock import patch, MagicMo
+ from ansible.executor.play_iterator import HostState, PlayIterator, IteratingStates, FailedStates
+ from ansible.playbook import Playbook
+ from ansible.playbook.play_context import PlayContext
+-from ansible.plugins.loader import init_plugin_loader
+
+ from units.mock.loader import DictDataLoader
+ from units.mock.path import mock_unfrackpath_noop
+@@ -86,8 +85,7 @@ class TestPlayIterator(unittest.TestCase
+ always:
+ - name: role always task
+ debug: msg="always task in block in role"
+- - name: role include_tasks
+- include_tasks: foo.yml
++ - include: foo.yml
+ - name: role task after include
+ debug: msg="after include in role"
+ - block:
+@@ -172,12 +170,12 @@ class TestPlayIterator(unittest.TestCase
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "role always task")
+ self.assertIsNotNone(task._role)
+- # role include_tasks
+- (host_state, task) = itr.get_next_task_for_host(hosts[0])
+- self.assertIsNotNone(task)
+- self.assertEqual(task.action, 'include_tasks')
+- self.assertEqual(task.name, "role include_tasks")
+- self.assertIsNotNone(task._role)
++ # role include task
++ # (host_state, task) = itr.get_next_task_for_host(hosts[0])
++ # self.assertIsNotNone(task)
++ # self.assertEqual(task.action, 'debug')
++ # self.assertEqual(task.name, "role included task")
++ # self.assertIsNotNone(task._role)
+ # role task after include
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+@@ -288,7 +286,6 @@ class TestPlayIterator(unittest.TestCase
+ self.assertNotIn(hosts[0], failed_hosts)
+
+ def test_play_iterator_nested_blocks(self):
+- init_plugin_loader()
+ fake_loader = DictDataLoader({
+ "test_play.yml": """
+ - hosts: all
+@@ -430,11 +427,12 @@ class TestPlayIterator(unittest.TestCase
+ )
+
+ # iterate past first task
+- dummy, task = itr.get_next_task_for_host(hosts[0])
++ _, task = itr.get_next_task_for_host(hosts[0])
+ while (task and task.action != 'debug'):
+- dummy, task = itr.get_next_task_for_host(hosts[0])
++ _, task = itr.get_next_task_for_host(hosts[0])
+
+- self.assertIsNotNone(task, 'iterated past end of play while looking for place to insert tasks')
++ if task is None:
++ raise Exception("iterated past end of play while looking for place to insert tasks")
+
+ # get the current host state and copy it so we can mutate it
+ s = itr.get_host_state(hosts[0])
+--- ansible-core-2.16.5.orig/test/units/executor/test_task_executor.py
++++ ansible-core-2.16.5/test/units/executor/test_task_executor.py
+@@ -25,7 +25,7 @@ from units.compat import unittest
+ from unittest.mock import patch, MagicMock
+ from ansible.errors import AnsibleError
+ from ansible.executor.task_executor import TaskExecutor, remove_omit
+-from ansible.plugins.loader import action_loader, lookup_loader
++from ansible.plugins.loader import action_loader, lookup_loader, module_loader
+ from ansible.parsing.yaml.objects import AnsibleUnicode
+ from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes
+ from ansible.module_utils.six import text_type
+@@ -57,7 +57,6 @@ class TestTaskExecutor(unittest.TestCase
+ loader=fake_loader,
+ shared_loader_obj=mock_shared_loader,
+ final_q=mock_queue,
+- variable_manager=MagicMock(),
+ )
+
+ def test_task_executor_run(self):
+@@ -85,7 +84,6 @@ class TestTaskExecutor(unittest.TestCase
+ loader=fake_loader,
+ shared_loader_obj=mock_shared_loader,
+ final_q=mock_queue,
+- variable_manager=MagicMock(),
+ )
+
+ te._get_loop_items = MagicMock(return_value=None)
+@@ -104,7 +102,7 @@ class TestTaskExecutor(unittest.TestCase
+ self.assertIn("failed", res)
+
+ def test_task_executor_run_clean_res(self):
+- te = TaskExecutor(None, MagicMock(), None, None, None, None, None, None, None)
++ te = TaskExecutor(None, MagicMock(), None, None, None, None, None, None)
+ te._get_loop_items = MagicMock(return_value=[1])
+ te._run_loop = MagicMock(
+ return_value=[
+@@ -152,7 +150,6 @@ class TestTaskExecutor(unittest.TestCase
+ loader=fake_loader,
+ shared_loader_obj=mock_shared_loader,
+ final_q=mock_queue,
+- variable_manager=MagicMock(),
+ )
+
+ items = te._get_loop_items()
+@@ -189,7 +186,6 @@ class TestTaskExecutor(unittest.TestCase
+ loader=fake_loader,
+ shared_loader_obj=mock_shared_loader,
+ final_q=mock_queue,
+- variable_manager=MagicMock(),
+ )
+
+ def _execute(variables):
+@@ -210,7 +206,6 @@ class TestTaskExecutor(unittest.TestCase
+ loader=DictDataLoader({}),
+ shared_loader_obj=MagicMock(),
+ final_q=MagicMock(),
+- variable_manager=MagicMock(),
+ )
+
+ context = MagicMock(resolved=False)
+@@ -219,20 +214,20 @@ class TestTaskExecutor(unittest.TestCase
+ action_loader.has_plugin.return_value = True
+ action_loader.get.return_value = mock.sentinel.handler
+
++ mock_connection = MagicMock()
+ mock_templar = MagicMock()
+ action = 'namespace.prefix_suffix'
+ te._task.action = action
+- te._connection = MagicMock()
+
+- with patch('ansible.executor.task_executor.start_connection'):
+- handler = te._get_action_handler(mock_templar)
++ handler = te._get_action_handler(mock_connection, mock_templar)
+
+ self.assertIs(mock.sentinel.handler, handler)
+
+- action_loader.has_plugin.assert_called_once_with(action, collection_list=te._task.collections)
++ action_loader.has_plugin.assert_called_once_with(
++ action, collection_list=te._task.collections)
+
+- action_loader.get.assert_called_with(
+- te._task.action, task=te._task, connection=te._connection,
++ action_loader.get.assert_called_once_with(
++ te._task.action, task=te._task, connection=mock_connection,
+ play_context=te._play_context, loader=te._loader,
+ templar=mock_templar, shared_loader_obj=te._shared_loader_obj,
+ collection_list=te._task.collections)
+@@ -247,7 +242,6 @@ class TestTaskExecutor(unittest.TestCase
+ loader=DictDataLoader({}),
+ shared_loader_obj=MagicMock(),
+ final_q=MagicMock(),
+- variable_manager=MagicMock(),
+ )
+
+ context = MagicMock(resolved=False)
+@@ -257,21 +251,20 @@ class TestTaskExecutor(unittest.TestCase
+ action_loader.get.return_value = mock.sentinel.handler
+ action_loader.__contains__.return_value = True
+
++ mock_connection = MagicMock()
+ mock_templar = MagicMock()
+ action = 'namespace.netconf_suffix'
+ module_prefix = action.split('_', 1)[0]
+ te._task.action = action
+- te._connection = MagicMock()
+
+- with patch('ansible.executor.task_executor.start_connection'):
+- handler = te._get_action_handler(mock_templar)
++ handler = te._get_action_handler(mock_connection, mock_templar)
+
+ self.assertIs(mock.sentinel.handler, handler)
+ action_loader.has_plugin.assert_has_calls([mock.call(action, collection_list=te._task.collections), # called twice
+ mock.call(module_prefix, collection_list=te._task.collections)])
+
+- action_loader.get.assert_called_with(
+- module_prefix, task=te._task, connection=te._connection,
++ action_loader.get.assert_called_once_with(
++ module_prefix, task=te._task, connection=mock_connection,
+ play_context=te._play_context, loader=te._loader,
+ templar=mock_templar, shared_loader_obj=te._shared_loader_obj,
+ collection_list=te._task.collections)
+@@ -286,7 +279,6 @@ class TestTaskExecutor(unittest.TestCase
+ loader=DictDataLoader({}),
+ shared_loader_obj=MagicMock(),
+ final_q=MagicMock(),
+- variable_manager=MagicMock(),
+ )
+
+ action_loader = te._shared_loader_obj.action_loader
+@@ -297,22 +289,20 @@ class TestTaskExecutor(unittest.TestCase
+ context = MagicMock(resolved=False)
+ module_loader.find_plugin_with_context.return_value = context
+
++ mock_connection = MagicMock()
+ mock_templar = MagicMock()
+ action = 'namespace.prefix_suffix'
+ module_prefix = action.split('_', 1)[0]
+ te._task.action = action
+- te._connection = MagicMock()
+-
+- with patch('ansible.executor.task_executor.start_connection'):
+- handler = te._get_action_handler(mock_templar)
++ handler = te._get_action_handler(mock_connection, mock_templar)
+
+ self.assertIs(mock.sentinel.handler, handler)
+
+ action_loader.has_plugin.assert_has_calls([mock.call(action, collection_list=te._task.collections),
+ mock.call(module_prefix, collection_list=te._task.collections)])
+
+- action_loader.get.assert_called_with(
+- 'ansible.legacy.normal', task=te._task, connection=te._connection,
++ action_loader.get.assert_called_once_with(
++ 'ansible.legacy.normal', task=te._task, connection=mock_connection,
+ play_context=te._play_context, loader=te._loader,
+ templar=mock_templar, shared_loader_obj=te._shared_loader_obj,
+ collection_list=None)
+@@ -328,7 +318,6 @@ class TestTaskExecutor(unittest.TestCase
+ mock_task.become = False
+ mock_task.retries = 0
+ mock_task.delay = -1
+- mock_task.delegate_to = None
+ mock_task.register = 'foo'
+ mock_task.until = None
+ mock_task.changed_when = None
+@@ -340,7 +329,6 @@ class TestTaskExecutor(unittest.TestCase
+ # other reason is that if I specify 0 here, the test fails. ;)
+ mock_task.async_val = 1
+ mock_task.poll = 0
+- mock_task.evaluate_conditional_with_result.return_value = (True, None)
+
+ mock_play_context = MagicMock()
+ mock_play_context.post_validate.return_value = None
+@@ -355,9 +343,6 @@ class TestTaskExecutor(unittest.TestCase
+ mock_action = MagicMock()
+ mock_queue = MagicMock()
+
+- mock_vm = MagicMock()
+- mock_vm.get_delegated_vars_and_hostname.return_value = {}, None
+-
+ shared_loader = MagicMock()
+ new_stdin = None
+ job_vars = dict(omit="XXXXXXXXXXXXXXXXXXX")
+@@ -371,14 +356,11 @@ class TestTaskExecutor(unittest.TestCase
+ loader=fake_loader,
+ shared_loader_obj=shared_loader,
+ final_q=mock_queue,
+- variable_manager=mock_vm,
+ )
+
+ te._get_connection = MagicMock(return_value=mock_connection)
+ context = MagicMock()
+-
+- with patch('ansible.executor.task_executor.start_connection'):
+- te._get_action_handler_with_context = MagicMock(return_value=get_with_context_result(mock_action, context))
++ te._get_action_handler_with_context = MagicMock(return_value=get_with_context_result(mock_action, context))
+
+ mock_action.run.return_value = dict(ansible_facts=dict())
+ res = te._execute()
+@@ -410,6 +392,8 @@ class TestTaskExecutor(unittest.TestCase
+
+ mock_play_context = MagicMock()
+
++ mock_connection = MagicMock()
++
+ mock_action = MagicMock()
+ mock_queue = MagicMock()
+
+@@ -428,7 +412,6 @@ class TestTaskExecutor(unittest.TestCase
+ loader=fake_loader,
+ shared_loader_obj=shared_loader,
+ final_q=mock_queue,
+- variable_manager=MagicMock(),
+ )
+
+ te._connection = MagicMock()
+--- ansible-core-2.16.5.orig/test/units/galaxy/test_api.py
++++ ansible-core-2.16.5/test/units/galaxy/test_api.py
+@@ -24,7 +24,7 @@ from ansible.errors import AnsibleError
+ from ansible.galaxy import api as galaxy_api
+ from ansible.galaxy.api import CollectionVersionMetadata, GalaxyAPI, GalaxyError
+ from ansible.galaxy.token import BasicAuthToken, GalaxyToken, KeycloakToken
+-from ansible.module_utils.common.text.converters import to_native, to_text
++from ansible.module_utils._text import to_native, to_text
+ from ansible.module_utils.six.moves.urllib import error as urllib_error
+ from ansible.utils import context_objects as co
+ from ansible.utils.display import Display
+@@ -463,9 +463,10 @@ def test_publish_failure(api_version, co
+ def test_wait_import_task(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
+ api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
+
+- mock_token_get = MagicMock()
+- mock_token_get.return_value = 'my token'
+- monkeypatch.setattr(token_ins, 'get', mock_token_get)
++ if token_ins:
++ mock_token_get = MagicMock()
++ mock_token_get.return_value = 'my token'
++ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ mock_open = MagicMock()
+ mock_open.return_value = StringIO(u'{"state":"success","finished_at":"time"}')
+@@ -495,9 +496,10 @@ def test_wait_import_task(server_url, ap
+ def test_wait_import_task_multiple_requests(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
+ api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
+
+- mock_token_get = MagicMock()
+- mock_token_get.return_value = 'my token'
+- monkeypatch.setattr(token_ins, 'get', mock_token_get)
++ if token_ins:
++ mock_token_get = MagicMock()
++ mock_token_get.return_value = 'my token'
++ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+@@ -541,9 +543,10 @@ def test_wait_import_task_multiple_reque
+ def test_wait_import_task_with_failure(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
+ api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
+
+- mock_token_get = MagicMock()
+- mock_token_get.return_value = 'my token'
+- monkeypatch.setattr(token_ins, 'get', mock_token_get)
++ if token_ins:
++ mock_token_get = MagicMock()
++ mock_token_get.return_value = 'my token'
++ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+@@ -617,9 +620,10 @@ def test_wait_import_task_with_failure(s
+ def test_wait_import_task_with_failure_no_error(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
+ api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
+
+- mock_token_get = MagicMock()
+- mock_token_get.return_value = 'my token'
+- monkeypatch.setattr(token_ins, 'get', mock_token_get)
++ if token_ins:
++ mock_token_get = MagicMock()
++ mock_token_get.return_value = 'my token'
++ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+@@ -689,9 +693,10 @@ def test_wait_import_task_with_failure_n
+ def test_wait_import_task_timeout(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
+ api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
+
+- mock_token_get = MagicMock()
+- mock_token_get.return_value = 'my token'
+- monkeypatch.setattr(token_ins, 'get', mock_token_get)
++ if token_ins:
++ mock_token_get = MagicMock()
++ mock_token_get.return_value = 'my token'
++ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ def return_response(*args, **kwargs):
+ return StringIO(u'{"state":"waiting"}')
+--- ansible-core-2.16.5.orig/test/units/galaxy/test_collection.py
++++ ansible-core-2.16.5/test/units/galaxy/test_collection.py
+@@ -20,11 +20,10 @@ from unittest.mock import MagicMock, moc
+
+ import ansible.constants as C
+ from ansible import context
+-from ansible.cli import galaxy
+-from ansible.cli.galaxy import GalaxyCLI
++from ansible.cli.galaxy import GalaxyCLI, SERVER_DEF
+ from ansible.errors import AnsibleError
+ from ansible.galaxy import api, collection, token
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.six.moves import builtins
+ from ansible.utils import context_objects as co
+ from ansible.utils.display import Display
+@@ -172,6 +171,28 @@ def manifest_info(manifest_template):
+
+
+ @pytest.fixture()
++def files_manifest_info():
++ return {
++ "files": [
++ {
++ "name": ".",
++ "ftype": "dir",
++ "chksum_type": None,
++ "chksum_sha256": None,
++ "format": 1
++ },
++ {
++ "name": "README.md",
++ "ftype": "file",
++ "chksum_type": "sha256",
++ "chksum_sha256": "individual_file_checksum",
++ "format": 1
++ }
++ ],
++ "format": 1}
++
++
++@pytest.fixture()
+ def manifest(manifest_info):
+ b_data = to_bytes(json.dumps(manifest_info))
+
+@@ -224,19 +245,23 @@ def test_cli_options(required_signature_
+ {
+ 'url': 'https://galaxy.ansible.com',
+ 'validate_certs': 'False',
++ 'v3': 'False',
+ },
+ # Expected server attributes
+ {
+ 'validate_certs': False,
++ '_available_api_versions': {},
+ },
+ ),
+ (
+ {
+ 'url': 'https://galaxy.ansible.com',
+ 'validate_certs': 'True',
++ 'v3': 'True',
+ },
+ {
+ 'validate_certs': True,
++ '_available_api_versions': {'v3': '/v3'},
+ },
+ ),
+ ],
+@@ -254,6 +279,7 @@ def test_bool_type_server_config_options
+ "server_list=server1\n",
+ "[galaxy_server.server1]",
+ "url=%s" % config['url'],
++ "v3=%s" % config['v3'],
+ "validate_certs=%s\n" % config['validate_certs'],
+ ]
+
+@@ -273,6 +299,7 @@ def test_bool_type_server_config_options
+
+ assert galaxy_cli.api_servers[0].name == 'server1'
+ assert galaxy_cli.api_servers[0].validate_certs == server['validate_certs']
++ assert galaxy_cli.api_servers[0]._available_api_versions == server['_available_api_versions']
+
+
+ @pytest.mark.parametrize('global_ignore_certs', [True, False])
+@@ -384,55 +411,6 @@ def test_validate_certs_server_config(ig
+ assert galaxy_cli.api_servers[2].validate_certs is expected_server3_validate_certs
+
+
+-@pytest.mark.parametrize(
+- ["timeout_cli", "timeout_cfg", "timeout_fallback", "expected_timeout"],
+- [
+- (None, None, None, 60),
+- (None, None, 10, 10),
+- (None, 20, 10, 20),
+- (30, 20, 10, 30),
+- ]
+-)
+-def test_timeout_server_config(timeout_cli, timeout_cfg, timeout_fallback, expected_timeout, monkeypatch):
+- cli_args = [
+- 'ansible-galaxy',
+- 'collection',
+- 'install',
+- 'namespace.collection:1.0.0',
+- ]
+- if timeout_cli is not None:
+- cli_args.extend(["--timeout", f"{timeout_cli}"])
+-
+- cfg_lines = ["[galaxy]", "server_list=server1"]
+- if timeout_fallback is not None:
+- cfg_lines.append(f"server_timeout={timeout_fallback}")
+-
+- # fix default in server config since C.GALAXY_SERVER_TIMEOUT was already evaluated
+- server_additional = galaxy.SERVER_ADDITIONAL.copy()
+- server_additional['timeout']['default'] = timeout_fallback
+- monkeypatch.setattr(galaxy, 'SERVER_ADDITIONAL', server_additional)
+-
+- cfg_lines.extend(["[galaxy_server.server1]", "url=https://galaxy.ansible.com/api/"])
+- if timeout_cfg is not None:
+- cfg_lines.append(f"timeout={timeout_cfg}")
+-
+- monkeypatch.setattr(C, 'GALAXY_SERVER_LIST', ['server1'])
+-
+- with tempfile.NamedTemporaryFile(suffix='.cfg') as tmp_file:
+- tmp_file.write(to_bytes('\n'.join(cfg_lines), errors='surrogate_or_strict'))
+- tmp_file.flush()
+-
+- monkeypatch.setattr(C.config, '_config_file', tmp_file.name)
+- C.config._parse_config_file()
+-
+- galaxy_cli = GalaxyCLI(args=cli_args)
+- mock_execute_install = MagicMock()
+- monkeypatch.setattr(galaxy_cli, '_execute_install_collection', mock_execute_install)
+- galaxy_cli.run()
+-
+- assert galaxy_cli.api_servers[0].timeout == expected_timeout
+-
+-
+ def test_build_collection_no_galaxy_yaml():
+ fake_path = u'/fake/ÅÑŚÌβŁÈ/path'
+ expected = to_native("The collection galaxy.yml path '%s/galaxy.yml' does not exist." % fake_path)
+@@ -501,19 +479,19 @@ def test_build_with_existing_files_and_m
+ with tarfile.open(output_artifact, mode='r') as actual:
+ members = actual.getmembers()
+
+- manifest_file = [m for m in members if m.path == "MANIFEST.json"][0]
++ manifest_file = next(m for m in members if m.path == "MANIFEST.json")
+ manifest_file_obj = actual.extractfile(manifest_file.name)
+ manifest_file_text = manifest_file_obj.read()
+ manifest_file_obj.close()
+ assert manifest_file_text != b'{"collection_info": {"version": "6.6.6"}, "version": 1}'
+
+- json_file = [m for m in members if m.path == "MANIFEST.json"][0]
++ json_file = next(m for m in members if m.path == "MANIFEST.json")
+ json_file_obj = actual.extractfile(json_file.name)
+ json_file_text = json_file_obj.read()
+ json_file_obj.close()
+ assert json_file_text != b'{"files": [], "format": 1}'
+
+- sub_manifest_file = [m for m in members if m.path == "plugins/MANIFEST.json"][0]
++ sub_manifest_file = next(m for m in members if m.path == "plugins/MANIFEST.json")
+ sub_manifest_file_obj = actual.extractfile(sub_manifest_file.name)
+ sub_manifest_file_text = sub_manifest_file_obj.read()
+ sub_manifest_file_obj.close()
+@@ -640,7 +618,7 @@ def test_build_ignore_files_and_folders(
+ tests_file.write('random')
+ tests_file.flush()
+
+- actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel, None)
++ actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel)
+
+ assert actual['format'] == 1
+ for manifest_entry in actual['files']:
+@@ -676,7 +654,7 @@ def test_build_ignore_older_release_in_r
+ file_obj.write('random')
+ file_obj.flush()
+
+- actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel, None)
++ actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel)
+ assert actual['format'] == 1
+
+ plugin_release_found = False
+@@ -704,7 +682,7 @@ def test_build_ignore_patterns(collectio
+
+ actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection',
+ ['*.md', 'plugins/action', 'playbooks/*.j2'],
+- Sentinel, None)
++ Sentinel)
+ assert actual['format'] == 1
+
+ expected_missing = [
+@@ -755,7 +733,7 @@ def test_build_ignore_symlink_target_out
+ link_path = os.path.join(input_dir, 'plugins', 'connection')
+ os.symlink(outside_dir, link_path)
+
+- actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel, None)
++ actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel)
+ for manifest_entry in actual['files']:
+ assert manifest_entry['name'] != 'plugins/connection'
+
+@@ -779,7 +757,7 @@ def test_build_copy_symlink_target_insid
+
+ os.symlink(roles_target, roles_link)
+
+- actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel, None)
++ actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel)
+
+ linked_entries = [e for e in actual['files'] if e['name'].startswith('playbooks/roles/linked')]
+ assert len(linked_entries) == 1
+@@ -812,11 +790,11 @@ def test_build_with_symlink_inside_colle
+ with tarfile.open(output_artifact, mode='r') as actual:
+ members = actual.getmembers()
+
+- linked_folder = [m for m in members if m.path == 'playbooks/roles/linked'][0]
++ linked_folder = next(m for m in members if m.path == 'playbooks/roles/linked')
+ assert linked_folder.type == tarfile.SYMTYPE
+ assert linked_folder.linkname == '../../roles/linked'
+
+- linked_file = [m for m in members if m.path == 'docs/README.md'][0]
++ linked_file = next(m for m in members if m.path == 'docs/README.md')
+ assert linked_file.type == tarfile.SYMTYPE
+ assert linked_file.linkname == '../README.md'
+
+@@ -824,7 +802,7 @@ def test_build_with_symlink_inside_colle
+ actual_file = secure_hash_s(linked_file_obj.read())
+ linked_file_obj.close()
+
+- assert actual_file == '08f24200b9fbe18903e7a50930c9d0df0b8d7da3' # shasum test/units/cli/test_data/collection_skeleton/README.md
++ assert actual_file == '63444bfc766154e1bc7557ef6280de20d03fcd81'
+
+
+ def test_publish_no_wait(galaxy_server, collection_artifact, monkeypatch):
+@@ -876,6 +854,57 @@ def test_publish_with_wait(galaxy_server
+ % galaxy_server.api_server
+
+
++def test_find_existing_collections(tmp_path_factory, monkeypatch):
++ test_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections'))
++ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
++ collection1 = os.path.join(test_dir, 'namespace1', 'collection1')
++ collection2 = os.path.join(test_dir, 'namespace2', 'collection2')
++ fake_collection1 = os.path.join(test_dir, 'namespace3', 'collection3')
++ fake_collection2 = os.path.join(test_dir, 'namespace4')
++ os.makedirs(collection1)
++ os.makedirs(collection2)
++ os.makedirs(os.path.split(fake_collection1)[0])
++
++ open(fake_collection1, 'wb+').close()
++ open(fake_collection2, 'wb+').close()
++
++ collection1_manifest = json.dumps({
++ 'collection_info': {
++ 'namespace': 'namespace1',
++ 'name': 'collection1',
++ 'version': '1.2.3',
++ 'authors': ['Jordan Borean'],
++ 'readme': 'README.md',
++ 'dependencies': {},
++ },
++ 'format': 1,
++ })
++ with open(os.path.join(collection1, 'MANIFEST.json'), 'wb') as manifest_obj:
++ manifest_obj.write(to_bytes(collection1_manifest))
++
++ mock_warning = MagicMock()
++ monkeypatch.setattr(Display, 'warning', mock_warning)
++
++ actual = list(collection.find_existing_collections(test_dir, artifacts_manager=concrete_artifact_cm))
++
++ assert len(actual) == 2
++ for actual_collection in actual:
++ if '%s.%s' % (actual_collection.namespace, actual_collection.name) == 'namespace1.collection1':
++ assert actual_collection.namespace == 'namespace1'
++ assert actual_collection.name == 'collection1'
++ assert actual_collection.ver == '1.2.3'
++ assert to_text(actual_collection.src) == collection1
++ else:
++ assert actual_collection.namespace == 'namespace2'
++ assert actual_collection.name == 'collection2'
++ assert actual_collection.ver == '*'
++ assert to_text(actual_collection.src) == collection2
++
++ assert mock_warning.call_count == 1
++ assert mock_warning.mock_calls[0][1][0] == "Collection at '%s' does not have a MANIFEST.json file, nor has it galaxy.yml: " \
++ "cannot detect version." % to_text(collection2)
++
++
+ def test_download_file(tmp_path_factory, monkeypatch):
+ temp_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections'))
+
+@@ -1082,7 +1111,7 @@ def test_verify_file_hash_deleted_file(m
+ with patch.object(collection.os.path, 'isfile', MagicMock(return_value=False)) as mock_isfile:
+ collection._verify_file_hash(b'path/', 'file', digest, error_queue)
+
+- mock_isfile.assert_called_once()
++ assert mock_isfile.called_once
+
+ assert len(error_queue) == 1
+ assert error_queue[0].installed is None
+@@ -1105,7 +1134,7 @@ def test_verify_file_hash_matching_hash(
+ with patch.object(collection.os.path, 'isfile', MagicMock(return_value=True)) as mock_isfile:
+ collection._verify_file_hash(b'path/', 'file', digest, error_queue)
+
+- mock_isfile.assert_called_once()
++ assert mock_isfile.called_once
+
+ assert error_queue == []
+
+@@ -1127,7 +1156,7 @@ def test_verify_file_hash_mismatching_ha
+ with patch.object(collection.os.path, 'isfile', MagicMock(return_value=True)) as mock_isfile:
+ collection._verify_file_hash(b'path/', 'file', different_digest, error_queue)
+
+- mock_isfile.assert_called_once()
++ assert mock_isfile.called_once
+
+ assert len(error_queue) == 1
+ assert error_queue[0].installed == digest
+--- ansible-core-2.16.5.orig/test/units/galaxy/test_collection_install.py
++++ ansible-core-2.16.5/test/units/galaxy/test_collection_install.py
+@@ -18,6 +18,7 @@ import yaml
+
+ from io import BytesIO, StringIO
+ from unittest.mock import MagicMock, patch
++from unittest import mock
+
+ import ansible.module_utils.six.moves.urllib.error as urllib_error
+
+@@ -26,7 +27,7 @@ from ansible.cli.galaxy import GalaxyCLI
+ from ansible.errors import AnsibleError
+ from ansible.galaxy import collection, api, dependency_resolution
+ from ansible.galaxy.dependency_resolution.dataclasses import Candidate, Requirement
+-from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
++from ansible.module_utils._text import to_bytes, to_native, to_text
+ from ansible.module_utils.common.process import get_bin_path
+ from ansible.utils import context_objects as co
+ from ansible.utils.display import Display
+@@ -52,6 +53,78 @@ def call_galaxy_cli(args):
+ co.GlobalCLIArgs._Singleton__instance = orig
+
+
++def artifact_json(namespace, name, version, dependencies, server):
++ json_str = json.dumps({
++ 'artifact': {
++ 'filename': '%s-%s-%s.tar.gz' % (namespace, name, version),
++ 'sha256': '2d76f3b8c4bab1072848107fb3914c345f71a12a1722f25c08f5d3f51f4ab5fd',
++ 'size': 1234,
++ },
++ 'download_url': '%s/download/%s-%s-%s.tar.gz' % (server, namespace, name, version),
++ 'metadata': {
++ 'namespace': namespace,
++ 'name': name,
++ 'dependencies': dependencies,
++ },
++ 'version': version
++ })
++ return to_text(json_str)
++
++
++def artifact_versions_json(namespace, name, versions, galaxy_api, available_api_versions=None):
++ results = []
++ available_api_versions = available_api_versions or {}
++ api_version = 'v2'
++ if 'v3' in available_api_versions:
++ api_version = 'v3'
++ for version in versions:
++ results.append({
++ 'href': '%s/api/%s/%s/%s/versions/%s/' % (galaxy_api.api_server, api_version, namespace, name, version),
++ 'version': version,
++ })
++
++ if api_version == 'v2':
++ json_str = json.dumps({
++ 'count': len(versions),
++ 'next': None,
++ 'previous': None,
++ 'results': results
++ })
++
++ if api_version == 'v3':
++ response = {'meta': {'count': len(versions)},
++ 'data': results,
++ 'links': {'first': None,
++ 'last': None,
++ 'next': None,
++ 'previous': None},
++ }
++ json_str = json.dumps(response)
++ return to_text(json_str)
++
++
++def error_json(galaxy_api, errors_to_return=None, available_api_versions=None):
++ errors_to_return = errors_to_return or []
++ available_api_versions = available_api_versions or {}
++
++ response = {}
++
++ api_version = 'v2'
++ if 'v3' in available_api_versions:
++ api_version = 'v3'
++
++ if api_version == 'v2':
++ assert len(errors_to_return) <= 1
++ if errors_to_return:
++ response = errors_to_return[0]
++
++ if api_version == 'v3':
++ response['errors'] = errors_to_return
++
++ json_str = json.dumps(response)
++ return to_text(json_str)
++
++
+ @pytest.fixture(autouse='function')
+ def reset_cli_args():
+ co.GlobalCLIArgs._Singleton__instance = None
+@@ -298,27 +371,6 @@ def test_build_requirement_from_tar(coll
+ assert actual.ver == u'0.1.0'
+
+
+-def test_build_requirement_from_tar_url(tmp_path_factory):
+- test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+- concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+- test_url = 'https://example.com/org/repo/sample.tar.gz'
+- expected = fr"^Failed to download collection tar from '{to_text(test_url)}'"
+-
+- with pytest.raises(AnsibleError, match=expected):
+- Requirement.from_requirement_dict({'name': test_url, 'type': 'url'}, concrete_artifact_cm)
+-
+-
+-def test_build_requirement_from_tar_url_wrong_type(tmp_path_factory):
+- test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+- concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+- test_url = 'https://example.com/org/repo/sample.tar.gz'
+- expected = fr"^Unable to find collection artifact file at '{to_text(test_url)}'\.$"
+-
+- with pytest.raises(AnsibleError, match=expected):
+- # Specified wrong collection type for http URL
+- Requirement.from_requirement_dict({'name': test_url, 'type': 'file'}, concrete_artifact_cm)
+-
+-
+ def test_build_requirement_from_tar_fail_not_tar(tmp_path_factory):
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ test_file = os.path.join(test_dir, b'fake.tar.gz')
+@@ -843,8 +895,7 @@ def test_install_collections_from_tar(co
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False)
+
+ requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)]
+- collection.install_collections(
+- requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False, set())
++ collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False)
+
+ assert os.path.isdir(collection_path)
+
+@@ -868,6 +919,57 @@ def test_install_collections_from_tar(co
+ assert display_msgs[2] == "Installing 'ansible_namespace.collection:0.1.0' to '%s'" % to_text(collection_path)
+
+
++def test_install_collections_existing_without_force(collection_artifact, monkeypatch):
++ collection_path, collection_tar = collection_artifact
++ temp_path = os.path.split(collection_tar)[0]
++
++ mock_display = MagicMock()
++ monkeypatch.setattr(Display, 'display', mock_display)
++
++ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False)
++
++ assert os.path.isdir(collection_path)
++
++ requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)]
++ collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False)
++
++ assert os.path.isdir(collection_path)
++
++ actual_files = os.listdir(collection_path)
++ actual_files.sort()
++ assert actual_files == [b'README.md', b'docs', b'galaxy.yml', b'playbooks', b'plugins', b'roles', b'runme.sh']
++
++ # Filter out the progress cursor display calls.
++ display_msgs = [m[1][0] for m in mock_display.mock_calls if 'newline' not in m[2] and len(m[1]) == 1]
++ assert len(display_msgs) == 1
++
++ assert display_msgs[0] == 'Nothing to do. All requested collections are already installed. If you want to reinstall them, consider using `--force`.'
++
++ for msg in display_msgs:
++ assert 'WARNING' not in msg
++
++
++def test_install_missing_metadata_warning(collection_artifact, monkeypatch):
++ collection_path, collection_tar = collection_artifact
++ temp_path = os.path.split(collection_tar)[0]
++
++ mock_display = MagicMock()
++ monkeypatch.setattr(Display, 'display', mock_display)
++
++ for file in [b'MANIFEST.json', b'galaxy.yml']:
++ b_path = os.path.join(collection_path, file)
++ if os.path.isfile(b_path):
++ os.unlink(b_path)
++
++ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False)
++ requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)]
++ collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False)
++
++ display_msgs = [m[1][0] for m in mock_display.mock_calls if 'newline' not in m[2] and len(m[1]) == 1]
++
++ assert 'WARNING' in display_msgs[0]
++
++
+ # Makes sure we don't get stuck in some recursive loop
+ @pytest.mark.parametrize('collection_artifact', [
+ {'ansible_namespace.collection': '>=0.0.1'},
+@@ -882,8 +984,7 @@ def test_install_collection_with_circula
+
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False)
+ requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)]
+- collection.install_collections(
+- requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False, set())
++ collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False)
+
+ assert os.path.isdir(collection_path)
+
+@@ -920,8 +1021,7 @@ def test_install_collection_with_no_depe
+
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False)
+ requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)]
+- collection.install_collections(
+- requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False, set())
++ collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False)
+
+ assert os.path.isdir(collection_path)
+
+--- ansible-core-2.16.5.orig/test/units/galaxy/test_role_install.py
++++ ansible-core-2.16.5/test/units/galaxy/test_role_install.py
+@@ -7,7 +7,6 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+
+-import json
+ import os
+ import functools
+ import pytest
+@@ -17,7 +16,7 @@ from io import StringIO
+ from ansible import context
+ from ansible.cli.galaxy import GalaxyCLI
+ from ansible.galaxy import api, role, Galaxy
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from ansible.utils import context_objects as co
+
+
+@@ -25,7 +24,7 @@ def call_galaxy_cli(args):
+ orig = co.GlobalCLIArgs._Singleton__instance
+ co.GlobalCLIArgs._Singleton__instance = None
+ try:
+- return GalaxyCLI(args=['ansible-galaxy', 'role'] + args).run()
++ GalaxyCLI(args=['ansible-galaxy', 'role'] + args).run()
+ finally:
+ co.GlobalCLIArgs._Singleton__instance = orig
+
+@@ -121,22 +120,6 @@ def test_role_download_github_no_downloa
+ assert mock_role_download_api.mock_calls[0][1][0] == 'https://github.com/test_owner/test_role/archive/0.0.1.tar.gz'
+
+
+-@pytest.mark.parametrize(
+- 'state,rc',
+- [('SUCCESS', 0), ('FAILED', 1),]
+-)
+-def test_role_import(state, rc, mocker, galaxy_server, monkeypatch):
+- responses = [
+- {"available_versions": {"v1": "v1/"}},
+- {"results": [{'id': 12345, 'github_user': 'user', 'github_repo': 'role', 'github_reference': None, 'summary_fields': {'role': {'name': 'role'}}}]},
+- {"results": [{'state': 'WAITING', 'id': 12345, 'summary_fields': {'task_messages': []}}]},
+- {"results": [{'state': state, 'id': 12345, 'summary_fields': {'task_messages': []}}]},
+- ]
+- mock_api = mocker.MagicMock(side_effect=[StringIO(json.dumps(rsp)) for rsp in responses])
+- monkeypatch.setattr(api, 'open_url', mock_api)
+- assert call_galaxy_cli(['import', 'user', 'role']) == rc
+-
+-
+ def test_role_download_url(init_mock_temp_file, mocker, galaxy_server, mock_role_download_api, monkeypatch):
+ mock_api = mocker.MagicMock()
+ mock_api.side_effect = [
+--- ansible-core-2.16.5.orig/test/units/galaxy/test_token.py
++++ ansible-core-2.16.5/test/units/galaxy/test_token.py
+@@ -13,7 +13,7 @@ from unittest.mock import MagicMock
+ import ansible.constants as C
+ from ansible.cli.galaxy import GalaxyCLI, SERVER_DEF
+ from ansible.galaxy.token import GalaxyToken, NoTokenSentinel
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+
+
+ @pytest.fixture()
+--- ansible-core-2.16.5.orig/test/units/inventory/test_host.py
++++ ansible-core-2.16.5/test/units/inventory/test_host.py
+@@ -69,10 +69,10 @@ class TestHost(unittest.TestCase):
+
+ def test_equals_none(self):
+ other = None
+- assert not (self.hostA == other)
+- assert not (other == self.hostA)
+- assert self.hostA != other
+- assert other != self.hostA
++ self.hostA == other
++ other == self.hostA
++ self.hostA != other
++ other != self.hostA
+ self.assertNotEqual(self.hostA, other)
+
+ def test_serialize(self):
+--- ansible-core-2.16.5.orig/test/units/mock/loader.py
++++ ansible-core-2.16.5/test/units/mock/loader.py
+@@ -21,15 +21,16 @@ __metaclass__ = type
+
+ import os
+
++from ansible.errors import AnsibleParserError
+ from ansible.parsing.dataloader import DataLoader
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+
+
+ class DictDataLoader(DataLoader):
+
+ def __init__(self, file_mapping=None):
+ file_mapping = {} if file_mapping is None else file_mapping
+- assert isinstance(file_mapping, dict)
++ assert type(file_mapping) == dict
+
+ super(DictDataLoader, self).__init__()
+
+@@ -47,7 +48,11 @@ class DictDataLoader(DataLoader):
+ # TODO: the real _get_file_contents returns a bytestring, so we actually convert the
+ # unicode/text it's created with to utf-8
+ def _get_file_contents(self, file_name):
+- return to_bytes(self._file_mapping[file_name]), False
++ path = to_text(file_name)
++ if path in self._file_mapping:
++ return to_bytes(self._file_mapping[file_name]), False
++ else:
++ raise AnsibleParserError("file not found: %s" % file_name)
+
+ def path_exists(self, path):
+ path = to_text(path)
+@@ -86,6 +91,25 @@ class DictDataLoader(DataLoader):
+ self._add_known_directory(dirname)
+ dirname = os.path.dirname(dirname)
+
++ def push(self, path, content):
++ rebuild_dirs = False
++ if path not in self._file_mapping:
++ rebuild_dirs = True
++
++ self._file_mapping[path] = content
++
++ if rebuild_dirs:
++ self._build_known_directories()
++
++ def pop(self, path):
++ if path in self._file_mapping:
++ del self._file_mapping[path]
++ self._build_known_directories()
++
++ def clear(self):
++ self._file_mapping = dict()
++ self._known_directories = []
++
+ def get_basedir(self):
+ return os.getcwd()
+
+--- ansible-core-2.16.5.orig/test/units/mock/procenv.py
++++ ansible-core-2.16.5/test/units/mock/procenv.py
+@@ -27,7 +27,7 @@ from contextlib import contextmanager
+ from io import BytesIO, StringIO
+ from units.compat import unittest
+ from ansible.module_utils.six import PY3
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+
+
+ @contextmanager
+@@ -54,9 +54,30 @@ def swap_stdin_and_argv(stdin_data='', a
+ sys.argv = real_argv
+
+
++@contextmanager
++def swap_stdout():
++ """
++ context manager that temporarily replaces stdout for tests that need to verify output
++ """
++ old_stdout = sys.stdout
++
++ if PY3:
++ fake_stream = StringIO()
++ else:
++ fake_stream = BytesIO()
++
++ try:
++ sys.stdout = fake_stream
++
++ yield fake_stream
++ finally:
++ sys.stdout = old_stdout
++
++
+ class ModuleTestCase(unittest.TestCase):
+- def setUp(self):
+- module_args = {'_ansible_remote_tmp': '/tmp', '_ansible_keep_remote_files': False}
++ def setUp(self, module_args=None):
++ if module_args is None:
++ module_args = {'_ansible_remote_tmp': '/tmp', '_ansible_keep_remote_files': False}
+
+ args = json.dumps(dict(ANSIBLE_MODULE_ARGS=module_args))
+
+--- ansible-core-2.16.5.orig/test/units/mock/vault_helper.py
++++ ansible-core-2.16.5/test/units/mock/vault_helper.py
+@@ -15,7 +15,7 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+
+ from ansible.parsing.vault import VaultSecret
+
+--- ansible-core-2.16.5.orig/test/units/mock/yaml_helper.py
++++ ansible-core-2.16.5/test/units/mock/yaml_helper.py
+@@ -4,6 +4,8 @@ __metaclass__ = type
+ import io
+ import yaml
+
++from ansible.module_utils.six import PY3
++from ansible.parsing.yaml.loader import AnsibleLoader
+ from ansible.parsing.yaml.dumper import AnsibleDumper
+
+
+@@ -13,14 +15,21 @@ class YamlTestUtils(object):
+ """Vault related tests will want to override this.
+
+ Vault cases should setup a AnsibleLoader that has the vault password."""
++ return AnsibleLoader(stream)
+
+ def _dump_stream(self, obj, stream, dumper=None):
+ """Dump to a py2-unicode or py3-string stream."""
+- return yaml.dump(obj, stream, Dumper=dumper)
++ if PY3:
++ return yaml.dump(obj, stream, Dumper=dumper)
++ else:
++ return yaml.dump(obj, stream, Dumper=dumper, encoding=None)
+
+ def _dump_string(self, obj, dumper=None):
+ """Dump to a py2-unicode or py3-string"""
+- return yaml.dump(obj, Dumper=dumper)
++ if PY3:
++ return yaml.dump(obj, Dumper=dumper)
++ else:
++ return yaml.dump(obj, Dumper=dumper, encoding=None)
+
+ def _dump_load_cycle(self, obj):
+ # Each pass though a dump or load revs the 'generation'
+@@ -53,3 +62,63 @@ class YamlTestUtils(object):
+ # should be transitive, but...
+ self.assertEqual(obj_2, obj_3)
+ self.assertEqual(string_from_object_dump, string_from_object_dump_3)
++
++ def _old_dump_load_cycle(self, obj):
++ '''Dump the passed in object to yaml, load it back up, dump again, compare.'''
++ stream = io.StringIO()
++
++ yaml_string = self._dump_string(obj, dumper=AnsibleDumper)
++ self._dump_stream(obj, stream, dumper=AnsibleDumper)
++
++ yaml_string_from_stream = stream.getvalue()
++
++ # reset stream
++ stream.seek(0)
++
++ loader = self._loader(stream)
++ # loader = AnsibleLoader(stream, vault_password=self.vault_password)
++ obj_from_stream = loader.get_data()
++
++ stream_from_string = io.StringIO(yaml_string)
++ loader2 = self._loader(stream_from_string)
++ # loader2 = AnsibleLoader(stream_from_string, vault_password=self.vault_password)
++ obj_from_string = loader2.get_data()
++
++ stream_obj_from_stream = io.StringIO()
++ stream_obj_from_string = io.StringIO()
++
++ if PY3:
++ yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper)
++ yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper)
++ else:
++ yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper, encoding=None)
++ yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper, encoding=None)
++
++ yaml_string_stream_obj_from_stream = stream_obj_from_stream.getvalue()
++ yaml_string_stream_obj_from_string = stream_obj_from_string.getvalue()
++
++ stream_obj_from_stream.seek(0)
++ stream_obj_from_string.seek(0)
++
++ if PY3:
++ yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper)
++ yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper)
++ else:
++ yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper, encoding=None)
++ yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper, encoding=None)
++
++ assert yaml_string == yaml_string_obj_from_stream
++ assert yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string
++ assert (yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string == yaml_string_stream_obj_from_stream ==
++ yaml_string_stream_obj_from_string)
++ assert obj == obj_from_stream
++ assert obj == obj_from_string
++ assert obj == yaml_string_obj_from_stream
++ assert obj == yaml_string_obj_from_string
++ assert obj == obj_from_stream == obj_from_string == yaml_string_obj_from_stream == yaml_string_obj_from_string
++ return {'obj': obj,
++ 'yaml_string': yaml_string,
++ 'yaml_string_from_stream': yaml_string_from_stream,
++ 'obj_from_stream': obj_from_stream,
++ 'obj_from_string': obj_from_string,
++ 'yaml_string_obj_from_string': yaml_string_obj_from_string}
+--- ansible-core-2.16.5.orig/test/units/module_utils/basic/test__symbolic_mode_to_octal.py
++++ ansible-core-2.16.5/test/units/module_utils/basic/test__symbolic_mode_to_octal.py
+@@ -63,14 +63,6 @@ DATA = ( # Going from no permissions to
+ # Multiple permissions
+ (0o040000, u'u=rw-x+X,g=r-x+X,o=r-x+X', 0o0755),
+ (0o100000, u'u=rw-x+X,g=r-x+X,o=r-x+X', 0o0644),
+- (0o040000, u'ug=rx,o=', 0o0550),
+- (0o100000, u'ug=rx,o=', 0o0550),
+- (0o040000, u'u=rx,g=r', 0o0540),
+- (0o100000, u'u=rx,g=r', 0o0540),
+- (0o040777, u'ug=rx,o=', 0o0550),
+- (0o100777, u'ug=rx,o=', 0o0550),
+- (0o040777, u'u=rx,g=r', 0o0547),
+- (0o100777, u'u=rx,g=r', 0o0547),
+ )
+
+ UMASK_DATA = (
+--- ansible-core-2.16.5.orig/test/units/module_utils/basic/test_argument_spec.py
++++ ansible-core-2.16.5/test/units/module_utils/basic/test_argument_spec.py
+@@ -453,7 +453,7 @@ class TestComplexOptions:
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
+ ),
+ # Check for elements in sub-options
+- ({"foobar": [{"foo": "good", "bam": "required_one_of", "bar1": [1, "good", "yes"], "bar2": ['1', 1], "bar3": ['1.3', 1.3, 1]}]},
++ ({"foobar": [{"foo": "good", "bam": "required_one_of", "bar1": [1, "good", "yes"], "bar2": ['1', 1], "bar3":['1.3', 1.3, 1]}]},
+ [{'foo': 'good', 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': None, 'baz': None, 'bam': 'required_one_of',
+ 'bar1': ["1", "good", "yes"], 'bar2': [1, 1], 'bar3': [1.3, 1.3, 1.0], 'bar4': None}]
+ ),
+--- ansible-core-2.16.5.orig/test/units/module_utils/basic/test_command_nonexisting.py
++++ ansible-core-2.16.5/test/units/module_utils/basic/test_command_nonexisting.py
+@@ -1,11 +1,14 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
++import sys
++import pytest
+ import json
+ import sys
+ import pytest
+ import subprocess
+-from ansible.module_utils.common.text.converters import to_bytes
++import ansible.module_utils.basic
++from ansible.module_utils._text import to_bytes
+ from ansible.module_utils import basic
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/basic/test_filesystem.py
++++ ansible-core-2.16.5/test/units/module_utils/basic/test_filesystem.py
+@@ -143,8 +143,6 @@ class TestOtherFilesystem(ModuleTestCase
+ argument_spec=dict(),
+ )
+
+- am.selinux_enabled = lambda: False
+-
+ file_args = {
+ 'path': '/path/to/file',
+ 'mode': None,
+--- ansible-core-2.16.5.orig/test/units/module_utils/basic/test_run_command.py
++++ ansible-core-2.16.5/test/units/module_utils/basic/test_run_command.py
+@@ -12,7 +12,7 @@ from io import BytesIO
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.six import PY2
+ from ansible.module_utils.compat import selectors
+
+@@ -109,7 +109,7 @@ def mock_subprocess(mocker):
+ super(MockSelector, self).close()
+ self._file_objs = []
+
+- selectors.PollSelector = MockSelector
++ selectors.DefaultSelector = MockSelector
+
+ subprocess = mocker.patch('ansible.module_utils.basic.subprocess')
+ subprocess._output = {mocker.sentinel.stdout: SpecialBytesIO(b'', fh=mocker.sentinel.stdout),
+@@ -194,7 +194,7 @@ class TestRunCommandPrompt:
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_prompt_no_match(self, mocker, rc_am):
+ rc_am._os._cmd_out[mocker.sentinel.stdout] = BytesIO(b'hello')
+- (rc, stdout, stderr) = rc_am.run_command('foo', prompt_regex='[pP]assword:')
++ (rc, _, _) = rc_am.run_command('foo', prompt_regex='[pP]assword:')
+ assert rc == 0
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+@@ -204,7 +204,7 @@ class TestRunCommandPrompt:
+ fh=mocker.sentinel.stdout),
+ mocker.sentinel.stderr:
+ SpecialBytesIO(b'', fh=mocker.sentinel.stderr)}
+- (rc, stdout, stderr) = rc_am.run_command('foo', prompt_regex=r'[pP]assword:', data=None)
++ (rc, _, _) = rc_am.run_command('foo', prompt_regex=r'[pP]assword:', data=None)
+ assert rc == 257
+
+
+@@ -212,7 +212,7 @@ class TestRunCommandRc:
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_check_rc_false(self, rc_am):
+ rc_am._subprocess.Popen.return_value.returncode = 1
+- (rc, stdout, stderr) = rc_am.run_command('/bin/false', check_rc=False)
++ (rc, _, _) = rc_am.run_command('/bin/false', check_rc=False)
+ assert rc == 1
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+--- ansible-core-2.16.5.orig/test/units/module_utils/basic/test_safe_eval.py
++++ ansible-core-2.16.5/test/units/module_utils/basic/test_safe_eval.py
+@@ -67,4 +67,4 @@ def test_invalid_strings_with_exceptions
+ if exception is None:
+ assert res[1] == exception
+ else:
+- assert isinstance(res[1], exception)
++ assert type(res[1]) == exception
+--- ansible-core-2.16.5.orig/test/units/module_utils/basic/test_sanitize_keys.py
++++ ansible-core-2.16.5/test/units/module_utils/basic/test_sanitize_keys.py
+@@ -6,6 +6,7 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
++import pytest
+ from ansible.module_utils.basic import sanitize_keys
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/basic/test_selinux.py
++++ ansible-core-2.16.5/test/units/module_utils/basic/test_selinux.py
+@@ -43,21 +43,16 @@ class TestSELinuxMU:
+ with patch.object(basic, 'HAVE_SELINUX', False):
+ assert no_args_module().selinux_enabled() is False
+
+- # test selinux present/not-enabled
+- disabled_mod = no_args_module()
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- selinux.is_selinux_enabled.return_value = 0
+- assert disabled_mod.selinux_enabled() is False
+-
++ # test selinux present/not-enabled
++ disabled_mod = no_args_module()
++ with patch('ansible.module_utils.compat.selinux.is_selinux_enabled', return_value=0):
++ assert disabled_mod.selinux_enabled() is False
+ # ensure value is cached (same answer after unpatching)
+ assert disabled_mod.selinux_enabled() is False
+-
+ # and present / enabled
+- with patch.object(basic, 'HAVE_SELINUX', True):
+- enabled_mod = no_args_module()
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- selinux.is_selinux_enabled.return_value = 1
+- assert enabled_mod.selinux_enabled() is True
++ enabled_mod = no_args_module()
++ with patch('ansible.module_utils.compat.selinux.is_selinux_enabled', return_value=1):
++ assert enabled_mod.selinux_enabled() is True
+ # ensure value is cached (same answer after unpatching)
+ assert enabled_mod.selinux_enabled() is True
+
+@@ -65,16 +60,12 @@ class TestSELinuxMU:
+ # selinux unavailable, should return false
+ with patch.object(basic, 'HAVE_SELINUX', False):
+ assert no_args_module().selinux_mls_enabled() is False
+- # selinux disabled, should return false
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- selinux.is_selinux_mls_enabled.return_value = 0
+- assert no_args_module(selinux_enabled=False).selinux_mls_enabled() is False
+-
+- with patch.object(basic, 'HAVE_SELINUX', True):
+- # selinux enabled, should pass through the value of is_selinux_mls_enabled
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- selinux.is_selinux_mls_enabled.return_value = 1
+- assert no_args_module(selinux_enabled=True).selinux_mls_enabled() is True
++ # selinux disabled, should return false
++ with patch('ansible.module_utils.compat.selinux.is_selinux_mls_enabled', return_value=0):
++ assert no_args_module(selinux_enabled=False).selinux_mls_enabled() is False
++ # selinux enabled, should pass through the value of is_selinux_mls_enabled
++ with patch('ansible.module_utils.compat.selinux.is_selinux_mls_enabled', return_value=1):
++ assert no_args_module(selinux_enabled=True).selinux_mls_enabled() is True
+
+ def test_selinux_initial_context(self):
+ # selinux missing/disabled/enabled sans MLS is 3-element None
+@@ -89,19 +80,16 @@ class TestSELinuxMU:
+ assert no_args_module().selinux_default_context(path='/foo/bar') == [None, None, None]
+
+ am = no_args_module(selinux_enabled=True, selinux_mls_enabled=True)
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- # matchpathcon success
+- selinux.matchpathcon.return_value = [0, 'unconfined_u:object_r:default_t:s0']
++ # matchpathcon success
++ with patch('ansible.module_utils.compat.selinux.matchpathcon', return_value=[0, 'unconfined_u:object_r:default_t:s0']):
+ assert am.selinux_default_context(path='/foo/bar') == ['unconfined_u', 'object_r', 'default_t', 's0']
+
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- # matchpathcon fail (return initial context value)
+- selinux.matchpathcon.return_value = [-1, '']
++ # matchpathcon fail (return initial context value)
++ with patch('ansible.module_utils.compat.selinux.matchpathcon', return_value=[-1, '']):
+ assert am.selinux_default_context(path='/foo/bar') == [None, None, None, None]
+
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- # matchpathcon OSError
+- selinux.matchpathcon.side_effect = OSError
++ # matchpathcon OSError
++ with patch('ansible.module_utils.compat.selinux.matchpathcon', side_effect=OSError):
+ assert am.selinux_default_context(path='/foo/bar') == [None, None, None, None]
+
+ def test_selinux_context(self):
+@@ -111,23 +99,19 @@ class TestSELinuxMU:
+
+ am = no_args_module(selinux_enabled=True, selinux_mls_enabled=True)
+ # lgetfilecon_raw passthru
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- selinux.lgetfilecon_raw.return_value = [0, 'unconfined_u:object_r:default_t:s0']
++ with patch('ansible.module_utils.compat.selinux.lgetfilecon_raw', return_value=[0, 'unconfined_u:object_r:default_t:s0']):
+ assert am.selinux_context(path='/foo/bar') == ['unconfined_u', 'object_r', 'default_t', 's0']
+
+ # lgetfilecon_raw returned a failure
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- selinux.lgetfilecon_raw.return_value = [-1, '']
++ with patch('ansible.module_utils.compat.selinux.lgetfilecon_raw', return_value=[-1, '']):
+ assert am.selinux_context(path='/foo/bar') == [None, None, None, None]
+
+ # lgetfilecon_raw OSError (should bomb the module)
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- selinux.lgetfilecon_raw.side_effect = OSError(errno.ENOENT, 'NotFound')
++ with patch('ansible.module_utils.compat.selinux.lgetfilecon_raw', side_effect=OSError(errno.ENOENT, 'NotFound')):
+ with pytest.raises(SystemExit):
+ am.selinux_context(path='/foo/bar')
+
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- selinux.lgetfilecon_raw.side_effect = OSError()
++ with patch('ansible.module_utils.compat.selinux.lgetfilecon_raw', side_effect=OSError()):
+ with pytest.raises(SystemExit):
+ am.selinux_context(path='/foo/bar')
+
+@@ -182,29 +166,25 @@ class TestSELinuxMU:
+ am.selinux_context = lambda path: ['bar_u', 'bar_r', None, None]
+ am.is_special_selinux_path = lambda path: (False, None)
+
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- selinux.lsetfilecon.return_value = 0
++ with patch('ansible.module_utils.compat.selinux.lsetfilecon', return_value=0) as m:
+ assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is True
+- selinux.lsetfilecon.assert_called_with('/path/to/file', 'foo_u:foo_r:foo_t:s0')
+- selinux.lsetfilecon.reset_mock()
++ m.assert_called_with('/path/to/file', 'foo_u:foo_r:foo_t:s0')
++ m.reset_mock()
+ am.check_mode = True
+ assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is True
+- assert not selinux.lsetfilecon.called
++ assert not m.called
+ am.check_mode = False
+
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- selinux.lsetfilecon.return_value = 1
++ with patch('ansible.module_utils.compat.selinux.lsetfilecon', return_value=1):
+ with pytest.raises(SystemExit):
+ am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], True)
+
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- selinux.lsetfilecon.side_effect = OSError
++ with patch('ansible.module_utils.compat.selinux.lsetfilecon', side_effect=OSError):
+ with pytest.raises(SystemExit):
+ am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], True)
+
+ am.is_special_selinux_path = lambda path: (True, ['sp_u', 'sp_r', 'sp_t', 's0'])
+
+- with patch.object(basic, 'selinux', create=True) as selinux:
+- selinux.lsetfilecon.return_value = 0
++ with patch('ansible.module_utils.compat.selinux.lsetfilecon', return_value=0) as m:
+ assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is True
+- selinux.lsetfilecon.assert_called_with('/path/to/file', 'sp_u:sp_r:sp_t:s0')
++ m.assert_called_with('/path/to/file', 'sp_u:sp_r:sp_t:s0')
+--- ansible-core-2.16.5.orig/test/units/module_utils/basic/test_set_cwd.py
++++ ansible-core-2.16.5/test/units/module_utils/basic/test_set_cwd.py
+@@ -8,10 +8,13 @@ __metaclass__ = type
+
+ import json
+ import os
++import shutil
+ import tempfile
+
+-from units.compat.mock import patch
+-from ansible.module_utils.common.text.converters import to_bytes
++import pytest
++
++from units.compat.mock import patch, MagicMock
++from ansible.module_utils._text import to_bytes
+
+ from ansible.module_utils import basic
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/basic/test_tmpdir.py
++++ ansible-core-2.16.5/test/units/module_utils/basic/test_tmpdir.py
+@@ -14,7 +14,7 @@ import tempfile
+ import pytest
+
+ from units.compat.mock import patch, MagicMock
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+
+ from ansible.module_utils import basic
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/arg_spec/test_aliases.py
++++ ansible-core-2.16.5/test/units/module_utils/common/arg_spec/test_aliases.py
+@@ -9,6 +9,7 @@ import pytest
+
+ from ansible.module_utils.errors import AnsibleValidationError, AnsibleValidationErrorMultiple
+ from ansible.module_utils.common.arg_spec import ArgumentSpecValidator, ValidationResult
++from ansible.module_utils.common.warnings import get_deprecation_messages, get_warning_messages
+
+ # id, argument spec, parameters, expected parameters, deprecation, warning
+ ALIAS_TEST_CASES = [
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/parameters/test_handle_aliases.py
++++ ansible-core-2.16.5/test/units/module_utils/common/parameters/test_handle_aliases.py
+@@ -9,7 +9,7 @@ __metaclass__ = type
+ import pytest
+
+ from ansible.module_utils.common.parameters import _handle_aliases
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+
+ def test_handle_aliases_no_aliases():
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/parameters/test_list_deprecations.py
++++ ansible-core-2.16.5/test/units/module_utils/common/parameters/test_list_deprecations.py
+@@ -5,10 +5,21 @@
+ from __future__ import absolute_import, division, print_function
+ __metaclass__ = type
+
++import pytest
+
+ from ansible.module_utils.common.parameters import _list_deprecations
+
+
++@pytest.fixture
++def params():
++ return {
++ 'name': 'bob',
++ 'dest': '/etc/hosts',
++ 'state': 'present',
++ 'value': 5,
++ }
++
++
+ def test_list_deprecations():
+ argument_spec = {
+ 'old': {'type': 'str', 'removed_in_version': '2.5'},
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/test_collections.py
++++ ansible-core-2.16.5/test/units/module_utils/common/test_collections.py
+@@ -8,7 +8,8 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.six.moves.collections_abc import Sequence
++from ansible.module_utils.six import Iterator
++from ansible.module_utils.common._collections_compat import Sequence
+ from ansible.module_utils.common.collections import ImmutableDict, is_iterable, is_sequence
+
+
+@@ -24,6 +25,16 @@ class SeqStub:
+ Sequence.register(SeqStub)
+
+
++class IteratorStub(Iterator):
++ def __next__(self):
++ raise StopIteration
++
++
++class IterableStub:
++ def __iter__(self):
++ return IteratorStub()
++
++
+ class FakeAnsibleVaultEncryptedUnicode(Sequence):
+ __ENCRYPTED__ = True
+
+@@ -31,10 +42,10 @@ class FakeAnsibleVaultEncryptedUnicode(S
+ self.data = data
+
+ def __getitem__(self, index):
+- raise NotImplementedError() # pragma: nocover
++ return self.data[index]
+
+ def __len__(self):
+- raise NotImplementedError() # pragma: nocover
++ return len(self.data)
+
+
+ TEST_STRINGS = u'he', u'Україна', u'Česká republika'
+@@ -82,14 +93,14 @@ def test_sequence_string_types_without_s
+
+ @pytest.mark.parametrize(
+ 'seq',
+- ([], (), {}, set(), frozenset()),
++ ([], (), {}, set(), frozenset(), IterableStub()),
+ )
+ def test_iterable_positive(seq):
+ assert is_iterable(seq)
+
+
+ @pytest.mark.parametrize(
+- 'seq', (object(), 5, 9.)
++ 'seq', (IteratorStub(), object(), 5, 9.)
+ )
+ def test_iterable_negative(seq):
+ assert not is_iterable(seq)
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/text/converters/test_json_encode_fallback.py
++++ ansible-core-2.16.5/test/units/module_utils/common/text/converters/test_json_encode_fallback.py
+@@ -20,6 +20,12 @@ class timezone(tzinfo):
+ def utcoffset(self, dt):
+ return self._offset
+
++ def dst(self, dt):
++ return timedelta(0)
++
++ def tzname(self, dt):
++ return None
++
+
+ @pytest.mark.parametrize(
+ 'test_input,expected',
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_missing_parameters.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_missing_parameters.py
+@@ -8,10 +8,16 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
++from ansible.module_utils.common.validation import check_required_one_of
+ from ansible.module_utils.common.validation import check_missing_parameters
+
+
++@pytest.fixture
++def arguments_terms():
++ return {"path": ""}
++
++
+ def test_check_missing_parameters():
+ assert check_missing_parameters([], {}) == []
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_mutually_exclusive.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_mutually_exclusive.py
+@@ -7,7 +7,7 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.validation import check_mutually_exclusive
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_required_arguments.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_required_arguments.py
+@@ -7,7 +7,7 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.validation import check_required_arguments
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_required_by.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_required_by.py
+@@ -8,7 +8,7 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.validation import check_required_by
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_required_if.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_required_if.py
+@@ -8,7 +8,7 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.validation import check_required_if
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_required_one_of.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_required_one_of.py
+@@ -8,7 +8,7 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.validation import check_required_one_of
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_required_together.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_required_together.py
+@@ -7,7 +7,7 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.validation import check_required_together
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_type_bits.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_type_bits.py
+@@ -7,7 +7,7 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.validation import check_type_bits
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_type_bool.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_type_bool.py
+@@ -7,7 +7,7 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.validation import check_type_bool
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_type_bytes.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_type_bytes.py
+@@ -7,7 +7,7 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.validation import check_type_bytes
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_type_float.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_type_float.py
+@@ -7,7 +7,7 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.validation import check_type_float
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_type_int.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_type_int.py
+@@ -7,7 +7,7 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.validation import check_type_int
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_type_jsonarg.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_type_jsonarg.py
+@@ -7,7 +7,7 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.validation import check_type_jsonarg
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/common/validation/test_check_type_str.py
++++ ansible-core-2.16.5/test/units/module_utils/common/validation/test_check_type_str.py
+@@ -7,7 +7,7 @@ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.module_utils.common.validation import check_type_str
+
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/conftest.py
++++ ansible-core-2.16.5/test/units/module_utils/conftest.py
+@@ -12,8 +12,8 @@ import pytest
+
+ import ansible.module_utils.basic
+ from ansible.module_utils.six import PY3, string_types
+-from ansible.module_utils.common.text.converters import to_bytes
+-from ansible.module_utils.six.moves.collections_abc import MutableMapping
++from ansible.module_utils._text import to_bytes
++from ansible.module_utils.common._collections_compat import MutableMapping
+
+
+ @pytest.fixture
+--- ansible-core-2.16.5.orig/test/units/module_utils/facts/base.py
++++ ansible-core-2.16.5/test/units/module_utils/facts/base.py
+@@ -48,9 +48,6 @@ class BaseFactsTest(unittest.TestCase):
+ @patch('platform.system', return_value='Linux')
+ @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value='systemd')
+ def test_collect(self, mock_gfc, mock_ps):
+- self._test_collect()
+-
+- def _test_collect(self):
+ module = self._mock_module()
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module, collected_facts=self.collected_facts)
+@@ -65,3 +62,4 @@ class BaseFactsTest(unittest.TestCase):
+ facts_dict = fact_collector.collect_with_namespace(module=module,
+ collected_facts=self.collected_facts)
+ self.assertIsInstance(facts_dict, dict)
++ return facts_dict
+--- ansible-core-2.16.5.orig/test/units/module_utils/facts/hardware/linux_data.py
++++ ansible-core-2.16.5/test/units/module_utils/facts/hardware/linux_data.py
+@@ -18,12 +18,6 @@ __metaclass__ = type
+
+ import os
+
+-
+-def read_lines(path):
+- with open(path) as file:
+- return file.readlines()
+-
+-
+ LSBLK_OUTPUT = b"""
+ /dev/sda
+ /dev/sda1 32caaec3-ef40-4691-a3b6-438c3f9bc1c0
+@@ -374,7 +368,7 @@ CPU_INFO_TEST_SCENARIOS = [
+ 'architecture': 'armv61',
+ 'nproc_out': 1,
+ 'sched_getaffinity': set([0]),
+- 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv6-rev7-1cpu-cpuinfo')),
++ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv6-rev7-1cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': ['0', 'ARMv6-compatible processor rev 7 (v6l)'],
+ 'processor_cores': 1,
+@@ -387,7 +381,7 @@ CPU_INFO_TEST_SCENARIOS = [
+ 'architecture': 'armv71',
+ 'nproc_out': 4,
+ 'sched_getaffinity': set([0, 1, 2, 3]),
+- 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv7-rev4-4cpu-cpuinfo')),
++ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv7-rev4-4cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': [
+ '0', 'ARMv7 Processor rev 4 (v7l)',
+@@ -405,7 +399,7 @@ CPU_INFO_TEST_SCENARIOS = [
+ 'architecture': 'aarch64',
+ 'nproc_out': 4,
+ 'sched_getaffinity': set([0, 1, 2, 3]),
+- 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/aarch64-4cpu-cpuinfo')),
++ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/aarch64-4cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': [
+ '0', 'AArch64 Processor rev 4 (aarch64)',
+@@ -423,7 +417,7 @@ CPU_INFO_TEST_SCENARIOS = [
+ 'architecture': 'x86_64',
+ 'nproc_out': 4,
+ 'sched_getaffinity': set([0, 1, 2, 3]),
+- 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-4cpu-cpuinfo')),
++ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-4cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': [
+ '0', 'AuthenticAMD', 'Dual-Core AMD Opteron(tm) Processor 2216',
+@@ -441,7 +435,7 @@ CPU_INFO_TEST_SCENARIOS = [
+ 'architecture': 'x86_64',
+ 'nproc_out': 4,
+ 'sched_getaffinity': set([0, 1, 2, 3]),
+- 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-8cpu-cpuinfo')),
++ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-8cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': [
+ '0', 'GenuineIntel', 'Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz',
+@@ -463,7 +457,7 @@ CPU_INFO_TEST_SCENARIOS = [
+ 'architecture': 'arm64',
+ 'nproc_out': 4,
+ 'sched_getaffinity': set([0, 1, 2, 3]),
+- 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/arm64-4cpu-cpuinfo')),
++ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/arm64-4cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': ['0', '1', '2', '3'],
+ 'processor_cores': 1,
+@@ -476,7 +470,7 @@ CPU_INFO_TEST_SCENARIOS = [
+ 'architecture': 'armv71',
+ 'nproc_out': 8,
+ 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7]),
+- 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv7-rev3-8cpu-cpuinfo')),
++ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv7-rev3-8cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': [
+ '0', 'ARMv7 Processor rev 3 (v7l)',
+@@ -498,7 +492,7 @@ CPU_INFO_TEST_SCENARIOS = [
+ 'architecture': 'x86_64',
+ 'nproc_out': 2,
+ 'sched_getaffinity': set([0, 1]),
+- 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-2cpu-cpuinfo')),
++ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-2cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': [
+ '0', 'GenuineIntel', 'Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz',
+@@ -511,7 +505,7 @@ CPU_INFO_TEST_SCENARIOS = [
+ 'processor_vcpus': 2},
+ },
+ {
+- 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/ppc64-power7-rhel7-8cpu-cpuinfo')),
++ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/ppc64-power7-rhel7-8cpu-cpuinfo')).readlines(),
+ 'architecture': 'ppc64',
+ 'nproc_out': 8,
+ 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7]),
+@@ -534,7 +528,7 @@ CPU_INFO_TEST_SCENARIOS = [
+ },
+ },
+ {
+- 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/ppc64le-power8-24cpu-cpuinfo')),
++ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/ppc64le-power8-24cpu-cpuinfo')).readlines(),
+ 'architecture': 'ppc64le',
+ 'nproc_out': 24,
+ 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]),
+@@ -573,41 +567,7 @@ CPU_INFO_TEST_SCENARIOS = [
+ },
+ },
+ {
+- 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/s390x-z13-2cpu-cpuinfo')),
+- 'architecture': 's390x',
+- 'nproc_out': 2,
+- 'sched_getaffinity': set([0, 1]),
+- 'expected_result': {
+- 'processor': [
+- 'IBM/S390',
+- ],
+- 'processor_cores': 2,
+- 'processor_count': 1,
+- 'processor_nproc': 2,
+- 'processor_threads_per_core': 1,
+- 'processor_vcpus': 2
+- },
+- },
+- {
+- 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/s390x-z14-64cpu-cpuinfo')),
+- 'architecture': 's390x',
+- 'nproc_out': 64,
+- 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
+- 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
+- 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]),
+- 'expected_result': {
+- 'processor': [
+- 'IBM/S390',
+- ],
+- 'processor_cores': 32,
+- 'processor_count': 1,
+- 'processor_nproc': 64,
+- 'processor_threads_per_core': 2,
+- 'processor_vcpus': 64
+- },
+- },
+- {
+- 'cpuinfo': read_lines(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/sparc-t5-debian-ldom-24vcpu')),
++ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/sparc-t5-debian-ldom-24vcpu')).readlines(),
+ 'architecture': 'sparc64',
+ 'nproc_out': 24,
+ 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]),
+--- ansible-core-2.16.5.orig/test/units/module_utils/facts/hardware/test_linux_get_cpu_info.py
++++ ansible-core-2.16.5/test/units/module_utils/facts/hardware/test_linux_get_cpu_info.py
+@@ -45,7 +45,7 @@ def test_get_cpu_info_missing_arch(mocke
+ module = mocker.Mock()
+ inst = linux.LinuxHardware(module)
+
+- # ARM, Power, and zSystems will report incorrect processor count if architecture is not available
++ # ARM and Power will report incorrect processor count if architecture is not available
+ mocker.patch('os.path.exists', return_value=False)
+ mocker.patch('os.access', return_value=True)
+ for test in CPU_INFO_TEST_SCENARIOS:
+@@ -56,7 +56,7 @@ def test_get_cpu_info_missing_arch(mocke
+
+ test_result = inst.get_cpu_facts()
+
+- if test['architecture'].startswith(('armv', 'aarch', 'ppc', 's390')):
++ if test['architecture'].startswith(('armv', 'aarch', 'ppc')):
+ assert test['expected_result'] != test_result
+ else:
+ assert test['expected_result'] == test_result
+--- ansible-core-2.16.5.orig/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_ClearLinux.py
++++ ansible-core-2.16.5/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_ClearLinux.py
+@@ -21,8 +21,7 @@ def test_input():
+
+
+ def test_parse_distribution_file_clear_linux(mock_module, test_input):
+- with open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files/ClearLinux')) as file:
+- test_input['data'] = file.read()
++ test_input['data'] = open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files/ClearLinux')).read()
+
+ result = (
+ True,
+@@ -44,8 +43,7 @@ def test_parse_distribution_file_clear_l
+ Test against data from Linux Mint and CoreOS to ensure we do not get a reported
+ match from parse_distribution_file_ClearLinux()
+ """
+- with open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files', distro_file)) as file:
+- test_input['data'] = file.read()
++ test_input['data'] = open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files', distro_file)).read()
+
+ result = (False, {})
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_Slackware.py
++++ ansible-core-2.16.5/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_Slackware.py
+@@ -19,12 +19,9 @@ from ansible.module_utils.facts.system.d
+ )
+ )
+ def test_parse_distribution_file_slackware(mock_module, distro_file, expected_version):
+- with open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files', distro_file)) as file:
+- data = file.read()
+-
+ test_input = {
+ 'name': 'Slackware',
+- 'data': data,
++ 'data': open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files', distro_file)).read(),
+ 'path': '/etc/os-release',
+ 'collected_facts': None,
+ }
+--- ansible-core-2.16.5.orig/test/units/module_utils/facts/test_collectors.py
++++ ansible-core-2.16.5/test/units/module_utils/facts/test_collectors.py
+@@ -93,7 +93,7 @@ class TestApparmorFacts(BaseFactsTest):
+ collector_class = ApparmorFactCollector
+
+ def test_collect(self):
+- facts_dict = super(TestApparmorFacts, self)._test_collect()
++ facts_dict = super(TestApparmorFacts, self).test_collect()
+ self.assertIn('status', facts_dict['apparmor'])
+
+
+@@ -191,7 +191,7 @@ class TestEnvFacts(BaseFactsTest):
+ collector_class = EnvFactCollector
+
+ def test_collect(self):
+- facts_dict = super(TestEnvFacts, self)._test_collect()
++ facts_dict = super(TestEnvFacts, self).test_collect()
+ self.assertIn('HOME', facts_dict['env'])
+
+
+@@ -355,6 +355,7 @@ class TestSelinuxFacts(BaseFactsTest):
+ facts_dict = fact_collector.collect(module=module)
+ self.assertIsInstance(facts_dict, dict)
+ self.assertEqual(facts_dict['selinux']['status'], 'Missing selinux Python library')
++ return facts_dict
+
+
+ class TestServiceMgrFacts(BaseFactsTest):
+--- ansible-core-2.16.5.orig/test/units/module_utils/facts/test_date_time.py
++++ ansible-core-2.16.5/test/units/module_utils/facts/test_date_time.py
+@@ -10,27 +10,28 @@ import datetime
+ import string
+ import time
+
+-from ansible.module_utils.compat.datetime import UTC
+ from ansible.module_utils.facts.system import date_time
+
+ EPOCH_TS = 1594449296.123456
+ DT = datetime.datetime(2020, 7, 11, 12, 34, 56, 124356)
+-UTC_DT = datetime.datetime(2020, 7, 11, 2, 34, 56, 124356)
++DT_UTC = datetime.datetime(2020, 7, 11, 2, 34, 56, 124356)
+
+
+ @pytest.fixture
+ def fake_now(monkeypatch):
+ """
+- Patch `datetime.datetime.fromtimestamp()`,
++ Patch `datetime.datetime.fromtimestamp()`, `datetime.datetime.utcfromtimestamp()`,
+ and `time.time()` to return deterministic values.
+ """
+
+ class FakeNow:
+ @classmethod
+- def fromtimestamp(cls, timestamp, tz=None):
+- if tz == UTC:
+- return UTC_DT.replace(tzinfo=tz)
+- return DT.replace(tzinfo=tz)
++ def fromtimestamp(cls, timestamp):
++ return DT
++
++ @classmethod
++ def utcfromtimestamp(cls, timestamp):
++ return DT_UTC
+
+ def _time():
+ return EPOCH_TS
+--- ansible-core-2.16.5.orig/test/units/module_utils/facts/test_sysctl.py
++++ ansible-core-2.16.5/test/units/module_utils/facts/test_sysctl.py
+@@ -20,9 +20,13 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
++import os
++
++import pytest
++
+ # for testing
+ from units.compat import unittest
+-from units.compat.mock import MagicMock
++from units.compat.mock import patch, MagicMock, mock_open, Mock
+
+ from ansible.module_utils.facts.sysctl import get_sysctl
+
+--- ansible-core-2.16.5.orig/test/units/module_utils/facts/test_timeout.py
++++ ansible-core-2.16.5/test/units/module_utils/facts/test_timeout.py
+@@ -139,7 +139,7 @@ def function_other_timeout():
+
+ @timeout.timeout(1)
+ def function_raises():
+- return 1 / 0
++ 1 / 0
+
+
+ @timeout.timeout(1)
+--- ansible-core-2.16.5.orig/test/units/module_utils/urls/test_Request.py
++++ ansible-core-2.16.5/test/units/module_utils/urls/test_Request.py
+@@ -33,7 +33,6 @@ def install_opener_mock(mocker):
+ def test_Request_fallback(urlopen_mock, install_opener_mock, mocker):
+ here = os.path.dirname(__file__)
+ pem = os.path.join(here, 'fixtures/client.pem')
+- client_key = os.path.join(here, 'fixtures/client.key')
+
+ cookies = cookiejar.CookieJar()
+ request = Request(
+@@ -47,8 +46,8 @@ def test_Request_fallback(urlopen_mock,
+ http_agent='ansible-tests',
+ force_basic_auth=True,
+ follow_redirects='all',
+- client_cert=pem,
+- client_key=client_key,
++ client_cert='/tmp/client.pem',
++ client_key='/tmp/client.key',
+ cookies=cookies,
+ unix_socket='/foo/bar/baz.sock',
+ ca_path=pem,
+@@ -69,8 +68,8 @@ def test_Request_fallback(urlopen_mock,
+ call(None, 'ansible-tests'), # http_agent
+ call(None, True), # force_basic_auth
+ call(None, 'all'), # follow_redirects
+- call(None, pem), # client_cert
+- call(None, client_key), # client_key
++ call(None, '/tmp/client.pem'), # client_cert
++ call(None, '/tmp/client.key'), # client_key
+ call(None, cookies), # cookies
+ call(None, '/foo/bar/baz.sock'), # unix_socket
+ call(None, pem), # ca_path
+@@ -359,7 +358,10 @@ def test_Request_open_client_cert(urlope
+ assert ssl_handler.client_cert == client_cert
+ assert ssl_handler.client_key == client_key
+
+- ssl_handler._build_https_connection('ansible.com')
++ https_connection = ssl_handler._build_https_connection('ansible.com')
++
++ assert https_connection.key_file == client_key
++ assert https_connection.cert_file == client_cert
+
+
+ def test_Request_open_cookies(urlopen_mock, install_opener_mock):
+--- ansible-core-2.16.5.orig/test/units/module_utils/urls/test_fetch_file.py
++++ ansible-core-2.16.5/test/units/module_utils/urls/test_fetch_file.py
+@@ -10,6 +10,7 @@ import os
+ from ansible.module_utils.urls import fetch_file
+
+ import pytest
++from units.compat.mock import MagicMock
+
+
+ class FakeTemporaryFile:
+--- ansible-core-2.16.5.orig/test/units/module_utils/urls/test_prepare_multipart.py
++++ ansible-core-2.16.5/test/units/module_utils/urls/test_prepare_multipart.py
+@@ -7,6 +7,8 @@ __metaclass__ = type
+
+ import os
+
++from io import StringIO
++
+ from email.message import Message
+
+ import pytest
+--- ansible-core-2.16.5.orig/test/units/module_utils/urls/test_urls.py
++++ ansible-core-2.16.5/test/units/module_utils/urls/test_urls.py
+@@ -6,7 +6,7 @@ from __future__ import absolute_import,
+ __metaclass__ = type
+
+ from ansible.module_utils import urls
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+ import pytest
+
+--- ansible-core-2.16.5.orig/test/units/modules/conftest.py
++++ ansible-core-2.16.5/test/units/modules/conftest.py
+@@ -8,15 +8,24 @@ import json
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils.six import string_types
++from ansible.module_utils._text import to_bytes
++from ansible.module_utils.common._collections_compat import MutableMapping
+
+
+ @pytest.fixture
+ def patch_ansible_module(request, mocker):
+- request.param = {'ANSIBLE_MODULE_ARGS': request.param}
+- request.param['ANSIBLE_MODULE_ARGS']['_ansible_remote_tmp'] = '/tmp'
+- request.param['ANSIBLE_MODULE_ARGS']['_ansible_keep_remote_files'] = False
+-
+- args = json.dumps(request.param)
++ if isinstance(request.param, string_types):
++ args = request.param
++ elif isinstance(request.param, MutableMapping):
++ if 'ANSIBLE_MODULE_ARGS' not in request.param:
++ request.param = {'ANSIBLE_MODULE_ARGS': request.param}
++ if '_ansible_remote_tmp' not in request.param['ANSIBLE_MODULE_ARGS']:
++ request.param['ANSIBLE_MODULE_ARGS']['_ansible_remote_tmp'] = '/tmp'
++ if '_ansible_keep_remote_files' not in request.param['ANSIBLE_MODULE_ARGS']:
++ request.param['ANSIBLE_MODULE_ARGS']['_ansible_keep_remote_files'] = False
++ args = json.dumps(request.param)
++ else:
++ raise Exception('Malformed data to the patch_ansible_module pytest fixture')
+
+ mocker.patch('ansible.module_utils.basic._ANSIBLE_ARGS', to_bytes(args))
+--- ansible-core-2.16.5.orig/test/units/modules/test_apt.py
++++ ansible-core-2.16.5/test/units/modules/test_apt.py
+@@ -2,13 +2,20 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ import collections
++import sys
+
+ from units.compat.mock import Mock
+ from units.compat import unittest
+
+-from ansible.modules.apt import (
+- expand_pkgspec_from_fnmatches,
+-)
++try:
++ from ansible.modules.apt import (
++ expand_pkgspec_from_fnmatches,
++ )
++except Exception:
++ # Need some more module_utils work (porting urls.py) before we can test
++ # modules. So don't error out in this case.
++ if sys.version_info[0] >= 3:
++ pass
+
+
+ class AptExpandPkgspecTestCase(unittest.TestCase):
+@@ -22,25 +29,25 @@ class AptExpandPkgspecTestCase(unittest.
+ ]
+
+ def test_trivial(self):
+- pkg = ["apt"]
++ foo = ["apt"]
+ self.assertEqual(
+- expand_pkgspec_from_fnmatches(None, pkg, self.fake_cache), pkg)
++ expand_pkgspec_from_fnmatches(None, foo, self.fake_cache), foo)
+
+ def test_version_wildcard(self):
+- pkg = ["apt=1.0*"]
++ foo = ["apt=1.0*"]
+ self.assertEqual(
+- expand_pkgspec_from_fnmatches(None, pkg, self.fake_cache), pkg)
++ expand_pkgspec_from_fnmatches(None, foo, self.fake_cache), foo)
+
+ def test_pkgname_wildcard_version_wildcard(self):
+- pkg = ["apt*=1.0*"]
++ foo = ["apt*=1.0*"]
+ m_mock = Mock()
+ self.assertEqual(
+- expand_pkgspec_from_fnmatches(m_mock, pkg, self.fake_cache),
++ expand_pkgspec_from_fnmatches(m_mock, foo, self.fake_cache),
+ ['apt', 'apt-utils'])
+
+ def test_pkgname_expands(self):
+- pkg = ["apt*"]
++ foo = ["apt*"]
+ m_mock = Mock()
+ self.assertEqual(
+- expand_pkgspec_from_fnmatches(m_mock, pkg, self.fake_cache),
++ expand_pkgspec_from_fnmatches(m_mock, foo, self.fake_cache),
+ ["apt", "apt-utils"])
+--- ansible-core-2.16.5.orig/test/units/modules/test_async_wrapper.py
++++ ansible-core-2.16.5/test/units/modules/test_async_wrapper.py
+@@ -7,21 +7,26 @@ __metaclass__ = type
+ import os
+ import json
+ import shutil
+-import sys
+ import tempfile
+
++import pytest
++
++from units.compat.mock import patch, MagicMock
+ from ansible.modules import async_wrapper
+
++from pprint import pprint
++
+
+ class TestAsyncWrapper:
+
+ def test_run_module(self, monkeypatch):
+
+ def mock_get_interpreter(module_path):
+- return [sys.executable]
++ return ['/usr/bin/python']
+
+ module_result = {'rc': 0}
+ module_lines = [
++ '#!/usr/bin/python',
+ 'import sys',
+ 'sys.stderr.write("stderr stuff")',
+ "print('%s')" % json.dumps(module_result)
+--- ansible-core-2.16.5.orig/test/units/modules/test_copy.py
++++ ansible-core-2.16.5/test/units/modules/test_copy.py
+@@ -128,19 +128,16 @@ def test_split_pre_existing_dir_working_
+ #
+ # Info helpful for making new test cases:
+ #
+-# base_mode = {
+-# 'dir no perms': 0o040000,
+-# 'file no perms': 0o100000,
+-# 'dir all perms': 0o040000 | 0o777,
+-# 'file all perms': 0o100000 | 0o777}
++# base_mode = {'dir no perms': 0o040000,
++# 'file no perms': 0o100000,
++# 'dir all perms': 0o400000 | 0o777,
++# 'file all perms': 0o100000, | 0o777}
+ #
+-# perm_bits = {
+-# 'x': 0b001,
++# perm_bits = {'x': 0b001,
+ # 'w': 0b010,
+ # 'r': 0b100}
+ #
+-# role_shift = {
+-# 'u': 6,
++# role_shift = {'u': 6,
+ # 'g': 3,
+ # 'o': 0}
+
+@@ -175,10 +172,6 @@ DATA = ( # Going from no permissions to
+ # chmod a-X statfile <== removes execute from statfile
+ (0o100777, u'a-X', 0o0666),
+
+- # Verify X uses computed not original mode
+- (0o100777, u'a=,u=rX', 0o0400),
+- (0o040777, u'a=,u=rX', 0o0500),
+-
+ # Multiple permissions
+ (0o040000, u'u=rw-x+X,g=r-x+X,o=r-x+X', 0o0755),
+ (0o100000, u'u=rw-x+X,g=r-x+X,o=r-x+X', 0o0644),
+@@ -192,10 +185,6 @@ UMASK_DATA = (
+ INVALID_DATA = (
+ (0o040000, u'a=foo', "bad symbolic permission for mode: a=foo"),
+ (0o040000, u'f=rwx', "bad symbolic permission for mode: f=rwx"),
+- (0o100777, u'of=r', "bad symbolic permission for mode: of=r"),
+-
+- (0o100777, u'ao=r', "bad symbolic permission for mode: ao=r"),
+- (0o100777, u'oa=r', "bad symbolic permission for mode: oa=r"),
+ )
+
+
+--- ansible-core-2.16.5.orig/test/units/modules/test_hostname.py
++++ ansible-core-2.16.5/test/units/modules/test_hostname.py
+@@ -6,6 +6,7 @@ import shutil
+ import tempfile
+
+ from units.compat.mock import patch, MagicMock, mock_open
++from ansible.module_utils import basic
+ from ansible.module_utils.common._utils import get_all_subclasses
+ from ansible.modules import hostname
+ from units.modules.utils import ModuleTestCase, set_module_args
+@@ -43,9 +44,12 @@ class TestHostname(ModuleTestCase):
+ classname = "%sStrategy" % prefix
+ cls = getattr(hostname, classname, None)
+
+- assert cls is not None
+-
+- self.assertTrue(issubclass(cls, hostname.BaseStrategy))
++ if cls is None:
++ self.assertFalse(
++ cls is None, "%s is None, should be a subclass" % classname
++ )
++ else:
++ self.assertTrue(issubclass(cls, hostname.BaseStrategy))
+
+
+ class TestRedhatStrategy(ModuleTestCase):
+--- ansible-core-2.16.5.orig/test/units/modules/test_iptables.py
++++ ansible-core-2.16.5/test/units/modules/test_iptables.py
+@@ -181,7 +181,7 @@ class TestIptables(ModuleTestCase):
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+- self.assertEqual(run_command.call_count, 1)
++ self.assertEqual(run_command.call_count, 2)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+@@ -208,6 +208,7 @@ class TestIptables(ModuleTestCase):
+
+ commands_results = [
+ (1, '', ''), # check_rule_present
++ (0, '', ''), # check_chain_present
+ (0, '', ''),
+ ]
+
+@@ -217,7 +218,7 @@ class TestIptables(ModuleTestCase):
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+- self.assertEqual(run_command.call_count, 2)
++ self.assertEqual(run_command.call_count, 3)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+@@ -231,7 +232,7 @@ class TestIptables(ModuleTestCase):
+ '-j',
+ 'ACCEPT'
+ ])
+- self.assertEqual(run_command.call_args_list[1][0][0], [
++ self.assertEqual(run_command.call_args_list[2][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+@@ -271,7 +272,7 @@ class TestIptables(ModuleTestCase):
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+- self.assertEqual(run_command.call_count, 1)
++ self.assertEqual(run_command.call_count, 2)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+@@ -320,7 +321,7 @@ class TestIptables(ModuleTestCase):
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+- self.assertEqual(run_command.call_count, 2)
++ self.assertEqual(run_command.call_count, 3)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+@@ -342,7 +343,7 @@ class TestIptables(ModuleTestCase):
+ '--to-ports',
+ '8600'
+ ])
+- self.assertEqual(run_command.call_args_list[1][0][0], [
++ self.assertEqual(run_command.call_args_list[2][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'nat',
+@@ -1018,8 +1019,10 @@ class TestIptables(ModuleTestCase):
+ })
+
+ commands_results = [
++ (1, '', ''), # check_rule_present
+ (1, '', ''), # check_chain_present
+ (0, '', ''), # create_chain
++ (0, '', ''), # append_rule
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+@@ -1028,20 +1031,32 @@ class TestIptables(ModuleTestCase):
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+- self.assertEqual(run_command.call_count, 2)
++ self.assertEqual(run_command.call_count, 4)
+
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+- '-L', 'FOOBAR',
++ '-C', 'FOOBAR',
+ ])
+
+ self.assertEqual(run_command.call_args_list[1][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
++ '-L', 'FOOBAR',
++ ])
++
++ self.assertEqual(run_command.call_args_list[2][0][0], [
++ '/sbin/iptables',
++ '-t', 'filter',
+ '-N', 'FOOBAR',
+ ])
+
++ self.assertEqual(run_command.call_args_list[3][0][0], [
++ '/sbin/iptables',
++ '-t', 'filter',
++ '-A', 'FOOBAR',
++ ])
++
+ commands_results = [
+ (0, '', ''), # check_rule_present
+ ]
+@@ -1063,6 +1078,7 @@ class TestIptables(ModuleTestCase):
+
+ commands_results = [
+ (1, '', ''), # check_rule_present
++ (1, '', ''), # check_chain_present
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+@@ -1071,11 +1087,17 @@ class TestIptables(ModuleTestCase):
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+- self.assertEqual(run_command.call_count, 1)
++ self.assertEqual(run_command.call_count, 2)
+
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
++ '-C', 'FOOBAR',
++ ])
++
++ self.assertEqual(run_command.call_args_list[1][0][0], [
++ '/sbin/iptables',
++ '-t', 'filter',
+ '-L', 'FOOBAR',
+ ])
+
+--- ansible-core-2.16.5.orig/test/units/modules/test_known_hosts.py
++++ ansible-core-2.16.5/test/units/modules/test_known_hosts.py
+@@ -6,7 +6,7 @@ import tempfile
+ from ansible.module_utils import basic
+
+ from units.compat import unittest
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+ from ansible.module_utils.basic import AnsibleModule
+
+ from ansible.modules.known_hosts import compute_diff, sanity_check
+--- ansible-core-2.16.5.orig/test/units/modules/test_unarchive.py
++++ ansible-core-2.16.5/test/units/modules/test_unarchive.py
+@@ -8,6 +8,20 @@ import pytest
+ from ansible.modules.unarchive import ZipArchive, TgzArchive
+
+
++class AnsibleModuleExit(Exception):
++ def __init__(self, *args, **kwargs):
++ self.args = args
++ self.kwargs = kwargs
++
++
++class ExitJson(AnsibleModuleExit):
++ pass
++
++
++class FailJson(AnsibleModuleExit):
++ pass
++
++
+ @pytest.fixture
+ def fake_ansible_module():
+ return FakeAnsibleModule()
+@@ -18,6 +32,12 @@ class FakeAnsibleModule:
+ self.params = {}
+ self.tmpdir = None
+
++ def exit_json(self, *args, **kwargs):
++ raise ExitJson(*args, **kwargs)
++
++ def fail_json(self, *args, **kwargs):
++ raise FailJson(*args, **kwargs)
++
+
+ class TestCaseZipArchive:
+ @pytest.mark.parametrize(
+--- ansible-core-2.16.5.orig/test/units/modules/utils.py
++++ ansible-core-2.16.5/test/units/modules/utils.py
+@@ -6,12 +6,14 @@ import json
+ from units.compat import unittest
+ from units.compat.mock import patch
+ from ansible.module_utils import basic
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+
+
+ def set_module_args(args):
+- args['_ansible_remote_tmp'] = '/tmp'
+- args['_ansible_keep_remote_files'] = False
++ if '_ansible_remote_tmp' not in args:
++ args['_ansible_remote_tmp'] = '/tmp'
++ if '_ansible_keep_remote_files' not in args:
++ args['_ansible_keep_remote_files'] = False
+
+ args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
+ basic._ANSIBLE_ARGS = to_bytes(args)
+@@ -26,6 +28,8 @@ class AnsibleFailJson(Exception):
+
+
+ def exit_json(*args, **kwargs):
++ if 'changed' not in kwargs:
++ kwargs['changed'] = False
+ raise AnsibleExitJson(kwargs)
+
+
+--- ansible-core-2.16.5.orig/test/units/parsing/test_ajson.py
++++ ansible-core-2.16.5/test/units/parsing/test_ajson.py
+@@ -109,11 +109,7 @@ class TestAnsibleJSONEncoder:
+ def __len__(self):
+ return len(self.__dict__)
+
+- mapping = M(request.param)
+-
+- assert isinstance(len(mapping), int) # ensure coverage of __len__
+-
+- return mapping
++ return M(request.param)
+
+ @pytest.fixture
+ def ansible_json_encoder(self):
+--- ansible-core-2.16.5.orig/test/units/parsing/test_dataloader.py
++++ ansible-core-2.16.5/test/units/parsing/test_dataloader.py
+@@ -25,7 +25,8 @@ from units.compat import unittest
+ from unittest.mock import patch, mock_open
+ from ansible.errors import AnsibleParserError, yaml_strings, AnsibleFileNotFound
+ from ansible.parsing.vault import AnsibleVaultError
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
++from ansible.module_utils.six import PY3
+
+ from units.mock.vault_helper import TextVaultSecret
+ from ansible.parsing.dataloader import DataLoader
+@@ -91,11 +92,11 @@ class TestDataLoader(unittest.TestCase):
+ - { role: 'testrole' }
+
+ testrole/tasks/main.yml:
+- - include_tasks: "include1.yml"
++ - include: "include1.yml"
+ static: no
+
+ testrole/tasks/include1.yml:
+- - include_tasks: include2.yml
++ - include: include2.yml
+ static: no
+
+ testrole/tasks/include2.yml:
+@@ -228,7 +229,11 @@ class TestDataLoaderWithVault(unittest.T
+ 3135306561356164310a343937653834643433343734653137383339323330626437313562306630
+ 3035
+ """
++ if PY3:
++ builtins_name = 'builtins'
++ else:
++ builtins_name = '__builtin__'
+
+- with patch('builtins.open', mock_open(read_data=vaulted_data.encode('utf-8'))):
++ with patch(builtins_name + '.open', mock_open(read_data=vaulted_data.encode('utf-8'))):
+ output = self._loader.load_from_file('dummy_vault.txt')
+ self.assertEqual(output, dict(foo='bar'))
+--- ansible-core-2.16.5.orig/test/units/parsing/test_mod_args.py
++++ ansible-core-2.16.5/test/units/parsing/test_mod_args.py
+@@ -6,10 +6,10 @@ from __future__ import absolute_import,
+ __metaclass__ = type
+
+ import pytest
++import re
+
+ from ansible.errors import AnsibleParserError
+ from ansible.parsing.mod_args import ModuleArgsParser
+-from ansible.plugins.loader import init_plugin_loader
+ from ansible.utils.sentinel import Sentinel
+
+
+@@ -119,19 +119,19 @@ class TestModArgsDwim:
+ assert err.value.args[0] == msg
+
+ def test_multiple_actions_ping_shell(self):
+- init_plugin_loader()
+ args_dict = {'ping': 'data=hi', 'shell': 'echo hi'}
+ m = ModuleArgsParser(args_dict)
+ with pytest.raises(AnsibleParserError) as err:
+ m.parse()
+
+- assert err.value.args[0] == f'conflicting action statements: {", ".join(args_dict)}'
++ assert err.value.args[0].startswith("conflicting action statements: ")
++ actions = set(re.search(r'(\w+), (\w+)', err.value.args[0]).groups())
++ assert actions == set(['ping', 'shell'])
+
+ def test_bogus_action(self):
+- init_plugin_loader()
+ args_dict = {'bogusaction': {}}
+ m = ModuleArgsParser(args_dict)
+ with pytest.raises(AnsibleParserError) as err:
+ m.parse()
+
+- assert err.value.args[0].startswith(f"couldn't resolve module/action '{next(iter(args_dict))}'")
++ assert err.value.args[0].startswith("couldn't resolve module/action 'bogusaction'")
+--- ansible-core-2.16.5.orig/test/units/parsing/test_splitter.py
++++ ansible-core-2.16.5/test/units/parsing/test_splitter.py
+@@ -21,17 +21,10 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ from ansible.parsing.splitter import split_args, parse_kv
+-from ansible.errors import AnsibleParserError
+
+ import pytest
+
+ SPLIT_DATA = (
+- (None,
+- [],
+- {}),
+- (u'',
+- [],
+- {}),
+ (u'a',
+ [u'a'],
+ {u'_raw_params': u'a'}),
+@@ -53,18 +46,6 @@ SPLIT_DATA = (
+ (u'a="echo \\"hello world\\"" b=bar',
+ [u'a="echo \\"hello world\\""', u'b=bar'],
+ {u'a': u'echo "hello world"', u'b': u'bar'}),
+- (u'a="nest\'ed"',
+- [u'a="nest\'ed"'],
+- {u'a': u'nest\'ed'}),
+- (u' ',
+- [u' '],
+- {u'_raw_params': u' '}),
+- (u'\\ ',
+- [u' '],
+- {u'_raw_params': u' '}),
+- (u'a\\=escaped',
+- [u'a\\=escaped'],
+- {u'_raw_params': u'a=escaped'}),
+ (u'a="multi\nline"',
+ [u'a="multi\nline"'],
+ {u'a': u'multi\nline'}),
+@@ -80,27 +61,12 @@ SPLIT_DATA = (
+ (u'a="multiline\nmessage1\\\n" b="multiline\nmessage2\\\n"',
+ [u'a="multiline\nmessage1\\\n"', u'b="multiline\nmessage2\\\n"'],
+ {u'a': 'multiline\nmessage1\\\n', u'b': u'multiline\nmessage2\\\n'}),
+- (u'line \\\ncontinuation',
+- [u'line', u'continuation'],
+- {u'_raw_params': u'line continuation'}),
+- (u'not jinja}}',
+- [u'not', u'jinja}}'],
+- {u'_raw_params': u'not jinja}}'}),
+- (u'a={{multiline\njinja}}',
+- [u'a={{multiline\njinja}}'],
+- {u'a': u'{{multiline\njinja}}'}),
+ (u'a={{jinja}}',
+ [u'a={{jinja}}'],
+ {u'a': u'{{jinja}}'}),
+ (u'a={{ jinja }}',
+ [u'a={{ jinja }}'],
+ {u'a': u'{{ jinja }}'}),
+- (u'a={% jinja %}',
+- [u'a={% jinja %}'],
+- {u'a': u'{% jinja %}'}),
+- (u'a={# jinja #}',
+- [u'a={# jinja #}'],
+- {u'a': u'{# jinja #}'}),
+ (u'a="{{jinja}}"',
+ [u'a="{{jinja}}"'],
+ {u'a': u'{{jinja}}'}),
+@@ -128,50 +94,17 @@ SPLIT_DATA = (
+ (u'One\n Two\n Three\n',
+ [u'One\n ', u'Two\n ', u'Three\n'],
+ {u'_raw_params': u'One\n Two\n Three\n'}),
+- (u'\nOne\n Two\n Three\n',
+- [u'\n', u'One\n ', u'Two\n ', u'Three\n'],
+- {u'_raw_params': u'\nOne\n Two\n Three\n'}),
+ )
+
+-PARSE_KV_CHECK_RAW = (
+- (u'raw=yes', {u'_raw_params': u'raw=yes'}),
+- (u'creates=something', {u'creates': u'something'}),
+-)
+-
+-PARSER_ERROR = (
+- '"',
+- "'",
+- '{{',
+- '{%',
+- '{#',
+-)
++SPLIT_ARGS = ((test[0], test[1]) for test in SPLIT_DATA)
++PARSE_KV = ((test[0], test[2]) for test in SPLIT_DATA)
+
+-SPLIT_ARGS = tuple((test[0], test[1]) for test in SPLIT_DATA)
+-PARSE_KV = tuple((test[0], test[2]) for test in SPLIT_DATA)
+
+-
+-@pytest.mark.parametrize("args, expected", SPLIT_ARGS, ids=[str(arg[0]) for arg in SPLIT_ARGS])
++@pytest.mark.parametrize("args, expected", SPLIT_ARGS)
+ def test_split_args(args, expected):
+ assert split_args(args) == expected
+
+
+-@pytest.mark.parametrize("args, expected", PARSE_KV, ids=[str(arg[0]) for arg in PARSE_KV])
++@pytest.mark.parametrize("args, expected", PARSE_KV)
+ def test_parse_kv(args, expected):
+ assert parse_kv(args) == expected
+-
+-
+-@pytest.mark.parametrize("args, expected", PARSE_KV_CHECK_RAW, ids=[str(arg[0]) for arg in PARSE_KV_CHECK_RAW])
+-def test_parse_kv_check_raw(args, expected):
+- assert parse_kv(args, check_raw=True) == expected
+-
+-
+-@pytest.mark.parametrize("args", PARSER_ERROR)
+-def test_split_args_error(args):
+- with pytest.raises(AnsibleParserError):
+- split_args(args)
+-
+-
+-@pytest.mark.parametrize("args", PARSER_ERROR)
+-def test_parse_kv_error(args):
+- with pytest.raises(AnsibleParserError):
+- parse_kv(args)
+--- ansible-core-2.16.5.orig/test/units/parsing/vault/test_vault.py
++++ ansible-core-2.16.5/test/units/parsing/vault/test_vault.py
+@@ -21,6 +21,7 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
++import binascii
+ import io
+ import os
+ import tempfile
+@@ -33,7 +34,7 @@ from unittest.mock import patch, MagicMo
+
+ from ansible import errors
+ from ansible.module_utils import six
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils._text import to_bytes, to_text
+ from ansible.parsing import vault
+
+ from units.mock.loader import DictDataLoader
+@@ -605,6 +606,9 @@ class TestVaultLib(unittest.TestCase):
+ ('test_id', text_secret)]
+ self.v = vault.VaultLib(self.vault_secrets)
+
++ def _vault_secrets(self, vault_id, secret):
++ return [(vault_id, secret)]
++
+ def _vault_secrets_from_password(self, vault_id, password):
+ return [(vault_id, TextVaultSecret(password))]
+
+@@ -775,6 +779,43 @@ class TestVaultLib(unittest.TestCase):
+ b_plaintext = self.v.decrypt(b_vaulttext)
+ self.assertEqual(b_plaintext, b_orig_plaintext, msg="decryption failed")
+
++ # FIXME This test isn't working quite yet.
++ @pytest.mark.skip(reason='This test is not ready yet')
++ def test_encrypt_decrypt_aes256_bad_hmac(self):
++
++ self.v.cipher_name = 'AES256'
++ # plaintext = "Setec Astronomy"
++ enc_data = '''$ANSIBLE_VAULT;1.1;AES256
++33363965326261303234626463623963633531343539616138316433353830356566396130353436
++3562643163366231316662386565383735653432386435610a306664636137376132643732393835
++63383038383730306639353234326630666539346233376330303938323639306661313032396437
++6233623062366136310a633866373936313238333730653739323461656662303864663666653563
++3138'''
++ b_data = to_bytes(enc_data, errors='strict', encoding='utf-8')
++ b_data = self.v._split_header(b_data)
++ foo = binascii.unhexlify(b_data)
++ lines = foo.splitlines()
++ # line 0 is salt, line 1 is hmac, line 2+ is ciphertext
++ b_salt = lines[0]
++ b_hmac = lines[1]
++ b_ciphertext_data = b'\n'.join(lines[2:])
++
++ b_ciphertext = binascii.unhexlify(b_ciphertext_data)
++ # b_orig_ciphertext = b_ciphertext[:]
++
++ # now muck with the text
++ # b_munged_ciphertext = b_ciphertext[:10] + b'\x00' + b_ciphertext[11:]
++ # b_munged_ciphertext = b_ciphertext
++ # assert b_orig_ciphertext != b_munged_ciphertext
++
++ b_ciphertext_data = binascii.hexlify(b_ciphertext)
++ b_payload = b'\n'.join([b_salt, b_hmac, b_ciphertext_data])
++ # reformat
++ b_invalid_ciphertext = self.v._format_output(b_payload)
++
++ # assert we throw an error
++ self.v.decrypt(b_invalid_ciphertext)
++
+ def test_decrypt_and_get_vault_id(self):
+ b_expected_plaintext = to_bytes('foo bar\n')
+ vaulttext = '''$ANSIBLE_VAULT;1.2;AES256;ansible_devel
+--- ansible-core-2.16.5.orig/test/units/parsing/vault/test_vault_editor.py
++++ ansible-core-2.16.5/test/units/parsing/vault/test_vault_editor.py
+@@ -33,7 +33,8 @@ from ansible import errors
+ from ansible.parsing import vault
+ from ansible.parsing.vault import VaultLib, VaultEditor, match_encrypt_secret
+
+-from ansible.module_utils.common.text.converters import to_bytes, to_text
++from ansible.module_utils.six import PY3
++from ansible.module_utils._text import to_bytes, to_text
+
+ from units.mock.vault_helper import TextVaultSecret
+
+@@ -87,10 +88,12 @@ class TestVaultEditor(unittest.TestCase)
+ suffix = '_ansible_unit_test_%s_' % (self.__class__.__name__)
+ return tempfile.mkdtemp(suffix=suffix)
+
+- def _create_file(self, test_dir, name, content, symlink=False):
++ def _create_file(self, test_dir, name, content=None, symlink=False):
+ file_path = os.path.join(test_dir, name)
+- with open(file_path, 'wb') as opened_file:
++ opened_file = open(file_path, 'wb')
++ if content:
+ opened_file.write(content)
++ opened_file.close()
+ return file_path
+
+ def _vault_editor(self, vault_secrets=None):
+@@ -115,8 +118,11 @@ class TestVaultEditor(unittest.TestCase)
+ def test_stdin_binary(self):
+ stdin_data = '\0'
+
+- fake_stream = StringIO(stdin_data)
+- fake_stream.buffer = BytesIO(to_bytes(stdin_data))
++ if PY3:
++ fake_stream = StringIO(stdin_data)
++ fake_stream.buffer = BytesIO(to_bytes(stdin_data))
++ else:
++ fake_stream = BytesIO(to_bytes(stdin_data))
+
+ with patch('sys.stdin', fake_stream):
+ ve = self._vault_editor()
+@@ -161,15 +167,17 @@ class TestVaultEditor(unittest.TestCase)
+ self.assertNotEqual(src_file_contents, b_ciphertext,
+ 'b_ciphertext should be encrypted and not equal to src_contents')
+
+- def _faux_editor(self, editor_args, new_src_contents):
++ def _faux_editor(self, editor_args, new_src_contents=None):
+ if editor_args[0] == 'shred':
+ return
+
+ tmp_path = editor_args[-1]
+
+ # simulate the tmp file being editted
+- with open(tmp_path, 'wb') as tmp_file:
++ tmp_file = open(tmp_path, 'wb')
++ if new_src_contents:
+ tmp_file.write(new_src_contents)
++ tmp_file.close()
+
+ def _faux_command(self, tmp_path):
+ pass
+@@ -190,13 +198,13 @@ class TestVaultEditor(unittest.TestCase)
+
+ ve._edit_file_helper(src_file_path, self.vault_secret, existing_data=src_file_contents)
+
+- with open(src_file_path, 'rb') as new_target_file:
+- new_target_file_contents = new_target_file.read()
+- self.assertEqual(src_file_contents, new_target_file_contents)
++ new_target_file = open(src_file_path, 'rb')
++ new_target_file_contents = new_target_file.read()
++ self.assertEqual(src_file_contents, new_target_file_contents)
+
+ def _assert_file_is_encrypted(self, vault_editor, src_file_path, src_contents):
+- with open(src_file_path, 'rb') as new_src_file:
+- new_src_file_contents = new_src_file.read()
++ new_src_file = open(src_file_path, 'rb')
++ new_src_file_contents = new_src_file.read()
+
+ # TODO: assert that it is encrypted
+ self.assertTrue(vault.is_encrypted(new_src_file_contents))
+@@ -331,8 +339,8 @@ class TestVaultEditor(unittest.TestCase)
+ ve.encrypt_file(src_file_path, self.vault_secret)
+ ve.edit_file(src_file_path)
+
+- with open(src_file_path, 'rb') as new_src_file:
+- new_src_file_contents = new_src_file.read()
++ new_src_file = open(src_file_path, 'rb')
++ new_src_file_contents = new_src_file.read()
+
+ self.assertTrue(b'$ANSIBLE_VAULT;1.1;AES256' in new_src_file_contents)
+
+@@ -359,8 +367,8 @@ class TestVaultEditor(unittest.TestCase)
+ vault_id='vault_secrets')
+ ve.edit_file(src_file_path)
+
+- with open(src_file_path, 'rb') as new_src_file:
+- new_src_file_contents = new_src_file.read()
++ new_src_file = open(src_file_path, 'rb')
++ new_src_file_contents = new_src_file.read()
+
+ self.assertTrue(b'$ANSIBLE_VAULT;1.2;AES256;vault_secrets' in new_src_file_contents)
+
+@@ -391,8 +399,8 @@ class TestVaultEditor(unittest.TestCase)
+
+ ve.edit_file(src_file_link_path)
+
+- with open(src_file_path, 'rb') as new_src_file:
+- new_src_file_contents = new_src_file.read()
++ new_src_file = open(src_file_path, 'rb')
++ new_src_file_contents = new_src_file.read()
+
+ src_file_plaintext = ve.vault.decrypt(new_src_file_contents)
+
+@@ -410,6 +418,13 @@ class TestVaultEditor(unittest.TestCase)
+
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents)
+
++ new_src_contents = to_bytes("The info is different now.")
++
++ def faux_editor(editor_args):
++ self._faux_editor(editor_args, new_src_contents)
++
++ mock_sp_call.side_effect = faux_editor
++
+ ve = self._vault_editor()
+ self.assertRaisesRegex(errors.AnsibleError,
+ 'input is not vault encrypted data',
+@@ -463,14 +478,20 @@ class TestVaultEditor(unittest.TestCase)
+ ve = self._vault_editor(self._secrets("ansible"))
+
+ # make sure the password functions for the cipher
+- ve.decrypt_file(v11_file.name)
++ error_hit = False
++ try:
++ ve.decrypt_file(v11_file.name)
++ except errors.AnsibleError:
++ error_hit = True
+
+ # verify decrypted content
+- with open(v11_file.name, "rb") as f:
+- fdata = to_text(f.read())
++ f = open(v11_file.name, "rb")
++ fdata = to_text(f.read())
++ f.close()
+
+ os.unlink(v11_file.name)
+
++ assert error_hit is False, "error decrypting 1.1 file"
+ assert fdata.strip() == "foo", "incorrect decryption of 1.1 file: %s" % fdata.strip()
+
+ def test_real_path_dash(self):
+@@ -480,9 +501,21 @@ class TestVaultEditor(unittest.TestCase)
+ res = ve._real_path(filename)
+ self.assertEqual(res, '-')
+
+- def test_real_path_not_dash(self):
++ def test_real_path_dev_null(self):
+ filename = '/dev/null'
+ ve = self._vault_editor()
+
+ res = ve._real_path(filename)
+- self.assertNotEqual(res, '-')
++ self.assertEqual(res, '/dev/null')
++
++ def test_real_path_symlink(self):
++ self._test_dir = os.path.realpath(self._create_test_dir())
++ file_path = self._create_file(self._test_dir, 'test_file', content=b'this is a test file')
++ file_link_path = os.path.join(self._test_dir, 'a_link_to_test_file')
++
++ os.symlink(file_path, file_link_path)
++
++ ve = self._vault_editor()
++
++ res = ve._real_path(file_link_path)
++ self.assertEqual(res, file_path)
+--- ansible-core-2.16.5.orig/test/units/parsing/yaml/test_dumper.py
++++ ansible-core-2.16.5/test/units/parsing/yaml/test_dumper.py
+@@ -19,6 +19,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ import io
++import yaml
+
+ from jinja2.exceptions import UndefinedError
+
+@@ -26,6 +27,7 @@ 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.template import AnsibleUndefined
+
+ from units.mock.yaml_helper import YamlTestUtils
+@@ -74,6 +76,20 @@ class TestAnsibleDumper(unittest.TestCas
+ data_from_yaml = loader.get_single_data()
+
+ result = b_text
++ if PY2:
++ # https://pyyaml.org/wiki/PyYAMLDocumentation#string-conversion-python-2-only
++ # pyyaml on Python 2 can return either unicode or bytes when given byte strings.
++ # We normalize that to always return unicode on Python2 as that's right most of the
++ # time. However, this means byte strings can round trip through yaml on Python3 but
++ # not on Python2. To make this code work the same on Python2 and Python3 (we want
++ # the Python3 behaviour) we need to change the methods in Ansible to:
++ # (1) Let byte strings pass through yaml without being converted on Python2
++ # (2) Convert byte strings to text strings before being given to pyyaml (Without this,
++ # strings would end up as byte strings most of the time which would mostly be wrong)
++ # In practice, we mostly read bytes in from files and then pass that to pyyaml, for which
++ # the present behavior is correct.
++ # This is a workaround for the current behavior.
++ result = u'tr\xe9ma'
+
+ self.assertEqual(result, data_from_yaml)
+
+@@ -89,7 +105,10 @@ class TestAnsibleDumper(unittest.TestCas
+ self.assertEqual(u_text, data_from_yaml)
+
+ def test_vars_with_sources(self):
+- self._dump_string(VarsWithSources(), dumper=self.dumper)
++ try:
++ self._dump_string(VarsWithSources(), dumper=self.dumper)
++ except yaml.representer.RepresenterError:
++ self.fail("Dump VarsWithSources raised RepresenterError unexpectedly!")
+
+ def test_undefined(self):
+ undefined_object = AnsibleUndefined()
+--- ansible-core-2.16.5.orig/test/units/parsing/yaml/test_objects.py
++++ ansible-core-2.16.5/test/units/parsing/yaml/test_objects.py
+@@ -24,7 +24,7 @@ from units.compat import unittest
+
+ from ansible.errors import AnsibleError
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+
+ from ansible.parsing import vault
+ from ansible.parsing.yaml.loader import AnsibleLoader
+@@ -105,6 +105,11 @@ class TestAnsibleVaultEncryptedUnicode(u
+ id_secret = vault.match_encrypt_secret(self.good_vault_secrets)
+ return objects.AnsibleVaultEncryptedUnicode.from_plaintext(seq, vault=self.vault, secret=id_secret[1])
+
++ def _from_ciphertext(self, ciphertext):
++ avu = objects.AnsibleVaultEncryptedUnicode(ciphertext)
++ avu.vault = self.vault
++ return avu
++
+ def test_empty_init(self):
+ self.assertRaises(TypeError, objects.AnsibleVaultEncryptedUnicode)
+
+--- ansible-core-2.16.5.orig/test/units/playbook/role/test_include_role.py
++++ ansible-core-2.16.5/test/units/playbook/role/test_include_role.py
+@@ -108,6 +108,8 @@ class TestIncludeRole(unittest.TestCase)
+ # skip meta: role_complete
+ continue
+ role = task._role
++ if not role:
++ continue
+
+ yield (role.get_name(),
+ self.var_manager.get_vars(play=play, task=task))
+@@ -199,7 +201,7 @@ class TestIncludeRole(unittest.TestCase)
+ self.assertEqual(task_vars.get('l3_variable'), 'l3-main')
+ self.assertEqual(task_vars.get('test_variable'), 'l3-main')
+ else:
+- self.fail() # pragma: nocover
++ self.fail()
+ self.assertFalse(expected_roles)
+
+ @patch('ansible.playbook.role.definition.unfrackpath',
+@@ -245,5 +247,5 @@ class TestIncludeRole(unittest.TestCase)
+ self.assertEqual(task_vars.get('l3_variable'), 'l3-alt')
+ self.assertEqual(task_vars.get('test_variable'), 'l3-alt')
+ else:
+- self.fail() # pragma: nocover
++ self.fail()
+ self.assertFalse(expected_roles)
+--- ansible-core-2.16.5.orig/test/units/playbook/role/test_role.py
++++ ansible-core-2.16.5/test/units/playbook/role/test_role.py
+@@ -21,12 +21,10 @@ __metaclass__ = type
+
+ from collections.abc import Container
+
+-import pytest
+-
+ from units.compat import unittest
+ from unittest.mock import patch, MagicMock
+
+-from ansible.errors import AnsibleParserError
++from ansible.errors import AnsibleError, AnsibleParserError
+ from ansible.playbook.block import Block
+
+ from units.mock.loader import DictDataLoader
+@@ -44,9 +42,12 @@ class TestHashParams(unittest.TestCase):
+ self._assert_set(res)
+ self._assert_hashable(res)
+
+- @staticmethod
+- def _assert_hashable(res):
+- hash(res)
++ def _assert_hashable(self, res):
++ a_dict = {}
++ try:
++ a_dict[res] = res
++ except TypeError as e:
++ self.fail('%s is not hashable: %s' % (res, e))
+
+ def _assert_set(self, res):
+ self.assertIsInstance(res, frozenset)
+@@ -86,28 +87,36 @@ class TestHashParams(unittest.TestCase):
+
+ def test_generator(self):
+ def my_generator():
+- yield
++ for i in ['a', 1, None, {}]:
++ yield i
+
+ params = my_generator()
+ res = hash_params(params)
+ self._assert_hashable(res)
+- assert list(params)
+
+ def test_container_but_not_iterable(self):
+ # This is a Container that is not iterable, which is unlikely but...
+ class MyContainer(Container):
+- def __init__(self, _some_thing):
+- pass
++ def __init__(self, some_thing):
++ self.data = []
++ self.data.append(some_thing)
+
+ def __contains__(self, item):
+- """Implementation omitted, since it will never be called."""
++ return item in self.data
++
++ def __hash__(self):
++ return hash(self.data)
++
++ def __len__(self):
++ return len(self.data)
+
+- params = MyContainer('foo bar')
++ def __call__(self):
++ return False
+
+- with pytest.raises(TypeError) as ex:
+- hash_params(params)
++ foo = MyContainer('foo bar')
++ params = foo
+
+- assert ex.value.args == ("'MyContainer' object is not iterable",)
++ self.assertRaises(TypeError, hash_params, params)
+
+ def test_param_dict_dupe_values(self):
+ params1 = {'foo': False}
+@@ -142,18 +151,18 @@ class TestHashParams(unittest.TestCase):
+ self.assertNotEqual(hash(res1), hash(res2))
+ self.assertNotEqual(res1, res2)
+
+- params_dict = {}
+- params_dict[res1] = 'params1'
+- params_dict[res2] = 'params2'
+-
+- self.assertEqual(len(params_dict), 2)
+-
+- del params_dict[res2]
+- self.assertEqual(len(params_dict), 1)
+-
+- for key in params_dict:
+- self.assertTrue(key in params_dict)
+- self.assertIn(key, params_dict)
++ foo = {}
++ foo[res1] = 'params1'
++ foo[res2] = 'params2'
++
++ self.assertEqual(len(foo), 2)
++
++ del foo[res2]
++ self.assertEqual(len(foo), 1)
++
++ for key in foo:
++ self.assertTrue(key in foo)
++ self.assertIn(key, foo)
+
+
+ class TestRole(unittest.TestCase):
+@@ -168,7 +177,7 @@ class TestRole(unittest.TestCase):
+ })
+
+ mock_play = MagicMock()
+- mock_play.role_cache = {}
++ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_tasks', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+@@ -190,7 +199,7 @@ class TestRole(unittest.TestCase):
+ })
+
+ mock_play = MagicMock()
+- mock_play.role_cache = {}
++ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_tasks', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play, from_files=dict(tasks='custom_main'))
+@@ -208,7 +217,7 @@ class TestRole(unittest.TestCase):
+ })
+
+ mock_play = MagicMock()
+- mock_play.role_cache = {}
++ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_handlers', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+@@ -229,7 +238,7 @@ class TestRole(unittest.TestCase):
+ })
+
+ mock_play = MagicMock()
+- mock_play.role_cache = {}
++ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+@@ -250,7 +259,7 @@ class TestRole(unittest.TestCase):
+ })
+
+ mock_play = MagicMock()
+- mock_play.role_cache = {}
++ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+@@ -271,7 +280,7 @@ class TestRole(unittest.TestCase):
+ })
+
+ mock_play = MagicMock()
+- mock_play.role_cache = {}
++ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+@@ -294,7 +303,7 @@ class TestRole(unittest.TestCase):
+ })
+
+ mock_play = MagicMock()
+- mock_play.role_cache = {}
++ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+@@ -314,7 +323,7 @@ class TestRole(unittest.TestCase):
+ })
+
+ mock_play = MagicMock()
+- mock_play.role_cache = {}
++ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+@@ -361,7 +370,7 @@ class TestRole(unittest.TestCase):
+
+ mock_play = MagicMock()
+ mock_play.collections = None
+- mock_play.role_cache = {}
++ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_metadata', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+@@ -406,7 +415,7 @@ class TestRole(unittest.TestCase):
+ })
+
+ mock_play = MagicMock()
+- mock_play.role_cache = {}
++ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load(dict(role='foo_complex'), play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+--- ansible-core-2.16.5.orig/test/units/playbook/test_base.py
++++ ansible-core-2.16.5/test/units/playbook/test_base.py
+@@ -21,12 +21,13 @@ __metaclass__ = type
+
+ from units.compat import unittest
+
+-from ansible.errors import AnsibleParserError, AnsibleAssertionError
++from ansible.errors import AnsibleParserError
+ from ansible.module_utils.six import string_types
+ from ansible.playbook.attribute import FieldAttribute, NonInheritableFieldAttribute
+ from ansible.template import Templar
+ from ansible.playbook import base
+-from ansible.utils.unsafe_proxy import AnsibleUnsafeText
++from ansible.utils.unsafe_proxy import AnsibleUnsafeBytes, AnsibleUnsafeText
++from ansible.utils.sentinel import Sentinel
+
+ from units.mock.loader import DictDataLoader
+
+@@ -330,6 +331,12 @@ class ExampleSubClass(base.Base):
+ def __init__(self):
+ super(ExampleSubClass, self).__init__()
+
++ def get_dep_chain(self):
++ if self._parent:
++ return self._parent.get_dep_chain()
++ else:
++ return None
++
+
+ class BaseSubClass(base.Base):
+ name = FieldAttribute(isa='string', default='', always_post_validate=True)
+@@ -581,11 +588,10 @@ class TestBaseSubClass(TestBase):
+ bsc.post_validate, templar)
+
+ def test_attr_unknown(self):
+- self.assertRaises(
+- AnsibleAssertionError,
+- self._base_validate,
+- {'test_attr_unknown_isa': True}
+- )
++ a_list = ['some string']
++ ds = {'test_attr_unknown_isa': a_list}
++ bsc = self._base_validate(ds)
++ self.assertEqual(bsc.test_attr_unknown_isa, a_list)
+
+ def test_attr_method(self):
+ ds = {'test_attr_method': 'value from the ds'}
+--- ansible-core-2.16.5.orig/test/units/playbook/test_collectionsearch.py
++++ ansible-core-2.16.5/test/units/playbook/test_collectionsearch.py
+@@ -22,6 +22,7 @@ from ansible.errors import AnsibleParser
+ from ansible.playbook.play import Play
+ from ansible.playbook.task import Task
+ from ansible.playbook.block import Block
++from ansible.playbook.collectionsearch import CollectionSearch
+
+ import pytest
+
+--- ansible-core-2.16.5.orig/test/units/playbook/test_helpers.py
++++ ansible-core-2.16.5/test/units/playbook/test_helpers.py
+@@ -52,6 +52,10 @@ class MixinForMocks(object):
+ self.mock_inventory = MagicMock(name='MockInventory')
+ self.mock_inventory._hosts_cache = dict()
+
++ def _get_host(host_name):
++ return None
++
++ self.mock_inventory.get_host.side_effect = _get_host
+ # TODO: can we use a real VariableManager?
+ self.mock_variable_manager = MagicMock(name='MockVariableManager')
+ self.mock_variable_manager.get_vars.return_value = dict()
+@@ -65,11 +69,11 @@ class MixinForMocks(object):
+
+ self._test_data_path = os.path.dirname(__file__)
+ self.fake_include_loader = DictDataLoader({"/dev/null/includes/test_include.yml": """
+- - include_tasks: other_test_include.yml
++ - include: other_test_include.yml
+ - shell: echo 'hello world'
+ """,
+ "/dev/null/includes/static_test_include.yml": """
+- - include_tasks: other_test_include.yml
++ - include: other_test_include.yml
+ - shell: echo 'hello static world'
+ """,
+ "/dev/null/includes/other_test_include.yml": """
+@@ -82,6 +86,10 @@ class TestLoadListOfTasks(unittest.TestC
+ def setUp(self):
+ self._setup()
+
++ def _assert_is_task_list(self, results):
++ for result in results:
++ self.assertIsInstance(result, Task)
++
+ def _assert_is_task_list_or_blocks(self, results):
+ self.assertIsInstance(results, list)
+ for result in results:
+@@ -160,57 +168,57 @@ class TestLoadListOfTasks(unittest.TestC
+ ds, play=self.mock_play, use_handlers=True,
+ variable_manager=self.mock_variable_manager, loader=self.fake_loader)
+
+- def test_one_bogus_include_tasks(self):
+- ds = [{'include_tasks': 'somefile.yml'}]
++ def test_one_bogus_include(self):
++ ds = [{'include': 'somefile.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_loader)
+ self.assertIsInstance(res, list)
+- self.assertEqual(len(res), 1)
+- self.assertIsInstance(res[0], TaskInclude)
++ self.assertEqual(len(res), 0)
+
+- def test_one_bogus_include_tasks_use_handlers(self):
+- ds = [{'include_tasks': 'somefile.yml'}]
++ def test_one_bogus_include_use_handlers(self):
++ ds = [{'include': 'somefile.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play, use_handlers=True,
+ variable_manager=self.mock_variable_manager, loader=self.fake_loader)
+ self.assertIsInstance(res, list)
+- self.assertEqual(len(res), 1)
+- self.assertIsInstance(res[0], TaskInclude)
++ self.assertEqual(len(res), 0)
+
+- def test_one_bogus_import_tasks(self):
++ def test_one_bogus_include_static(self):
+ ds = [{'import_tasks': 'somefile.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_loader)
+ self.assertIsInstance(res, list)
+ self.assertEqual(len(res), 0)
+
+- def test_one_include_tasks(self):
+- ds = [{'include_tasks': '/dev/null/includes/other_test_include.yml'}]
++ def test_one_include(self):
++ ds = [{'include': '/dev/null/includes/other_test_include.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+ self.assertEqual(len(res), 1)
+ self._assert_is_task_list_or_blocks(res)
+
+- def test_one_parent_include_tasks(self):
+- ds = [{'include_tasks': '/dev/null/includes/test_include.yml'}]
++ def test_one_parent_include(self):
++ ds = [{'include': '/dev/null/includes/test_include.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+ self._assert_is_task_list_or_blocks(res)
+- self.assertIsInstance(res[0], TaskInclude)
+- self.assertIsNone(res[0]._parent)
++ self.assertIsInstance(res[0], Block)
++ self.assertIsInstance(res[0]._parent, TaskInclude)
+
+- def test_one_include_tasks_tags(self):
+- ds = [{'include_tasks': '/dev/null/includes/other_test_include.yml',
++ # TODO/FIXME: do this non deprecated way
++ def test_one_include_tags(self):
++ ds = [{'include': '/dev/null/includes/other_test_include.yml',
+ 'tags': ['test_one_include_tags_tag1', 'and_another_tagB']
+ }]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+ self._assert_is_task_list_or_blocks(res)
+- self.assertIsInstance(res[0], TaskInclude)
++ self.assertIsInstance(res[0], Block)
+ self.assertIn('test_one_include_tags_tag1', res[0].tags)
+ self.assertIn('and_another_tagB', res[0].tags)
+
+- def test_one_parent_include_tasks_tags(self):
+- ds = [{'include_tasks': '/dev/null/includes/test_include.yml',
++ # TODO/FIXME: do this non deprecated way
++ def test_one_parent_include_tags(self):
++ ds = [{'include': '/dev/null/includes/test_include.yml',
+ # 'vars': {'tags': ['test_one_parent_include_tags_tag1', 'and_another_tag2']}
+ 'tags': ['test_one_parent_include_tags_tag1', 'and_another_tag2']
+ }
+@@ -218,20 +226,20 @@ class TestLoadListOfTasks(unittest.TestC
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+ self._assert_is_task_list_or_blocks(res)
+- self.assertIsInstance(res[0], TaskInclude)
++ self.assertIsInstance(res[0], Block)
+ self.assertIn('test_one_parent_include_tags_tag1', res[0].tags)
+ self.assertIn('and_another_tag2', res[0].tags)
+
+- def test_one_include_tasks_use_handlers(self):
+- ds = [{'include_tasks': '/dev/null/includes/other_test_include.yml'}]
++ def test_one_include_use_handlers(self):
++ ds = [{'include': '/dev/null/includes/other_test_include.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ use_handlers=True,
+ variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+ self._assert_is_task_list_or_blocks(res)
+ self.assertIsInstance(res[0], Handler)
+
+- def test_one_parent_include_tasks_use_handlers(self):
+- ds = [{'include_tasks': '/dev/null/includes/test_include.yml'}]
++ def test_one_parent_include_use_handlers(self):
++ ds = [{'include': '/dev/null/includes/test_include.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ use_handlers=True,
+ variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+--- ansible-core-2.16.5.orig/test/units/playbook/test_included_file.py
++++ ansible-core-2.16.5/test/units/playbook/test_included_file.py
+@@ -105,7 +105,7 @@ def test_included_file_instantiation():
+ assert inc_file._task is None
+
+
+-def test_process_include_tasks_results(mock_iterator, mock_variable_manager):
++def test_process_include_results(mock_iterator, mock_variable_manager):
+ hostname = "testhost1"
+ hostname2 = "testhost2"
+
+@@ -113,7 +113,7 @@ def test_process_include_tasks_results(m
+ parent_task = Task.load(parent_task_ds)
+ parent_task._play = None
+
+- task_ds = {'include_tasks': 'include_test.yml'}
++ task_ds = {'include': 'include_test.yml'}
+ loaded_task = TaskInclude.load(task_ds, task_include=parent_task)
+
+ return_data = {'include': 'include_test.yml'}
+@@ -133,7 +133,7 @@ def test_process_include_tasks_results(m
+ assert res[0]._vars == {}
+
+
+-def test_process_include_tasks_diff_files(mock_iterator, mock_variable_manager):
++def test_process_include_diff_files(mock_iterator, mock_variable_manager):
+ hostname = "testhost1"
+ hostname2 = "testhost2"
+
+@@ -141,11 +141,11 @@ def test_process_include_tasks_diff_file
+ parent_task = Task.load(parent_task_ds)
+ parent_task._play = None
+
+- task_ds = {'include_tasks': 'include_test.yml'}
++ task_ds = {'include': 'include_test.yml'}
+ loaded_task = TaskInclude.load(task_ds, task_include=parent_task)
+ loaded_task._play = None
+
+- child_task_ds = {'include_tasks': 'other_include_test.yml'}
++ child_task_ds = {'include': 'other_include_test.yml'}
+ loaded_child_task = TaskInclude.load(child_task_ds, task_include=loaded_task)
+ loaded_child_task._play = None
+
+@@ -175,7 +175,7 @@ def test_process_include_tasks_diff_file
+ assert res[1]._vars == {}
+
+
+-def test_process_include_tasks_simulate_free(mock_iterator, mock_variable_manager):
++def test_process_include_simulate_free(mock_iterator, mock_variable_manager):
+ hostname = "testhost1"
+ hostname2 = "testhost2"
+
+@@ -186,7 +186,7 @@ def test_process_include_tasks_simulate_
+ parent_task1._play = None
+ parent_task2._play = None
+
+- task_ds = {'include_tasks': 'include_test.yml'}
++ task_ds = {'include': 'include_test.yml'}
+ loaded_task1 = TaskInclude.load(task_ds, task_include=parent_task1)
+ loaded_task2 = TaskInclude.load(task_ds, task_include=parent_task2)
+
+--- ansible-core-2.16.5.orig/test/units/playbook/test_play_context.py
++++ ansible-core-2.16.5/test/units/playbook/test_play_context.py
+@@ -12,8 +12,10 @@ import pytest
+ from ansible import constants as C
+ from ansible import context
+ from ansible.cli.arguments import option_helpers as opt_help
++from ansible.errors import AnsibleError
+ from ansible.playbook.play_context import PlayContext
+ from ansible.playbook.play import Play
++from ansible.plugins.loader import become_loader
+ from ansible.utils import context_objects as co
+
+
+--- ansible-core-2.16.5.orig/test/units/playbook/test_taggable.py
++++ ansible-core-2.16.5/test/units/playbook/test_taggable.py
+@@ -29,7 +29,6 @@ class TaggableTestObj(Taggable):
+ def __init__(self):
+ self._loader = DictDataLoader({})
+ self.tags = []
+- self._parent = None
+
+
+ class TestTaggable(unittest.TestCase):
+--- ansible-core-2.16.5.orig/test/units/playbook/test_task.py
++++ ansible-core-2.16.5/test/units/playbook/test_task.py
+@@ -22,7 +22,6 @@ __metaclass__ = type
+ from units.compat import unittest
+ from unittest.mock import patch
+ from ansible.playbook.task import Task
+-from ansible.plugins.loader import init_plugin_loader
+ from ansible.parsing.yaml import objects
+ from ansible import errors
+
+@@ -75,7 +74,6 @@ class TestTask(unittest.TestCase):
+
+ @patch.object(errors.AnsibleError, '_get_error_lines_from_file')
+ def test_load_task_kv_form_error_36848(self, mock_get_err_lines):
+- init_plugin_loader()
+ ds = objects.AnsibleMapping(kv_bad_args_ds)
+ ds.ansible_pos = ('test_task_faux_playbook.yml', 1, 1)
+ mock_get_err_lines.return_value = (kv_bad_args_str, '')
+--- ansible-core-2.16.5.orig/test/units/plugins/action/test_action.py
++++ ansible-core-2.16.5/test/units/plugins/action/test_action.py
+@@ -22,7 +22,6 @@ __metaclass__ = type
+
+ import os
+ import re
+-from importlib import import_module
+
+ from ansible import constants as C
+ from units.compat import unittest
+@@ -31,10 +30,9 @@ from unittest.mock import patch, MagicMo
+ from ansible.errors import AnsibleError, AnsibleAuthenticationFailure
+ from ansible.module_utils.six import text_type
+ from ansible.module_utils.six.moves import shlex_quote, builtins
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+ from ansible.playbook.play_context import PlayContext
+ from ansible.plugins.action import ActionBase
+-from ansible.plugins.loader import init_plugin_loader
+ from ansible.template import Templar
+ from ansible.vars.clean import clean_facts
+
+@@ -111,11 +109,6 @@ class TestActionBase(unittest.TestCase):
+ self.assertEqual(results, {})
+
+ def test_action_base__configure_module(self):
+- init_plugin_loader()
+- # Pre-populate the ansible.builtin collection
+- # so reading the ansible_builtin_runtime.yml happens
+- # before the mock_open below
+- import_module('ansible_collections.ansible.builtin')
+ fake_loader = DictDataLoader({
+ })
+
+@@ -269,8 +262,11 @@ class TestActionBase(unittest.TestCase):
+
+ def get_shell_opt(opt):
+
+- assert opt == 'admin_users'
+- ret = ['root', 'toor', 'Administrator']
++ ret = None
++ if opt == 'admin_users':
++ ret = ['root', 'toor', 'Administrator']
++ elif opt == 'remote_tmp':
++ ret = '~/.ansible/tmp'
+
+ return ret
+
+@@ -666,10 +662,17 @@ class TestActionBase(unittest.TestCase):
+ mock_task.no_log = False
+
+ # create a mock connection, so we don't actually try and connect to things
++ def build_module_command(env_string, shebang, cmd, arg_path=None):
++ to_run = [env_string, cmd]
++ if arg_path:
++ to_run.append(arg_path)
++ return " ".join(to_run)
++
+ def get_option(option):
+ return {'admin_users': ['root', 'toor']}.get(option)
+
+ mock_connection = MagicMock()
++ mock_connection.build_module_command.side_effect = build_module_command
+ mock_connection.socket_path = None
+ mock_connection._shell.get_remote_filename.return_value = 'copy.py'
+ mock_connection._shell.join_path.side_effect = os.path.join
+@@ -796,7 +799,41 @@ class TestActionBase(unittest.TestCase):
+
+ class TestActionBaseCleanReturnedData(unittest.TestCase):
+ def test(self):
++
++ fake_loader = DictDataLoader({
++ })
++ mock_module_loader = MagicMock()
++ mock_shared_loader_obj = MagicMock()
++ mock_shared_loader_obj.module_loader = mock_module_loader
++ connection_loader_paths = ['/tmp/asdfadf', '/usr/lib64/whatever',
++ 'dfadfasf',
++ 'foo.py',
++ '.*',
++ # FIXME: a path with parans breaks the regex
++ # '(.*)',
++ '/path/to/ansible/lib/ansible/plugins/connection/custom_connection.py',
++ '/path/to/ansible/lib/ansible/plugins/connection/ssh.py']
++
++ def fake_all(path_only=None):
++ for path in connection_loader_paths:
++ yield path
++
++ mock_connection_loader = MagicMock()
++ mock_connection_loader.all = fake_all
++
++ mock_shared_loader_obj.connection_loader = mock_connection_loader
++ mock_connection = MagicMock()
++ # mock_connection._shell.env_prefix.side_effect = env_prefix
++
++ # action_base = DerivedActionBase(mock_task, mock_connection, play_context, None, None, None)
++ action_base = DerivedActionBase(task=None,
++ connection=mock_connection,
++ play_context=None,
++ loader=fake_loader,
++ templar=None,
++ shared_loader_obj=mock_shared_loader_obj)
+ data = {'ansible_playbook_python': '/usr/bin/python',
++ # 'ansible_rsync_path': '/usr/bin/rsync',
+ 'ansible_python_interpreter': '/usr/bin/python',
+ 'ansible_ssh_some_var': 'whatever',
+ 'ansible_ssh_host_key_somehost': 'some key here',
+--- ansible-core-2.16.5.orig/test/units/plugins/action/test_raw.py
++++ ansible-core-2.16.5/test/units/plugins/action/test_raw.py
+@@ -20,6 +20,7 @@ __metaclass__ = type
+
+ import os
+
++from ansible.errors import AnsibleActionFail
+ from units.compat import unittest
+ from unittest.mock import MagicMock, Mock
+ from ansible.plugins.action.raw import ActionModule
+@@ -67,7 +68,10 @@ class TestCopyResultExclude(unittest.Tes
+ task.args = {'_raw_params': 'Args1'}
+ self.play_context.check_mode = True
+
+- self.mock_am = ActionModule(task, self.connection, self.play_context, loader=None, templar=None, shared_loader_obj=None)
++ try:
++ self.mock_am = ActionModule(task, self.connection, self.play_context, loader=None, templar=None, shared_loader_obj=None)
++ except AnsibleActionFail:
++ pass
+
+ def test_raw_test_environment_is_None(self):
+
+--- ansible-core-2.16.5.orig/test/units/plugins/cache/test_cache.py
++++ ansible-core-2.16.5/test/units/plugins/cache/test_cache.py
+@@ -29,7 +29,7 @@ from units.compat import unittest
+ from ansible.errors import AnsibleError
+ from ansible.plugins.cache import CachePluginAdjudicator
+ from ansible.plugins.cache.memory import CacheModule as MemoryCache
+-from ansible.plugins.loader import cache_loader, init_plugin_loader
++from ansible.plugins.loader import cache_loader
+ from ansible.vars.fact_cache import FactCache
+
+ import pytest
+@@ -66,7 +66,7 @@ class TestCachePluginAdjudicator(unittes
+
+ def test___getitem__(self):
+ with pytest.raises(KeyError):
+- self.cache['foo'] # pylint: disable=pointless-statement
++ self.cache['foo']
+
+ def test_pop_with_default(self):
+ assert self.cache.pop('foo', 'bar') == 'bar'
+@@ -183,7 +183,6 @@ class TestFactCache(unittest.TestCase):
+ assert len(self.cache.keys()) == 0
+
+ def test_plugin_load_failure(self):
+- init_plugin_loader()
+ # See https://github.com/ansible/ansible/issues/18751
+ # Note no fact_connection config set, so this will fail
+ with mock.patch('ansible.constants.CACHE_PLUGIN', 'json'):
+--- ansible-core-2.16.5.orig/test/units/plugins/connection/test_connection.py
++++ ansible-core-2.16.5/test/units/plugins/connection/test_connection.py
+@@ -27,28 +27,6 @@ from ansible.plugins.connection import C
+ from ansible.plugins.loader import become_loader
+
+
+-class NoOpConnection(ConnectionBase):
+-
+- @property
+- def transport(self):
+- """This method is never called by unit tests."""
+-
+- def _connect(self):
+- """This method is never called by unit tests."""
+-
+- def exec_command(self):
+- """This method is never called by unit tests."""
+-
+- def put_file(self):
+- """This method is never called by unit tests."""
+-
+- def fetch_file(self):
+- """This method is never called by unit tests."""
+-
+- def close(self):
+- """This method is never called by unit tests."""
+-
+-
+ class TestConnectionBaseClass(unittest.TestCase):
+
+ def setUp(self):
+@@ -67,8 +45,36 @@ class TestConnectionBaseClass(unittest.T
+ with self.assertRaises(TypeError):
+ ConnectionModule1() # pylint: disable=abstract-class-instantiated
+
++ class ConnectionModule2(ConnectionBase):
++ def get(self, key):
++ super(ConnectionModule2, self).get(key)
++
++ with self.assertRaises(TypeError):
++ ConnectionModule2() # pylint: disable=abstract-class-instantiated
++
+ def test_subclass_success(self):
+- self.assertIsInstance(NoOpConnection(self.play_context, self.in_stream), NoOpConnection)
++ class ConnectionModule3(ConnectionBase):
++
++ @property
++ def transport(self):
++ pass
++
++ def _connect(self):
++ pass
++
++ def exec_command(self):
++ pass
++
++ def put_file(self):
++ pass
++
++ def fetch_file(self):
++ pass
++
++ def close(self):
++ pass
++
++ self.assertIsInstance(ConnectionModule3(self.play_context, self.in_stream), ConnectionModule3)
+
+ def test_check_password_prompt(self):
+ local = (
+@@ -123,7 +129,28 @@ debug3: receive packet: type 98
+ debug1: Sending command: /bin/sh -c 'sudo -H -S -p "[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: " -u root /bin/sh -c '"'"'echo
+ '''
+
+- c = NoOpConnection(self.play_context, self.in_stream)
++ class ConnectionFoo(ConnectionBase):
++
++ @property
++ def transport(self):
++ pass
++
++ def _connect(self):
++ pass
++
++ def exec_command(self):
++ pass
++
++ def put_file(self):
++ pass
++
++ def fetch_file(self):
++ pass
++
++ def close(self):
++ pass
++
++ c = ConnectionFoo(self.play_context, self.in_stream)
+ c.set_become_plugin(become_loader.get('sudo'))
+ c.become.prompt = '[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: '
+
+--- ansible-core-2.16.5.orig/test/units/plugins/connection/test_local.py
++++ ansible-core-2.16.5/test/units/plugins/connection/test_local.py
+@@ -21,6 +21,7 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ from io import StringIO
++import pytest
+
+ from units.compat import unittest
+ from ansible.plugins.connection import local
+--- ansible-core-2.16.5.orig/test/units/plugins/connection/test_ssh.py
++++ ansible-core-2.16.5/test/units/plugins/connection/test_ssh.py
+@@ -24,13 +24,14 @@ from io import StringIO
+ import pytest
+
+
++from ansible import constants as C
+ from ansible.errors import AnsibleAuthenticationFailure
+ from units.compat import unittest
+ from unittest.mock import patch, MagicMock, PropertyMock
+ from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
+ from ansible.module_utils.compat.selectors import SelectorKey, EVENT_READ
+ from ansible.module_utils.six.moves import shlex_quote
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+ from ansible.playbook.play_context import PlayContext
+ from ansible.plugins.connection import ssh
+ from ansible.plugins.loader import connection_loader, become_loader
+@@ -141,8 +142,9 @@ class TestConnectionBaseClass(unittest.T
+ conn.become.check_missing_password = MagicMock(side_effect=_check_missing_password)
+
+ def get_option(option):
+- assert option == 'become_pass'
+- return 'password'
++ if option == 'become_pass':
++ return 'password'
++ return None
+
+ conn.become.get_option = get_option
+ output, unprocessed = conn._examine_output(u'source', u'state', b'line 1\nline 2\nfoo\nline 3\nthis should be the remainder', False)
+@@ -349,7 +351,7 @@ class MockSelector(object):
+ self.register = MagicMock(side_effect=self._register)
+ self.unregister = MagicMock(side_effect=self._unregister)
+ self.close = MagicMock()
+- self.get_map = MagicMock()
++ self.get_map = MagicMock(side_effect=self._get_map)
+ self.select = MagicMock()
+
+ def _register(self, *args, **kwargs):
+@@ -358,6 +360,9 @@ class MockSelector(object):
+ def _unregister(self, *args, **kwargs):
+ self.files_watched -= 1
+
++ def _get_map(self, *args, **kwargs):
++ return self.files_watched
++
+
+ @pytest.fixture
+ def mock_run_env(request, mocker):
+@@ -452,8 +457,7 @@ class TestSSHConnectionRun(object):
+ def _password_with_prompt_examine_output(self, sourice, state, b_chunk, sudoable):
+ if state == 'awaiting_prompt':
+ self.conn._flags['become_prompt'] = True
+- else:
+- assert state == 'awaiting_escalation'
++ elif state == 'awaiting_escalation':
+ self.conn._flags['become_success'] = True
+ return (b'', b'')
+
+@@ -542,6 +546,7 @@ class TestSSHConnectionRetries(object):
+ def test_incorrect_password(self, monkeypatch):
+ self.conn.set_option('host_key_checking', False)
+ self.conn.set_option('reconnection_retries', 5)
++ monkeypatch.setattr('time.sleep', lambda x: None)
+
+ self.mock_popen_res.stdout.read.side_effect = [b'']
+ self.mock_popen_res.stderr.read.side_effect = [b'Permission denied, please try again.\r\n']
+@@ -664,6 +669,7 @@ class TestSSHConnectionRetries(object):
+ self.conn.set_option('reconnection_retries', 3)
+
+ monkeypatch.setattr('time.sleep', lambda x: None)
++ monkeypatch.setattr('ansible.plugins.connection.ssh.os.path.exists', lambda x: True)
+
+ self.mock_popen_res.stdout.read.side_effect = [b"", b"my_stdout\n", b"second_line"]
+ self.mock_popen_res.stderr.read.side_effect = [b"", b"my_stderr"]
+--- ansible-core-2.16.5.orig/test/units/plugins/connection/test_winrm.py
++++ ansible-core-2.16.5/test/units/plugins/connection/test_winrm.py
+@@ -13,8 +13,8 @@ import pytest
+ from io import StringIO
+
+ from unittest.mock import MagicMock
+-from ansible.errors import AnsibleConnectionFailure, AnsibleError
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.errors import AnsibleConnectionFailure
++from ansible.module_utils._text import to_bytes
+ from ansible.playbook.play_context import PlayContext
+ from ansible.plugins.loader import connection_loader
+ from ansible.plugins.connection import winrm
+@@ -441,103 +441,3 @@ class TestWinRMKerbAuth(object):
+ assert str(err.value) == \
+ "Kerberos auth failure for principal username with pexpect: " \
+ "Error with kinit\n<redacted>"
+-
+- def test_exec_command_with_timeout(self, monkeypatch):
+- requests_exc = pytest.importorskip("requests.exceptions")
+-
+- pc = PlayContext()
+- new_stdin = StringIO()
+- conn = connection_loader.get('winrm', pc, new_stdin)
+-
+- mock_proto = MagicMock()
+- mock_proto.run_command.side_effect = requests_exc.Timeout("msg")
+-
+- conn._connected = True
+- conn._winrm_host = 'hostname'
+-
+- monkeypatch.setattr(conn, "_winrm_connect", lambda: mock_proto)
+-
+- with pytest.raises(AnsibleConnectionFailure) as e:
+- conn.exec_command('cmd', in_data=None, sudoable=True)
+-
+- assert str(e.value) == "winrm connection error: msg"
+-
+- def test_exec_command_get_output_timeout(self, monkeypatch):
+- requests_exc = pytest.importorskip("requests.exceptions")
+-
+- pc = PlayContext()
+- new_stdin = StringIO()
+- conn = connection_loader.get('winrm', pc, new_stdin)
+-
+- mock_proto = MagicMock()
+- mock_proto.run_command.return_value = "command_id"
+- mock_proto.send_message.side_effect = requests_exc.Timeout("msg")
+-
+- conn._connected = True
+- conn._winrm_host = 'hostname'
+-
+- monkeypatch.setattr(conn, "_winrm_connect", lambda: mock_proto)
+-
+- with pytest.raises(AnsibleConnectionFailure) as e:
+- conn.exec_command('cmd', in_data=None, sudoable=True)
+-
+- assert str(e.value) == "winrm connection error: msg"
+-
+- def test_connect_failure_auth_401(self, monkeypatch):
+- pc = PlayContext()
+- new_stdin = StringIO()
+- conn = connection_loader.get('winrm', pc, new_stdin)
+- conn.set_options(var_options={"ansible_winrm_transport": "basic", "_extras": {}})
+-
+- mock_proto = MagicMock()
+- mock_proto.open_shell.side_effect = ValueError("Custom exc Code 401")
+-
+- mock_proto_init = MagicMock()
+- mock_proto_init.return_value = mock_proto
+- monkeypatch.setattr(winrm, "Protocol", mock_proto_init)
+-
+- with pytest.raises(AnsibleConnectionFailure, match="the specified credentials were rejected by the server"):
+- conn.exec_command('cmd', in_data=None, sudoable=True)
+-
+- def test_connect_failure_other_exception(self, monkeypatch):
+- pc = PlayContext()
+- new_stdin = StringIO()
+- conn = connection_loader.get('winrm', pc, new_stdin)
+- conn.set_options(var_options={"ansible_winrm_transport": "basic", "_extras": {}})
+-
+- mock_proto = MagicMock()
+- mock_proto.open_shell.side_effect = ValueError("Custom exc")
+-
+- mock_proto_init = MagicMock()
+- mock_proto_init.return_value = mock_proto
+- monkeypatch.setattr(winrm, "Protocol", mock_proto_init)
+-
+- with pytest.raises(AnsibleConnectionFailure, match="basic: Custom exc"):
+- conn.exec_command('cmd', in_data=None, sudoable=True)
+-
+- def test_connect_failure_operation_timed_out(self, monkeypatch):
+- pc = PlayContext()
+- new_stdin = StringIO()
+- conn = connection_loader.get('winrm', pc, new_stdin)
+- conn.set_options(var_options={"ansible_winrm_transport": "basic", "_extras": {}})
+-
+- mock_proto = MagicMock()
+- mock_proto.open_shell.side_effect = ValueError("Custom exc Operation timed out")
+-
+- mock_proto_init = MagicMock()
+- mock_proto_init.return_value = mock_proto
+- monkeypatch.setattr(winrm, "Protocol", mock_proto_init)
+-
+- with pytest.raises(AnsibleError, match="the connection attempt timed out"):
+- conn.exec_command('cmd', in_data=None, sudoable=True)
+-
+- def test_connect_no_transport(self):
+- pc = PlayContext()
+- new_stdin = StringIO()
+- conn = connection_loader.get('winrm', pc, new_stdin)
+- conn.set_options(var_options={"_extras": {}})
+- conn._build_winrm_kwargs()
+- conn._winrm_transport = []
+-
+- with pytest.raises(AnsibleError, match="No transport found for WinRM connection"):
+- conn._winrm_connect()
+--- ansible-core-2.16.5.orig/test/units/plugins/filter/test_core.py
++++ ansible-core-2.16.5/test/units/plugins/filter/test_core.py
+@@ -3,11 +3,13 @@
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ from __future__ import absolute_import, division, print_function
++from jinja2.runtime import Undefined
++from jinja2.exceptions import UndefinedError
+ __metaclass__ = type
+
+ import pytest
+
+-from ansible.module_utils.common.text.converters import to_native
++from ansible.module_utils._text import to_native
+ from ansible.plugins.filter.core import to_uuid
+ from ansible.errors import AnsibleFilterError
+
+--- ansible-core-2.16.5.orig/test/units/plugins/filter/test_mathstuff.py
++++ ansible-core-2.16.5/test/units/plugins/filter/test_mathstuff.py
+@@ -1,8 +1,9 @@
+ # Copyright: (c) 2017, Ansible Project
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+-from __future__ import annotations
+-
++# Make coding more python3-ish
++from __future__ import (absolute_import, division, print_function)
++__metaclass__ = type
+ import pytest
+
+ from jinja2 import Environment
+@@ -11,68 +12,54 @@ import ansible.plugins.filter.mathstuff
+ from ansible.errors import AnsibleFilterError, AnsibleFilterTypeError
+
+
+-UNIQUE_DATA = [
+- ([], []),
+- ([1, 3, 4, 2], [1, 3, 4, 2]),
+- ([1, 3, 2, 4, 2, 3], [1, 3, 2, 4]),
+- ([1, 2, 3, 4], [1, 2, 3, 4]),
+- ([1, 1, 4, 2, 1, 4, 3, 2], [1, 4, 2, 3]),
+-]
+-
+-TWO_SETS_DATA = [
+- ([], [], ([], [], [])),
+- ([1, 2], [1, 2], ([1, 2], [], [])),
+- ([1, 2], [3, 4], ([], [1, 2], [1, 2, 3, 4])),
+- ([1, 2, 3], [5, 3, 4], ([3], [1, 2], [1, 2, 5, 4])),
+- ([1, 2, 3], [4, 3, 5], ([3], [1, 2], [1, 2, 4, 5])),
+-]
+-
+-
+-def dict_values(values: list[int]) -> list[dict[str, int]]:
+- """Return a list of non-hashable values derived from the given list."""
+- return [dict(x=value) for value in values]
+-
+-
+-for _data, _expected in list(UNIQUE_DATA):
+- UNIQUE_DATA.append((dict_values(_data), dict_values(_expected)))
+-
+-for _dataset1, _dataset2, _expected in list(TWO_SETS_DATA):
+- TWO_SETS_DATA.append((dict_values(_dataset1), dict_values(_dataset2), tuple(dict_values(answer) for answer in _expected)))
+-
++UNIQUE_DATA = (([1, 3, 4, 2], [1, 3, 4, 2]),
++ ([1, 3, 2, 4, 2, 3], [1, 3, 2, 4]),
++ (['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd']),
++ (['a', 'a', 'd', 'b', 'a', 'd', 'c', 'b'], ['a', 'd', 'b', 'c']),
++ )
++
++TWO_SETS_DATA = (([1, 2], [3, 4], ([], sorted([1, 2]), sorted([1, 2, 3, 4]), sorted([1, 2, 3, 4]))),
++ ([1, 2, 3], [5, 3, 4], ([3], sorted([1, 2]), sorted([1, 2, 5, 4]), sorted([1, 2, 3, 4, 5]))),
++ (['a', 'b', 'c'], ['d', 'c', 'e'], (['c'], sorted(['a', 'b']), sorted(['a', 'b', 'd', 'e']), sorted(['a', 'b', 'c', 'e', 'd']))),
++ )
+
+ env = Environment()
+
+
+-def assert_lists_contain_same_elements(a, b) -> None:
+- """Assert that the two values given are lists that contain the same elements, even when the elements cannot be sorted or hashed."""
+- assert isinstance(a, list)
+- assert isinstance(b, list)
++@pytest.mark.parametrize('data, expected', UNIQUE_DATA)
++class TestUnique:
++ def test_unhashable(self, data, expected):
++ assert ms.unique(env, list(data)) == expected
+
+- missing_from_a = [item for item in b if item not in a]
+- missing_from_b = [item for item in a if item not in b]
++ def test_hashable(self, data, expected):
++ assert ms.unique(env, tuple(data)) == expected
+
+- assert not missing_from_a, f'elements from `b` {missing_from_a} missing from `a` {a}'
+- assert not missing_from_b, f'elements from `a` {missing_from_b} missing from `b` {b}'
+
++@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA)
++class TestIntersect:
++ def test_unhashable(self, dataset1, dataset2, expected):
++ assert sorted(ms.intersect(env, list(dataset1), list(dataset2))) == expected[0]
+
+-@pytest.mark.parametrize('data, expected', UNIQUE_DATA, ids=str)
+-def test_unique(data, expected):
+- assert_lists_contain_same_elements(ms.unique(env, data), expected)
++ def test_hashable(self, dataset1, dataset2, expected):
++ assert sorted(ms.intersect(env, tuple(dataset1), tuple(dataset2))) == expected[0]
+
+
+-@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA, ids=str)
+-def test_intersect(dataset1, dataset2, expected):
+- assert_lists_contain_same_elements(ms.intersect(env, dataset1, dataset2), expected[0])
++@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA)
++class TestDifference:
++ def test_unhashable(self, dataset1, dataset2, expected):
++ assert sorted(ms.difference(env, list(dataset1), list(dataset2))) == expected[1]
+
++ def test_hashable(self, dataset1, dataset2, expected):
++ assert sorted(ms.difference(env, tuple(dataset1), tuple(dataset2))) == expected[1]
+
+-@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA, ids=str)
+-def test_difference(dataset1, dataset2, expected):
+- assert_lists_contain_same_elements(ms.difference(env, dataset1, dataset2), expected[1])
+
++@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA)
++class TestSymmetricDifference:
++ def test_unhashable(self, dataset1, dataset2, expected):
++ assert sorted(ms.symmetric_difference(env, list(dataset1), list(dataset2))) == expected[2]
+
+-@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA, ids=str)
+-def test_symmetric_difference(dataset1, dataset2, expected):
+- assert_lists_contain_same_elements(ms.symmetric_difference(env, dataset1, dataset2), expected[2])
++ def test_hashable(self, dataset1, dataset2, expected):
++ assert sorted(ms.symmetric_difference(env, tuple(dataset1), tuple(dataset2))) == expected[2]
+
+
+ class TestLogarithm:
+--- ansible-core-2.16.5.orig/test/units/plugins/inventory/test_constructed.py
++++ ansible-core-2.16.5/test/units/plugins/inventory/test_constructed.py
+@@ -194,11 +194,11 @@ def test_parent_group_templating_error(i
+ 'parent_group': '{{ location.barn-yard }}'
+ }
+ ]
+- with pytest.raises(AnsibleParserError) as ex:
++ with pytest.raises(AnsibleParserError) as err_message:
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=True
+ )
+- assert 'Could not generate parent group' in str(ex.value)
++ assert 'Could not generate parent group' in err_message
+ # invalid parent group did not raise an exception with strict=False
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=False
+@@ -213,17 +213,17 @@ def test_keyed_group_exclusive_argument(
+ host = inventory_module.inventory.get_host('cow')
+ keyed_groups = [
+ {
+- 'key': 'nickname',
++ 'key': 'tag',
+ 'separator': '_',
+ 'default_value': 'default_value_name',
+ 'trailing_separator': True
+ }
+ ]
+- with pytest.raises(AnsibleParserError) as ex:
++ with pytest.raises(AnsibleParserError) as err_message:
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=True
+ )
+- assert 'parameters are mutually exclusive' in str(ex.value)
++ assert 'parameters are mutually exclusive' in err_message
+
+
+ def test_keyed_group_empty_value(inventory_module):
+--- ansible-core-2.16.5.orig/test/units/plugins/inventory/test_inventory.py
++++ ansible-core-2.16.5/test/units/plugins/inventory/test_inventory.py
+@@ -27,7 +27,7 @@ from unittest import mock
+ from ansible import constants as C
+ from units.compat import unittest
+ from ansible.module_utils.six import string_types
+-from ansible.module_utils.common.text.converters import to_text
++from ansible.module_utils._text import to_text
+ from units.mock.path import mock_unfrackpath_noop
+
+ from ansible.inventory.manager import InventoryManager, split_host_pattern
+--- ansible-core-2.16.5.orig/test/units/plugins/inventory/test_script.py
++++ ansible-core-2.16.5/test/units/plugins/inventory/test_script.py
+@@ -28,7 +28,7 @@ from ansible import constants as C
+ from ansible.errors import AnsibleError
+ from ansible.plugins.loader import PluginLoader
+ from units.compat import unittest
+-from ansible.module_utils.common.text.converters import to_bytes, to_native
++from ansible.module_utils._text import to_bytes, to_native
+
+
+ class TestInventoryModule(unittest.TestCase):
+@@ -103,11 +103,3 @@ class TestInventoryModule(unittest.TestC
+ self.inventory_module.parse(self.inventory, self.loader, '/foo/bar/foobar.py')
+ assert e.value.message == to_native("failed to parse executable inventory script results from "
+ "/foo/bar/foobar.py: needs to be a json dict\ndummyédata\n")
+-
+- def test_get_host_variables_subprocess_script_raises_error(self):
+- self.popen_result.returncode = 1
+- self.popen_result.stderr = to_bytes("dummyéerror")
+-
+- with pytest.raises(AnsibleError) as e:
+- self.inventory_module.get_host_variables('/foo/bar/foobar.py', 'dummy host')
+- assert e.value.message == "Inventory script (/foo/bar/foobar.py) had an execution error: dummyéerror"
+--- ansible-core-2.16.5.orig/test/units/plugins/lookup/test_password.py
++++ ansible-core-2.16.5/test/units/plugins/lookup/test_password.py
+@@ -23,7 +23,7 @@ __metaclass__ = type
+ try:
+ import passlib
+ from passlib.handlers import pbkdf2
+-except ImportError: # pragma: nocover
++except ImportError:
+ passlib = None
+ pbkdf2 = None
+
+@@ -36,7 +36,7 @@ from unittest.mock import mock_open, pat
+ from ansible.errors import AnsibleError
+ from ansible.module_utils.six import text_type
+ from ansible.module_utils.six.moves import builtins
+-from ansible.module_utils.common.text.converters import to_bytes
++from ansible.module_utils._text import to_bytes
+ from ansible.plugins.loader import PluginLoader, lookup_loader
+ from ansible.plugins.lookup import password
+
+@@ -416,6 +416,8 @@ class BaseTestLookupModule(unittest.Test
+ password.os.open = lambda path, flag: None
+ self.os_close = password.os.close
+ password.os.close = lambda fd: None
++ self.os_remove = password.os.remove
++ password.os.remove = lambda path: None
+ self.makedirs_safe = password.makedirs_safe
+ password.makedirs_safe = lambda path, mode: None
+
+@@ -423,6 +425,7 @@ class BaseTestLookupModule(unittest.Test
+ password.os.path.exists = self.os_path_exists
+ password.os.open = self.os_open
+ password.os.close = self.os_close
++ password.os.remove = self.os_remove
+ password.makedirs_safe = self.makedirs_safe
+
+
+@@ -464,17 +467,23 @@ class TestLookupModuleWithoutPasslib(Bas
+ def test_lock_been_held(self, mock_sleep):
+ # pretend the lock file is here
+ password.os.path.exists = lambda x: True
+- with pytest.raises(AnsibleError):
++ try:
+ with patch.object(builtins, 'open', mock_open(read_data=b'hunter42 salt=87654321\n')) as m:
+ # should timeout here
+- self.password_lookup.run([u'/path/to/somewhere chars=anything'], None)
++ results = self.password_lookup.run([u'/path/to/somewhere chars=anything'], None)
++ self.fail("Lookup didn't timeout when lock already been held")
++ except AnsibleError:
++ pass
+
+ def test_lock_not_been_held(self):
+ # pretend now there is password file but no lock
+ password.os.path.exists = lambda x: x == to_bytes('/path/to/somewhere')
+- with patch.object(builtins, 'open', mock_open(read_data=b'hunter42 salt=87654321\n')) as m:
+- # should not timeout here
+- results = self.password_lookup.run([u'/path/to/somewhere chars=anything'], None)
++ try:
++ with patch.object(builtins, 'open', mock_open(read_data=b'hunter42 salt=87654321\n')) as m:
++ # should not timeout here
++ results = self.password_lookup.run([u'/path/to/somewhere chars=anything'], None)
++ except AnsibleError:
++ self.fail('Lookup timeouts when lock is free')
+
+ for result in results:
+ self.assertEqual(result, u'hunter42')
+@@ -522,8 +531,10 @@ class TestLookupModuleWithPasslib(BaseTe
+ self.assertEqual(int(str_parts[2]), crypt_parts['rounds'])
+ self.assertIsInstance(result, text_type)
+
++ @patch.object(PluginLoader, '_get_paths')
+ @patch('ansible.plugins.lookup.password._write_password_file')
+- def test_password_already_created_encrypt(self, mock_write_file):
++ def test_password_already_created_encrypt(self, mock_get_paths, mock_write_file):
++ mock_get_paths.return_value = ['/path/one', '/path/two', '/path/three']
+ password.os.path.exists = lambda x: x == to_bytes('/path/to/somewhere')
+
+ with patch.object(builtins, 'open', mock_open(read_data=b'hunter42 salt=87654321\n')) as m:
+@@ -531,9 +542,6 @@ class TestLookupModuleWithPasslib(BaseTe
+ for result in results:
+ self.assertEqual(result, u'$pbkdf2-sha256$20000$ODc2NTQzMjE$Uikde0cv0BKaRaAXMrUQB.zvG4GmnjClwjghwIRf2gU')
+
+- # Assert the password file is not rewritten
+- mock_write_file.assert_not_called()
+-
+
+ @pytest.mark.skipif(passlib is None, reason='passlib must be installed to run these tests')
+ class TestLookupModuleWithPasslibWrappedAlgo(BaseTestLookupModule):
+--- ansible-core-2.16.5.orig/test/units/plugins/test_plugins.py
++++ ansible-core-2.16.5/test/units/plugins/test_plugins.py
+@@ -46,14 +46,14 @@ class TestErrors(unittest.TestCase):
+ # python library, and then uses the __file__ attribute of
+ # the result for that to get the library path, so we mock
+ # that here and patch the builtin to use our mocked result
+- foo_pkg = MagicMock()
+- bar_pkg = MagicMock()
++ foo = MagicMock()
++ bar = MagicMock()
+ bam = MagicMock()
+ bam.__file__ = '/path/to/my/foo/bar/bam/__init__.py'
+- bar_pkg.bam = bam
+- foo_pkg.return_value.bar = bar_pkg
++ bar.bam = bam
++ foo.return_value.bar = bar
+ pl = PluginLoader('test', 'foo.bar.bam', 'test', 'test_plugin')
+- with patch('builtins.__import__', foo_pkg):
++ with patch('builtins.__import__', foo):
+ self.assertEqual(pl._get_package_paths(), ['/path/to/my/foo/bar/bam'])
+
+ def test_plugins__get_paths(self):
+--- ansible-core-2.16.5.orig/test/units/requirements.txt
++++ ansible-core-2.16.5/test/units/requirements.txt
+@@ -1,4 +1,4 @@
+-bcrypt ; python_version >= '3.10' # controller only
+-passlib ; python_version >= '3.10' # controller only
+-pexpect ; python_version >= '3.10' # controller only
+-pywinrm ; python_version >= '3.10' # controller only
++bcrypt ; python_version >= '3.9' # controller only
++passlib ; python_version >= '3.9' # controller only
++pexpect ; python_version >= '3.9' # controller only
++pywinrm ; python_version >= '3.9' # controller only
+--- ansible-core-2.16.5.orig/test/units/template/test_templar.py
++++ ansible-core-2.16.5/test/units/template/test_templar.py
+@@ -22,10 +22,11 @@ __metaclass__ = type
+ from jinja2.runtime import Context
+
+ from units.compat import unittest
++from unittest.mock import patch
+
+ from ansible import constants as C
+ from ansible.errors import AnsibleError, AnsibleUndefinedVariable
+-from ansible.plugins.loader import init_plugin_loader
++from ansible.module_utils.six import string_types
+ from ansible.template import Templar, AnsibleContext, AnsibleEnvironment, AnsibleUndefined
+ from ansible.utils.unsafe_proxy import AnsibleUnsafe, wrap_var
+ from units.mock.loader import DictDataLoader
+@@ -33,7 +34,6 @@ from units.mock.loader import DictDataLo
+
+ class BaseTemplar(object):
+ def setUp(self):
+- init_plugin_loader()
+ self.test_vars = dict(
+ foo="bar",
+ bam="{{foo}}",
+@@ -62,6 +62,14 @@ class BaseTemplar(object):
+ return self._ansible_context._is_unsafe(obj)
+
+
++# class used for testing arbitrary objects passed to template
++class SomeClass(object):
++ foo = 'bar'
++
++ def __init__(self):
++ self.blip = 'blip'
++
++
+ class SomeUnsafeClass(AnsibleUnsafe):
+ def __init__(self):
+ super(SomeUnsafeClass, self).__init__()
+@@ -258,6 +266,8 @@ class TestTemplarMisc(BaseTemplar, unitt
+ templar.available_variables = "foo=bam"
+ except AssertionError:
+ pass
++ except Exception as e:
++ self.fail(e)
+
+ def test_templar_escape_backslashes(self):
+ # Rule of thumb: If escape backslashes is True you should end up with
+--- ansible-core-2.16.5.orig/test/units/template/test_vars.py
++++ ansible-core-2.16.5/test/units/template/test_vars.py
+@@ -19,16 +19,23 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-from ansible.template import Templar
++from units.compat import unittest
++from unittest.mock import MagicMock
++
+ from ansible.template.vars import AnsibleJ2Vars
+
+
+-def test_globals_empty():
+- assert isinstance(dict(AnsibleJ2Vars(Templar(None), {})), dict)
++class TestVars(unittest.TestCase):
++ def setUp(self):
++ self.mock_templar = MagicMock(name='mock_templar')
+
++ def test_globals_empty(self):
++ ajvars = AnsibleJ2Vars(self.mock_templar, {})
++ res = dict(ajvars)
++ self.assertIsInstance(res, dict)
+
+-def test_globals():
+- res = dict(AnsibleJ2Vars(Templar(None), {'foo': 'bar', 'blip': [1, 2, 3]}))
+- assert isinstance(res, dict)
+- assert 'foo' in res
+- assert res['foo'] == 'bar'
++ def test_globals(self):
++ res = dict(AnsibleJ2Vars(self.mock_templar, {'foo': 'bar', 'blip': [1, 2, 3]}))
++ self.assertIsInstance(res, dict)
++ self.assertIn('foo', res)
++ self.assertEqual(res['foo'], 'bar')
+--- ansible-core-2.16.5.orig/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py
++++ ansible-core-2.16.5/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py
+@@ -1,7 +1,7 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-from ..module_utils.my_util import question # pylint: disable=unused-import
++from ..module_utils.my_util import question
+
+
+ def action_code():
+--- ansible-core-2.16.5.orig/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_other_util.py
++++ ansible-core-2.16.5/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_other_util.py
+@@ -1,4 +1,4 @@
+ from __future__ import (absolute_import, division, print_function)
+ __metaclass__ = type
+
+-from .my_util import question # pylint: disable=unused-import
++from .my_util import question
+--- ansible-core-2.16.5.orig/test/units/utils/collection_loader/test_collection_loader.py
++++ ansible-core-2.16.5/test/units/utils/collection_loader/test_collection_loader.py
+@@ -13,7 +13,7 @@ from ansible.modules import ping as ping
+ from ansible.utils.collection_loader import AnsibleCollectionConfig, AnsibleCollectionRef
+ from ansible.utils.collection_loader._collection_finder import (
+ _AnsibleCollectionFinder, _AnsibleCollectionLoader, _AnsibleCollectionNSPkgLoader, _AnsibleCollectionPkgLoader,
+- _AnsibleCollectionPkgLoaderBase, _AnsibleCollectionRootPkgLoader, _AnsibleNSTraversable, _AnsiblePathHookFinder,
++ _AnsibleCollectionPkgLoaderBase, _AnsibleCollectionRootPkgLoader, _AnsiblePathHookFinder,
+ _get_collection_name_from_path, _get_collection_role_path, _get_collection_metadata, _iter_modules_impl
+ )
+ from ansible.utils.collection_loader._collection_config import _EventSource
+@@ -29,16 +29,8 @@ def teardown(*args, **kwargs):
+ # BEGIN STANDALONE TESTS - these exercise behaviors of the individual components without the import machinery
+
+
+-@pytest.mark.filterwarnings(
+- 'ignore:'
+- r'find_module\(\) is deprecated and slated for removal in Python 3\.12; use find_spec\(\) instead'
+- ':DeprecationWarning',
+- 'ignore:'
+- r'FileFinder\.find_loader\(\) is deprecated and slated for removal in Python 3\.12; use find_spec\(\) instead'
+- ':DeprecationWarning',
+-)
+-@pytest.mark.skipif(not PY3 or sys.version_info >= (3, 12), reason='Testing Python 2 codepath (find_module) on Python 3, <= 3.11')
+-def test_find_module_py3_lt_312():
++@pytest.mark.skipif(not PY3, reason='Testing Python 2 codepath (find_module) on Python 3')
++def test_find_module_py3():
+ dir_to_a_file = os.path.dirname(ping_module.__file__)
+ path_hook_finder = _AnsiblePathHookFinder(_AnsibleCollectionFinder(), dir_to_a_file)
+
+@@ -48,16 +40,6 @@ def test_find_module_py3_lt_312():
+ assert path_hook_finder.find_module('missing') is None
+
+
+-@pytest.mark.skipif(sys.version_info < (3, 12), reason='Testing Python 2 codepath (find_module) on Python >= 3.12')
+-def test_find_module_py3_gt_311():
+- dir_to_a_file = os.path.dirname(ping_module.__file__)
+- path_hook_finder = _AnsiblePathHookFinder(_AnsibleCollectionFinder(), dir_to_a_file)
+-
+- # setuptools may fall back to find_module on Python 3 if find_spec returns None
+- # see https://github.com/pypa/setuptools/pull/2918
+- assert path_hook_finder.find_spec('missing') is None
+-
+-
+ def test_finder_setup():
+ # ensure scalar path is listified
+ f = _AnsibleCollectionFinder(paths='/bogus/bogus')
+@@ -846,53 +828,6 @@ def test_collectionref_components_invali
+ assert re.search(expected_error_expression, str(curerr.value))
+
+
+-@pytest.mark.skipif(not PY3, reason='importlib.resources only supported for py3')
+-def test_importlib_resources():
+- if sys.version_info < (3, 10):
+- from importlib_resources import files
+- else:
+- from importlib.resources import files
+- from pathlib import Path
+-
+- f = get_default_finder()
+- reset_collections_loader_state(f)
+-
+- ansible_collections_ns = files('ansible_collections')
+- ansible_ns = files('ansible_collections.ansible')
+- testns = files('ansible_collections.testns')
+- testcoll = files('ansible_collections.testns.testcoll')
+- testcoll2 = files('ansible_collections.testns.testcoll2')
+- module_utils = files('ansible_collections.testns.testcoll.plugins.module_utils')
+-
+- assert isinstance(ansible_collections_ns, _AnsibleNSTraversable)
+- assert isinstance(ansible_ns, _AnsibleNSTraversable)
+- assert isinstance(testcoll, Path)
+- assert isinstance(module_utils, Path)
+-
+- assert ansible_collections_ns.is_dir()
+- assert ansible_ns.is_dir()
+- assert testcoll.is_dir()
+- assert module_utils.is_dir()
+-
+- first_path = Path(default_test_collection_paths[0])
+- second_path = Path(default_test_collection_paths[1])
+- testns_paths = []
+- ansible_ns_paths = []
+- for path in default_test_collection_paths[:2]:
+- ansible_ns_paths.append(Path(path) / 'ansible_collections' / 'ansible')
+- testns_paths.append(Path(path) / 'ansible_collections' / 'testns')
+-
+- assert testns._paths == testns_paths
+- # NOTE: The next two asserts check for subsets to accommodate running the unit tests when externally installed collections are available.
+- assert set(ansible_ns_paths).issubset(ansible_ns._paths)
+- assert set(Path(p) / 'ansible_collections' for p in default_test_collection_paths[:2]).issubset(ansible_collections_ns._paths)
+- assert testcoll2 == second_path / 'ansible_collections' / 'testns' / 'testcoll2'
+-
+- assert {p.name for p in module_utils.glob('*.py')} == {'__init__.py', 'my_other_util.py', 'my_util.py'}
+- nestcoll_mu_init = first_path / 'ansible_collections' / 'testns' / 'testcoll' / 'plugins' / 'module_utils' / '__init__.py'
+- assert next(module_utils.glob('__init__.py')) == nestcoll_mu_init
+-
+-
+ # BEGIN TEST SUPPORT
+
+ default_test_collection_paths = [
+--- ansible-core-2.16.5.orig/test/units/utils/display/test_broken_cowsay.py
++++ ansible-core-2.16.5/test/units/utils/display/test_broken_cowsay.py
+@@ -12,13 +12,16 @@ 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")
+
++ def mock_communicate(input=None, timeout=None):
++ return b"", b""
++
+ mock_popen = MagicMock()
++ mock_popen.return_value.communicate = mock_communicate
+ 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
+--- ansible-core-2.16.5.orig/test/units/utils/test_cleanup_tmp_file.py
++++ ansible-core-2.16.5/test/units/utils/test_cleanup_tmp_file.py
+@@ -6,11 +6,16 @@ from __future__ import (absolute_import,
+ __metaclass__ = type
+
+ import os
++import pytest
+ import tempfile
+
+ from ansible.utils.path import cleanup_tmp_file
+
+
++def raise_error():
++ raise OSError
++
++
+ def test_cleanup_tmp_file_file():
+ tmp_fd, tmp = tempfile.mkstemp()
+ cleanup_tmp_file(tmp)
+@@ -29,21 +34,15 @@ def test_cleanup_tmp_file_nonexistant():
+ assert None is cleanup_tmp_file('nope')
+
+
+-def test_cleanup_tmp_file_failure(mocker, capsys):
++def test_cleanup_tmp_file_failure(mocker):
+ tmp = tempfile.mkdtemp()
+- rmtree = mocker.patch('shutil.rmtree', side_effect=OSError('test induced failure'))
+- cleanup_tmp_file(tmp)
+- out, err = capsys.readouterr()
+- assert out == ''
+- assert err == ''
+- rmtree.assert_called_once()
++ with pytest.raises(Exception):
++ mocker.patch('shutil.rmtree', side_effect=raise_error())
++ cleanup_tmp_file(tmp)
+
+
+ def test_cleanup_tmp_file_failure_warning(mocker, capsys):
+ tmp = tempfile.mkdtemp()
+- rmtree = mocker.patch('shutil.rmtree', side_effect=OSError('test induced failure'))
+- cleanup_tmp_file(tmp, warn=True)
+- out, err = capsys.readouterr()
+- assert out == 'Unable to remove temporary file test induced failure\n'
+- assert err == ''
+- rmtree.assert_called_once()
++ with pytest.raises(Exception):
++ mocker.patch('shutil.rmtree', side_effect=raise_error())
++ cleanup_tmp_file(tmp, warn=True)
+--- ansible-core-2.16.5.orig/test/units/utils/test_display.py
++++ ansible-core-2.16.5/test/units/utils/test_display.py
+@@ -18,14 +18,16 @@ from ansible.utils.multiprocessing impor
+
+ @pytest.fixture
+ def problematic_wcswidth_chars():
+- locale.setlocale(locale.LC_ALL, 'C.UTF-8')
++ problematic = []
++ try:
++ locale.setlocale(locale.LC_ALL, 'C.UTF-8')
++ except Exception:
++ return problematic
+
+ candidates = set(chr(c) for c in range(sys.maxunicode) if unicodedata.category(chr(c)) == 'Cf')
+- problematic = [candidate for candidate in candidates if _LIBC.wcswidth(candidate, _MAX_INT) == -1]
+-
+- if not problematic:
+- # Newer distributions (Ubuntu 22.04, Fedora 38) include a libc which does not report problematic characters.
+- pytest.skip("no problematic wcswidth chars found") # pragma: nocover
++ for c in candidates:
++ if _LIBC.wcswidth(c, _MAX_INT) == -1:
++ problematic.append(c)
+
+ return problematic
+
+@@ -52,6 +54,9 @@ def test_get_text_width():
+
+
+ def test_get_text_width_no_locale(problematic_wcswidth_chars):
++ if not problematic_wcswidth_chars:
++ pytest.skip("No problmatic wcswidth chars")
++ locale.setlocale(locale.LC_ALL, 'C.UTF-8')
+ pytest.raises(EnvironmentError, get_text_width, problematic_wcswidth_chars[0])
+
+
+@@ -103,21 +108,9 @@ def test_Display_display_fork():
+ display = Display()
+ display.set_queue(queue)
+ display.display('foo')
+- queue.send_display.assert_called_once_with('display', 'foo')
+-
+- p = multiprocessing_context.Process(target=test)
+- p.start()
+- p.join()
+- assert p.exitcode == 0
+-
+-
+-def test_Display_display_warn_fork():
+- def test():
+- queue = MagicMock()
+- display = Display()
+- display.set_queue(queue)
+- display.warning('foo')
+- queue.send_display.assert_called_once_with('warning', 'foo')
++ queue.send_display.assert_called_once_with(
++ 'foo', color=None, stderr=False, screen_only=False, log_only=False, newline=True
++ )
+
+ p = multiprocessing_context.Process(target=test)
+ p.start()
+--- ansible-core-2.16.5.orig/test/units/utils/test_encrypt.py
++++ ansible-core-2.16.5/test/units/utils/test_encrypt.py
+@@ -27,26 +27,17 @@ class passlib_off(object):
+
+ def assert_hash(expected, secret, algorithm, **settings):
+
+- assert encrypt.do_encrypt(secret, algorithm, **settings) == expected
+ if encrypt.PASSLIB_AVAILABLE:
++ assert encrypt.passlib_or_crypt(secret, algorithm, **settings) == expected
+ assert encrypt.PasslibHash(algorithm).hash(secret, **settings) == expected
+ else:
++ assert encrypt.passlib_or_crypt(secret, algorithm, **settings) == expected
+ with pytest.raises(AnsibleError) as excinfo:
+ encrypt.PasslibHash(algorithm).hash(secret, **settings)
+ assert excinfo.value.args[0] == "passlib must be installed and usable to hash with '%s'" % algorithm
+
+
+ @pytest.mark.skipif(sys.platform.startswith('darwin'), reason='macOS requires passlib')
+-def test_passlib_or_crypt():
+- with passlib_off():
+- expected = "$5$rounds=5000$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7"
+- assert encrypt.passlib_or_crypt("123", "sha256_crypt", salt="12345678", rounds=5000) == expected
+-
+- expected = "$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7"
+- assert encrypt.passlib_or_crypt("123", "sha256_crypt", salt="12345678", rounds=5000) == expected
+-
+-
+-@pytest.mark.skipif(sys.platform.startswith('darwin'), reason='macOS requires passlib')
+ def test_encrypt_with_rounds_no_passlib():
+ with passlib_off():
+ assert_hash("$5$rounds=5000$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7",
+--- ansible-core-2.16.5.orig/test/units/utils/test_unsafe_proxy.py
++++ ansible-core-2.16.5/test/units/utils/test_unsafe_proxy.py
+@@ -5,9 +5,7 @@
+ from __future__ import absolute_import, division, print_function
+ __metaclass__ = type
+
+-import pathlib
+-import sys
+-
++from ansible.module_utils.six import PY3
+ from ansible.utils.unsafe_proxy import AnsibleUnsafe, AnsibleUnsafeBytes, AnsibleUnsafeText, wrap_var
+ from ansible.module_utils.common.text.converters import to_text, to_bytes
+
+@@ -21,7 +19,10 @@ def test_wrap_var_bytes():
+
+
+ def test_wrap_var_string():
+- assert isinstance(wrap_var('foo'), AnsibleUnsafeText)
++ if PY3:
++ assert isinstance(wrap_var('foo'), AnsibleUnsafeText)
++ else:
++ assert isinstance(wrap_var('foo'), AnsibleUnsafeBytes)
+
+
+ def test_wrap_var_dict():
+@@ -94,12 +95,12 @@ def test_wrap_var_no_ref():
+ 'text': 'text',
+ }
+ wrapped_thing = wrap_var(thing)
+- assert thing is not wrapped_thing
+- assert thing['foo'] is not wrapped_thing['foo']
+- assert thing['bar'][0] is not wrapped_thing['bar'][0]
+- assert thing['baz'][0] is not wrapped_thing['baz'][0]
+- assert thing['none'] is wrapped_thing['none']
+- assert thing['text'] is not wrapped_thing['text']
++ thing is not wrapped_thing
++ thing['foo'] is not wrapped_thing['foo']
++ thing['bar'][0] is not wrapped_thing['bar'][0]
++ thing['baz'][0] is not wrapped_thing['baz'][0]
++ thing['none'] is not wrapped_thing['none']
++ thing['text'] is not wrapped_thing['text']
+
+
+ def test_AnsibleUnsafeText():
+@@ -118,10 +119,3 @@ def test_to_text_unsafe():
+ def test_to_bytes_unsafe():
+ assert isinstance(to_bytes(AnsibleUnsafeText(u'foo')), AnsibleUnsafeBytes)
+ assert to_bytes(AnsibleUnsafeText(u'foo')) == AnsibleUnsafeBytes(b'foo')
+-
+-
+-def test_unsafe_with_sys_intern():
+- # Specifically this is actually about sys.intern, test of pathlib
+- # because that is a specific affected use
+- assert sys.intern(AnsibleUnsafeText('foo')) == 'foo'
+- assert pathlib.Path(AnsibleUnsafeText('/tmp')) == pathlib.Path('/tmp')
+--- ansible-core-2.16.5.orig/test/units/vars/test_module_response_deepcopy.py
++++ ansible-core-2.16.5/test/units/vars/test_module_response_deepcopy.py
+@@ -7,6 +7,8 @@ __metaclass__ = type
+
+ from ansible.vars.clean import module_response_deepcopy
+
++import pytest
++
+
+ def test_module_response_deepcopy_basic():
+ x = 42
+@@ -35,6 +37,15 @@ def test_module_response_deepcopy_empty_
+ assert x is y
+
+
++@pytest.mark.skip(reason='No current support for this situation')
++def test_module_response_deepcopy_tuple():
++ x = ([1, 2], 3)
++ y = module_response_deepcopy(x)
++ assert y == x
++ assert x is not y
++ assert x[0] is not y[0]
++
++
+ def test_module_response_deepcopy_tuple_of_immutables():
+ x = ((1, 2), 3)
+ y = module_response_deepcopy(x)
+--- ansible-core-2.16.5.orig/test/units/vars/test_variable_manager.py
++++ ansible-core-2.16.5/test/units/vars/test_variable_manager.py
+@@ -141,8 +141,10 @@ class TestVariableManager(unittest.TestC
+ return
+
+ # pylint: disable=unreachable
+- # Tests complex variations and combinations of get_vars() with different
+- # objects to modify the context under which variables are merged.
++ '''
++ Tests complex variations and combinations of get_vars() with different
++ objects to modify the context under which variables are merged.
++ '''
+ # FIXME: BCS makethiswork
+ # return True
+