From bbed591285e1882d05198e518f75873af58939f5 Mon Sep 17 00:00:00 2001 From: cos Date: Sat, 20 Apr 2024 09:15:12 +0200 Subject: Attempt to recreate upstream branch state from tar files Unfortunately this was a too naive approach, and the result fails to build. Work around that version control is behind the actual package version in trixie. As is obvious from the lacking commits in the salsa repository and also visible on https://tracker.debian.org/pkg/ansible-core with the report from vcswatch stating: VCS repository is not up to date. This commit contains all changes from ansible-core_2.14.13.orig.tar.gz to ansible-core_2.16.5.orig.tar.gz, which should hopefully be a squashed representation on the same set of changes on the uploader's unpushed git tree. --- lib/ansible/plugins/action/__init__.py | 84 ++++--- lib/ansible/plugins/action/add_host.py | 5 +- lib/ansible/plugins/action/assemble.py | 2 +- lib/ansible/plugins/action/assert.py | 3 +- lib/ansible/plugins/action/async_status.py | 1 - lib/ansible/plugins/action/command.py | 1 - lib/ansible/plugins/action/copy.py | 4 +- lib/ansible/plugins/action/debug.py | 34 +-- lib/ansible/plugins/action/fail.py | 1 + lib/ansible/plugins/action/fetch.py | 4 +- lib/ansible/plugins/action/gather_facts.py | 50 ++-- lib/ansible/plugins/action/group_by.py | 1 + lib/ansible/plugins/action/include_vars.py | 19 +- lib/ansible/plugins/action/normal.py | 29 +-- lib/ansible/plugins/action/pause.py | 257 ++++----------------- lib/ansible/plugins/action/reboot.py | 37 +-- lib/ansible/plugins/action/script.py | 31 ++- lib/ansible/plugins/action/set_fact.py | 1 + lib/ansible/plugins/action/set_stats.py | 2 +- lib/ansible/plugins/action/shell.py | 6 + lib/ansible/plugins/action/template.py | 43 ++-- lib/ansible/plugins/action/unarchive.py | 2 +- lib/ansible/plugins/action/uri.py | 3 +- .../plugins/action/validate_argument_spec.py | 3 +- lib/ansible/plugins/action/wait_for_connection.py | 8 +- lib/ansible/plugins/action/yum.py | 8 +- 26 files changed, 268 insertions(+), 371 deletions(-) (limited to 'lib/ansible/plugins/action') diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py index 8f923253..5ba3bd78 100644 --- a/lib/ansible/plugins/action/__init__.py +++ b/lib/ansible/plugins/action/__init__.py @@ -27,7 +27,7 @@ from ansible.module_utils.common.arg_spec import ArgumentSpecValidator from ansible.module_utils.errors import UnsupportedError from ansible.module_utils.json_utils import _filter_non_json_lines from ansible.module_utils.six import binary_type, string_types, text_type -from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.parsing.utils.jsonify import jsonify from ansible.release import __version__ from ansible.utils.collection_loader import resource_from_fqcr @@ -39,6 +39,18 @@ from ansible.utils.plugin_docs import get_versioned_doclink display = Display() +def _validate_utf8_json(d): + if isinstance(d, text_type): + # Purposefully not using to_bytes here for performance reasons + d.encode(encoding='utf-8', errors='strict') + elif isinstance(d, dict): + for o in d.items(): + _validate_utf8_json(o) + elif isinstance(d, (list, tuple)): + for o in d: + _validate_utf8_json(o) + + class ActionBase(ABC): ''' @@ -51,6 +63,13 @@ class ActionBase(ABC): # A set of valid arguments _VALID_ARGS = frozenset([]) # type: frozenset[str] + # behavioral attributes + BYPASS_HOST_LOOP = False + TRANSFERS_FILES = False + _requires_connection = True + _supports_check_mode = True + _supports_async = False + def __init__(self, task, connection, play_context, loader, templar, shared_loader_obj): self._task = task self._connection = connection @@ -60,20 +79,16 @@ class ActionBase(ABC): self._shared_loader_obj = shared_loader_obj self._cleanup_remote_tmp = False - self._supports_check_mode = True - self._supports_async = False - # interpreter discovery state self._discovered_interpreter_key = None self._discovered_interpreter = False self._discovery_deprecation_warnings = [] self._discovery_warnings = [] + self._used_interpreter = None # Backwards compat: self._display isn't really needed, just import the global display and use that. self._display = display - self._used_interpreter = None - @abstractmethod def run(self, tmp=None, task_vars=None): """ Action Plugins should implement this method to perform their @@ -284,7 +299,8 @@ class ActionBase(ABC): try: (module_data, module_style, module_shebang) = modify_module(module_name, module_path, module_args, self._templar, task_vars=use_vars, - module_compression=self._play_context.module_compression, + module_compression=C.config.get_config_value('DEFAULT_MODULE_COMPRESSION', + variables=task_vars), async_timeout=self._task.async_val, environment=final_environment, remote_is_local=bool(getattr(self._connection, '_remote_is_local', False)), @@ -723,8 +739,7 @@ class ActionBase(ABC): return remote_paths # we'll need this down here - become_link = get_versioned_doclink('playbook_guide/playbooks_privilege_escalation.html#risks-of-becoming-an-unprivileged-user') - + become_link = get_versioned_doclink('playbook_guide/playbooks_privilege_escalation.html') # Step 3f: Common group # Otherwise, we're a normal user. We failed to chown the paths to the # unprivileged user, but if we have a common group with them, we should @@ -861,38 +876,6 @@ class ActionBase(ABC): return mystat['stat'] - def _remote_checksum(self, path, all_vars, follow=False): - """Deprecated. Use _execute_remote_stat() instead. - - Produces a remote checksum given a path, - Returns a number 0-4 for specific errors instead of checksum, also ensures it is different - 0 = unknown error - 1 = file does not exist, this might not be an error - 2 = permissions issue - 3 = its a directory, not a file - 4 = stat module failed, likely due to not finding python - 5 = appropriate json module not found - """ - self._display.deprecated("The '_remote_checksum()' method is deprecated. " - "The plugin author should update the code to use '_execute_remote_stat()' instead", "2.16") - x = "0" # unknown error has occurred - try: - remote_stat = self._execute_remote_stat(path, all_vars, follow=follow) - if remote_stat['exists'] and remote_stat['isdir']: - x = "3" # its a directory not a file - else: - x = remote_stat['checksum'] # if 1, file is missing - except AnsibleError as e: - errormsg = to_text(e) - if errormsg.endswith(u'Permission denied'): - x = "2" # cannot read file - elif errormsg.endswith(u'MODULE FAILURE'): - x = "4" # python not found or module uncaught exception - elif 'json' in errormsg: - x = "5" # json module needed - finally: - return x # pylint: disable=lost-exception - def _remote_expand_user(self, path, sudoable=True, pathsep=None): ''' takes a remote path and performs tilde/$HOME expansion on the remote host ''' @@ -1232,6 +1215,18 @@ class ActionBase(ABC): display.warning(w) data = json.loads(filtered_output) + + if C.MODULE_STRICT_UTF8_RESPONSE and not data.pop('_ansible_trusted_utf8', None): + try: + _validate_utf8_json(data) + except UnicodeEncodeError: + # When removing this, also remove the loop and latin-1 from ansible.module_utils.common.text.converters.jsonify + display.deprecated( + f'Module "{self._task.resolved_action or self._task.action}" returned non UTF-8 data in ' + 'the JSON response. This will become an error in the future', + version='2.18', + ) + data['_ansible_parsed'] = True except ValueError: # not valid json, lets try to capture error @@ -1344,7 +1339,7 @@ class ActionBase(ABC): display.debug(u"_low_level_execute_command() done: rc=%d, stdout=%s, stderr=%s" % (rc, out, err)) return dict(rc=rc, stdout=out, stdout_lines=out.splitlines(), stderr=err, stderr_lines=err.splitlines()) - def _get_diff_data(self, destination, source, task_vars, source_file=True): + def _get_diff_data(self, destination, source, task_vars, content, source_file=True): # Note: Since we do not diff the source and destination before we transform from bytes into # text the diff between source and destination may not be accurate. To fix this, we'd need @@ -1402,7 +1397,10 @@ class ActionBase(ABC): if b"\x00" in src_contents: diff['src_binary'] = 1 else: - diff['after_header'] = source + if content: + diff['after_header'] = destination + else: + diff['after_header'] = source diff['after'] = to_text(src_contents) else: display.debug(u"source of file passed in") diff --git a/lib/ansible/plugins/action/add_host.py b/lib/ansible/plugins/action/add_host.py index e5697399..ede2e05f 100644 --- a/lib/ansible/plugins/action/add_host.py +++ b/lib/ansible/plugins/action/add_host.py @@ -37,12 +37,11 @@ class ActionModule(ActionBase): # We need to be able to modify the inventory BYPASS_HOST_LOOP = True - TRANSFERS_FILES = False + _requires_connection = False + _supports_check_mode = True def run(self, tmp=None, task_vars=None): - self._supports_check_mode = True - result = super(ActionModule, self).run(tmp, task_vars) del tmp # tmp no longer has any effect diff --git a/lib/ansible/plugins/action/assemble.py b/lib/ansible/plugins/action/assemble.py index 06fa2df3..da794edd 100644 --- a/lib/ansible/plugins/action/assemble.py +++ b/lib/ansible/plugins/action/assemble.py @@ -27,7 +27,7 @@ import tempfile from ansible import constants as C from ansible.errors import AnsibleError, AnsibleAction, _AnsibleActionDone, AnsibleActionFail -from ansible.module_utils._text import to_native, to_text +from ansible.module_utils.common.text.converters import to_native, to_text from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase from ansible.utils.hashing import checksum_s diff --git a/lib/ansible/plugins/action/assert.py b/lib/ansible/plugins/action/assert.py index e8ab6a9a..e2fe329e 100644 --- a/lib/ansible/plugins/action/assert.py +++ b/lib/ansible/plugins/action/assert.py @@ -27,7 +27,8 @@ from ansible.module_utils.parsing.convert_bool import boolean class ActionModule(ActionBase): ''' Fail with custom message ''' - TRANSFERS_FILES = False + _requires_connection = False + _VALID_ARGS = frozenset(('fail_msg', 'msg', 'quiet', 'success_msg', 'that')) def run(self, tmp=None, task_vars=None): diff --git a/lib/ansible/plugins/action/async_status.py b/lib/ansible/plugins/action/async_status.py index ad839f1e..4f50fe62 100644 --- a/lib/ansible/plugins/action/async_status.py +++ b/lib/ansible/plugins/action/async_status.py @@ -4,7 +4,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible.errors import AnsibleActionFail from ansible.plugins.action import ActionBase from ansible.utils.vars import merge_hash diff --git a/lib/ansible/plugins/action/command.py b/lib/ansible/plugins/action/command.py index 82a85dcd..64e1a094 100644 --- a/lib/ansible/plugins/action/command.py +++ b/lib/ansible/plugins/action/command.py @@ -4,7 +4,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible import constants as C from ansible.plugins.action import ActionBase from ansible.utils.vars import merge_hash diff --git a/lib/ansible/plugins/action/copy.py b/lib/ansible/plugins/action/copy.py index cb3d15b3..048f98dd 100644 --- a/lib/ansible/plugins/action/copy.py +++ b/lib/ansible/plugins/action/copy.py @@ -30,7 +30,7 @@ import traceback from ansible import constants as C from ansible.errors import AnsibleError, AnsibleFileNotFound from ansible.module_utils.basic import FILE_COMMON_ARGUMENTS -from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase from ansible.utils.hashing import checksum @@ -286,7 +286,7 @@ class ActionModule(ActionBase): # The checksums don't match and we will change or error out. if self._play_context.diff and not raw: - result['diff'].append(self._get_diff_data(dest_file, source_full, task_vars)) + result['diff'].append(self._get_diff_data(dest_file, source_full, task_vars, content)) if self._play_context.check_mode: self._remove_tempfile_if_content_defined(content, content_tempfile) diff --git a/lib/ansible/plugins/action/debug.py b/lib/ansible/plugins/action/debug.py index 2584fd3d..9e23c5fa 100644 --- a/lib/ansible/plugins/action/debug.py +++ b/lib/ansible/plugins/action/debug.py @@ -20,7 +20,7 @@ __metaclass__ = type from ansible.errors import AnsibleUndefinedVariable from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.plugins.action import ActionBase @@ -29,28 +29,34 @@ class ActionModule(ActionBase): TRANSFERS_FILES = False _VALID_ARGS = frozenset(('msg', 'var', 'verbosity')) + _requires_connection = False def run(self, tmp=None, task_vars=None): if task_vars is None: task_vars = dict() - if 'msg' in self._task.args and 'var' in self._task.args: - return {"failed": True, "msg": "'msg' and 'var' are incompatible options"} + validation_result, new_module_args = self.validate_argument_spec( + argument_spec={ + 'msg': {'type': 'raw', 'default': 'Hello world!'}, + 'var': {'type': 'raw'}, + 'verbosity': {'type': 'int', 'default': 0}, + }, + mutually_exclusive=( + ('msg', 'var'), + ), + ) result = super(ActionModule, self).run(tmp, task_vars) del tmp # tmp no longer has any effect # get task verbosity - verbosity = int(self._task.args.get('verbosity', 0)) + verbosity = new_module_args['verbosity'] if verbosity <= self._display.verbosity: - if 'msg' in self._task.args: - result['msg'] = self._task.args['msg'] - - elif 'var' in self._task.args: + if new_module_args['var']: try: - results = self._templar.template(self._task.args['var'], convert_bare=True, fail_on_undefined=True) - if results == self._task.args['var']: + results = self._templar.template(new_module_args['var'], convert_bare=True, fail_on_undefined=True) + if results == new_module_args['var']: # if results is not str/unicode type, raise an exception if not isinstance(results, string_types): raise AnsibleUndefinedVariable @@ -61,13 +67,13 @@ class ActionModule(ActionBase): if self._display.verbosity > 0: results += u": %s" % to_text(e) - if isinstance(self._task.args['var'], (list, dict)): + if isinstance(new_module_args['var'], (list, dict)): # If var is a list or dict, use the type as key to display - result[to_text(type(self._task.args['var']))] = results + result[to_text(type(new_module_args['var']))] = results else: - result[self._task.args['var']] = results + result[new_module_args['var']] = results else: - result['msg'] = 'Hello world!' + result['msg'] = new_module_args['msg'] # force flag to make debug output module always verbose result['_ansible_verbose_always'] = True diff --git a/lib/ansible/plugins/action/fail.py b/lib/ansible/plugins/action/fail.py index 8d3450c8..dedfc8c4 100644 --- a/lib/ansible/plugins/action/fail.py +++ b/lib/ansible/plugins/action/fail.py @@ -26,6 +26,7 @@ class ActionModule(ActionBase): TRANSFERS_FILES = False _VALID_ARGS = frozenset(('msg',)) + _requires_connection = False def run(self, tmp=None, task_vars=None): if task_vars is None: diff --git a/lib/ansible/plugins/action/fetch.py b/lib/ansible/plugins/action/fetch.py index 992ba5a5..11c91eb2 100644 --- a/lib/ansible/plugins/action/fetch.py +++ b/lib/ansible/plugins/action/fetch.py @@ -19,7 +19,7 @@ __metaclass__ = type import os import base64 -from ansible.errors import AnsibleError, AnsibleActionFail, AnsibleActionSkip +from ansible.errors import AnsibleConnectionFailure, AnsibleError, AnsibleActionFail, AnsibleActionSkip from ansible.module_utils.common.text.converters import to_bytes, to_text from ansible.module_utils.six import string_types from ansible.module_utils.parsing.convert_bool import boolean @@ -75,6 +75,8 @@ class ActionModule(ActionBase): # Follow symlinks because fetch always follows symlinks try: remote_stat = self._execute_remote_stat(source, all_vars=task_vars, follow=True) + except AnsibleConnectionFailure: + raise except AnsibleError as ae: result['changed'] = False result['file'] = source diff --git a/lib/ansible/plugins/action/gather_facts.py b/lib/ansible/plugins/action/gather_facts.py index 3ff7beb5..23962c83 100644 --- a/lib/ansible/plugins/action/gather_facts.py +++ b/lib/ansible/plugins/action/gather_facts.py @@ -6,6 +6,7 @@ __metaclass__ = type import os import time +import typing as t from ansible import constants as C from ansible.executor.module_common import get_action_args_with_defaults @@ -16,12 +17,15 @@ from ansible.utils.vars import merge_hash class ActionModule(ActionBase): - def _get_module_args(self, fact_module, task_vars): + _supports_check_mode = True + + def _get_module_args(self, fact_module: str, task_vars: dict[str, t.Any]) -> dict[str, t.Any]: mod_args = self._task.args.copy() # deal with 'setup specific arguments' if fact_module not in C._ACTION_SETUP: + # TODO: remove in favor of controller side argspec detecing valid arguments # network facts modules must support gather_subset try: @@ -30,16 +34,16 @@ class ActionModule(ActionBase): name = self._connection._load_name.split('.')[-1] if name not in ('network_cli', 'httpapi', 'netconf'): subset = mod_args.pop('gather_subset', None) - if subset not in ('all', ['all']): - self._display.warning('Ignoring subset(%s) for %s' % (subset, fact_module)) + if subset not in ('all', ['all'], None): + self._display.warning('Not passing subset(%s) to %s' % (subset, fact_module)) timeout = mod_args.pop('gather_timeout', None) if timeout is not None: - self._display.warning('Ignoring timeout(%s) for %s' % (timeout, fact_module)) + self._display.warning('Not passing timeout(%s) to %s' % (timeout, fact_module)) fact_filter = mod_args.pop('filter', None) if fact_filter is not None: - self._display.warning('Ignoring filter(%s) for %s' % (fact_filter, fact_module)) + self._display.warning('Not passing filter(%s) to %s' % (fact_filter, fact_module)) # Strip out keys with ``None`` values, effectively mimicking ``omit`` behavior # This ensures we don't pass a ``None`` value as an argument expecting a specific type @@ -57,7 +61,7 @@ class ActionModule(ActionBase): return mod_args - def _combine_task_result(self, result, task_result): + def _combine_task_result(self, result: dict[str, t.Any], task_result: dict[str, t.Any]) -> dict[str, t.Any]: filtered_res = { 'ansible_facts': task_result.get('ansible_facts', {}), 'warnings': task_result.get('warnings', []), @@ -67,9 +71,7 @@ class ActionModule(ActionBase): # on conflict the last plugin processed wins, but try to do deep merge and append to lists. return merge_hash(result, filtered_res, list_merge='append_rp') - def run(self, tmp=None, task_vars=None): - - self._supports_check_mode = True + def run(self, tmp: t.Optional[str] = None, task_vars: t.Optional[dict[str, t.Any]] = None) -> dict[str, t.Any]: result = super(ActionModule, self).run(tmp, task_vars) result['ansible_facts'] = {} @@ -87,16 +89,23 @@ class ActionModule(ActionBase): failed = {} skipped = {} - if parallel is None and len(modules) >= 1: - parallel = True + if parallel is None: + if len(modules) > 1: + parallel = True + else: + parallel = False else: parallel = boolean(parallel) - if parallel: + timeout = self._task.args.get('gather_timeout', None) + async_val = self._task.async_val + + if not parallel: # serially execute each module for fact_module in modules: # just one module, no need for fancy async mod_args = self._get_module_args(fact_module, task_vars) + # TODO: use gather_timeout to cut module execution if module itself does not support gather_timeout res = self._execute_module(module_name=fact_module, module_args=mod_args, task_vars=task_vars, wrap_async=False) if res.get('failed', False): failed[fact_module] = res @@ -107,10 +116,21 @@ class ActionModule(ActionBase): self._remove_tmp_path(self._connection._shell.tmpdir) else: - # do it async + # do it async, aka parallel jobs = {} + for fact_module in modules: mod_args = self._get_module_args(fact_module, task_vars) + + # if module does not handle timeout, use timeout to handle module, hijack async_val as this is what async_wrapper uses + # TODO: make this action compain about async/async settings, use parallel option instead .. or remove parallel in favor of async settings? + if timeout and 'gather_timeout' not in mod_args: + self._task.async_val = int(timeout) + elif async_val != 0: + self._task.async_val = async_val + else: + self._task.async_val = 0 + self._display.vvvv("Running %s" % fact_module) jobs[fact_module] = (self._execute_module(module_name=fact_module, module_args=mod_args, task_vars=task_vars, wrap_async=True)) @@ -132,6 +152,10 @@ class ActionModule(ActionBase): else: time.sleep(0.5) + # restore value for post processing + if self._task.async_val != async_val: + self._task.async_val = async_val + if skipped: result['msg'] = "The following modules were skipped: %s\n" % (', '.join(skipped.keys())) result['skipped_modules'] = skipped diff --git a/lib/ansible/plugins/action/group_by.py b/lib/ansible/plugins/action/group_by.py index 0958ad80..e0c70231 100644 --- a/lib/ansible/plugins/action/group_by.py +++ b/lib/ansible/plugins/action/group_by.py @@ -27,6 +27,7 @@ class ActionModule(ActionBase): # We need to be able to modify the inventory TRANSFERS_FILES = False _VALID_ARGS = frozenset(('key', 'parents')) + _requires_connection = False def run(self, tmp=None, task_vars=None): if task_vars is None: diff --git a/lib/ansible/plugins/action/include_vars.py b/lib/ansible/plugins/action/include_vars.py index 3c3cb9e1..83835b37 100644 --- a/lib/ansible/plugins/action/include_vars.py +++ b/lib/ansible/plugins/action/include_vars.py @@ -6,11 +6,12 @@ __metaclass__ = type from os import path, walk import re +import pathlib import ansible.constants as C from ansible.errors import AnsibleError from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_native, to_text +from ansible.module_utils.common.text.converters import to_native, to_text from ansible.plugins.action import ActionBase from ansible.utils.vars import combine_vars @@ -23,6 +24,7 @@ class ActionModule(ActionBase): VALID_DIR_ARGUMENTS = ['dir', 'depth', 'files_matching', 'ignore_files', 'extensions', 'ignore_unknown_extensions'] VALID_FILE_ARGUMENTS = ['file', '_raw_params'] VALID_ALL = ['name', 'hash_behaviour'] + _requires_connection = False def _set_dir_defaults(self): if not self.depth: @@ -181,16 +183,15 @@ class ActionModule(ActionBase): alphabetical order. Do not iterate pass the set depth. The default depth is unlimited. """ - current_depth = 0 - sorted_walk = list(walk(self.source_dir, onerror=self._log_walk)) + sorted_walk = list(walk(self.source_dir, onerror=self._log_walk, followlinks=True)) sorted_walk.sort(key=lambda x: x[0]) for current_root, current_dir, current_files in sorted_walk: - current_depth += 1 - if current_depth <= self.depth or self.depth == 0: - current_files.sort() - yield (current_root, current_files) - else: - break + # Depth 1 is the root, relative_to omits the root + current_depth = len(pathlib.Path(current_root).relative_to(self.source_dir).parts) + 1 + if self.depth != 0 and current_depth > self.depth: + continue + current_files.sort() + yield (current_root, current_files) def _ignore_file(self, filename): """ Return True if a file matches the list of ignore_files. diff --git a/lib/ansible/plugins/action/normal.py b/lib/ansible/plugins/action/normal.py index cb91521a..b2212e62 100644 --- a/lib/ansible/plugins/action/normal.py +++ b/lib/ansible/plugins/action/normal.py @@ -24,33 +24,24 @@ from ansible.utils.vars import merge_hash class ActionModule(ActionBase): + _supports_check_mode = True + _supports_async = True + def run(self, tmp=None, task_vars=None): # individual modules might disagree but as the generic the action plugin, pass at this point. - self._supports_check_mode = True - self._supports_async = True - result = super(ActionModule, self).run(tmp, task_vars) del tmp # tmp no longer has any effect - if not result.get('skipped'): - - if result.get('invocation', {}).get('module_args'): - # avoid passing to modules in case of no_log - # should not be set anymore but here for backwards compatibility - del result['invocation']['module_args'] - - # FUTURE: better to let _execute_module calculate this internally? - wrap_async = self._task.async_val and not self._connection.has_native_async + wrap_async = self._task.async_val and not self._connection.has_native_async - # do work! - result = merge_hash(result, self._execute_module(task_vars=task_vars, wrap_async=wrap_async)) + # do work! + result = merge_hash(result, self._execute_module(task_vars=task_vars, wrap_async=wrap_async)) - # hack to keep --verbose from showing all the setup module result - # moved from setup module as now we filter out all _ansible_ from result - # FIXME: is this still accurate with gather_facts etc, or does it need support for FQ and other names? - if self._task.action in C._ACTION_SETUP: - result['_ansible_verbose_override'] = True + # hack to keep --verbose from showing all the setup module result + # moved from setup module as now we filter out all _ansible_ from result + if self._task.action in C._ACTION_SETUP: + result['_ansible_verbose_override'] = True if not wrap_async: # remove a temporary path we created diff --git a/lib/ansible/plugins/action/pause.py b/lib/ansible/plugins/action/pause.py index 4c98cbbf..d306fbfa 100644 --- a/lib/ansible/plugins/action/pause.py +++ b/lib/ansible/plugins/action/pause.py @@ -18,92 +18,15 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import datetime -import signal -import sys -import termios import time -import tty -from os import ( - getpgrp, - isatty, - tcgetpgrp, -) -from ansible.errors import AnsibleError -from ansible.module_utils._text import to_text, to_native -from ansible.module_utils.parsing.convert_bool import boolean +from ansible.errors import AnsibleError, AnsiblePromptInterrupt, AnsiblePromptNoninteractive +from ansible.module_utils.common.text.converters import to_text from ansible.plugins.action import ActionBase from ansible.utils.display import Display display = Display() -try: - import curses - import io - - # Nest the try except since curses.error is not available if curses did not import - try: - curses.setupterm() - HAS_CURSES = True - except (curses.error, TypeError, io.UnsupportedOperation): - HAS_CURSES = False -except ImportError: - HAS_CURSES = False - -MOVE_TO_BOL = b'\r' -CLEAR_TO_EOL = b'\x1b[K' -if HAS_CURSES: - # curses.tigetstr() returns None in some circumstances - MOVE_TO_BOL = curses.tigetstr('cr') or MOVE_TO_BOL - CLEAR_TO_EOL = curses.tigetstr('el') or CLEAR_TO_EOL - - -def setraw(fd, when=termios.TCSAFLUSH): - """Put terminal into a raw mode. - - Copied from ``tty`` from CPython 3.11.0, and modified to not remove OPOST from OFLAG - - OPOST is kept to prevent an issue with multi line prompts from being corrupted now that display - is proxied via the queue from forks. The problem is a race condition, in that we proxy the display - over the fork, but before it can be displayed, this plugin will have continued executing, potentially - setting stdout and stdin to raw which remove output post processing that commonly converts NL to CRLF - """ - mode = termios.tcgetattr(fd) - mode[tty.IFLAG] = mode[tty.IFLAG] & ~(termios.BRKINT | termios.ICRNL | termios.INPCK | termios.ISTRIP | termios.IXON) - # mode[tty.OFLAG] = mode[tty.OFLAG] & ~(termios.OPOST) - mode[tty.CFLAG] = mode[tty.CFLAG] & ~(termios.CSIZE | termios.PARENB) - mode[tty.CFLAG] = mode[tty.CFLAG] | termios.CS8 - mode[tty.LFLAG] = mode[tty.LFLAG] & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG) - mode[tty.CC][termios.VMIN] = 1 - mode[tty.CC][termios.VTIME] = 0 - termios.tcsetattr(fd, when, mode) - - -class AnsibleTimeoutExceeded(Exception): - pass - - -def timeout_handler(signum, frame): - raise AnsibleTimeoutExceeded - - -def clear_line(stdout): - stdout.write(b'\x1b[%s' % MOVE_TO_BOL) - stdout.write(b'\x1b[%s' % CLEAR_TO_EOL) - - -def is_interactive(fd=None): - if fd is None: - return False - - if isatty(fd): - # Compare the current process group to the process group associated - # with terminal of the given file descriptor to determine if the process - # is running in the background. - return getpgrp() == tcgetpgrp(fd) - else: - return False - class ActionModule(ActionBase): ''' pauses execution for a length or time, or until input is received ''' @@ -169,143 +92,57 @@ class ActionModule(ActionBase): result['start'] = to_text(datetime.datetime.now()) result['user_input'] = b'' - stdin_fd = None - old_settings = None - try: - if seconds is not None: - if seconds < 1: - seconds = 1 - - # setup the alarm handler - signal.signal(signal.SIGALRM, timeout_handler) - signal.alarm(seconds) + default_input_complete = None + if seconds is not None: + if seconds < 1: + seconds = 1 - # show the timer and control prompts - display.display("Pausing for %d seconds%s" % (seconds, echo_prompt)) - display.display("(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r"), - - # show the prompt specified in the task - if new_module_args['prompt']: - display.display(prompt) + # show the timer and control prompts + display.display("Pausing for %d seconds%s" % (seconds, echo_prompt)) + # show the prompt specified in the task + if new_module_args['prompt']: + display.display("(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r") else: - display.display(prompt) + # corner case where enter does not continue, wait for timeout/interrupt only + prompt = "(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r" - # save the attributes on the existing (duped) stdin so - # that we can restore them later after we set raw mode - stdin_fd = None - stdout_fd = None - try: - stdin = self._connection._new_stdin.buffer - stdout = sys.stdout.buffer - stdin_fd = stdin.fileno() - stdout_fd = stdout.fileno() - except (ValueError, AttributeError): - # ValueError: someone is using a closed file descriptor as stdin - # AttributeError: someone is using a null file descriptor as stdin on windoze - stdin = None - interactive = is_interactive(stdin_fd) - if interactive: - # grab actual Ctrl+C sequence - try: - intr = termios.tcgetattr(stdin_fd)[6][termios.VINTR] - except Exception: - # unsupported/not present, use default - intr = b'\x03' # value for Ctrl+C + # don't complete on LF/CR; we expect a timeout/interrupt and ignore user input when a pause duration is specified + default_input_complete = tuple() - # get backspace sequences - try: - backspace = termios.tcgetattr(stdin_fd)[6][termios.VERASE] - except Exception: - backspace = [b'\x7f', b'\x08'] + # Only echo input if no timeout is specified + echo = seconds is None and echo - old_settings = termios.tcgetattr(stdin_fd) - setraw(stdin_fd) - - # Only set stdout to raw mode if it is a TTY. This is needed when redirecting - # stdout to a file since a file cannot be set to raw mode. - if isatty(stdout_fd): - setraw(stdout_fd) - - # Only echo input if no timeout is specified - if not seconds and echo: - new_settings = termios.tcgetattr(stdin_fd) - new_settings[3] = new_settings[3] | termios.ECHO - termios.tcsetattr(stdin_fd, termios.TCSANOW, new_settings) - - # flush the buffer to make sure no previous key presses - # are read in below - termios.tcflush(stdin, termios.TCIFLUSH) - - while True: - if not interactive: - if seconds is None: - display.warning("Not waiting for response to prompt as stdin is not interactive") - if seconds is not None: - # Give the signal handler enough time to timeout - time.sleep(seconds + 1) - break - - try: - key_pressed = stdin.read(1) - - if key_pressed == intr: # value for Ctrl+C - clear_line(stdout) - raise KeyboardInterrupt - - if not seconds: - # read key presses and act accordingly - if key_pressed in (b'\r', b'\n'): - clear_line(stdout) - break - elif key_pressed in backspace: - # delete a character if backspace is pressed - result['user_input'] = result['user_input'][:-1] - clear_line(stdout) - if echo: - stdout.write(result['user_input']) - stdout.flush() - else: - result['user_input'] += key_pressed - - except KeyboardInterrupt: - signal.alarm(0) - display.display("Press 'C' to continue the play or 'A' to abort \r"), - if self._c_or_a(stdin): - clear_line(stdout) - break - - clear_line(stdout) - - raise AnsibleError('user requested abort!') - - except AnsibleTimeoutExceeded: - # this is the exception we expect when the alarm signal - # fires, so we simply ignore it to move into the cleanup - pass - finally: - # cleanup and save some information - # restore the old settings for the duped stdin stdin_fd - if not (None in (stdin_fd, old_settings)) and isatty(stdin_fd): - termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings) - - duration = time.time() - start - result['stop'] = to_text(datetime.datetime.now()) - result['delta'] = int(duration) - - if duration_unit == 'minutes': - duration = round(duration / 60.0, 2) + user_input = b'' + try: + _user_input = display.prompt_until(prompt, private=not echo, seconds=seconds, complete_input=default_input_complete) + except AnsiblePromptInterrupt: + user_input = None + except AnsiblePromptNoninteractive: + if seconds is None: + display.warning("Not waiting for response to prompt as stdin is not interactive") else: - duration = round(duration, 2) - result['stdout'] = "Paused for %s %s" % (duration, duration_unit) + # wait specified duration + time.sleep(seconds) + else: + if seconds is None: + user_input = _user_input + # user interrupt + if user_input is None: + prompt = "Press 'C' to continue the play or 'A' to abort \r" + try: + user_input = display.prompt_until(prompt, private=not echo, interrupt_input=(b'a',), complete_input=(b'c',)) + except AnsiblePromptInterrupt: + raise AnsibleError('user requested abort!') - result['user_input'] = to_text(result['user_input'], errors='surrogate_or_strict') - return result + duration = time.time() - start + result['stop'] = to_text(datetime.datetime.now()) + result['delta'] = int(duration) - def _c_or_a(self, stdin): - while True: - key_pressed = stdin.read(1) - if key_pressed.lower() == b'a': - return False - elif key_pressed.lower() == b'c': - return True + if duration_unit == 'minutes': + duration = round(duration / 60.0, 2) + else: + duration = round(duration, 2) + result['stdout'] = "Paused for %s %s" % (duration, duration_unit) + result['user_input'] = to_text(user_input, errors='surrogate_or_strict') + return result diff --git a/lib/ansible/plugins/action/reboot.py b/lib/ansible/plugins/action/reboot.py index 40447d19..c75fba8e 100644 --- a/lib/ansible/plugins/action/reboot.py +++ b/lib/ansible/plugins/action/reboot.py @@ -8,10 +8,10 @@ __metaclass__ = type import random import time -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from ansible.errors import AnsibleError, AnsibleConnectionFailure -from ansible.module_utils._text import to_native, to_text +from ansible.module_utils.common.text.converters import to_native, to_text from ansible.module_utils.common.validation import check_type_list, check_type_str from ansible.plugins.action import ActionBase from ansible.utils.display import Display @@ -129,7 +129,7 @@ class ActionModule(ActionBase): else: args = self._get_value_from_facts('SHUTDOWN_COMMAND_ARGS', distribution, 'DEFAULT_SHUTDOWN_COMMAND_ARGS') - # Convert seconds to minutes. If less that 60, set it to 0. + # Convert seconds to minutes. If less than 60, set it to 0. delay_min = self.pre_reboot_delay // 60 reboot_message = self._task.args.get('msg', self.DEFAULT_REBOOT_MESSAGE) return args.format(delay_sec=self.pre_reboot_delay, delay_min=delay_min, message=reboot_message) @@ -236,7 +236,7 @@ class ActionModule(ActionBase): display.vvv("{action}: attempting to get system boot time".format(action=self._task.action)) connect_timeout = self._task.args.get('connect_timeout', self._task.args.get('connect_timeout_sec', self.DEFAULT_CONNECT_TIMEOUT)) - # override connection timeout from defaults to custom value + # override connection timeout from defaults to the custom value if connect_timeout: try: display.debug("{action}: setting connect_timeout to {value}".format(action=self._task.action, value=connect_timeout)) @@ -280,14 +280,15 @@ class ActionModule(ActionBase): display.vvv("{action}: system successfully rebooted".format(action=self._task.action)) def do_until_success_or_timeout(self, action, reboot_timeout, action_desc, distribution, action_kwargs=None): - max_end_time = datetime.utcnow() + timedelta(seconds=reboot_timeout) + max_end_time = datetime.now(timezone.utc) + timedelta(seconds=reboot_timeout) if action_kwargs is None: action_kwargs = {} fail_count = 0 max_fail_sleep = 12 + last_error_msg = '' - while datetime.utcnow() < max_end_time: + while datetime.now(timezone.utc) < max_end_time: try: action(distribution=distribution, **action_kwargs) if action_desc: @@ -299,7 +300,7 @@ class ActionModule(ActionBase): self._connection.reset() except AnsibleConnectionFailure: pass - # Use exponential backoff with a max timout, plus a little bit of randomness + # Use exponential backoff with a max timeout, plus a little bit of randomness random_int = random.randint(0, 1000) / 1000 fail_sleep = 2 ** fail_count + random_int if fail_sleep > max_fail_sleep: @@ -310,14 +311,18 @@ class ActionModule(ActionBase): error = to_text(e).splitlines()[-1] except IndexError as e: error = to_text(e) - display.debug("{action}: {desc} fail '{err}', retrying in {sleep:.4} seconds...".format( - action=self._task.action, - desc=action_desc, - err=error, - sleep=fail_sleep)) + last_error_msg = f"{self._task.action}: {action_desc} fail '{error}'" + msg = f"{last_error_msg}, retrying in {fail_sleep:.4f} seconds..." + + display.debug(msg) + display.vvv(msg) fail_count += 1 time.sleep(fail_sleep) + if last_error_msg: + msg = f"Last error message before the timeout exception - {last_error_msg}" + display.debug(msg) + display.vvv(msg) raise TimedOutException('Timed out waiting for {desc} (timeout={timeout})'.format(desc=action_desc, timeout=reboot_timeout)) def perform_reboot(self, task_vars, distribution): @@ -336,7 +341,7 @@ class ActionModule(ActionBase): display.debug('{action}: AnsibleConnectionFailure caught and handled: {error}'.format(action=self._task.action, error=to_text(e))) reboot_result['rc'] = 0 - result['start'] = datetime.utcnow() + result['start'] = datetime.now(timezone.utc) if reboot_result['rc'] != 0: result['failed'] = True @@ -406,7 +411,7 @@ class ActionModule(ActionBase): self._supports_check_mode = True self._supports_async = True - # If running with local connection, fail so we don't reboot ourself + # If running with local connection, fail so we don't reboot ourselves if self._connection.transport == 'local': msg = 'Running {0} with local connection would reboot the control node.'.format(self._task.action) return {'changed': False, 'elapsed': 0, 'rebooted': False, 'failed': True, 'msg': msg} @@ -447,7 +452,7 @@ class ActionModule(ActionBase): if reboot_result['failed']: result = reboot_result - elapsed = datetime.utcnow() - reboot_result['start'] + elapsed = datetime.now(timezone.utc) - reboot_result['start'] result['elapsed'] = elapsed.seconds return result @@ -459,7 +464,7 @@ class ActionModule(ActionBase): # Make sure reboot was successful result = self.validate_reboot(distribution, original_connection_timeout, action_kwargs={'previous_boot_time': previous_boot_time}) - elapsed = datetime.utcnow() - reboot_result['start'] + elapsed = datetime.now(timezone.utc) - reboot_result['start'] result['elapsed'] = elapsed.seconds return result diff --git a/lib/ansible/plugins/action/script.py b/lib/ansible/plugins/action/script.py index 1bbb8001..e6ebd094 100644 --- a/lib/ansible/plugins/action/script.py +++ b/lib/ansible/plugins/action/script.py @@ -23,7 +23,7 @@ import shlex from ansible.errors import AnsibleError, AnsibleAction, _AnsibleActionDone, AnsibleActionFail, AnsibleActionSkip from ansible.executor.powershell import module_manifest as ps_manifest -from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.plugins.action import ActionBase @@ -40,11 +40,25 @@ class ActionModule(ActionBase): if task_vars is None: task_vars = dict() + validation_result, new_module_args = self.validate_argument_spec( + argument_spec={ + '_raw_params': {}, + 'cmd': {'type': 'str'}, + 'creates': {'type': 'str'}, + 'removes': {'type': 'str'}, + 'chdir': {'type': 'str'}, + 'executable': {'type': 'str'}, + }, + required_one_of=[ + ['_raw_params', 'cmd'] + ] + ) + result = super(ActionModule, self).run(tmp, task_vars) del tmp # tmp no longer has any effect try: - creates = self._task.args.get('creates') + creates = new_module_args['creates'] if creates: # do not run the command if the line contains creates=filename # and the filename already exists. This allows idempotence @@ -52,7 +66,7 @@ class ActionModule(ActionBase): if self._remote_file_exists(creates): raise AnsibleActionSkip("%s exists, matching creates option" % creates) - removes = self._task.args.get('removes') + removes = new_module_args['removes'] if removes: # do not run the command if the line contains removes=filename # and the filename does not exist. This allows idempotence @@ -62,7 +76,7 @@ class ActionModule(ActionBase): # The chdir must be absolute, because a relative path would rely on # remote node behaviour & user config. - chdir = self._task.args.get('chdir') + chdir = new_module_args['chdir'] if chdir: # Powershell is the only Windows-path aware shell if getattr(self._connection._shell, "_IS_WINDOWS", False) and \ @@ -75,13 +89,14 @@ class ActionModule(ActionBase): # Split out the script as the first item in raw_params using # shlex.split() in order to support paths and files with spaces in the name. # Any arguments passed to the script will be added back later. - raw_params = to_native(self._task.args.get('_raw_params', ''), errors='surrogate_or_strict') + raw_params = to_native(new_module_args.get('_raw_params', ''), errors='surrogate_or_strict') parts = [to_text(s, errors='surrogate_or_strict') for s in shlex.split(raw_params.strip())] source = parts[0] # Support executable paths and files with spaces in the name. - executable = to_native(self._task.args.get('executable', ''), errors='surrogate_or_strict') - + executable = new_module_args['executable'] + if executable: + executable = to_native(new_module_args['executable'], errors='surrogate_or_strict') try: source = self._loader.get_real_file(self._find_needle('files', source), decrypt=self._task.args.get('decrypt', True)) except AnsibleError as e: @@ -90,7 +105,7 @@ class ActionModule(ActionBase): if self._task.check_mode: # check mode is supported if 'creates' or 'removes' are provided # the task has already been skipped if a change would not occur - if self._task.args.get('creates') or self._task.args.get('removes'): + if new_module_args['creates'] or new_module_args['removes']: result['changed'] = True raise _AnsibleActionDone(result=result) # If the script doesn't return changed in the result, it defaults to True, diff --git a/lib/ansible/plugins/action/set_fact.py b/lib/ansible/plugins/action/set_fact.py index ae92de80..ee3ceb28 100644 --- a/lib/ansible/plugins/action/set_fact.py +++ b/lib/ansible/plugins/action/set_fact.py @@ -30,6 +30,7 @@ import ansible.constants as C class ActionModule(ActionBase): TRANSFERS_FILES = False + _requires_connection = False def run(self, tmp=None, task_vars=None): if task_vars is None: diff --git a/lib/ansible/plugins/action/set_stats.py b/lib/ansible/plugins/action/set_stats.py index 9d429ced..5c4f0055 100644 --- a/lib/ansible/plugins/action/set_stats.py +++ b/lib/ansible/plugins/action/set_stats.py @@ -18,7 +18,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible.module_utils.six import string_types from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase from ansible.utils.vars import isidentifier @@ -28,6 +27,7 @@ class ActionModule(ActionBase): TRANSFERS_FILES = False _VALID_ARGS = frozenset(('aggregate', 'data', 'per_host')) + _requires_connection = False # TODO: document this in non-empty set_stats.py module def run(self, tmp=None, task_vars=None): diff --git a/lib/ansible/plugins/action/shell.py b/lib/ansible/plugins/action/shell.py index 617a373d..dd4df461 100644 --- a/lib/ansible/plugins/action/shell.py +++ b/lib/ansible/plugins/action/shell.py @@ -4,6 +4,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +from ansible.errors import AnsibleActionFail from ansible.plugins.action import ActionBase @@ -15,6 +16,11 @@ class ActionModule(ActionBase): # Shell module is implemented via command with a special arg self._task.args['_uses_shell'] = True + # Shell shares the same module code as command. Fail if command + # specific options are set. + if "expand_argument_vars" in self._task.args: + raise AnsibleActionFail(f"Unsupported parameters for ({self._task.action}) module: expand_argument_vars") + command_action = self._shared_loader_obj.action_loader.get('ansible.legacy.command', task=self._task, connection=self._connection, diff --git a/lib/ansible/plugins/action/template.py b/lib/ansible/plugins/action/template.py index d2b3df9a..4bfd9670 100644 --- a/lib/ansible/plugins/action/template.py +++ b/lib/ansible/plugins/action/template.py @@ -10,10 +10,19 @@ import shutil import stat import tempfile +from jinja2.defaults import ( + BLOCK_END_STRING, + BLOCK_START_STRING, + COMMENT_END_STRING, + COMMENT_START_STRING, + VARIABLE_END_STRING, + VARIABLE_START_STRING, +) + from ansible import constants as C from ansible.config.manager import ensure_type from ansible.errors import AnsibleError, AnsibleFileNotFound, AnsibleAction, AnsibleActionFail -from ansible.module_utils._text import to_bytes, to_text, to_native +from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native from ansible.module_utils.parsing.convert_bool import boolean from ansible.module_utils.six import string_types from ansible.plugins.action import ActionBase @@ -57,12 +66,12 @@ class ActionModule(ActionBase): dest = self._task.args.get('dest', None) state = self._task.args.get('state', None) newline_sequence = self._task.args.get('newline_sequence', self.DEFAULT_NEWLINE_SEQUENCE) - variable_start_string = self._task.args.get('variable_start_string', None) - variable_end_string = self._task.args.get('variable_end_string', None) - block_start_string = self._task.args.get('block_start_string', None) - block_end_string = self._task.args.get('block_end_string', None) - comment_start_string = self._task.args.get('comment_start_string', None) - comment_end_string = self._task.args.get('comment_end_string', None) + variable_start_string = self._task.args.get('variable_start_string', VARIABLE_START_STRING) + variable_end_string = self._task.args.get('variable_end_string', VARIABLE_END_STRING) + block_start_string = self._task.args.get('block_start_string', BLOCK_START_STRING) + block_end_string = self._task.args.get('block_end_string', BLOCK_END_STRING) + comment_start_string = self._task.args.get('comment_start_string', COMMENT_START_STRING) + comment_end_string = self._task.args.get('comment_end_string', COMMENT_END_STRING) output_encoding = self._task.args.get('output_encoding', 'utf-8') or 'utf-8' wrong_sequences = ["\\n", "\\r", "\\r\\n"] @@ -129,16 +138,18 @@ class ActionModule(ActionBase): templar = self._templar.copy_with_new_env(environment_class=AnsibleEnvironment, searchpath=searchpath, newline_sequence=newline_sequence, - block_start_string=block_start_string, - block_end_string=block_end_string, - variable_start_string=variable_start_string, - variable_end_string=variable_end_string, - comment_start_string=comment_start_string, - comment_end_string=comment_end_string, - trim_blocks=trim_blocks, - lstrip_blocks=lstrip_blocks, available_variables=temp_vars) - resultant = templar.do_template(template_data, preserve_trailing_newlines=True, escape_backslashes=False) + overrides = dict( + block_start_string=block_start_string, + block_end_string=block_end_string, + variable_start_string=variable_start_string, + variable_end_string=variable_end_string, + comment_start_string=comment_start_string, + comment_end_string=comment_end_string, + trim_blocks=trim_blocks, + lstrip_blocks=lstrip_blocks + ) + resultant = templar.do_template(template_data, preserve_trailing_newlines=True, escape_backslashes=False, overrides=overrides) except AnsibleAction: raise except Exception as e: diff --git a/lib/ansible/plugins/action/unarchive.py b/lib/ansible/plugins/action/unarchive.py index 4d188e3d..9bce1227 100644 --- a/lib/ansible/plugins/action/unarchive.py +++ b/lib/ansible/plugins/action/unarchive.py @@ -21,7 +21,7 @@ __metaclass__ = type import os from ansible.errors import AnsibleError, AnsibleAction, AnsibleActionFail, AnsibleActionSkip -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase diff --git a/lib/ansible/plugins/action/uri.py b/lib/ansible/plugins/action/uri.py index bbaf092e..ffd1c89a 100644 --- a/lib/ansible/plugins/action/uri.py +++ b/lib/ansible/plugins/action/uri.py @@ -10,10 +10,9 @@ __metaclass__ = type import os from ansible.errors import AnsibleError, AnsibleAction, _AnsibleActionDone, AnsibleActionFail -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.collections import Mapping, MutableMapping from ansible.module_utils.parsing.convert_bool import boolean -from ansible.module_utils.six import text_type from ansible.plugins.action import ActionBase diff --git a/lib/ansible/plugins/action/validate_argument_spec.py b/lib/ansible/plugins/action/validate_argument_spec.py index dc7d6cb3..b2c1d7b5 100644 --- a/lib/ansible/plugins/action/validate_argument_spec.py +++ b/lib/ansible/plugins/action/validate_argument_spec.py @@ -6,9 +6,7 @@ __metaclass__ = type from ansible.errors import AnsibleError from ansible.plugins.action import ActionBase -from ansible.module_utils.six import string_types from ansible.module_utils.common.arg_spec import ArgumentSpecValidator -from ansible.module_utils.errors import AnsibleValidationErrorMultiple from ansible.utils.vars import combine_vars @@ -16,6 +14,7 @@ class ActionModule(ActionBase): ''' Validate an arg spec''' TRANSFERS_FILES = False + _requires_connection = False def get_args_from_task_vars(self, argument_spec, task_vars): ''' diff --git a/lib/ansible/plugins/action/wait_for_connection.py b/lib/ansible/plugins/action/wait_for_connection.py index 8489c767..df549d94 100644 --- a/lib/ansible/plugins/action/wait_for_connection.py +++ b/lib/ansible/plugins/action/wait_for_connection.py @@ -20,9 +20,9 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import time -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.plugins.action import ActionBase from ansible.utils.display import Display @@ -43,10 +43,10 @@ class ActionModule(ActionBase): DEFAULT_TIMEOUT = 600 def do_until_success_or_timeout(self, what, timeout, connect_timeout, what_desc, sleep=1): - max_end_time = datetime.utcnow() + timedelta(seconds=timeout) + max_end_time = datetime.now(timezone.utc) + timedelta(seconds=timeout) e = None - while datetime.utcnow() < max_end_time: + while datetime.now(timezone.utc) < max_end_time: try: what(connect_timeout) if what_desc: diff --git a/lib/ansible/plugins/action/yum.py b/lib/ansible/plugins/action/yum.py index d90a9e00..9121e812 100644 --- a/lib/ansible/plugins/action/yum.py +++ b/lib/ansible/plugins/action/yum.py @@ -23,7 +23,7 @@ from ansible.utils.display import Display display = Display() -VALID_BACKENDS = frozenset(('yum', 'yum4', 'dnf')) +VALID_BACKENDS = frozenset(('yum', 'yum4', 'dnf', 'dnf4', 'dnf5')) class ActionModule(ActionBase): @@ -53,6 +53,9 @@ class ActionModule(ActionBase): module = self._task.args.get('use', self._task.args.get('use_backend', 'auto')) + if module == 'dnf': + module = 'auto' + if module == 'auto': try: if self._task.delegate_to: # if we delegate, we should use delegated host's facts @@ -81,7 +84,7 @@ class ActionModule(ActionBase): ) else: - if module == "yum4": + if module in {"yum4", "dnf4"}: module = "dnf" # eliminate collisions with collections search while still allowing local override @@ -90,7 +93,6 @@ class ActionModule(ActionBase): if not self._shared_loader_obj.module_loader.has_plugin(module): result.update({'failed': True, 'msg': "Could not find a yum module backend for %s." % module}) else: - # run either the yum (yum3) or dnf (yum4) backend module new_module_args = self._task.args.copy() if 'use_backend' in new_module_args: del new_module_args['use_backend'] -- cgit v1.2.3