New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow subspec defaults to be processed when the parent argument is not supplied #38967
Conversation
b9d291f
to
0364849
Compare
A quick look indicates that some of the workarounds may not be fully compatible with this fix. |
If the parent value is not given in that case child values shouldn't be initialized to default (at least that's how network data model works). In this particular case if the subspec has an option with required=True and if parent value is not given as input this change will initialize child option value to default and will result in required condition failure. This might break the existing modules. |
The way I always thought of the subspec defaults and requires as working is that they would only be applicable when someone specified the top-level arg. As such, I think that would break the modules I maintain. It would certainly change the behavior of the tasks that use this method. This would be a problem and would likely result in me continuing to pull more and more functionality into the module execution code because I continually cant rely on argument spec functionality |
In other words, I parrot @ganeshrn's remarks |
I do understand that this will require changes to existing modules. And I understand you have concerns. However... Let's say that the top level option is As it is, there is no way to just rely on the defaults. As a result, this requires authors to do exactly what you said you would do. Pull more functionality into the module, because this requires workarounds or duplication of data to allow defaults to actually work. I sincerely believe the way that subspec works currently is broken, as evidenced by all the work arounds. That being said, I want to understand the concerns more. Words are unclear. I'd be interested to see specifics where you believe this would cause issues. There may be ways to extend this to meet the needs. |
An example of a workaround that was submitted to get around this issue #31774 |
And maybe a solution is to make this configurable, if the problem will require too much up front work. |
So the current functionality some modules rely on is ability to tell if parent arg is completely omitted, by it being set to None? |
The way I would read your example is "two" is populated if and only if "one" is specified.
Would cause "two"'s default to be set
Would cause "two" to be None The places where I use this are cases where a value may be required for creation, but not for update or delete. Say, a parameters called is_remote that we're considering adding to the bigip_gtm_pool module. That module has a The is_remote is required due to a inconsistency in the F5 product. With this change, whether you specified any virtual_server values or server values, there would always be an is_remote value in that dict. So we could not easily check if "no members" we're specified like we do now (checking for None) Today, we only find the Ex.
Didn't specify members, don't need to worry about processing members
Gets the default of is_remote because members we're specified. This "fix" would change that. So we would end up removing our usage of argument spec; making argument spec, and the docs, even more useless to us, and bake that logic into the module classes themselves. |
@cben, the default is already none, and Ansible has linting logic that rejects modules when you specify a none default |
@caphrim007 I am seeing that there are 2 sets of functionality here that could be useful here. In my specific example, I would expect
and
But based on your need, it sounds like, there may be cases where this is unwanted. I'm thinking of adding an Thoughts? |
I just pushed a change with |
@sivel my first inclination when I was CC'd on this was that both parties might be satisfied with a new reserved arg. Since you mention it, yes, I think that would be workable |
@sivel +1 for |
e4c06db
to
692fcc6
Compare
@cben I just pushed a change to switch Thanks! |
The test
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+100 for added documentation alone!
manageiq modules changes LGTM.
Some minor suggestions to the logic, driven by my fear of _handle_options
😉
@@ -412,6 +412,19 @@ def test_fallback_in_option(self, mocker, stdin, options_argspec_dict): | |||
assert isinstance(am.params['foobar']['baz'], str) | |||
assert am.params['foobar']['baz'] == 'test data' | |||
|
|||
@pytest.mark.parametrize('stdin,spec,expected', [ | |||
({}, | |||
{'one': {'type': 'dict', 'apply_defaults': True, 'options': {'two': {'default': True, 'type': 'bool'}}}}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cosmetic: if you move apply_defaults
to the end, the difference from the next input will become immediately obvious :)
if params.get(k) is None: | ||
params[k] = {} | ||
else: | ||
continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can extract if spec is None: continue
to be first, above the apply_defaults check, and the rest would be simpler.
This branch is untested, consider adding a test case with apply_defaults
but no options
.
After that, I think lifting if params.get(k) is None
to be the outer check might (?) make things even clearer:
spec = v.get('options', None)
if spec is None:
continue
if params.get(k) is None:
if v.get('apply_defaults', False):
params[k] = {} # allows individual option defaults to apply
else:
continue
(untested so likely buggy. and "clearer" is of course subjective, your call...)
|
||
``apply_defaults`` works alongside ``options`` and allows the ``default`` of the sub-options to be applied even when the top-level argument is not supplied. | ||
|
||
In the example of the ``argument_spec`` at the top of this section, it would allow ``module.params['top_level']['second_level']`` to be defined, even if the user does not provide ``top_level`` when calling the module. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: Does default
work together with options
, for cases where top-level arg is not supplied?
Is apply_defaults
simply shorthand for a default
of corresponding dict (in your example giving top_level a default=dict(second_level=True)
)?
If that is true, perhaps implementation doesn't need to complicate _handle_options
further, could derive an "effective default" without even looking at given params.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: Does default work together with options, for cases where top-level arg is not supplied?
yes
Is apply_defaults simply shorthand for a default of corresponding dict (in your example giving top_level a default=dict(second_level=True))?
Yes, effectively the goal here, is that a user should not have to duplicate default options from the suboptions, to allow this functionality to work.
A future linting changing will ensure a module does not supply both apply_defaults
and default
together on the same argument.
…t supplied (ansible#38967) * Allow subspec defaults to be processed when the parent argument is not supplied * Allow this to be configurable via apply_defaults on the parent * Document attributes of arguments in argument_spec * Switch manageiq_connection to use apply_defaults * add choices to api_version in argument_spec
…t supplied (ansible#38967) * Allow subspec defaults to be processed when the parent argument is not supplied * Allow this to be configurable via apply_defaults on the parent * Document attributes of arguments in argument_spec * Switch manageiq_connection to use apply_defaults * add choices to api_version in argument_spec
…t supplied (ansible#38967) * Allow subspec defaults to be processed when the parent argument is not supplied * Allow this to be configurable via apply_defaults on the parent * Document attributes of arguments in argument_spec * Switch manageiq_connection to use apply_defaults * add choices to api_version in argument_spec
…t supplied (ansible#38967) * Allow subspec defaults to be processed when the parent argument is not supplied * Allow this to be configurable via apply_defaults on the parent * Document attributes of arguments in argument_spec * Switch manageiq_connection to use apply_defaults * add choices to api_version in argument_spec
…t supplied (ansible#38967) * Allow subspec defaults to be processed when the parent argument is not supplied * Allow this to be configurable via apply_defaults on the parent * Document attributes of arguments in argument_spec * Switch manageiq_connection to use apply_defaults * add choices to api_version in argument_spec
…t supplied (ansible#38967) * Allow subspec defaults to be processed when the parent argument is not supplied * Allow this to be configurable via apply_defaults on the parent * Document attributes of arguments in argument_spec * Switch manageiq_connection to use apply_defaults * add choices to api_version in argument_spec
…t supplied (ansible#38967) * Allow subspec defaults to be processed when the parent argument is not supplied * Allow this to be configurable via apply_defaults on the parent * Document attributes of arguments in argument_spec * Switch manageiq_connection to use apply_defaults * add choices to api_version in argument_spec
SUMMARY
There has been un unnoticed or unreported bug in handling defaults within a subspec when the parent argument is not provided.
An example module:
When called like:
It would result in
module.params['one'] == None
which is not expected.Many modules have worked around this in various ways, often needing to duplicate the defaults.
This PR addresses this issue, and ensures that
defaults
fromoptions
are processed when the top level argument is omitted.This now would produce:
ISSUE TYPE
COMPONENT NAME
lib/ansible/module_utils/basic.py
ANSIBLE VERSION
ADDITIONAL INFORMATION
cc @gundalow @Qalthos @rcarrillocruz @caphrim007
You have been CCed, because you are responsible for modules, particularly network modules that utilize the
provider
argument which may be affected by this change. Please test and let me know if this is problematic.