diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:04:21 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:04:21 +0000 |
commit | 8a754e0858d922e955e71b253c139e071ecec432 (patch) | |
tree | 527d16e74bfd1840c85efd675fdecad056c54107 /lib/ansible/parsing/mod_args.py | |
parent | Initial commit. (diff) | |
download | ansible-core-8a754e0858d922e955e71b253c139e071ecec432.tar.xz ansible-core-8a754e0858d922e955e71b253c139e071ecec432.zip |
Adding upstream version 2.14.3.upstream/2.14.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/ansible/parsing/mod_args.py')
-rw-r--r-- | lib/ansible/parsing/mod_args.py | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/lib/ansible/parsing/mod_args.py b/lib/ansible/parsing/mod_args.py new file mode 100644 index 0000000..aeb58b0 --- /dev/null +++ b/lib/ansible/parsing/mod_args.py @@ -0,0 +1,345 @@ +# (c) 2014 Michael DeHaan, <michael@ansible.com> +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import ansible.constants as C +from ansible.errors import AnsibleParserError, AnsibleError, AnsibleAssertionError +from ansible.module_utils.six import string_types +from ansible.module_utils._text import to_text +from ansible.parsing.splitter import parse_kv, split_args +from ansible.plugins.loader import module_loader, action_loader +from ansible.template import Templar +from ansible.utils.fqcn import add_internal_fqcns +from ansible.utils.sentinel import Sentinel + + +# For filtering out modules correctly below +FREEFORM_ACTIONS = frozenset(C.MODULE_REQUIRE_ARGS) + +RAW_PARAM_MODULES = FREEFORM_ACTIONS.union(add_internal_fqcns(( + 'include', + 'include_vars', + 'include_tasks', + 'include_role', + 'import_tasks', + 'import_role', + 'add_host', + 'group_by', + 'set_fact', + 'meta', +))) + +BUILTIN_TASKS = frozenset(add_internal_fqcns(( + 'meta', + 'include', + 'include_tasks', + 'include_role', + 'import_tasks', + 'import_role' +))) + + +class ModuleArgsParser: + + """ + There are several ways a module and argument set can be expressed: + + # legacy form (for a shell command) + - action: shell echo hi + + # common shorthand for local actions vs delegate_to + - local_action: shell echo hi + + # most commonly: + - copy: src=a dest=b + + # legacy form + - action: copy src=a dest=b + + # complex args form, for passing structured data + - copy: + src: a + dest: b + + # gross, but technically legal + - action: + module: copy + args: + src: a + dest: b + + # Standard YAML form for command-type modules. In this case, the args specified + # will act as 'defaults' and will be overridden by any args specified + # in one of the other formats (complex args under the action, or + # parsed from the k=v string + - command: 'pwd' + args: + chdir: '/tmp' + + + This class has some of the logic to canonicalize these into the form + + - module: <module_name> + delegate_to: <optional> + args: <args> + + Args may also be munged for certain shell command parameters. + """ + + def __init__(self, task_ds=None, collection_list=None): + task_ds = {} if task_ds is None else task_ds + + if not isinstance(task_ds, dict): + raise AnsibleAssertionError("the type of 'task_ds' should be a dict, but is a %s" % type(task_ds)) + self._task_ds = task_ds + self._collection_list = collection_list + # delayed local imports to prevent circular import + from ansible.playbook.task import Task + from ansible.playbook.handler import Handler + # store the valid Task/Handler attrs for quick access + self._task_attrs = set(Task.fattributes) + self._task_attrs.update(set(Handler.fattributes)) + # HACK: why are these not FieldAttributes on task with a post-validate to check usage? + self._task_attrs.update(['local_action', 'static']) + self._task_attrs = frozenset(self._task_attrs) + + self.resolved_action = None + + def _split_module_string(self, module_string): + ''' + when module names are expressed like: + action: copy src=a dest=b + the first part of the string is the name of the module + and the rest are strings pertaining to the arguments. + ''' + + tokens = split_args(module_string) + if len(tokens) > 1: + return (tokens[0].strip(), " ".join(tokens[1:])) + else: + return (tokens[0].strip(), "") + + def _normalize_parameters(self, thing, action=None, additional_args=None): + ''' + arguments can be fuzzy. Deal with all the forms. + ''' + + additional_args = {} if additional_args is None else additional_args + + # final args are the ones we'll eventually return, so first update + # them with any additional args specified, which have lower priority + # than those which may be parsed/normalized next + final_args = dict() + if additional_args: + if isinstance(additional_args, string_types): + templar = Templar(loader=None) + if templar.is_template(additional_args): + final_args['_variable_params'] = additional_args + else: + raise AnsibleParserError("Complex args containing variables cannot use bare variables (without Jinja2 delimiters), " + "and must use the full variable style ('{{var_name}}')") + elif isinstance(additional_args, dict): + final_args.update(additional_args) + else: + raise AnsibleParserError('Complex args must be a dictionary or variable string ("{{var}}").') + + # how we normalize depends if we figured out what the module name is + # yet. If we have already figured it out, it's a 'new style' invocation. + # otherwise, it's not + + if action is not None: + args = self._normalize_new_style_args(thing, action) + else: + (action, args) = self._normalize_old_style_args(thing) + + # this can occasionally happen, simplify + if args and 'args' in args: + tmp_args = args.pop('args') + if isinstance(tmp_args, string_types): + tmp_args = parse_kv(tmp_args) + args.update(tmp_args) + + # only internal variables can start with an underscore, so + # we don't allow users to set them directly in arguments + if args and action not in FREEFORM_ACTIONS: + for arg in args: + arg = to_text(arg) + if arg.startswith('_ansible_'): + raise AnsibleError("invalid parameter specified for action '%s': '%s'" % (action, arg)) + + # finally, update the args we're going to return with the ones + # which were normalized above + if args: + final_args.update(args) + + return (action, final_args) + + def _normalize_new_style_args(self, thing, action): + ''' + deals with fuzziness in new style module invocations + accepting key=value pairs and dictionaries, and returns + a dictionary of arguments + + possible example inputs: + 'echo hi', 'shell' + {'region': 'xyz'}, 'ec2' + standardized outputs like: + { _raw_params: 'echo hi', _uses_shell: True } + ''' + + if isinstance(thing, dict): + # form is like: { xyz: { x: 2, y: 3 } } + args = thing + elif isinstance(thing, string_types): + # form is like: copy: src=a dest=b + check_raw = action in FREEFORM_ACTIONS + args = parse_kv(thing, check_raw=check_raw) + elif thing is None: + # this can happen with modules which take no params, like ping: + args = None + else: + raise AnsibleParserError("unexpected parameter type in action: %s" % type(thing), obj=self._task_ds) + return args + + def _normalize_old_style_args(self, thing): + ''' + deals with fuzziness in old-style (action/local_action) module invocations + returns tuple of (module_name, dictionary_args) + + possible example inputs: + { 'shell' : 'echo hi' } + 'shell echo hi' + {'module': 'ec2', 'x': 1 } + standardized outputs like: + ('ec2', { 'x': 1} ) + ''' + + action = None + args = None + + if isinstance(thing, dict): + # form is like: action: { module: 'copy', src: 'a', dest: 'b' } + thing = thing.copy() + if 'module' in thing: + action, module_args = self._split_module_string(thing['module']) + args = thing.copy() + check_raw = action in FREEFORM_ACTIONS + args.update(parse_kv(module_args, check_raw=check_raw)) + del args['module'] + + elif isinstance(thing, string_types): + # form is like: action: copy src=a dest=b + (action, args) = self._split_module_string(thing) + check_raw = action in FREEFORM_ACTIONS + args = parse_kv(args, check_raw=check_raw) + + else: + # need a dict or a string, so giving up + raise AnsibleParserError("unexpected parameter type in action: %s" % type(thing), obj=self._task_ds) + + return (action, args) + + def parse(self, skip_action_validation=False): + ''' + Given a task in one of the supported forms, parses and returns + returns the action, arguments, and delegate_to values for the + task, dealing with all sorts of levels of fuzziness. + ''' + + thing = None + + action = None + delegate_to = self._task_ds.get('delegate_to', Sentinel) + args = dict() + + # This is the standard YAML form for command-type modules. We grab + # the args and pass them in as additional arguments, which can/will + # be overwritten via dict updates from the other arg sources below + additional_args = self._task_ds.get('args', dict()) + + # We can have one of action, local_action, or module specified + # action + if 'action' in self._task_ds: + # an old school 'action' statement + thing = self._task_ds['action'] + action, args = self._normalize_parameters(thing, action=action, additional_args=additional_args) + + # local_action + if 'local_action' in self._task_ds: + # local_action is similar but also implies a delegate_to + if action is not None: + raise AnsibleParserError("action and local_action are mutually exclusive", obj=self._task_ds) + thing = self._task_ds.get('local_action', '') + delegate_to = 'localhost' + action, args = self._normalize_parameters(thing, action=action, additional_args=additional_args) + + # module: <stuff> is the more new-style invocation + + # filter out task attributes so we're only querying unrecognized keys as actions/modules + non_task_ds = dict((k, v) for k, v in self._task_ds.items() if (k not in self._task_attrs) and (not k.startswith('with_'))) + + # walk the filtered input dictionary to see if we recognize a module name + for item, value in non_task_ds.items(): + context = None + is_action_candidate = False + if item in BUILTIN_TASKS: + is_action_candidate = True + elif skip_action_validation: + is_action_candidate = True + else: + context = action_loader.find_plugin_with_context(item, collection_list=self._collection_list) + if not context.resolved: + context = module_loader.find_plugin_with_context(item, collection_list=self._collection_list) + + is_action_candidate = context.resolved and bool(context.redirect_list) + + if is_action_candidate: + # finding more than one module name is a problem + if action is not None: + raise AnsibleParserError("conflicting action statements: %s, %s" % (action, item), obj=self._task_ds) + + if context is not None and context.resolved: + self.resolved_action = context.resolved_fqcn + + action = item + thing = value + action, args = self._normalize_parameters(thing, action=action, additional_args=additional_args) + + # if we didn't see any module in the task at all, it's not a task really + if action is None: + if non_task_ds: # there was one non-task action, but we couldn't find it + bad_action = list(non_task_ds.keys())[0] + raise AnsibleParserError("couldn't resolve module/action '{0}'. This often indicates a " + "misspelling, missing collection, or incorrect module path.".format(bad_action), + obj=self._task_ds) + else: + raise AnsibleParserError("no module/action detected in task.", + obj=self._task_ds) + elif args.get('_raw_params', '') != '' and action not in RAW_PARAM_MODULES: + templar = Templar(loader=None) + raw_params = args.pop('_raw_params') + if templar.is_template(raw_params): + args['_variable_params'] = raw_params + else: + raise AnsibleParserError("this task '%s' has extra params, which is only allowed in the following modules: %s" % (action, + ", ".join(RAW_PARAM_MODULES)), + obj=self._task_ds) + + return (action, args, delegate_to) |