diff options
45 files changed, 545 insertions, 106 deletions
@@ -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]) |