diff --git a/lib/ansible/parsing/splitter.py b/lib/ansible/parsing/splitter.py index 18ef976496e..2f2be4228d9 100644 --- a/lib/ansible/parsing/splitter.py +++ b/lib/ansible/parsing/splitter.py @@ -46,7 +46,7 @@ def _decode_escapes(s): return _ESCAPE_SEQUENCE_RE.sub(decode_match, s) -def parse_kv(args, check_raw=False): +def parse_kv(args, check_raw=False, action=None): """ Convert a string of key/value items to a dict. If any free-form params are found and the check_raw option is set to True, they will be added @@ -61,6 +61,9 @@ def parse_kv(args, check_raw=False): if trusted_tag := TrustedAsTemplate.get_tag(args): tags.append(trusted_tag) + # avoid circular import + from ansible.plugins.action import get_action_options + args = to_text(args, nonstring='passthru') options = {} @@ -87,7 +90,7 @@ def parse_kv(args, check_raw=False): v = x[pos + 1:] # FIXME: make the retrieval of this list of shell/command options a function, so the list is centralized - if check_raw and k not in ('creates', 'removes', 'chdir', 'executable', 'warn', 'stdin', 'stdin_add_newline', 'strip_empty_ends'): + if check_raw and k not in get_action_options(action): raw_params.append(orig_x) else: options[k.strip()] = unquote(v.strip()) @@ -219,7 +222,6 @@ def split_args(args): params[-1] += ' ' continue - # if we hit a line continuation character, but # we're not inside quotes, ignore it and continue # on to the next token while setting a flag if token == '\\' and not inside_quotes: diff --git a/lib/ansible/plugins/__init__.py b/lib/ansible/plugins/__init__.py index e259960299e..68290f3c414 100644 --- a/lib/ansible/plugins/__init__.py +++ b/lib/ansible/plugins/__init__.py @@ -42,17 +42,10 @@ PATH_CACHE = {} # type: dict[str, list[_t_loader.PluginPathContext] | None] PLUGIN_PATH_CACHE = {} # type: dict[str, dict[str, dict[str, _t_loader.PluginPathContext]]] -def get_plugin_class(obj): - if isinstance(obj, str): - return obj.lower().replace('module', '') - else: - return obj.__class__.__name__.lower().replace('module', '') - - class _ConfigurablePlugin(t.Protocol): """Protocol to provide type-safe access to config for plugin-related mixins.""" - def get_option(self, option: str, hostvars: dict[str, object] | None = None) -> t.Any: ... + def get_option(self, option: str, hostvars: dict[str, object] | None = None) -> object: ... class _AnsiblePluginInfoMixin(_plugin_info.HasPluginInfo): @@ -159,6 +152,10 @@ class AnsiblePlugin(_AnsiblePluginInfoMixin, _ConfigurablePlugin, metaclass=abc. self.set_options() return option in self._options + @property + def plugin_type(self): + return self.__class__.__name__.lower().replace('module', '') + @property def option_definitions(self): if (not hasattr(self, "_defs")) or self._defs is None: @@ -189,7 +186,7 @@ class AnsibleJinja2Plugin(AnsiblePlugin, metaclass=abc.ABCMeta): def plugin_type(self) -> str: ... - def _no_options(self, *args, **kwargs) -> t.NoReturn: + def _no_options(self, *args, **kwargs) -> t.Never: raise NotImplementedError() has_option = get_option = get_options = option_definitions = set_option = set_options = _no_options diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py index 881e0f414e1..6473cbad8f9 100644 --- a/lib/ansible/plugins/action/__init__.py +++ b/lib/ansible/plugins/action/__init__.py @@ -38,6 +38,9 @@ from ansible._internal._templating import _engine from ansible._internal import _task from .. import _AnsiblePluginInfoMixin +from ansible.plugins.loader import module_loader +from ansible.cli.doc import DocCLI + display = Display() if t.TYPE_CHECKING: @@ -50,6 +53,19 @@ if t.TYPE_CHECKING: VariableLayer = _task.VariableLayer # public API +def get_action_options(action): + # avoid circulairty + + if action is None: + # fallback/default hardcoded list from before + options = ('creates', 'removes', 'chdir', 'executable', 'warn', 'stdin', 'stdin_add_newline', 'strip_empty_ends') + else: + doc, *stuff = DocCLI._get_plugin_doc(action, 'module', module_loader, []) + options = doc.get('options', {}).keys() + + return options + + def _validate_utf8_json(d): if isinstance(d, str): # Purposefully not using to_bytes here for performance reasons diff --git a/lib/ansible/plugins/loader.py b/lib/ansible/plugins/loader.py index 60e7efb0a06..731388ebb55 100644 --- a/lib/ansible/plugins/loader.py +++ b/lib/ansible/plugins/loader.py @@ -438,7 +438,7 @@ class PluginLoader: for i in paths: if i not in ret: ret.append(i) - return os.pathsep.join(ret) + return to_text(os.pathsep.join(ret), errors='surrogate_or_strict') def print_paths(self): return self.format_paths(self._get_paths(subdirs=False)) @@ -531,7 +531,7 @@ class PluginLoader: # plugins w/o class name don't support config if self.class_name: - type_name = get_plugin_class(self.class_name) + type_name = self.class_name.lower().replace('module', '') # if type name != 'module_doc_fragment': if type_name in C.CONFIGURABLE_PLUGINS and not C.config.has_configuration_definition(type_name, name):