diff options
Diffstat (limited to 'lib/ansible/plugins/action')
26 files changed, 268 insertions, 371 deletions
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'] |