diff --git a/changelogs/fragments/config_set_option_fix.yml b/changelogs/fragments/config_set_option_fix.yml new file mode 100644 index 00000000000..57ebec932a3 --- /dev/null +++ b/changelogs/fragments/config_set_option_fix.yml @@ -0,0 +1,2 @@ +bugfixes: + - set_option method for plugins to update config now properly passes through type casting and validation. diff --git a/lib/ansible/plugins/__init__.py b/lib/ansible/plugins/__init__.py index 90452dff3e0..c083dee93e8 100644 --- a/lib/ansible/plugins/__init__.py +++ b/lib/ansible/plugins/__init__.py @@ -91,7 +91,7 @@ class AnsiblePlugin(ABC): return options def set_option(self, option, value): - self._options[option] = value + self._options[option] = C.config.get_config_value(option, plugin_type=self.plugin_type, plugin_name=self._load_name, direct={option: value}) def set_options(self, task_keys=None, var_options=None, direct=None): ''' @@ -106,7 +106,8 @@ class AnsiblePlugin(ABC): # allow extras/wildcards from vars that are not directly consumed in configuration # this is needed to support things like winrm that can have extended protocol options we don't directly handle if self.allow_extras and var_options and '_extras' in var_options: - self.set_option('_extras', var_options['_extras']) + # these are largely unvalidated passthroughs, either plugin or underlying API will validate + self._options['_extras'] = var_options['_extras'] def has_option(self, option): if not self._options: diff --git a/lib/ansible/plugins/callback/__init__.py b/lib/ansible/plugins/callback/__init__.py index f6a647f4a98..5659887b731 100644 --- a/lib/ansible/plugins/callback/__init__.py +++ b/lib/ansible/plugins/callback/__init__.py @@ -165,7 +165,7 @@ class CallbackBase(AnsiblePlugin): _copy_result = deepcopy def set_option(self, k, v): - self._plugin_options[k] = v + self._plugin_options[k] = C.config.get_config_value(k, plugin_type=self.plugin_type, plugin_name=self._load_name, direct={k: v}) def get_option(self, k): return self._plugin_options[k] diff --git a/test/integration/targets/config/lookup_plugins/casting.py b/test/integration/targets/config/lookup_plugins/casting.py new file mode 100644 index 00000000000..4e7338d6dd3 --- /dev/null +++ b/test/integration/targets/config/lookup_plugins/casting.py @@ -0,0 +1,59 @@ +# (c) 2021 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import annotations + + +DOCUMENTATION = """ + name: casting + author: Ansible Core Team + version_added: histerical + short_description: returns what you gave it + description: + - this is mostly a noop + options: + _terms: + description: stuff to pass through + test_list: + description: does nothihng, just for testing values + type: list + test_int: + description: does nothihng, just to test casting + type: int + test_bool: + description: does nothihng, just to test casting + type: bool + test_str: + description: does nothihng, just to test casting + type: str +""" + +EXAMPLES = """ +- name: like some other plugins, this is mostly useless + debug: msg={{ q('casting', [1,2,3])}} +""" + +RETURN = """ + _list: + description: basically the same as you fed in + type: list + elements: raw +""" + +from ansible.plugins.lookup import LookupBase + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + + self.set_options(var_options=variables, direct=kwargs) + + for cast in (list, int, bool, str): + option = 'test_%s' % str(cast).replace("", '') + value = self.get_option(option) + if value is None or type(value) is cast: + continue + raise Exception('%s is not a %s: got %s/%s' % (option, cast, type(value), value)) + + return terms diff --git a/test/integration/targets/config/lookup_plugins/casting_individual.py b/test/integration/targets/config/lookup_plugins/casting_individual.py new file mode 100644 index 00000000000..af1f60acdfc --- /dev/null +++ b/test/integration/targets/config/lookup_plugins/casting_individual.py @@ -0,0 +1,58 @@ +# (c) 2021 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import annotations + + +DOCUMENTATION = """ + name: casting_individual + author: Ansible Core Team + version_added: histerical + short_description: returns what you gave it + description: + - this is mostly a noop + options: + _terms: + description: stuff to pass through + test_list: + description: does nothihng, just for testing values + type: list + test_int: + description: does nothihng, just to test casting + type: int + test_bool: + description: does nothihng, just to test casting + type: bool + test_str: + description: does nothihng, just to test casting + type: str +""" + +EXAMPLES = """ +- name: like some other plugins, this is mostly useless + debug: msg={{ q('casting_individual', [1,2,3])}} +""" + +RETURN = """ + _list: + description: basically the same as you fed in + type: list + elements: raw +""" + +from ansible.plugins.lookup import LookupBase + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + + for cast in (list, int, bool, str): + option = 'test_%s' % str(cast).replace("", '') + if option in kwargs: + self.set_option(option, kwargs[option]) + value = self.get_option(option) + if type(value) is not cast: + raise Exception('%s is not a %s: got %s/%s' % (option, cast, type(value), value)) + + return terms diff --git a/test/integration/targets/config/validation.yml b/test/integration/targets/config/validation.yml index 1c81e662b6e..20800e03c40 100644 --- a/test/integration/targets/config/validation.yml +++ b/test/integration/targets/config/validation.yml @@ -15,3 +15,10 @@ - bad_input is failed - '"Invalid value " in bad_input.msg' - '"valid values are:" in bad_input.msg' + + - name: test config option casting + set_fact: + direct: "{{ lookup('casting', 1, test_list=[1,2,3], test_int=1, test_bool=True, test_str='lola') }}" + from_strings: "{{ lookup('casting', 1, test_list='1,2,3', test_int='1', test_bool='true', test_str='lola') }}" + direct_individual: "{{ lookup('casting_individual', 1, test_list='1,2,3', test_int='1', test_bool='true', test_str='lola') }}" + from_strings_individual: "{{ lookup('casting_individual', 1, test_list='1,2,3', test_int='1', test_bool='true', test_str='lola') }}" diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index 13f127a8ccf..c4339bcdd54 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -103,6 +103,8 @@ test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/un test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/vendored_pty.py pep8!skip # vendored code test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/my_module.py pylint:relative-beyond-top-level test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util2.py pylint:relative-beyond-top-level +test/integration/targets/config/lookup_plugins/casting.py pylint:unidiomatic-typecheck +test/integration/targets/config/lookup_plugins/casting_individual.py pylint:unidiomatic-typecheck test/integration/targets/fork_safe_stdio/vendored_pty.py pep8!skip # vendored code test/integration/targets/gathering_facts/library/bogus_facts shebang test/integration/targets/gathering_facts/library/dummy1 shebang