diff options
author | Matt Martz <matt@sivel.net> | 2022-03-25 11:53:32 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-25 11:53:32 -0500 |
commit | afecc6400e39621ce7856913381fb214b3dac7dd (patch) | |
tree | 54233cb451f51422f7f003a708e3ea3679da4464 /lib/ansible/plugins | |
parent | 4635c75ef7e76342fac076cfa5f730ea48706bb9 (diff) | |
download | ansible-afecc6400e39621ce7856913381fb214b3dac7dd.tar.gz |
Action Plugin argspec validation (#77013)
Diffstat (limited to 'lib/ansible/plugins')
-rw-r--r-- | lib/ansible/plugins/action/__init__.py | 52 | ||||
-rw-r--r-- | lib/ansible/plugins/action/async_status.py | 22 | ||||
-rw-r--r-- | lib/ansible/plugins/action/pause.py | 58 |
3 files changed, 88 insertions, 44 deletions
diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py index 5742536eb1..9ee9a1c122 100644 --- a/lib/ansible/plugins/action/__init__.py +++ b/lib/ansible/plugins/action/__init__.py @@ -23,6 +23,8 @@ from ansible import constants as C from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleActionSkip, AnsibleActionFail, AnsibleAuthenticationFailure from ansible.executor.module_common import modify_module from ansible.executor.interpreter_discovery import discover_interpreter, InterpreterDiscoveryRequiredError +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 @@ -118,6 +120,56 @@ class ActionBase(ABC): return result + def validate_argument_spec(self, argument_spec=None, + mutually_exclusive=None, + required_together=None, + required_one_of=None, + required_if=None, + required_by=None, + ): + """Validate an argument spec against the task args + + This will return a tuple of (ValidationResult, dict) where the dict + is the validated, coerced, and normalized task args. + + Be cautious when directly passing ``new_module_args`` directly to a + module invocation, as it will contain the defaults, and not only + the args supplied from the task. If you do this, the module + should not define ``mututally_exclusive`` or similar. + + This code is roughly copied from the ``validate_argument_spec`` + action plugin for use by other action plugins. + """ + + new_module_args = self._task.args.copy() + + validator = ArgumentSpecValidator( + argument_spec, + mutually_exclusive=mutually_exclusive, + required_together=required_together, + required_one_of=required_one_of, + required_if=required_if, + required_by=required_by, + ) + validation_result = validator.validate(new_module_args) + + new_module_args.update(validation_result.validated_parameters) + + try: + error = validation_result.errors[0] + except IndexError: + error = None + + # Fail for validation errors, even in check mode + if error: + msg = validation_result.errors.msg + if isinstance(error, UnsupportedError): + msg = f"Unsupported parameters for ({self._load_name}) module: {msg}" + + raise AnsibleActionFail(msg) + + return validation_result, new_module_args + def cleanup(self, force=False): """Method to perform a clean up at the end of an action plugin execution diff --git a/lib/ansible/plugins/action/async_status.py b/lib/ansible/plugins/action/async_status.py index fc48716f7e..ad839f1e34 100644 --- a/lib/ansible/plugins/action/async_status.py +++ b/lib/ansible/plugins/action/async_status.py @@ -11,8 +11,6 @@ from ansible.utils.vars import merge_hash class ActionModule(ActionBase): - _VALID_ARGS = frozenset(('jid', 'mode')) - def _get_async_dir(self): # async directory based on the shell option @@ -24,18 +22,20 @@ class ActionModule(ActionBase): results = super(ActionModule, self).run(tmp, task_vars) + validation_result, new_module_args = self.validate_argument_spec( + argument_spec={ + 'jid': {'type': 'str', 'required': True}, + 'mode': {'type': 'str', 'choices': ['status', 'cleanup'], 'default': 'status'}, + }, + ) + # initialize response results['started'] = results['finished'] = 0 results['stdout'] = results['stderr'] = '' results['stdout_lines'] = results['stderr_lines'] = [] - # read params - try: - jid = self._task.args["jid"] - except KeyError: - raise AnsibleActionFail("jid is required", result=results) - - mode = self._task.args.get("mode", "status") + jid = new_module_args["jid"] + mode = new_module_args["mode"] results['ansible_job_id'] = jid async_dir = self._get_async_dir() @@ -47,7 +47,7 @@ class ActionModule(ActionBase): results['results_file'] = log_path results['started'] = 1 - module_args = dict(jid=jid, mode=mode, _async_dir=async_dir) - results = merge_hash(results, self._execute_module(module_name='ansible.legacy.async_status', task_vars=task_vars, module_args=module_args)) + new_module_args['_async_dir'] = async_dir + results = merge_hash(results, self._execute_module(module_name='ansible.legacy.async_status', task_vars=task_vars, module_args=new_module_args)) return results diff --git a/lib/ansible/plugins/action/pause.py b/lib/ansible/plugins/action/pause.py index 6130d860d2..4d06b00e03 100644 --- a/lib/ansible/plugins/action/pause.py +++ b/lib/ansible/plugins/action/pause.py @@ -88,7 +88,6 @@ class ActionModule(ActionBase): ''' pauses execution for a length or time, or until input is received ''' BYPASS_HOST_LOOP = True - _VALID_ARGS = frozenset(('echo', 'minutes', 'prompt', 'seconds')) def run(self, tmp=None, task_vars=None): ''' run the pause action module ''' @@ -98,6 +97,18 @@ class ActionModule(ActionBase): result = super(ActionModule, self).run(tmp, task_vars) del tmp # tmp no longer has any effect + validation_result, new_module_args = self.validate_argument_spec( + argument_spec={ + 'echo': {'type': 'bool', 'default': True}, + 'minutes': {'type': int}, # Don't break backwards compat, allow floats, by using int callable + 'seconds': {'type': int}, # Don't break backwards compat, allow floats, by using int callable + 'prompt': {'type': 'str'}, + }, + mutually_exclusive=( + ('minutes', 'seconds'), + ), + ) + duration_unit = 'minutes' prompt = None seconds = None @@ -114,41 +125,22 @@ class ActionModule(ActionBase): echo=echo )) - # Should keystrokes be echoed to stdout? - if 'echo' in self._task.args: - try: - echo = boolean(self._task.args['echo']) - except TypeError as e: - result['failed'] = True - result['msg'] = to_native(e) - return result - - # Add a note saying the output is hidden if echo is disabled - if not echo: - echo_prompt = ' (output is hidden)' - - # Is 'prompt' a key in 'args'? - if 'prompt' in self._task.args: - prompt = "[%s]\n%s%s:" % (self._task.get_name().strip(), self._task.args['prompt'], echo_prompt) + echo = new_module_args['echo'] + # Add a note saying the output is hidden if echo is disabled + if not echo: + echo_prompt = ' (output is hidden)' + + if new_module_args['prompt']: + prompt = "[%s]\n%s%s:" % (self._task.get_name().strip(), new_module_args['prompt'], echo_prompt) else: # If no custom prompt is specified, set a default prompt prompt = "[%s]\n%s%s:" % (self._task.get_name().strip(), 'Press enter to continue, Ctrl+C to interrupt', echo_prompt) - # Are 'minutes' or 'seconds' keys that exist in 'args'? - if 'minutes' in self._task.args or 'seconds' in self._task.args: - try: - if 'minutes' in self._task.args: - # The time() command operates in seconds so we need to - # recalculate for minutes=X values. - seconds = int(self._task.args['minutes']) * 60 - else: - seconds = int(self._task.args['seconds']) - duration_unit = 'seconds' - - except ValueError as e: - result['failed'] = True - result['msg'] = u"non-integer value given for prompt duration:\n%s" % to_text(e) - return result + if new_module_args['minutes'] is not None: + seconds = new_module_args['minutes'] * 60 + elif new_module_args['seconds'] is not None: + seconds = new_module_args['seconds'] + duration_unit = 'seconds' ######################################################################## # Begin the hard work! @@ -173,7 +165,7 @@ class ActionModule(ActionBase): display.display("(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r"), # show the prompt specified in the task - if 'prompt' in self._task.args: + if new_module_args['prompt']: display.display(prompt) else: |