Skip to content
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

Merged
merged 5 commits into from May 7, 2018

Conversation

sivel
Copy link
Member

@sivel sivel commented Apr 18, 2018

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:

    module = AnsibleModule(argument_spec={
        'one': {
            'type': 'dict',
            'options': {
                'two': {
                    'default': True,
                    'type': 'bool',
                }
            }
        }
    })

When called like:

- suboptions_test:

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 from options are processed when the top level argument is omitted.

This now would produce:

module.params['one']['two'] == True
ISSUE TYPE
  • Bugfix Pull Request
COMPONENT NAME

lib/ansible/module_utils/basic.py

ANSIBLE VERSION
devel
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.

@ansibot ansibot added bug This issue/PR relates to a bug. needs_triage Needs a first human triage before being processed. support:core This issue/PR relates to code supported by the Ansible Engineering Team. test This PR relates to tests. labels Apr 18, 2018
@sivel sivel removed the needs_triage Needs a first human triage before being processed. label Apr 18, 2018
@sivel sivel force-pushed the arg_spec-subspec-defaults branch from b9d291f to 0364849 Compare April 18, 2018 18:22
@sivel
Copy link
Member Author

sivel commented Apr 18, 2018

A quick look indicates that some of the workarounds may not be fully compatible with this fix.

@ansibot ansibot added the needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR. label Apr 18, 2018
@ganeshrn
Copy link
Member

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.

@caphrim007
Copy link
Contributor

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

@caphrim007
Copy link
Contributor

In other words, I parrot @ganeshrn's remarks

@sivel
Copy link
Member Author

sivel commented Apr 18, 2018

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 required=False (in my example this is one). And all of the sub options have defaults and are also required=False (example is two). Regardless of the specifics of provider, isn't it unexpected that you cannot just rely on the defaults? As it is one will be None, but to match how default works, I would expect one to have a value of {'two': True}.

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.

@sivel
Copy link
Member Author

sivel commented Apr 18, 2018

An example of a workaround that was submitted to get around this issue #31774

@sivel
Copy link
Member Author

sivel commented Apr 18, 2018

And maybe a solution is to make this configurable, if the problem will require too much up front work.

@cben
Copy link
Contributor

cben commented Apr 18, 2018

So the current functionality some modules rely on is ability to tell if parent arg is completely omitted, by it being set to None?
May I suggest then they would opt-in to this by explicitly setting default=None on the parent param?
IOW, omitting parent arg would result in parent's default if set, or a dict with child defaults otherwise.

@caphrim007
Copy link
Contributor

The way I would read your example is

"two" is populated if and only if "one" is specified.

module:
  one:

Would cause "two"'s default to be set

module:

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 members param. It is a suboption that has additional params server and virtual_server.

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 members param (and everything under it relevant) if it itself is specified.

Ex.

bigip_gtm_pool:
  members:

Or

bigip_gtm_pool:

Didn't specify members, don't need to worry about processing members

bigip_gtm_pool:
  members:
    - server: foo
      virtual_server: bar

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.

@caphrim007
Copy link
Contributor

@cben, the default is already none, and Ansible has linting logic that rejects modules when you specify a none default

@sivel
Copy link
Member Author

sivel commented Apr 18, 2018

@caphrim007 I am seeing that there are 2 sets of functionality here that could be useful here. In my specific example, I would expect two to be set in both:

module:

and

module:
  one:

But based on your need, it sounds like, there may be cases where this is unwanted.

I'm thinking of adding an apply_defaults option (defaulted to False) to the parent, that allows the behavior I describe here. Which would give users a way to use this API without workarounds, when this functionality is desired.

Thoughts?

@sivel
Copy link
Member Author

sivel commented Apr 18, 2018

I just pushed a change with apply_defaults

@caphrim007
Copy link
Contributor

@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

@ganeshrn
Copy link
Member

@sivel +1 for apply_defaults

@ansibot
Copy link
Contributor

ansibot commented Apr 19, 2018

@ansibot ansibot added the docs This issue/PR relates to or includes documentation. label Apr 19, 2018
@sivel sivel force-pushed the arg_spec-subspec-defaults branch from e4c06db to 692fcc6 Compare April 19, 2018 22:03
@sivel
Copy link
Member Author

sivel commented Apr 20, 2018

@cben I just pushed a change to switch manageiq_connection over to use apply_defaults. Just wanted your confirmation on this change.

Thanks!

@ansibot
Copy link
Contributor

ansibot commented Apr 20, 2018

@ansibot
Copy link
Contributor

ansibot commented Apr 20, 2018

The test ansible-test sanity --test validate-modules [explain] failed with 1 error:

lib/ansible/modules/remote_management/manageiq/manageiq_provider.py:0:0: E326 Value for "choices" from the argument_spec ([]) for "api_version" does not match the documentation (['v2', 'v3'])

click here for bot help

@ansibot
Copy link
Contributor

ansibot commented Apr 20, 2018

cc @dkorn
click here for bot help

@ansibot ansibot added module This issue/PR relates to a module. support:community This issue/PR relates to code supported by the Ansible community. core_review In order to be merged, this PR must follow the core review workflow. and removed needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR. labels Apr 20, 2018
Copy link
Contributor

@cben cben left a 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'}}}},
Copy link
Contributor

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
Copy link
Contributor

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.
Copy link
Contributor

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.

Copy link
Member Author

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.

@sivel sivel merged commit 1663b64 into ansible:devel May 7, 2018
oolongbrothers pushed a commit to oolongbrothers/ansible that referenced this pull request May 12, 2018
…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
oolongbrothers pushed a commit to oolongbrothers/ansible that referenced this pull request May 14, 2018
…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
oolongbrothers pushed a commit to oolongbrothers/ansible that referenced this pull request May 14, 2018
…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
tonal pushed a commit to tonal/ansible that referenced this pull request May 15, 2018
…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
oolongbrothers pushed a commit to oolongbrothers/ansible that referenced this pull request May 15, 2018
…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
oolongbrothers pushed a commit to oolongbrothers/ansible that referenced this pull request May 15, 2018
…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
ilicmilan pushed a commit to ilicmilan/ansible that referenced this pull request Nov 7, 2018
…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
@ansible ansible locked and limited conversation to collaborators May 7, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug This issue/PR relates to a bug. core_review In order to be merged, this PR must follow the core review workflow. docs This issue/PR relates to or includes documentation. module This issue/PR relates to a module. support:community This issue/PR relates to code supported by the Ansible community. support:core This issue/PR relates to code supported by the Ansible Engineering Team. test This PR relates to tests.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants