summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--PKG-INFO2
-rwxr-xr-xbin/ansible-config8
-rwxr-xr-xbin/ansible-inventory22
-rw-r--r--changelogs/CHANGELOG-v2.16.rst30
-rw-r--r--changelogs/changelog.yaml53
-rwxr-xr-xlib/ansible/cli/config.py8
-rwxr-xr-xlib/ansible/cli/inventory.py22
-rw-r--r--lib/ansible/config/manager.py23
-rw-r--r--lib/ansible/constants.py40
-rw-r--r--lib/ansible/executor/task_executor.py10
-rw-r--r--lib/ansible/module_utils/ansible_release.py2
-rw-r--r--lib/ansible/module_utils/facts/virtual/linux.py2
-rw-r--r--lib/ansible/modules/blockinfile.py2
-rw-r--r--lib/ansible/modules/dnf.py6
-rw-r--r--lib/ansible/modules/dnf5.py17
-rw-r--r--lib/ansible/modules/find.py11
-rw-r--r--lib/ansible/modules/unarchive.py2
-rw-r--r--lib/ansible/playbook/role/__init__.py2
-rw-r--r--lib/ansible/plugins/action/fetch.py4
-rw-r--r--lib/ansible/plugins/cache/__init__.py1
-rw-r--r--lib/ansible/plugins/connection/winrm.py16
-rw-r--r--lib/ansible/plugins/strategy/free.py2
-rw-r--r--lib/ansible/plugins/strategy/linear.py2
-rw-r--r--lib/ansible/release.py2
-rw-r--r--lib/ansible/vars/hostvars.py28
-rw-r--r--lib/ansible_core.egg-info/PKG-INFO2
-rw-r--r--lib/ansible_core.egg-info/SOURCES.txt5
-rw-r--r--test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/_module3.py32
-rw-r--r--test/integration/targets/async/check_task_test.yml8
-rw-r--r--test/integration/targets/async/tasks/main.yml12
-rw-r--r--test/integration/targets/blockinfile/tasks/create_file.yml14
-rwxr-xr-xtest/integration/targets/config/runme.sh3
-rw-r--r--test/integration/targets/connection_local/connection_plugins/network_noop.py95
-rwxr-xr-xtest/integration/targets/connection_local/runme.sh7
-rw-r--r--test/integration/targets/connection_local/test_network_connection.inventory2
-rw-r--r--test/integration/targets/connection_windows_ssh/tests.yml12
-rw-r--r--test/integration/targets/connection_winrm/tests.yml12
-rw-r--r--test/integration/targets/dnf/tasks/dnf.yml11
-rw-r--r--test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml24
-rw-r--r--test/integration/targets/find/aliases2
-rw-r--r--test/integration/targets/find/meta/main.yml1
-rw-r--r--test/integration/targets/find/tasks/main.yml56
-rwxr-xr-xtest/integration/targets/include_import/runme.sh4
-rw-r--r--test/integration/targets/include_import/tasks/test_dynamic_allow_dup.yml30
-rw-r--r--test/lib/ansible_test/_internal/commands/sanity/ansible_doc.py2
45 files changed, 545 insertions, 106 deletions
diff --git a/PKG-INFO b/PKG-INFO
index 263e42f..406d6ef 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: ansible-core
-Version: 2.16.5
+Version: 2.16.6
Summary: Radically simple IT automation
Home-page: https://ansible.com/
Author: Ansible, Inc.
diff --git a/bin/ansible-config b/bin/ansible-config
index f394ef7..eac8a31 100755
--- a/bin/ansible-config
+++ b/bin/ansible-config
@@ -270,7 +270,7 @@ class ConfigCLI(CLI):
if not settings[setting].get('description'):
continue
- default = settings[setting].get('default', '')
+ default = self.config.template_default(settings[setting].get('default', ''), get_constants())
if subkey == 'env':
stype = settings[setting].get('type', '')
if stype == 'boolean':
@@ -352,7 +352,7 @@ class ConfigCLI(CLI):
if entry['key'] not in seen[entry['section']]:
seen[entry['section']].append(entry['key'])
- default = opt.get('default', '')
+ default = self.config.template_default(opt.get('default', ''), get_constants())
if opt.get('type', '') == 'list' and not isinstance(default, string_types):
# python lists are not valid ini ones
default = ', '.join(default)
@@ -414,14 +414,16 @@ class ConfigCLI(CLI):
if context.CLIARGS['format'] == 'display':
if isinstance(config[setting], Setting):
# proceed normally
+ value = config[setting].value
if config[setting].origin == 'default':
color = 'green'
+ value = self.config.template_default(value, get_constants())
elif config[setting].origin == 'REQUIRED':
# should include '_terms', '_input', etc
color = 'red'
else:
color = 'yellow'
- msg = "%s(%s) = %s" % (setting, config[setting].origin, config[setting].value)
+ msg = "%s(%s) = %s" % (setting, config[setting].origin, value)
else:
color = 'green'
msg = "%s(%s) = %s" % (setting, 'default', config[setting].get('default'))
diff --git a/bin/ansible-inventory b/bin/ansible-inventory
index 3550079..02e5eb2 100755
--- a/bin/ansible-inventory
+++ b/bin/ansible-inventory
@@ -25,26 +25,6 @@ from ansible.vars.plugins import get_vars_from_inventory_sources, get_vars_from_
display = Display()
-INTERNAL_VARS = frozenset(['ansible_diff_mode',
- 'ansible_config_file',
- 'ansible_facts',
- 'ansible_forks',
- 'ansible_inventory_sources',
- 'ansible_limit',
- 'ansible_playbook_python',
- 'ansible_run_tags',
- 'ansible_skip_tags',
- 'ansible_verbosity',
- 'ansible_version',
- 'inventory_dir',
- 'inventory_file',
- 'inventory_hostname',
- 'inventory_hostname_short',
- 'groups',
- 'group_names',
- 'omit',
- 'playbook_dir', ])
-
class InventoryCLI(CLI):
''' used to display or dump the configured inventory as Ansible sees it '''
@@ -247,7 +227,7 @@ class InventoryCLI(CLI):
@staticmethod
def _remove_internal(dump):
- for internal in INTERNAL_VARS:
+ for internal in C.INTERNAL_STATIC_VARS:
if internal in dump:
del dump[internal]
diff --git a/changelogs/CHANGELOG-v2.16.rst b/changelogs/CHANGELOG-v2.16.rst
index a2966d4..5cd4604 100644
--- a/changelogs/CHANGELOG-v2.16.rst
+++ b/changelogs/CHANGELOG-v2.16.rst
@@ -5,6 +5,36 @@ ansible-core 2.16 "All My Love" Release Notes
.. contents:: Topics
+v2.16.6
+=======
+
+Release Summary
+---------------
+
+| Release Date: 2024-04-15
+| `Porting Guide <https://docs.ansible.com/ansible-core/2.16/porting_guides/porting_guide_core_2.16.html>`__
+
+
+Bugfixes
+--------
+
+- Consolidated the list of internal static vars, centralized them as constant and completed from some missing entries.
+- Fix check for missing _sub_plugin attribute in older connection plugins (https://github.com/ansible/ansible/pull/82954)
+- Fixes permission for cache json file from 600 to 644 (https://github.com/ansible/ansible/issues/82683).
+- Slight optimization to hostvars (instantiate template only once per host, vs per call to var).
+- allow_duplicates - fix evaluating if the current role allows duplicates instead of using the initial value from the duplicate's cached role.
+- ansible-config will now properly template defaults before dumping them.
+- ansible-test ansible-doc sanity test - do not remove underscores from plugin names in collections before calling ``ansible-doc`` (https://github.com/ansible/ansible/pull/82574).
+- async - Fix bug that stopped running async task in ``--check`` when ``check_mode: False`` was set as a task attribute - https://github.com/ansible/ansible/issues/82811
+- blockinfile - when ``create=true`` is used with a filename without path, the module crashed (https://github.com/ansible/ansible/pull/81638).
+- dnf - fix an issue when cached RPMs were left in the cache directory even when the keepcache setting was unset (https://github.com/ansible/ansible/issues/81954)
+- dnf5 - replace removed API calls
+- facts - add a generic detection for VMware in product name.
+- fetch - add error message when using ``dest`` with a trailing slash that becomes a local directory - https://github.com/ansible/ansible/issues/82878
+- find - do not fail on Permission errors (https://github.com/ansible/ansible/issues/82027).
+- unarchive modules now uses zipinfo options without relying on implementation defaults, making it more compatible with all OS/distributions.
+- winrm - Do not raise another exception during cleanup when a task is timed out - https://github.com/ansible/ansible/issues/81095
+
v2.16.5
=======
diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml
index 3e24122..d11fe14 100644
--- a/changelogs/changelog.yaml
+++ b/changelogs/changelog.yaml
@@ -975,3 +975,56 @@ releases:
- py-tmpl-hardening.yml
- winrm-timeout.yml
release_date: '2024-03-18'
+ 2.16.6:
+ changes:
+ bugfixes:
+ - Consolidated the list of internal static vars, centralized them as constant
+ and completed from some missing entries.
+ - Fix check for missing _sub_plugin attribute in older connection plugins (https://github.com/ansible/ansible/pull/82954)
+ - Fixes permission for cache json file from 600 to 644 (https://github.com/ansible/ansible/issues/82683).
+ - Slight optimization to hostvars (instantiate template only once per host,
+ vs per call to var).
+ - allow_duplicates - fix evaluating if the current role allows duplicates instead
+ of using the initial value from the duplicate's cached role.
+ - ansible-config will now properly template defaults before dumping them.
+ - ansible-test ansible-doc sanity test - do not remove underscores from plugin
+ names in collections before calling ``ansible-doc`` (https://github.com/ansible/ansible/pull/82574).
+ - 'async - Fix bug that stopped running async task in ``--check`` when ``check_mode:
+ False`` was set as a task attribute - https://github.com/ansible/ansible/issues/82811'
+ - blockinfile - when ``create=true`` is used with a filename without path, the
+ module crashed (https://github.com/ansible/ansible/pull/81638).
+ - dnf - fix an issue when cached RPMs were left in the cache directory even
+ when the keepcache setting was unset (https://github.com/ansible/ansible/issues/81954)
+ - dnf5 - replace removed API calls
+ - facts - add a generic detection for VMware in product name.
+ - fetch - add error message when using ``dest`` with a trailing slash that becomes
+ a local directory - https://github.com/ansible/ansible/issues/82878
+ - find - do not fail on Permission errors (https://github.com/ansible/ansible/issues/82027).
+ - unarchive modules now uses zipinfo options without relying on implementation
+ defaults, making it more compatible with all OS/distributions.
+ - winrm - Do not raise another exception during cleanup when a task is timed
+ out - https://github.com/ansible/ansible/issues/81095
+ release_summary: '| Release Date: 2024-04-15
+
+ | `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.6_summary.yaml
+ - 81638-blockinfile.yml
+ - 81954-dnf-keepcache.yml
+ - 82027_find.yml
+ - 82574-ansible-test-ansible-doc-underscore.yml
+ - 82683-ansible-fact_cache-permissions-changed-after-ansible-coreupdate.yml
+ - 82878-fetch-dest-is-dir.yml
+ - 82954-fix-older-connection-plugins.yml
+ - async-task-check-mode.yml
+ - config_init_fix.yml
+ - dnf5-api-breaks.yml
+ - fix-allow-duplicates.yml
+ - internal_static_vars.yml
+ - unarchive_fix.yml
+ - vmware_facts.yml
+ - winrm-task-timeout.yml
+ release_date: '2024-04-15'
diff --git a/lib/ansible/cli/config.py b/lib/ansible/cli/config.py
index f394ef7..eac8a31 100755
--- a/lib/ansible/cli/config.py
+++ b/lib/ansible/cli/config.py
@@ -270,7 +270,7 @@ class ConfigCLI(CLI):
if not settings[setting].get('description'):
continue
- default = settings[setting].get('default', '')
+ default = self.config.template_default(settings[setting].get('default', ''), get_constants())
if subkey == 'env':
stype = settings[setting].get('type', '')
if stype == 'boolean':
@@ -352,7 +352,7 @@ class ConfigCLI(CLI):
if entry['key'] not in seen[entry['section']]:
seen[entry['section']].append(entry['key'])
- default = opt.get('default', '')
+ default = self.config.template_default(opt.get('default', ''), get_constants())
if opt.get('type', '') == 'list' and not isinstance(default, string_types):
# python lists are not valid ini ones
default = ', '.join(default)
@@ -414,14 +414,16 @@ class ConfigCLI(CLI):
if context.CLIARGS['format'] == 'display':
if isinstance(config[setting], Setting):
# proceed normally
+ value = config[setting].value
if config[setting].origin == 'default':
color = 'green'
+ value = self.config.template_default(value, get_constants())
elif config[setting].origin == 'REQUIRED':
# should include '_terms', '_input', etc
color = 'red'
else:
color = 'yellow'
- msg = "%s(%s) = %s" % (setting, config[setting].origin, config[setting].value)
+ msg = "%s(%s) = %s" % (setting, config[setting].origin, value)
else:
color = 'green'
msg = "%s(%s) = %s" % (setting, 'default', config[setting].get('default'))
diff --git a/lib/ansible/cli/inventory.py b/lib/ansible/cli/inventory.py
index 3550079..02e5eb2 100755
--- a/lib/ansible/cli/inventory.py
+++ b/lib/ansible/cli/inventory.py
@@ -25,26 +25,6 @@ from ansible.vars.plugins import get_vars_from_inventory_sources, get_vars_from_
display = Display()
-INTERNAL_VARS = frozenset(['ansible_diff_mode',
- 'ansible_config_file',
- 'ansible_facts',
- 'ansible_forks',
- 'ansible_inventory_sources',
- 'ansible_limit',
- 'ansible_playbook_python',
- 'ansible_run_tags',
- 'ansible_skip_tags',
- 'ansible_verbosity',
- 'ansible_version',
- 'inventory_dir',
- 'inventory_file',
- 'inventory_hostname',
- 'inventory_hostname_short',
- 'groups',
- 'group_names',
- 'omit',
- 'playbook_dir', ])
-
class InventoryCLI(CLI):
''' used to display or dump the configured inventory as Ansible sees it '''
@@ -247,7 +227,7 @@ class InventoryCLI(CLI):
@staticmethod
def _remove_internal(dump):
- for internal in INTERNAL_VARS:
+ for internal in C.INTERNAL_STATIC_VARS:
if internal in dump:
del dump[internal]
diff --git a/lib/ansible/config/manager.py b/lib/ansible/config/manager.py
index 418528a..041e96e 100644
--- a/lib/ansible/config/manager.py
+++ b/lib/ansible/config/manager.py
@@ -305,6 +305,17 @@ class ConfigManager(object):
# ensure we always have config def entry
self._base_defs['CONFIG_FILE'] = {'default': None, 'type': 'path'}
+ def template_default(self, value, variables):
+ if isinstance(value, string_types) and (value.startswith('{{') and value.endswith('}}')) and variables is not None:
+ # template default values if possible
+ # NOTE: cannot use is_template due to circular dep
+ try:
+ t = NativeEnvironment().from_string(value)
+ value = t.render(variables)
+ except Exception:
+ pass # not templatable
+ return value
+
def _read_config_yaml_file(self, yml_file):
# TODO: handle relative paths as relative to the directory containing the current playbook instead of CWD
# Currently this is only used with absolute paths to the `ansible/config` directory
@@ -548,17 +559,7 @@ class ConfigManager(object):
to_native(_get_entry(plugin_type, plugin_name, config)))
else:
origin = 'default'
- value = defs[config].get('default')
- if isinstance(value, string_types) and (value.startswith('{{') and value.endswith('}}')) and variables is not None:
- # template default values if possible
- # NOTE: cannot use is_template due to circular dep
- try:
- t = NativeEnvironment().from_string(value)
- value = t.render(variables)
- except Exception:
- pass # not templatable
-
- # ensure correct type, can raise exceptions on mismatched types
+ value = self.template_default(defs[config].get('default'), variables)
try:
value = ensure_type(value, defs[config].get('type'), origin=origin)
except ValueError as e:
diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py
index 514357b..d66ff16 100644
--- a/lib/ansible/constants.py
+++ b/lib/ansible/constants.py
@@ -112,6 +112,46 @@ CONFIGURABLE_PLUGINS = ('become', 'cache', 'callback', 'cliconf', 'connection',
DOCUMENTABLE_PLUGINS = CONFIGURABLE_PLUGINS + ('module', 'strategy', 'test', 'filter')
IGNORE_FILES = ("COPYING", "CONTRIBUTING", "LICENSE", "README", "VERSION", "GUIDELINES", "MANIFEST", "Makefile") # ignore during module search
INTERNAL_RESULT_KEYS = ('add_host', 'add_group')
+INTERNAL_STATIC_VARS = frozenset(
+ [
+ "ansible_async_path",
+ "ansible_collection_name",
+ "ansible_config_file",
+ "ansible_dependent_role_names",
+ "ansible_diff_mode",
+ "ansible_config_file",
+ "ansible_facts",
+ "ansible_forks",
+ "ansible_inventory_sources",
+ "ansible_limit",
+ "ansible_play_batch",
+ "ansible_play_hosts",
+ "ansible_play_hosts_all",
+ "ansible_play_role_names",
+ "ansible_playbook_python",
+ "ansible_role_name",
+ "ansible_role_names",
+ "ansible_run_tags",
+ "ansible_skip_tags",
+ "ansible_verbosity",
+ "ansible_version",
+ "inventory_dir",
+ "inventory_file",
+ "inventory_hostname",
+ "inventory_hostname_short",
+ "groups",
+ "group_names",
+ "omit",
+ "hostvars",
+ "playbook_dir",
+ "play_hosts",
+ "role_name",
+ "role_names",
+ "role_path",
+ "role_uuid",
+ "role_names",
+ ]
+)
LOCALHOST = ('127.0.0.1', 'localhost', '::1')
MODULE_REQUIRE_ARGS = tuple(add_internal_fqcns(('command', 'win_command', 'ansible.windows.win_command', 'shell', 'win_shell',
'ansible.windows.win_shell', 'raw', 'script')))
diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py
index 0e7394f..d20635a 100644
--- a/lib/ansible/executor/task_executor.py
+++ b/lib/ansible/executor/task_executor.py
@@ -841,7 +841,12 @@ class TaskExecutor:
# that (with a sleep for "poll" seconds between each retry) until the
# async time limit is exceeded.
- async_task = Task.load(dict(action='async_status', args={'jid': async_jid}, environment=self._task.environment))
+ async_task = Task.load(dict(
+ action='async_status',
+ args={'jid': async_jid},
+ check_mode=self._task.check_mode,
+ environment=self._task.environment,
+ ))
# FIXME: this is no longer the case, normal takes care of all, see if this can just be generalized
# Because this is an async task, the action handler is async. However,
@@ -913,6 +918,7 @@ class TaskExecutor:
'jid': async_jid,
'mode': 'cleanup',
},
+ 'check_mode': self._task.check_mode,
'environment': self._task.environment,
}
)
@@ -1086,7 +1092,7 @@ class TaskExecutor:
# deals with networking sub_plugins (network_cli/httpapi/netconf)
sub = getattr(self._connection, '_sub_plugin', None)
- if sub is not None and sub.get('type') != 'external':
+ if sub and sub.get('type') != 'external':
plugin_type = get_plugin_class(sub.get("obj"))
varnames.extend(self._set_plugin_options(plugin_type, variables, templar, task_keys))
sub_conn = getattr(self._connection, 'ssh_type_conn', None)
diff --git a/lib/ansible/module_utils/ansible_release.py b/lib/ansible/module_utils/ansible_release.py
index f8530dc..60200a0 100644
--- a/lib/ansible/module_utils/ansible_release.py
+++ b/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.16.6'
__author__ = 'Ansible, Inc.'
__codename__ = "All My Love"
diff --git a/lib/ansible/module_utils/facts/virtual/linux.py b/lib/ansible/module_utils/facts/virtual/linux.py
index 31fa061..c368245 100644
--- a/lib/ansible/module_utils/facts/virtual/linux.py
+++ b/lib/ansible/module_utils/facts/virtual/linux.py
@@ -176,7 +176,7 @@ class LinuxVirtual(Virtual):
virtual_facts['virtualization_type'] = 'RHEV'
found_virt = True
- if product_name in ('VMware Virtual Platform', 'VMware7,1'):
+ if product_name and product_name.startswith(("VMware",)):
guest_tech.add('VMware')
if not found_virt:
virtual_facts['virtualization_type'] = 'VMware'
diff --git a/lib/ansible/modules/blockinfile.py b/lib/ansible/modules/blockinfile.py
index 8c83bf0..3ede6fd 100644
--- a/lib/ansible/modules/blockinfile.py
+++ b/lib/ansible/modules/blockinfile.py
@@ -269,7 +269,7 @@ def main():
module.fail_json(rc=257,
msg='Path %s does not exist !' % path)
destpath = os.path.dirname(path)
- if not os.path.exists(destpath) and not module.check_mode:
+ if destpath and not os.path.exists(destpath) and not module.check_mode:
try:
os.makedirs(destpath)
except OSError as e:
diff --git a/lib/ansible/modules/dnf.py b/lib/ansible/modules/dnf.py
index 7f5afc3..50d0ca6 100644
--- a/lib/ansible/modules/dnf.py
+++ b/lib/ansible/modules/dnf.py
@@ -1441,8 +1441,10 @@ class DnfModule(YumDnf):
if self.with_modules:
self.module_base = dnf.module.module_base.ModuleBase(self.base)
-
- self.ensure()
+ try:
+ self.ensure()
+ finally:
+ self.base.close()
def main():
diff --git a/lib/ansible/modules/dnf5.py b/lib/ansible/modules/dnf5.py
index 823d3a7..c55b673 100644
--- a/lib/ansible/modules/dnf5.py
+++ b/lib/ansible/modules/dnf5.py
@@ -484,7 +484,7 @@ class Dnf5Module(YumDnf):
conf.config_file_path = self.conf_file
try:
- base.load_config_from_file()
+ base.load_config()
except RuntimeError as e:
self.module.fail_json(
msg=str(e),
@@ -520,7 +520,8 @@ class Dnf5Module(YumDnf):
log_router = base.get_logger()
global_logger = libdnf5.logger.GlobalLogger()
global_logger.set(log_router.get(), libdnf5.logger.Logger.Level_DEBUG)
- logger = libdnf5.logger.create_file_logger(base)
+ # FIXME hardcoding the filename does not seem right, should libdnf5 expose the default file name?
+ logger = libdnf5.logger.create_file_logger(base, "dnf5.log")
log_router.add_logger(logger)
if self.update_cache:
@@ -545,7 +546,11 @@ class Dnf5Module(YumDnf):
for repo in repo_query:
repo.enable()
- sack.update_and_load_enabled_repos(True)
+ try:
+ sack.load_repos()
+ except AttributeError:
+ # dnf5 < 5.2.0.0
+ sack.update_and_load_enabled_repos(True)
if self.update_cache and not self.names and not self.list:
self.module.exit_json(
@@ -577,7 +582,11 @@ class Dnf5Module(YumDnf):
self.module.exit_json(msg="", results=results, rc=0)
settings = libdnf5.base.GoalJobSettings()
- settings.group_with_name = True
+ try:
+ settings.set_group_with_name(True)
+ except AttributeError:
+ # dnf5 < 5.2.0.0
+ settings.group_with_name = True
if self.bugfix or self.security:
advisory_query = libdnf5.advisory.AdvisoryQuery(base)
types = []
diff --git a/lib/ansible/modules/find.py b/lib/ansible/modules/find.py
index d2e6c8b..0251224 100644
--- a/lib/ansible/modules/find.py
+++ b/lib/ansible/modules/find.py
@@ -258,6 +258,7 @@ skipped_paths:
version_added: '2.12'
'''
+import errno
import fnmatch
import grp
import os
@@ -434,10 +435,6 @@ def statinfo(st):
}
-def handle_walk_errors(e):
- raise e
-
-
def main():
module = AnsibleModule(
argument_spec=dict(
@@ -482,6 +479,12 @@ def main():
filelist = []
skipped = {}
+ def handle_walk_errors(e):
+ if e.errno in (errno.EPERM, errno.EACCES):
+ skipped[e.filename] = to_text(e)
+ return
+ raise e
+
if params['age'] is None:
age = None
else:
diff --git a/lib/ansible/modules/unarchive.py b/lib/ansible/modules/unarchive.py
index ec15a57..b3e8058 100644
--- a/lib/ansible/modules/unarchive.py
+++ b/lib/ansible/modules/unarchive.py
@@ -969,7 +969,7 @@ class TarZstdArchive(TgzArchive):
class ZipZArchive(ZipArchive):
def __init__(self, src, b_dest, file_args, module):
super(ZipZArchive, self).__init__(src, b_dest, file_args, module)
- self.zipinfoflag = '-Z'
+ self.zipinfoflag = '-Zl'
self.binaries = (
('unzip', 'cmd_path'),
('unzip', 'zipinfo_cmd_path'),
diff --git a/lib/ansible/playbook/role/__init__.py b/lib/ansible/playbook/role/__init__.py
index 34d8ba9..49254fc 100644
--- a/lib/ansible/playbook/role/__init__.py
+++ b/lib/ansible/playbook/role/__init__.py
@@ -586,7 +586,7 @@ class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable):
at least one task was run
'''
- return host.name in self._completed and not self._metadata.allow_duplicates
+ return host.name in self._completed
def compile(self, play, dep_chain=None):
'''
diff --git a/lib/ansible/plugins/action/fetch.py b/lib/ansible/plugins/action/fetch.py
index 11c91eb..d057ed2 100644
--- a/lib/ansible/plugins/action/fetch.py
+++ b/lib/ansible/plugins/action/fetch.py
@@ -150,6 +150,10 @@ class ActionModule(ActionBase):
# destination filename
base = os.path.basename(source_local)
dest = os.path.join(dest, base)
+
+ if os.path.isdir(to_bytes(dest, errors='surrogate_or_strict')):
+ raise AnsibleActionFail(
+ f"calculated dest '{dest}' is an existing directory, use another path that does not point to an existing directory")
if not dest.startswith("/"):
# if dest does not start with "/", we'll assume a relative path
dest = self._loader.path_dwim(dest)
diff --git a/lib/ansible/plugins/cache/__init__.py b/lib/ansible/plugins/cache/__init__.py
index f3abcb7..24f4e77 100644
--- a/lib/ansible/plugins/cache/__init__.py
+++ b/lib/ansible/plugins/cache/__init__.py
@@ -165,6 +165,7 @@ class BaseFileCacheModule(BaseCacheModule):
display.warning("error in '%s' cache plugin while trying to write to '%s' : %s" % (self.plugin_name, tmpfile_path, to_bytes(e)))
try:
os.rename(tmpfile_path, cachefile)
+ os.chmod(cachefile, mode=0o644)
except (OSError, IOError) as e:
display.warning("error in '%s' cache plugin while trying to move '%s' to '%s' : %s" % (self.plugin_name, tmpfile_path, cachefile, to_bytes(e)))
finally:
diff --git a/lib/ansible/plugins/connection/winrm.py b/lib/ansible/plugins/connection/winrm.py
index 7104369..b297495 100644
--- a/lib/ansible/plugins/connection/winrm.py
+++ b/lib/ansible/plugins/connection/winrm.py
@@ -199,7 +199,7 @@ from ansible.utils.display import Display
try:
import winrm
- from winrm.exceptions import WinRMError, WinRMOperationTimeoutError
+ from winrm.exceptions import WinRMError, WinRMOperationTimeoutError, WinRMTransportError
from winrm.protocol import Protocol
import requests.exceptions
HAS_WINRM = True
@@ -684,7 +684,19 @@ class Connection(ConnectionBase):
raise AnsibleConnectionFailure('winrm connection error: %s' % to_native(exc))
finally:
if command_id:
- self.protocol.cleanup_command(self.shell_id, command_id)
+ # Due to a bug in how pywinrm works with message encryption we
+ # ignore a 400 error which can occur when a task timeout is
+ # set and the code tries to clean up the command. This happens
+ # as the cleanup msg is sent over a new socket but still uses
+ # the already encrypted payload bound to the other socket
+ # causing the server to reply with 400 Bad Request.
+ try:
+ self.protocol.cleanup_command(self.shell_id, command_id)
+ except WinRMTransportError as e:
+ if e.code != 400:
+ raise
+
+ display.warning("Failed to cleanup running WinRM command, resources might still be in use on the target server")
def _connect(self) -> Connection:
diff --git a/lib/ansible/plugins/strategy/free.py b/lib/ansible/plugins/strategy/free.py
index 82a21b1..5e64ef3 100644
--- a/lib/ansible/plugins/strategy/free.py
+++ b/lib/ansible/plugins/strategy/free.py
@@ -177,7 +177,7 @@ class StrategyModule(StrategyBase):
# 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 role_obj.has_run(host) and task._role._metadata.allow_duplicates is False:
display.debug("'%s' skipped because role has already run" % task, host=host_name)
del self._blocked_hosts[host_name]
continue
diff --git a/lib/ansible/plugins/strategy/linear.py b/lib/ansible/plugins/strategy/linear.py
index 2fd4cba..f3b117b 100644
--- a/lib/ansible/plugins/strategy/linear.py
+++ b/lib/ansible/plugins/strategy/linear.py
@@ -172,7 +172,7 @@ class StrategyModule(StrategyBase):
# 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 role_obj.has_run(host) and task._role._metadata.allow_duplicates is False:
display.debug("'%s' skipped because role has already run" % task)
continue
diff --git a/lib/ansible/release.py b/lib/ansible/release.py
index f8530dc..60200a0 100644
--- a/lib/ansible/release.py
+++ b/lib/ansible/release.py
@@ -19,6 +19,6 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
-__version__ = '2.16.5'
+__version__ = '2.16.6'
__author__ = 'Ansible, Inc.'
__codename__ = "All My Love"
diff --git a/lib/ansible/vars/hostvars.py b/lib/ansible/vars/hostvars.py
index 6222954..a76811b 100644
--- a/lib/ansible/vars/hostvars.py
+++ b/lib/ansible/vars/hostvars.py
@@ -21,26 +21,9 @@ __metaclass__ = type
from collections.abc import Mapping
+from ansible import constants as C
from ansible.template import Templar, AnsibleUndefined
-STATIC_VARS = [
- 'ansible_version',
- 'ansible_play_hosts',
- 'ansible_dependent_role_names',
- 'ansible_play_role_names',
- 'ansible_role_names',
- 'inventory_hostname',
- 'inventory_hostname_short',
- 'inventory_file',
- 'inventory_dir',
- 'groups',
- 'group_names',
- 'omit',
- 'playbook_dir',
- 'play_hosts',
- 'role_names',
- 'ungrouped',
-]
__all__ = ['HostVars', 'HostVarsVars']
@@ -134,10 +117,12 @@ class HostVarsVars(Mapping):
def __init__(self, variables, loader):
self._vars = variables
self._loader = loader
+ # NOTE: this only has access to the host's own vars,
+ # so templates that depend on vars in other scopes will not work.
+ self._templar = Templar(variables=self._vars, loader=self._loader)
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)
+ return self._templar.template(self._vars[var], fail_on_undefined=False, static_vars=C.INTERNAL_STATIC_VARS)
def __contains__(self, var):
return (var in self._vars)
@@ -150,5 +135,4 @@ class HostVarsVars(Mapping):
return len(self._vars.keys())
def __repr__(self):
- templar = Templar(variables=self._vars, loader=self._loader)
- return repr(templar.template(self._vars, fail_on_undefined=False, static_vars=STATIC_VARS))
+ return repr(self._templar.template(self._vars, fail_on_undefined=False, static_vars=C.INTERNAL_STATIC_VARS))
diff --git a/lib/ansible_core.egg-info/PKG-INFO b/lib/ansible_core.egg-info/PKG-INFO
index 263e42f..406d6ef 100644
--- a/lib/ansible_core.egg-info/PKG-INFO
+++ b/lib/ansible_core.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: ansible-core
-Version: 2.16.5
+Version: 2.16.6
Summary: Radically simple IT automation
Home-page: https://ansible.com/
Author: Ansible, Inc.
diff --git a/lib/ansible_core.egg-info/SOURCES.txt b/lib/ansible_core.egg-info/SOURCES.txt
index 3c8d1f4..6bcc388 100644
--- a/lib/ansible_core.egg-info/SOURCES.txt
+++ b/lib/ansible_core.egg-info/SOURCES.txt
@@ -1038,6 +1038,7 @@ test/integration/targets/ansible-test-sanity-ansible-doc/aliases
test/integration/targets/ansible-test-sanity-ansible-doc/runme.sh
test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/lookup/lookup1.py
test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/lookup/a/b/lookup2.py
+test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/_module3.py
test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/module1.py
test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/a/b/module2.py
test/integration/targets/ansible-test-sanity-import/aliases
@@ -1279,6 +1280,7 @@ test/integration/targets/assert/quiet.yml
test/integration/targets/assert/runme.sh
test/integration/targets/async/aliases
test/integration/targets/async/callback_test.yml
+test/integration/targets/async/check_task_test.yml
test/integration/targets/async/library/async_test.py
test/integration/targets/async/meta/main.yml
test/integration/targets/async/tasks/main.yml
@@ -1666,6 +1668,8 @@ test/integration/targets/connection_delegation/connection_plugins/delegation_con
test/integration/targets/connection_local/aliases
test/integration/targets/connection_local/runme.sh
test/integration/targets/connection_local/test_connection.inventory
+test/integration/targets/connection_local/test_network_connection.inventory
+test/integration/targets/connection_local/connection_plugins/network_noop.py
test/integration/targets/connection_paramiko_ssh/aliases
test/integration/targets/connection_paramiko_ssh/runme.sh
test/integration/targets/connection_paramiko_ssh/test.sh
@@ -2352,6 +2356,7 @@ test/integration/targets/include_import/tasks/tasks4.yml
test/integration/targets/include_import/tasks/tasks5.yml
test/integration/targets/include_import/tasks/tasks6.yml
test/integration/targets/include_import/tasks/test_allow_single_role_dup.yml
+test/integration/targets/include_import/tasks/test_dynamic_allow_dup.yml
test/integration/targets/include_import/tasks/test_import_tasks.yml
test/integration/targets/include_import/tasks/test_import_tasks_tags.yml
test/integration/targets/include_import/tasks/test_include_dupe_loop.yml
diff --git a/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/_module3.py b/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/_module3.py
new file mode 100644
index 0000000..41784ae
--- /dev/null
+++ b/test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/_module3.py
@@ -0,0 +1,32 @@
+#!/usr/bin/python
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import annotations
+
+DOCUMENTATION = '''
+module: _module3
+short_description: Another test module
+description: This is a test module that has not been deprecated.
+author:
+ - Ansible Core Team
+'''
+
+EXAMPLES = '''
+- minimal:
+'''
+
+RETURN = ''''''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec={},
+ )
+
+ module.exit_json()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/async/check_task_test.yml b/test/integration/targets/async/check_task_test.yml
new file mode 100644
index 0000000..f875640
--- /dev/null
+++ b/test/integration/targets/async/check_task_test.yml
@@ -0,0 +1,8 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Async in check mode task disabled test
+ command: sleep 5
+ async: 6
+ poll: 1
+ check_mode: False
diff --git a/test/integration/targets/async/tasks/main.yml b/test/integration/targets/async/tasks/main.yml
index f5e5c99..491be96 100644
--- a/test/integration/targets/async/tasks/main.yml
+++ b/test/integration/targets/async/tasks/main.yml
@@ -298,3 +298,15 @@
- assert:
that:
- '"ASYNC POLL on localhost" in callback_output.stdout'
+
+- name: run playbook in --check with task disabling check mode
+ command: ansible-playbook {{ role_path }}/check_task_test.yml --check
+ register: check_task_disabled_output
+ delegate_to: localhost
+ environment:
+ ANSIBLE_NOCOLOR: 'true'
+ ANSIBLE_FORCE_COLOR: 'false'
+
+- assert:
+ that:
+ - '"ASYNC OK on localhost" in check_task_disabled_output.stdout'
diff --git a/test/integration/targets/blockinfile/tasks/create_file.yml b/test/integration/targets/blockinfile/tasks/create_file.yml
index c8ded30..9a5cf05 100644
--- a/test/integration/targets/blockinfile/tasks/create_file.yml
+++ b/test/integration/targets/blockinfile/tasks/create_file.yml
@@ -30,3 +30,17 @@
- empty_test_2 is changed
- "'Block removed' in empty_test_2.msg"
- empty_test_stat.stat.size == 0
+
+- block:
+ - name: Create file in current directory
+ blockinfile:
+ path: "empty.txt"
+ block: Hello.
+ state: present
+ create: yes
+
+ always:
+ - name: Remove file
+ file:
+ path: "empty.txt"
+ state: absent
diff --git a/test/integration/targets/config/runme.sh b/test/integration/targets/config/runme.sh
index 122e15d..5b999e3 100755
--- a/test/integration/targets/config/runme.sh
+++ b/test/integration/targets/config/runme.sh
@@ -41,3 +41,6 @@ do
ANSIBLE_LOOKUP_PLUGINS=./ ansible-config init types -t lookup -f "${format}" > "files/types.new.${format}"
diff -u "files/types.${format}" "files/types.new.${format}"
done
+
+# ensure we don't show default templates, but templated defaults
+[ "$(ansible-config init |grep '={{' -c )" -eq 0 ]
diff --git a/test/integration/targets/connection_local/connection_plugins/network_noop.py b/test/integration/targets/connection_local/connection_plugins/network_noop.py
new file mode 100644
index 0000000..5b0c584
--- /dev/null
+++ b/test/integration/targets/connection_local/connection_plugins/network_noop.py
@@ -0,0 +1,95 @@
+# (c) 2024 Red Hat Inc.
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import annotations
+
+DOCUMENTATION = """
+connection: network_noop
+author: ansible-core
+short_description: legacy-ish connection plugin with only minimal config
+description:
+ - A wrapper around NetworkConnectionBase to test that the default attributes don't cause internal errors in TE.
+options:
+ persistent_log_messages:
+ type: boolean
+ description:
+ - This flag will enable logging the command executed and response received from
+ target device in the ansible log file. For this option to work 'log_path' ansible
+ configuration option is required to be set to a file path with write access.
+ - Be sure to fully understand the security implications of enabling this
+ option as it could create a security vulnerability by logging sensitive information in log file.
+ default: False
+ ini:
+ - section: persistent_connection
+ key: log_messages
+ env:
+ - name: ANSIBLE_PERSISTENT_LOG_MESSAGES
+ vars:
+ - name: ansible_persistent_log_messages
+ persistent_command_timeout:
+ type: int
+ description:
+ - Configures, in seconds, the amount of time to wait for a command to
+ return from the remote device. If this timer is exceeded before the
+ command returns, the connection plugin will raise an exception and
+ close.
+ default: 30
+ ini:
+ - section: persistent_connection
+ key: command_timeout
+ env:
+ - name: ANSIBLE_PERSISTENT_COMMAND_TIMEOUT
+ vars:
+ - name: ansible_command_timeout
+ persistent_connect_timeout:
+ type: int
+ description:
+ - Configures, in seconds, the amount of time to wait when trying to
+ initially establish a persistent connection. If this value expires
+ before the connection to the remote device is completed, the connection
+ will fail.
+ default: 30
+ ini:
+ - section: persistent_connection
+ key: connect_timeout
+ env:
+ - name: ANSIBLE_PERSISTENT_CONNECT_TIMEOUT
+ vars:
+extends_documentation_fragment:
+ - connection_pipelining
+"""
+
+from ansible.plugins.connection import NetworkConnectionBase, ensure_connect
+from ansible.utils.display import Display
+
+display = Display()
+
+
+class Connection(NetworkConnectionBase):
+ transport = 'network_noop'
+ has_pipelining = True
+
+ def __init__(self, play_context, new_stdin, *args, **kwargs):
+ super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+ @ensure_connect
+ def exec_command(self, *args, **kwargs):
+ return super(Connection, self).exec_command(*args, **kwargs)
+
+ @ensure_connect
+ def put_file(self, *args, **kwargs):
+ return super(Connection, self).put_file(*args, **kwargs)
+
+ @ensure_connect
+ def fetch_file(self, *args, **kwargs):
+ return super(Connection, self).fetch_file(*args, **kwargs)
+
+ def _connect(self):
+ if not self.connected:
+ self._connected = True
+ display.vvv("ESTABLISH NEW CONNECTION")
+
+ def close(self):
+ if self.connected:
+ display.vvv("CLOSING CONNECTION")
+ super(Connection, self).close()
diff --git a/test/integration/targets/connection_local/runme.sh b/test/integration/targets/connection_local/runme.sh
index a2c32ad..42b2b82 100755
--- a/test/integration/targets/connection_local/runme.sh
+++ b/test/integration/targets/connection_local/runme.sh
@@ -12,3 +12,10 @@ INVENTORY="../connection_${group}/test_connection.inventory" ./test.sh \
-e local_tmp=/tmp/ansible-local \
-e remote_tmp=/tmp/ansible-remote \
"$@"
+
+ANSIBLE_CONNECTION_PLUGINS="../connection_${group}/connection_plugins" INVENTORY="../connection_${group}/test_network_connection.inventory" ./test.sh \
+ -e target_hosts="${group}" \
+ -e action_prefix= \
+ -e local_tmp=/tmp/ansible-local \
+ -e remote_tmp=/tmp/ansible-remote \
+ "$@"
diff --git a/test/integration/targets/connection_local/test_network_connection.inventory b/test/integration/targets/connection_local/test_network_connection.inventory
new file mode 100644
index 0000000..8114023
--- /dev/null
+++ b/test/integration/targets/connection_local/test_network_connection.inventory
@@ -0,0 +1,2 @@
+[all]
+local ansible_host=127.0.0.1 ansible_connection=network_noop
diff --git a/test/integration/targets/connection_windows_ssh/tests.yml b/test/integration/targets/connection_windows_ssh/tests.yml
index e9b538b..3b09f62 100644
--- a/test/integration/targets/connection_windows_ssh/tests.yml
+++ b/test/integration/targets/connection_windows_ssh/tests.yml
@@ -30,3 +30,15 @@
- win_ssh_async.rc == 0
- win_ssh_async.stdout == "café\n"
- win_ssh_async.stderr == ""
+
+ # Ensures the connection plugin can handle a timeout
+ # without raising another error.
+ - name: run command with timeout
+ win_shell: Start-Sleep -Seconds 10
+ timeout: 5
+ register: timeout_cmd
+ ignore_errors: true
+
+ - assert:
+ that:
+ - timeout_cmd.msg == 'The win_shell action failed to execute in the expected time frame (5) and was terminated'
diff --git a/test/integration/targets/connection_winrm/tests.yml b/test/integration/targets/connection_winrm/tests.yml
index b086a3a..cf109a8 100644
--- a/test/integration/targets/connection_winrm/tests.yml
+++ b/test/integration/targets/connection_winrm/tests.yml
@@ -29,3 +29,15 @@
that:
- winrm_copy_empty is changed
- winrm_copy_empty_actual.stat.size == 0
+
+ # Ensures the connection plugin can handle a timeout
+ # without raising another error.
+ - name: run command with timeout
+ win_shell: Start-Sleep -Seconds 10
+ timeout: 5
+ register: timeout_cmd
+ ignore_errors: true
+
+ - assert:
+ that:
+ - timeout_cmd.msg == 'The win_shell action failed to execute in the expected time frame (5) and was terminated'
diff --git a/test/integration/targets/dnf/tasks/dnf.yml b/test/integration/targets/dnf/tasks/dnf.yml
index 9845f3d..c5f0173 100644
--- a/test/integration/targets/dnf/tasks/dnf.yml
+++ b/test/integration/targets/dnf/tasks/dnf.yml
@@ -67,6 +67,17 @@
update_cache: True
register: dnf_result
+- find:
+ paths: /var/cache/dnf
+ patterns: "*.rpm"
+ recurse: true
+ register: r
+
+- name: verify that RPM cache is cleared after installation as keepcache is off by default
+ assert:
+ that:
+ - r.matched == 0
+
- name: check sos with rpm
shell: rpm -q sos
failed_when: False
diff --git a/test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml b/test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml
index d0bf9bd..94941ed 100644
--- a/test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml
+++ b/test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml
@@ -28,6 +28,28 @@
register: failed_fetch_dest_dir
ignore_errors: true
+- block:
+ - name: create local dir for test
+ file:
+ path: "{{ output_dir }}/test dir/orig"
+ state: directory
+ delegate_to: localhost
+
+ - name: Dest is a path that is calculated as an existing directory, should fail
+ fetch:
+ src: "{{ remote_tmp_dir }}/orig"
+ dest: "{{ output_dir }}/test dir/"
+ flat: true
+ register: failed_detch_dest_calc_dir
+ ignore_errors: true
+
+ always:
+ - name: remote local dir for test
+ file:
+ path: "{{ output_dir }}/test dir"
+ state: absent
+ delegate_to: localhost
+
- name: Test unreachable
fetch:
src: "{{ remote_tmp_dir }}/orig"
@@ -48,4 +70,6 @@
- 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')
+ - failed_detch_dest_calc_dir is failed
+ - failed_detch_dest_calc_dir.msg is search("calculated dest '" ~ output_dir ~ "/test dir/orig' is an existing directory")
- unreachable_fetch is unreachable
diff --git a/test/integration/targets/find/aliases b/test/integration/targets/find/aliases
index a6dafcf..cdd75ef 100644
--- a/test/integration/targets/find/aliases
+++ b/test/integration/targets/find/aliases
@@ -1 +1,3 @@
shippable/posix/group1
+destructive
+needs/root \ No newline at end of file
diff --git a/test/integration/targets/find/meta/main.yml b/test/integration/targets/find/meta/main.yml
index cb6005d..c384e11 100644
--- a/test/integration/targets/find/meta/main.yml
+++ b/test/integration/targets/find/meta/main.yml
@@ -1,3 +1,4 @@
dependencies:
- prepare_tests
+ - setup_test_user
- setup_remote_tmp_dir
diff --git a/test/integration/targets/find/tasks/main.yml b/test/integration/targets/find/tasks/main.yml
index 9c4a960..c526dc7 100644
--- a/test/integration/targets/find/tasks/main.yml
+++ b/test/integration/targets/find/tasks/main.yml
@@ -375,5 +375,61 @@
- 'remote_tmp_dir_test ~ "/astest/.hidden.txt" in astest_list'
- '"checksum" in result.files[0]'
+# Test permission error is correctly handled by find module
+- vars:
+ test_dir: /tmp/permission_test
+ block:
+ - name: Set up content
+ file:
+ path: "{{ test_dir }}/{{ item.name }}"
+ state: "{{ item.state }}"
+ mode: "{{ item.mode }}"
+ owner: "{{ item.owner | default(omit) }}"
+ group: "{{ item.group | default(omit) }}"
+ loop:
+ - name: readable
+ state: directory
+ owner: "{{ test_user_name }}"
+ mode: "1711"
+ - name: readable/1-unreadable
+ state: directory
+ mode: "0700"
+ - name: readable/2-readable
+ state: touch
+ owner: "{{ test_user_name }}"
+ mode: "0777"
+
+ - name: Find a file in readable directory
+ find:
+ paths: "{{ test_dir }}/readable/"
+ patterns: "*"
+ recurse: true
+ register: permission_issue
+ become_user: "{{ test_user_name }}"
+
+ - name: Find a file in readable directory
+ find:
+ paths: "{{ test_dir }}/readable/"
+ patterns: "*"
+ recurse: true
+ register: permission_issue
+ become_user: "{{ test_user_name }}"
+ become: yes
+
+ - name: Check if the skipped_paths are populated correctly with permission error
+ assert:
+ that:
+ - permission_issue is success
+ - not permission_issue.changed
+ - permission_issue.skipped_paths|length == 1
+ - "'{{ test_dir }}/readable/1-unreadable' in permission_issue.skipped_paths"
+ - "'Permission denied' in permission_issue.skipped_paths['{{ test_dir }}/readable/1-unreadable']"
+ - permission_issue.matched == 1
+ always:
+ - name: cleanup test directory
+ file:
+ dest: "{{ test_dir }}"
+ state: absent
+
- name: Run mode tests
import_tasks: mode.yml
diff --git a/test/integration/targets/include_import/runme.sh b/test/integration/targets/include_import/runme.sh
index 078f080..d85b22d 100755
--- a/test/integration/targets/include_import/runme.sh
+++ b/test/integration/targets/include_import/runme.sh
@@ -121,6 +121,10 @@ ansible-playbook valid_include_keywords/playbook.yml "$@"
ansible-playbook tasks/test_allow_single_role_dup.yml 2>&1 | tee test_allow_single_role_dup.out
test "$(grep -c 'ok=3' test_allow_single_role_dup.out)" = 1
+# Test allow_duplicate with include_role and import_role
+test "$(ansible-playbook tasks/test_dynamic_allow_dup.yml --tags include | grep -c 'Tasks file inside role')" = 2
+test "$(ansible-playbook tasks/test_dynamic_allow_dup.yml --tags import | grep -c 'Tasks file inside role')" = 2
+
# test templating public, allow_duplicates, and rolespec_validate
ansible-playbook tasks/test_templating_IncludeRole_FA.yml 2>&1 | tee IncludeRole_FA_template.out
test "$(grep -c 'ok=4' IncludeRole_FA_template.out)" = 1
diff --git a/test/integration/targets/include_import/tasks/test_dynamic_allow_dup.yml b/test/integration/targets/include_import/tasks/test_dynamic_allow_dup.yml
new file mode 100644
index 0000000..82e08b3
--- /dev/null
+++ b/test/integration/targets/include_import/tasks/test_dynamic_allow_dup.yml
@@ -0,0 +1,30 @@
+---
+- name: test for allow_duplicates with include_role
+ hosts: localhost
+ gather_facts: false
+ tags:
+ - include
+ tasks:
+ - include_role:
+ name: dup_allowed_role
+ allow_duplicates: false
+ - include_role:
+ name: dup_allowed_role
+ - include_role:
+ name: dup_allowed_role
+ allow_duplicates: false
+
+- name: test for allow_duplicates with import_role
+ hosts: localhost
+ gather_facts: false
+ tags:
+ - import
+ tasks:
+ - import_role:
+ name: dup_allowed_role
+ allow_duplicates: false
+ - import_role:
+ name: dup_allowed_role
+ - import_role:
+ name: dup_allowed_role
+ allow_duplicates: false
diff --git a/test/lib/ansible_test/_internal/commands/sanity/ansible_doc.py b/test/lib/ansible_test/_internal/commands/sanity/ansible_doc.py
index ff035ef..1b3b402 100644
--- a/test/lib/ansible_test/_internal/commands/sanity/ansible_doc.py
+++ b/test/lib/ansible_test/_internal/commands/sanity/ansible_doc.py
@@ -79,7 +79,7 @@ class AnsibleDocTest(SanitySingleVersion):
plugin_parts = os.path.relpath(plugin_file_path, plugin_path).split(os.path.sep)
plugin_name = os.path.splitext(plugin_parts[-1])[0]
- if plugin_name.startswith('_'):
+ if plugin_name.startswith('_') and not data_context().content.collection:
plugin_name = plugin_name[1:]
plugin_fqcn = data_context().content.prefix + '.'.join(plugin_parts[:-1] + [plugin_name])