Skip to content

Commit

Permalink
Allow subspec defaults to be processed when the parent argument is no…
Browse files Browse the repository at this point in the history
…t supplied (#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
  • Loading branch information
sivel committed May 7, 2018
1 parent 108eac9 commit 1663b64
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 10 deletions.
108 changes: 108 additions & 0 deletions docs/docsite/rst/dev_guide/developing_program_flow_modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,111 @@ Passing arguments via stdin was chosen for the following reasons:
systems limit the total size of the environment. This could lead to
truncation of the parameters if we hit that limit.


.. _ansiblemodule:

AnsibleModule
-------------

.. _argument_spec:

Argument Spec
^^^^^^^^^^^^^

The ``argument_spec`` provided to ``AnsibleModule`` defines the supported arguments for a module, as well as their type, defaults and more.

Example ``argument_spec``:

.. code-block:: python
module = AnsibleModule(argument_spec=dict(
top_level=dict(
type='dict',
options=dict(
second_level=dict(
default=True,
type='bool',
)
)
)
))
This section will discss the behavioral attributes for arguments

type
~~~~

``type`` allows you to define the type of the value accepted for the argument. The default value for ``type`` is ``str``. Possible values are:

* str
* list
* dict
* bool
* int
* float
* path
* raw
* jsonarg
* json
* bytes
* bits

The ``raw`` type, performs no type validation or type casing, and maintains the type of the passed value.

elements
~~~~~~~~

``elements`` works in combination with ``type`` when ``type='list'``. ``elements`` can then be defined as ``elements='int'`` or any other type, indicating that each element of the specified list should be of that type.

default
~~~~~~~

The ``default`` option allows sets a default value for the argument for the scenario when the argument is not provided to the module. When not specified, the default value is ``None``.

fallback
~~~~~~~~

``fallback`` accepts a ``tuple`` where the first argument is a callable (function) that will be used to perform the lookup, based on the second argument. The second argument is a list of values to be accepted by the callable.

The most common callable used is ``env_fallback`` which will allow an argument to optionally use an environment variable when the argument is not supplied.

Example::

username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME']))

choices
~~~~~~~

``choices`` accepts a list of choices that the argument will accept. The types of ``choices`` should match the ``type``.

required
~~~~~~~~

``required`` accepts a boolean, either ``True`` or ``False`` that indicates that the argument is required. This should not be used in combination with ``default``.

no_log
~~~~~~

``no_log`` indicates that the value of the argument should not be logged or displayed.

aliases
~~~~~~~

``aliases`` accepts a list of alternative argument names for the argument, such as the case where the argument is ``name`` but the module accepts ``aliases=['pkg']`` to allow ``pkg`` to be interchangably with ``name``

options
~~~~~~~

``options`` implements the ability to create a sub-argument_spec, where the sub options of the top level argument are also validated using the attributes discussed in this section. The example at the top of this section demonstrates use of ``options``. ``type`` or ``elements`` should be ``dict`` is this case.

apply_defaults
~~~~~~~~~~~~~~

``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.

removed_in_version
~~~~~~~~~~~~~~~~~~

``removed_in_version`` indicates which version of Ansible a deprecated argument will be removed in.
8 changes: 7 additions & 1 deletion lib/ansible/module_utils/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1975,7 +1975,13 @@ def _handle_options(self, argument_spec=None, params=None):
wanted = v.get('type', None)
if wanted == 'dict' or (wanted == 'list' and v.get('elements', '') == 'dict'):
spec = v.get('options', None)
if spec is None or k not in params or params[k] is None:
if v.get('apply_defaults', False):
if spec is not None:
if params.get(k) is None:
params[k] = {}
else:
continue
elif spec is None or k not in params or params[k] is None:
continue

self._options_context.append(k)
Expand Down
2 changes: 1 addition & 1 deletion lib/ansible/module_utils/manageiq.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def manageiq_argument_spec():

return dict(
manageiq_connection=dict(type='dict',
default=dict(verify_ssl=True),
apply_defaults=True,
options=options),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ def main():
project=dict(),
azure_tenant_id=dict(aliases=['keystone_v3_domain_id']),
tenant_mapping_enabled=dict(default=False, type='bool'),
api_version=dict(),
api_version=dict(choices=['v2', 'v3']),
type=dict(choices=supported_providers().keys()),
)
# add the manageiq connection arguments to the arguments
Expand Down
7 changes: 0 additions & 7 deletions test/sanity/validate-modules/ignore.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1338,13 +1338,6 @@ lib/ansible/modules/remote_management/hpilo/hpilo_boot.py E324
lib/ansible/modules/remote_management/hpilo/hpilo_boot.py E326
lib/ansible/modules/remote_management/ipmi/ipmi_boot.py E326
lib/ansible/modules/remote_management/ipmi/ipmi_power.py E326
lib/ansible/modules/remote_management/manageiq/manageiq_alert_profiles.py E324
lib/ansible/modules/remote_management/manageiq/manageiq_alerts.py E324
lib/ansible/modules/remote_management/manageiq/manageiq_policies.py E324
lib/ansible/modules/remote_management/manageiq/manageiq_provider.py E324
lib/ansible/modules/remote_management/manageiq/manageiq_provider.py E326
lib/ansible/modules/remote_management/manageiq/manageiq_tags.py E324
lib/ansible/modules/remote_management/manageiq/manageiq_user.py E324
lib/ansible/modules/remote_management/oneview/oneview_datacenter_facts.py E322
lib/ansible/modules/remote_management/oneview/oneview_enclosure_facts.py E322
lib/ansible/modules/remote_management/oneview/oneview_ethernet_network.py E322
Expand Down
13 changes: 13 additions & 0 deletions test/units/module_utils/basic/test_argument_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'}}}},
{'two': True}),
({},
{'one': {'type': 'dict', 'options': {'two': {'default': True, 'type': 'bool'}}}},
None),
], indirect=['stdin'])
def test_subspec_not_required_defaults(self, stdin, spec, expected):
# Check that top level not required, processed subspec defaults
am = basic.AnsibleModule(spec)
assert am.params['one'] == expected


class TestLoadFileCommonArguments:
@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
Expand Down

0 comments on commit 1663b64

Please sign in to comment.