2017-12-18 19:17:13 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
|
|
|
# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
|
|
|
|
# Copyright: Ansible Project
|
2017-07-27 01:12:50 +02:00
|
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
|
2017-12-18 19:17:13 +01:00
|
|
|
from __future__ import absolute_import, division, print_function
|
2017-07-27 01:12:50 +02:00
|
|
|
__metaclass__ = type
|
|
|
|
|
|
|
|
import json
|
2017-12-18 19:17:13 +01:00
|
|
|
import os
|
2017-07-27 01:12:50 +02:00
|
|
|
|
2017-12-05 21:43:13 +01:00
|
|
|
import pytest
|
|
|
|
|
2018-10-13 05:01:14 +02:00
|
|
|
from units.compat.mock import MagicMock, patch
|
2017-07-27 01:12:50 +02:00
|
|
|
from ansible.module_utils import basic
|
2017-12-18 19:17:13 +01:00
|
|
|
from ansible.module_utils.six import string_types
|
|
|
|
from ansible.module_utils.six.moves import builtins
|
|
|
|
|
|
|
|
from units.mock.procenv import ModuleTestCase, swap_stdin_and_argv
|
2017-07-27 01:12:50 +02:00
|
|
|
|
2017-12-05 21:43:13 +01:00
|
|
|
MOCK_VALIDATOR_FAIL = MagicMock(side_effect=TypeError("bad conversion"))
|
|
|
|
# Data is argspec, argument, expected
|
|
|
|
VALID_SPECS = (
|
2017-12-18 19:17:13 +01:00
|
|
|
# Simple type=int
|
|
|
|
({'arg': {'type': 'int'}}, {'arg': 42}, 42),
|
2019-02-19 13:05:20 +01:00
|
|
|
# Simple type=list, elements=int
|
|
|
|
({'arg': {'type': 'list', 'elements': 'int'}}, {'arg': [42, 32]}, [42, 32]),
|
2017-12-18 19:17:13 +01:00
|
|
|
# Type=int with conversion from string
|
|
|
|
({'arg': {'type': 'int'}}, {'arg': '42'}, 42),
|
2019-02-19 13:05:20 +01:00
|
|
|
# Type=list elements=int with conversion from string
|
|
|
|
({'arg': {'type': 'list', 'elements': 'int'}}, {'arg': ['42', '32']}, [42, 32]),
|
2017-12-18 19:17:13 +01:00
|
|
|
# Simple type=float
|
|
|
|
({'arg': {'type': 'float'}}, {'arg': 42.0}, 42.0),
|
2019-02-19 13:05:20 +01:00
|
|
|
# Simple type=list, elements=float
|
|
|
|
({'arg': {'type': 'list', 'elements': 'float'}}, {'arg': [42.1, 32.2]}, [42.1, 32.2]),
|
2017-12-18 19:17:13 +01:00
|
|
|
# Type=float conversion from int
|
|
|
|
({'arg': {'type': 'float'}}, {'arg': 42}, 42.0),
|
2019-02-19 13:05:20 +01:00
|
|
|
# type=list, elements=float conversion from int
|
|
|
|
({'arg': {'type': 'list', 'elements': 'float'}}, {'arg': [42, 32]}, [42.0, 32.0]),
|
2017-12-18 19:17:13 +01:00
|
|
|
# Type=float conversion from string
|
|
|
|
({'arg': {'type': 'float'}}, {'arg': '42.0'}, 42.0),
|
2019-02-19 13:05:20 +01:00
|
|
|
# type=list, elements=float conversion from string
|
|
|
|
({'arg': {'type': 'list', 'elements': 'float'}}, {'arg': ['42.1', '32.2']}, [42.1, 32.2]),
|
2017-12-18 19:17:13 +01:00
|
|
|
# Type=float conversion from string without decimal point
|
|
|
|
({'arg': {'type': 'float'}}, {'arg': '42'}, 42.0),
|
2019-02-19 13:05:20 +01:00
|
|
|
# Type=list elements=float conversion from string without decimal point
|
|
|
|
({'arg': {'type': 'list', 'elements': 'float'}}, {'arg': ['42', '32.2']}, [42.0, 32.2]),
|
2017-12-18 19:17:13 +01:00
|
|
|
# Simple type=bool
|
|
|
|
({'arg': {'type': 'bool'}}, {'arg': True}, True),
|
2019-02-19 13:05:20 +01:00
|
|
|
# Simple type=list elements=bool
|
|
|
|
({'arg': {'type': 'list', 'elements': 'bool'}}, {'arg': [True, 'true', 1, 'yes', False, 'false', 'no', 0]},
|
|
|
|
[True, True, True, True, False, False, False, False]),
|
2017-12-18 19:17:13 +01:00
|
|
|
# Type=int with conversion from string
|
|
|
|
({'arg': {'type': 'bool'}}, {'arg': 'yes'}, True),
|
|
|
|
# Type=str converts to string
|
|
|
|
({'arg': {'type': 'str'}}, {'arg': 42}, '42'),
|
2019-02-19 13:05:20 +01:00
|
|
|
# Type=list elements=str simple converts to string
|
|
|
|
({'arg': {'type': 'list', 'elements': 'str'}}, {'arg': ['42', '32']}, ['42', '32']),
|
2017-12-18 19:17:13 +01:00
|
|
|
# Type is implicit, converts to string
|
|
|
|
({'arg': {'type': 'str'}}, {'arg': 42}, '42'),
|
2019-02-19 13:05:20 +01:00
|
|
|
# Type=list elements=str implicit converts to string
|
|
|
|
({'arg': {'type': 'list', 'elements': 'str'}}, {'arg': [42, 32]}, ['42', '32']),
|
2017-12-18 19:17:13 +01:00
|
|
|
# parameter is required
|
|
|
|
({'arg': {'required': True}}, {'arg': 42}, '42'),
|
2017-12-05 21:43:13 +01:00
|
|
|
)
|
2017-07-27 01:12:50 +02:00
|
|
|
|
2017-12-05 21:43:13 +01:00
|
|
|
INVALID_SPECS = (
|
2017-12-18 19:17:13 +01:00
|
|
|
# Type is int; unable to convert this string
|
|
|
|
({'arg': {'type': 'int'}}, {'arg': "bad"}, "invalid literal for int() with base 10: 'bad'"),
|
2019-02-19 13:05:20 +01:00
|
|
|
# Type is list elements is int; unable to convert this string
|
|
|
|
({'arg': {'type': 'list', 'elements': 'int'}}, {'arg': [1, "bad"]}, "invalid literal for int() with base 10: 'bad'"),
|
2017-12-18 19:17:13 +01:00
|
|
|
# Type is int; unable to convert float
|
|
|
|
({'arg': {'type': 'int'}}, {'arg': 42.1}, "'float'> cannot be converted to an int"),
|
2019-02-19 13:05:20 +01:00
|
|
|
# Type is list, elements is int; unable to convert float
|
|
|
|
({'arg': {'type': 'list', 'elements': 'int'}}, {'arg': [42.1, 32, 2]}, "'float'> cannot be converted to an int"),
|
2017-12-18 19:17:13 +01:00
|
|
|
# type is a callable that fails to convert
|
2017-12-05 21:43:13 +01:00
|
|
|
({'arg': {'type': MOCK_VALIDATOR_FAIL}}, {'arg': "bad"}, "bad conversion"),
|
2019-02-19 13:05:20 +01:00
|
|
|
# type is a list, elements is callable that fails to convert
|
|
|
|
({'arg': {'type': 'list', 'elements': MOCK_VALIDATOR_FAIL}}, {'arg': [1, "bad"]}, "bad conversion"),
|
2017-12-18 19:17:13 +01:00
|
|
|
# unknown parameter
|
|
|
|
({'arg': {'type': 'int'}}, {'other': 'bad', '_ansible_module_name': 'ansible_unittest'},
|
|
|
|
'Unsupported parameters for (ansible_unittest) module: other Supported parameters include: arg'),
|
|
|
|
# parameter is required
|
|
|
|
({'arg': {'required': True}}, {}, 'missing required arguments: arg'),
|
2017-12-05 21:43:13 +01:00
|
|
|
)
|
2017-07-27 01:12:50 +02:00
|
|
|
|
|
|
|
|
2017-12-18 19:17:13 +01:00
|
|
|
@pytest.fixture
|
|
|
|
def complex_argspec():
|
|
|
|
arg_spec = dict(
|
|
|
|
foo=dict(required=True, aliases=['dup']),
|
|
|
|
bar=dict(),
|
|
|
|
bam=dict(),
|
|
|
|
baz=dict(fallback=(basic.env_fallback, ['BAZ'])),
|
|
|
|
bar1=dict(type='bool'),
|
2019-02-19 13:05:20 +01:00
|
|
|
bar3=dict(type='list', elements='path'),
|
2017-12-18 19:17:13 +01:00
|
|
|
zardoz=dict(choices=['one', 'two']),
|
2018-02-08 07:59:21 +01:00
|
|
|
zardoz2=dict(type='list', choices=['one', 'two', 'three']),
|
2017-12-18 19:17:13 +01:00
|
|
|
)
|
|
|
|
mut_ex = (('bar', 'bam'),)
|
|
|
|
req_to = (('bam', 'baz'),)
|
|
|
|
|
|
|
|
kwargs = dict(
|
|
|
|
argument_spec=arg_spec,
|
|
|
|
mutually_exclusive=mut_ex,
|
|
|
|
required_together=req_to,
|
|
|
|
no_log=True,
|
|
|
|
add_file_common_args=True,
|
|
|
|
supports_check_mode=True,
|
|
|
|
)
|
|
|
|
return kwargs
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def options_argspec_list():
|
|
|
|
options_spec = dict(
|
|
|
|
foo=dict(required=True, aliases=['dup']),
|
|
|
|
bar=dict(),
|
2019-02-19 13:05:20 +01:00
|
|
|
bar1=dict(type='list', elements='str'),
|
|
|
|
bar2=dict(type='list', elements='int'),
|
|
|
|
bar3=dict(type='list', elements='float'),
|
|
|
|
bar4=dict(type='list', elements='path'),
|
2017-12-18 19:17:13 +01:00
|
|
|
bam=dict(),
|
|
|
|
baz=dict(fallback=(basic.env_fallback, ['BAZ'])),
|
|
|
|
bam1=dict(),
|
|
|
|
bam2=dict(default='test'),
|
|
|
|
bam3=dict(type='bool'),
|
Introduce new 'required_by' argument_spec option (#28662)
* Introduce new "required_by' argument_spec option
This PR introduces a new **required_by** argument_spec option which allows you to say *"if parameter A is set, parameter B and C are required as well"*.
- The difference with **required_if** is that it can only add dependencies if a parameter is set to a specific value, not when it is just defined.
- The difference with **required_together** is that it has a commutative property, so: *"Parameter A and B are required together, if one of them has been defined"*.
As an example, we need this for the complex options that the xml module provides. One of the issues we often see is that users are not using the correct combination of options, and then are surprised that the module does not perform the requested action(s).
This would be solved by adding the correct dependencies, and mutual exclusives. For us this is important to get this shipped together with the new xml module in Ansible v2.4. (This is related to bugfix https://github.com/ansible/ansible/pull/28657)
```python
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', aliases=['dest', 'file']),
xmlstring=dict(type='str'),
xpath=dict(type='str'),
namespaces=dict(type='dict', default={}),
state=dict(type='str', default='present', choices=['absent',
'present'], aliases=['ensure']),
value=dict(type='raw'),
attribute=dict(type='raw'),
add_children=dict(type='list'),
set_children=dict(type='list'),
count=dict(type='bool', default=False),
print_match=dict(type='bool', default=False),
pretty_print=dict(type='bool', default=False),
content=dict(type='str', choices=['attribute', 'text']),
input_type=dict(type='str', default='yaml', choices=['xml',
'yaml']),
backup=dict(type='bool', default=False),
),
supports_check_mode=True,
required_by=dict(
add_children=['xpath'],
attribute=['value', 'xpath'],
content=['xpath'],
set_children=['xpath'],
value=['xpath'],
),
required_if=[
['count', True, ['xpath']],
['print_match', True, ['xpath']],
],
required_one_of=[
['path', 'xmlstring'],
['add_children', 'content', 'count', 'pretty_print', 'print_match', 'set_children', 'value'],
],
mutually_exclusive=[
['add_children', 'content', 'count', 'print_match','set_children', 'value'],
['path', 'xmlstring'],
],
)
```
* Rebase and fix conflict
* Add modules that use required_by functionality
* Update required_by schema
* Fix rebase issue
2019-02-15 01:57:45 +01:00
|
|
|
bam4=dict(type='str'),
|
2017-12-18 19:17:13 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
arg_spec = dict(
|
|
|
|
foobar=dict(
|
|
|
|
type='list',
|
|
|
|
elements='dict',
|
|
|
|
options=options_spec,
|
|
|
|
mutually_exclusive=[
|
|
|
|
['bam', 'bam1']
|
|
|
|
],
|
|
|
|
required_if=[
|
|
|
|
['foo', 'hello', ['bam']],
|
|
|
|
['foo', 'bam2', ['bam2']]
|
|
|
|
],
|
|
|
|
required_one_of=[
|
|
|
|
['bar', 'bam']
|
|
|
|
],
|
|
|
|
required_together=[
|
|
|
|
['bam1', 'baz']
|
Introduce new 'required_by' argument_spec option (#28662)
* Introduce new "required_by' argument_spec option
This PR introduces a new **required_by** argument_spec option which allows you to say *"if parameter A is set, parameter B and C are required as well"*.
- The difference with **required_if** is that it can only add dependencies if a parameter is set to a specific value, not when it is just defined.
- The difference with **required_together** is that it has a commutative property, so: *"Parameter A and B are required together, if one of them has been defined"*.
As an example, we need this for the complex options that the xml module provides. One of the issues we often see is that users are not using the correct combination of options, and then are surprised that the module does not perform the requested action(s).
This would be solved by adding the correct dependencies, and mutual exclusives. For us this is important to get this shipped together with the new xml module in Ansible v2.4. (This is related to bugfix https://github.com/ansible/ansible/pull/28657)
```python
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', aliases=['dest', 'file']),
xmlstring=dict(type='str'),
xpath=dict(type='str'),
namespaces=dict(type='dict', default={}),
state=dict(type='str', default='present', choices=['absent',
'present'], aliases=['ensure']),
value=dict(type='raw'),
attribute=dict(type='raw'),
add_children=dict(type='list'),
set_children=dict(type='list'),
count=dict(type='bool', default=False),
print_match=dict(type='bool', default=False),
pretty_print=dict(type='bool', default=False),
content=dict(type='str', choices=['attribute', 'text']),
input_type=dict(type='str', default='yaml', choices=['xml',
'yaml']),
backup=dict(type='bool', default=False),
),
supports_check_mode=True,
required_by=dict(
add_children=['xpath'],
attribute=['value', 'xpath'],
content=['xpath'],
set_children=['xpath'],
value=['xpath'],
),
required_if=[
['count', True, ['xpath']],
['print_match', True, ['xpath']],
],
required_one_of=[
['path', 'xmlstring'],
['add_children', 'content', 'count', 'pretty_print', 'print_match', 'set_children', 'value'],
],
mutually_exclusive=[
['add_children', 'content', 'count', 'print_match','set_children', 'value'],
['path', 'xmlstring'],
],
)
```
* Rebase and fix conflict
* Add modules that use required_by functionality
* Update required_by schema
* Fix rebase issue
2019-02-15 01:57:45 +01:00
|
|
|
],
|
|
|
|
required_by={
|
|
|
|
'bam4': ('bam1', 'bam3'),
|
|
|
|
},
|
2017-12-18 19:17:13 +01:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
kwargs = dict(
|
|
|
|
argument_spec=arg_spec,
|
|
|
|
no_log=True,
|
|
|
|
add_file_common_args=True,
|
|
|
|
supports_check_mode=True
|
|
|
|
)
|
|
|
|
return kwargs
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2018-11-15 02:41:15 +01:00
|
|
|
def options_argspec_dict(options_argspec_list):
|
2017-12-18 19:17:13 +01:00
|
|
|
# should test ok, for options in dict format.
|
2018-11-15 02:41:15 +01:00
|
|
|
kwargs = options_argspec_list
|
2017-12-18 19:17:13 +01:00
|
|
|
kwargs['argument_spec']['foobar']['type'] = 'dict'
|
2019-02-19 13:05:20 +01:00
|
|
|
kwargs['argument_spec']['foobar']['elements'] = None
|
2017-12-18 19:17:13 +01:00
|
|
|
|
|
|
|
return kwargs
|
2017-07-27 01:12:50 +02:00
|
|
|
|
2017-12-18 19:17:13 +01:00
|
|
|
|
|
|
|
#
|
|
|
|
# Tests for one aspect of arg_spec
|
|
|
|
#
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('argspec, expected, stdin', [(s[0], s[2], s[1]) for s in VALID_SPECS],
|
|
|
|
indirect=['stdin'])
|
|
|
|
def test_validator_basic_types(argspec, expected, stdin):
|
|
|
|
|
|
|
|
am = basic.AnsibleModule(argspec)
|
|
|
|
|
|
|
|
if 'type' in argspec['arg']:
|
|
|
|
type_ = getattr(builtins, argspec['arg']['type'])
|
2017-12-05 21:43:13 +01:00
|
|
|
else:
|
2017-12-18 19:17:13 +01:00
|
|
|
type_ = str
|
|
|
|
|
|
|
|
assert isinstance(am.params['arg'], type_)
|
2017-12-05 21:43:13 +01:00
|
|
|
assert am.params['arg'] == expected
|
2017-07-27 01:12:50 +02:00
|
|
|
|
|
|
|
|
2017-12-18 19:17:13 +01:00
|
|
|
@pytest.mark.parametrize('stdin', [{'arg': 42}], indirect=['stdin'])
|
|
|
|
def test_validator_function(mocker, stdin):
|
|
|
|
# Type is a callable
|
|
|
|
MOCK_VALIDATOR_SUCCESS = mocker.MagicMock(return_value=27)
|
|
|
|
argspec = {'arg': {'type': MOCK_VALIDATOR_SUCCESS}}
|
|
|
|
am = basic.AnsibleModule(argspec)
|
|
|
|
|
|
|
|
assert isinstance(am.params['arg'], int)
|
|
|
|
assert am.params['arg'] == 27
|
|
|
|
|
|
|
|
|
2017-12-05 21:43:13 +01:00
|
|
|
@pytest.mark.parametrize('argspec, expected, stdin', [(s[0], s[2], s[1]) for s in INVALID_SPECS],
|
|
|
|
indirect=['stdin'])
|
|
|
|
def test_validator_fail(stdin, capfd, argspec, expected):
|
2017-12-18 19:17:13 +01:00
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
basic.AnsibleModule(argument_spec=argspec)
|
2017-07-27 01:12:50 +02:00
|
|
|
|
2017-12-05 21:43:13 +01:00
|
|
|
out, err = capfd.readouterr()
|
|
|
|
assert not err
|
|
|
|
assert expected in json.loads(out)['msg']
|
|
|
|
assert json.loads(out)['failed']
|
2017-12-18 19:17:13 +01:00
|
|
|
|
|
|
|
|
|
|
|
class TestComplexArgSpecs:
|
|
|
|
"""Test with a more complex arg_spec"""
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('stdin', [{'foo': 'hello'}, {'dup': 'hello'}], indirect=['stdin'])
|
|
|
|
def test_complex_required(self, stdin, complex_argspec):
|
|
|
|
"""Test that the complex argspec works if we give it its required param as either the canonical or aliased name"""
|
|
|
|
am = basic.AnsibleModule(**complex_argspec)
|
|
|
|
assert isinstance(am.params['foo'], str)
|
|
|
|
assert am.params['foo'] == 'hello'
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bam': 'test'}], indirect=['stdin'])
|
|
|
|
def test_complex_type_fallback(self, mocker, stdin, complex_argspec):
|
|
|
|
"""Test that the complex argspec works if we get a required parameter via fallback"""
|
|
|
|
environ = os.environ.copy()
|
|
|
|
environ['BAZ'] = 'test data'
|
|
|
|
mocker.patch('ansible.module_utils.basic.os.environ', environ)
|
|
|
|
|
|
|
|
am = basic.AnsibleModule(**complex_argspec)
|
|
|
|
|
|
|
|
assert isinstance(am.params['baz'], str)
|
|
|
|
assert am.params['baz'] == 'test data'
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bar': 'bad', 'bam': 'bad2'}], indirect=['stdin'])
|
|
|
|
def test_fail_mutually_exclusive(self, capfd, stdin, complex_argspec):
|
|
|
|
"""Fail because of mutually exclusive parameters"""
|
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
am = basic.AnsibleModule(**complex_argspec)
|
|
|
|
|
|
|
|
out, err = capfd.readouterr()
|
|
|
|
results = json.loads(out)
|
|
|
|
|
|
|
|
assert results['failed']
|
|
|
|
assert results['msg'] == "parameters are mutually exclusive: bar, bam"
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bam': 'bad2'}], indirect=['stdin'])
|
|
|
|
def test_fail_required_together(self, capfd, stdin, complex_argspec):
|
|
|
|
"""Fail because only one of a required_together pair of parameters was specified"""
|
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
am = basic.AnsibleModule(**complex_argspec)
|
|
|
|
|
|
|
|
out, err = capfd.readouterr()
|
|
|
|
results = json.loads(out)
|
|
|
|
|
|
|
|
assert results['failed']
|
|
|
|
assert results['msg'] == "parameters are required together: bam, baz"
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bar': 'hi'}], indirect=['stdin'])
|
|
|
|
def test_fail_required_together_and_default(self, capfd, stdin, complex_argspec):
|
|
|
|
"""Fail because one of a required_together pair of parameters has a default and the other was not specified"""
|
|
|
|
complex_argspec['argument_spec']['baz'] = {'default': 42}
|
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
am = basic.AnsibleModule(**complex_argspec)
|
|
|
|
|
|
|
|
out, err = capfd.readouterr()
|
|
|
|
results = json.loads(out)
|
|
|
|
|
|
|
|
assert results['failed']
|
|
|
|
assert results['msg'] == "parameters are required together: bam, baz"
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('stdin', [{'foo': 'hello'}], indirect=['stdin'])
|
|
|
|
def test_fail_required_together_and_fallback(self, capfd, mocker, stdin, complex_argspec):
|
|
|
|
"""Fail because one of a required_together pair of parameters has a fallback and the other was not specified"""
|
|
|
|
environ = os.environ.copy()
|
|
|
|
environ['BAZ'] = 'test data'
|
|
|
|
mocker.patch('ansible.module_utils.basic.os.environ', environ)
|
|
|
|
|
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
am = basic.AnsibleModule(**complex_argspec)
|
|
|
|
|
|
|
|
out, err = capfd.readouterr()
|
|
|
|
results = json.loads(out)
|
|
|
|
|
|
|
|
assert results['failed']
|
|
|
|
assert results['msg'] == "parameters are required together: bam, baz"
|
|
|
|
|
2018-02-08 07:59:21 +01:00
|
|
|
@pytest.mark.parametrize('stdin', [{'foo': 'hello', 'zardoz2': ['one', 'four', 'five']}], indirect=['stdin'])
|
|
|
|
def test_fail_list_with_choices(self, capfd, mocker, stdin, complex_argspec):
|
|
|
|
"""Fail because one of the items is not in the choice"""
|
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
basic.AnsibleModule(**complex_argspec)
|
|
|
|
|
|
|
|
out, err = capfd.readouterr()
|
|
|
|
results = json.loads(out)
|
|
|
|
|
|
|
|
assert results['failed']
|
|
|
|
assert results['msg'] == "value of zardoz2 must be one or more of: one, two, three. Got no match for: four, five"
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('stdin', [{'foo': 'hello', 'zardoz2': ['one', 'three']}], indirect=['stdin'])
|
|
|
|
def test_list_with_choices(self, capfd, mocker, stdin, complex_argspec):
|
|
|
|
"""Test choices with list"""
|
|
|
|
am = basic.AnsibleModule(**complex_argspec)
|
|
|
|
assert isinstance(am.params['zardoz2'], list)
|
|
|
|
assert am.params['zardoz2'] == ['one', 'three']
|
|
|
|
|
2019-02-19 13:05:20 +01:00
|
|
|
@pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bar3': ['~/test', 'test/']}], indirect=['stdin'])
|
|
|
|
def test_list_with_elements_path(self, capfd, mocker, stdin, complex_argspec):
|
|
|
|
"""Test choices with list"""
|
|
|
|
am = basic.AnsibleModule(**complex_argspec)
|
|
|
|
assert isinstance(am.params['bar3'], list)
|
|
|
|
assert am.params['bar3'][0].startswith('/')
|
|
|
|
assert am.params['bar3'][1] == 'test/'
|
|
|
|
|
2017-12-18 19:17:13 +01:00
|
|
|
|
|
|
|
class TestComplexOptions:
|
|
|
|
"""Test arg spec options"""
|
|
|
|
|
Introduce new 'required_by' argument_spec option (#28662)
* Introduce new "required_by' argument_spec option
This PR introduces a new **required_by** argument_spec option which allows you to say *"if parameter A is set, parameter B and C are required as well"*.
- The difference with **required_if** is that it can only add dependencies if a parameter is set to a specific value, not when it is just defined.
- The difference with **required_together** is that it has a commutative property, so: *"Parameter A and B are required together, if one of them has been defined"*.
As an example, we need this for the complex options that the xml module provides. One of the issues we often see is that users are not using the correct combination of options, and then are surprised that the module does not perform the requested action(s).
This would be solved by adding the correct dependencies, and mutual exclusives. For us this is important to get this shipped together with the new xml module in Ansible v2.4. (This is related to bugfix https://github.com/ansible/ansible/pull/28657)
```python
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', aliases=['dest', 'file']),
xmlstring=dict(type='str'),
xpath=dict(type='str'),
namespaces=dict(type='dict', default={}),
state=dict(type='str', default='present', choices=['absent',
'present'], aliases=['ensure']),
value=dict(type='raw'),
attribute=dict(type='raw'),
add_children=dict(type='list'),
set_children=dict(type='list'),
count=dict(type='bool', default=False),
print_match=dict(type='bool', default=False),
pretty_print=dict(type='bool', default=False),
content=dict(type='str', choices=['attribute', 'text']),
input_type=dict(type='str', default='yaml', choices=['xml',
'yaml']),
backup=dict(type='bool', default=False),
),
supports_check_mode=True,
required_by=dict(
add_children=['xpath'],
attribute=['value', 'xpath'],
content=['xpath'],
set_children=['xpath'],
value=['xpath'],
),
required_if=[
['count', True, ['xpath']],
['print_match', True, ['xpath']],
],
required_one_of=[
['path', 'xmlstring'],
['add_children', 'content', 'count', 'pretty_print', 'print_match', 'set_children', 'value'],
],
mutually_exclusive=[
['add_children', 'content', 'count', 'print_match','set_children', 'value'],
['path', 'xmlstring'],
],
)
```
* Rebase and fix conflict
* Add modules that use required_by functionality
* Update required_by schema
* Fix rebase issue
2019-02-15 01:57:45 +01:00
|
|
|
# (Parameters, expected value of module.params['foobar'])
|
2017-12-18 19:17:13 +01:00
|
|
|
OPTIONS_PARAMS_LIST = (
|
|
|
|
({'foobar': [{"foo": "hello", "bam": "good"}, {"foo": "test", "bar": "good"}]},
|
2019-02-19 13:05:20 +01:00
|
|
|
[{'foo': 'hello', 'bam': 'good', 'bam2': 'test', 'bar': None, 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
|
|
|
|
'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None},
|
|
|
|
{'foo': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
|
|
|
|
'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
|
|
|
|
),
|
2017-12-18 19:17:13 +01:00
|
|
|
# Alias for required param
|
|
|
|
({'foobar': [{"dup": "test", "bar": "good"}]},
|
2019-02-19 13:05:20 +01:00
|
|
|
[{'foo': 'test', 'dup': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
|
|
|
|
'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
|
2017-12-18 19:17:13 +01:00
|
|
|
),
|
|
|
|
# Required_if utilizing default value of the requirement
|
|
|
|
({'foobar': [{"foo": "bam2", "bar": "required_one_of"}]},
|
2019-02-19 13:05:20 +01:00
|
|
|
[{'bam': None, 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': 'required_one_of', 'baz': None, 'foo': 'bam2',
|
|
|
|
'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
|
2017-12-18 19:17:13 +01:00
|
|
|
),
|
|
|
|
# Check that a bool option is converted
|
|
|
|
({"foobar": [{"foo": "required", "bam": "good", "bam3": "yes"}]},
|
2019-02-19 13:05:20 +01:00
|
|
|
[{'bam': 'good', 'bam1': None, 'bam2': 'test', 'bam3': True, 'bam4': None, 'bar': None, 'baz': None, 'foo': 'required',
|
|
|
|
'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
|
Introduce new 'required_by' argument_spec option (#28662)
* Introduce new "required_by' argument_spec option
This PR introduces a new **required_by** argument_spec option which allows you to say *"if parameter A is set, parameter B and C are required as well"*.
- The difference with **required_if** is that it can only add dependencies if a parameter is set to a specific value, not when it is just defined.
- The difference with **required_together** is that it has a commutative property, so: *"Parameter A and B are required together, if one of them has been defined"*.
As an example, we need this for the complex options that the xml module provides. One of the issues we often see is that users are not using the correct combination of options, and then are surprised that the module does not perform the requested action(s).
This would be solved by adding the correct dependencies, and mutual exclusives. For us this is important to get this shipped together with the new xml module in Ansible v2.4. (This is related to bugfix https://github.com/ansible/ansible/pull/28657)
```python
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', aliases=['dest', 'file']),
xmlstring=dict(type='str'),
xpath=dict(type='str'),
namespaces=dict(type='dict', default={}),
state=dict(type='str', default='present', choices=['absent',
'present'], aliases=['ensure']),
value=dict(type='raw'),
attribute=dict(type='raw'),
add_children=dict(type='list'),
set_children=dict(type='list'),
count=dict(type='bool', default=False),
print_match=dict(type='bool', default=False),
pretty_print=dict(type='bool', default=False),
content=dict(type='str', choices=['attribute', 'text']),
input_type=dict(type='str', default='yaml', choices=['xml',
'yaml']),
backup=dict(type='bool', default=False),
),
supports_check_mode=True,
required_by=dict(
add_children=['xpath'],
attribute=['value', 'xpath'],
content=['xpath'],
set_children=['xpath'],
value=['xpath'],
),
required_if=[
['count', True, ['xpath']],
['print_match', True, ['xpath']],
],
required_one_of=[
['path', 'xmlstring'],
['add_children', 'content', 'count', 'pretty_print', 'print_match', 'set_children', 'value'],
],
mutually_exclusive=[
['add_children', 'content', 'count', 'print_match','set_children', 'value'],
['path', 'xmlstring'],
],
)
```
* Rebase and fix conflict
* Add modules that use required_by functionality
* Update required_by schema
* Fix rebase issue
2019-02-15 01:57:45 +01:00
|
|
|
),
|
|
|
|
# Check required_by options
|
|
|
|
({"foobar": [{"foo": "required", "bar": "good", "baz": "good", "bam4": "required_by", "bam1": "ok", "bam3": "yes"}]},
|
2019-02-19 13:05:20 +01:00
|
|
|
[{'bar': 'good', 'baz': 'good', 'bam1': 'ok', 'bam2': 'test', 'bam3': True, 'bam4': 'required_by', 'bam': None, 'foo': 'required',
|
|
|
|
'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
|
|
|
|
),
|
|
|
|
# Check for elements in sub-options
|
|
|
|
({"foobar": [{"foo": "good", "bam": "required_one_of", "bar1": [1, "good", "yes"], "bar2": ['1', 1], "bar3":['1.3', 1.3, 1]}]},
|
|
|
|
[{'foo': 'good', 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': None, 'baz': None, 'bam': 'required_one_of',
|
|
|
|
'bar1': ["1", "good", "yes"], 'bar2': [1, 1], 'bar3': [1.3, 1.3, 1.0], 'bar4': None}]
|
2017-12-18 19:17:13 +01:00
|
|
|
),
|
|
|
|
)
|
|
|
|
|
Introduce new 'required_by' argument_spec option (#28662)
* Introduce new "required_by' argument_spec option
This PR introduces a new **required_by** argument_spec option which allows you to say *"if parameter A is set, parameter B and C are required as well"*.
- The difference with **required_if** is that it can only add dependencies if a parameter is set to a specific value, not when it is just defined.
- The difference with **required_together** is that it has a commutative property, so: *"Parameter A and B are required together, if one of them has been defined"*.
As an example, we need this for the complex options that the xml module provides. One of the issues we often see is that users are not using the correct combination of options, and then are surprised that the module does not perform the requested action(s).
This would be solved by adding the correct dependencies, and mutual exclusives. For us this is important to get this shipped together with the new xml module in Ansible v2.4. (This is related to bugfix https://github.com/ansible/ansible/pull/28657)
```python
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', aliases=['dest', 'file']),
xmlstring=dict(type='str'),
xpath=dict(type='str'),
namespaces=dict(type='dict', default={}),
state=dict(type='str', default='present', choices=['absent',
'present'], aliases=['ensure']),
value=dict(type='raw'),
attribute=dict(type='raw'),
add_children=dict(type='list'),
set_children=dict(type='list'),
count=dict(type='bool', default=False),
print_match=dict(type='bool', default=False),
pretty_print=dict(type='bool', default=False),
content=dict(type='str', choices=['attribute', 'text']),
input_type=dict(type='str', default='yaml', choices=['xml',
'yaml']),
backup=dict(type='bool', default=False),
),
supports_check_mode=True,
required_by=dict(
add_children=['xpath'],
attribute=['value', 'xpath'],
content=['xpath'],
set_children=['xpath'],
value=['xpath'],
),
required_if=[
['count', True, ['xpath']],
['print_match', True, ['xpath']],
],
required_one_of=[
['path', 'xmlstring'],
['add_children', 'content', 'count', 'pretty_print', 'print_match', 'set_children', 'value'],
],
mutually_exclusive=[
['add_children', 'content', 'count', 'print_match','set_children', 'value'],
['path', 'xmlstring'],
],
)
```
* Rebase and fix conflict
* Add modules that use required_by functionality
* Update required_by schema
* Fix rebase issue
2019-02-15 01:57:45 +01:00
|
|
|
# (Parameters, expected value of module.params['foobar'])
|
2017-12-18 19:17:13 +01:00
|
|
|
OPTIONS_PARAMS_DICT = (
|
|
|
|
({'foobar': {"foo": "hello", "bam": "good"}},
|
2019-02-19 13:05:20 +01:00
|
|
|
{'foo': 'hello', 'bam': 'good', 'bam2': 'test', 'bar': None, 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
|
|
|
|
'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}
|
2017-12-18 19:17:13 +01:00
|
|
|
),
|
|
|
|
# Alias for required param
|
|
|
|
({'foobar': {"dup": "test", "bar": "good"}},
|
2019-02-19 13:05:20 +01:00
|
|
|
{'foo': 'test', 'dup': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
|
|
|
|
'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}
|
2017-12-18 19:17:13 +01:00
|
|
|
),
|
|
|
|
# Required_if utilizing default value of the requirement
|
|
|
|
({'foobar': {"foo": "bam2", "bar": "required_one_of"}},
|
2019-02-19 13:05:20 +01:00
|
|
|
{'bam': None, 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': 'required_one_of', 'baz': None, 'foo': 'bam2',
|
|
|
|
'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}
|
2017-12-18 19:17:13 +01:00
|
|
|
),
|
|
|
|
# Check that a bool option is converted
|
|
|
|
({"foobar": {"foo": "required", "bam": "good", "bam3": "yes"}},
|
2019-02-19 13:05:20 +01:00
|
|
|
{'bam': 'good', 'bam1': None, 'bam2': 'test', 'bam3': True, 'bam4': None, 'bar': None, 'baz': None, 'foo': 'required',
|
|
|
|
'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}
|
Introduce new 'required_by' argument_spec option (#28662)
* Introduce new "required_by' argument_spec option
This PR introduces a new **required_by** argument_spec option which allows you to say *"if parameter A is set, parameter B and C are required as well"*.
- The difference with **required_if** is that it can only add dependencies if a parameter is set to a specific value, not when it is just defined.
- The difference with **required_together** is that it has a commutative property, so: *"Parameter A and B are required together, if one of them has been defined"*.
As an example, we need this for the complex options that the xml module provides. One of the issues we often see is that users are not using the correct combination of options, and then are surprised that the module does not perform the requested action(s).
This would be solved by adding the correct dependencies, and mutual exclusives. For us this is important to get this shipped together with the new xml module in Ansible v2.4. (This is related to bugfix https://github.com/ansible/ansible/pull/28657)
```python
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', aliases=['dest', 'file']),
xmlstring=dict(type='str'),
xpath=dict(type='str'),
namespaces=dict(type='dict', default={}),
state=dict(type='str', default='present', choices=['absent',
'present'], aliases=['ensure']),
value=dict(type='raw'),
attribute=dict(type='raw'),
add_children=dict(type='list'),
set_children=dict(type='list'),
count=dict(type='bool', default=False),
print_match=dict(type='bool', default=False),
pretty_print=dict(type='bool', default=False),
content=dict(type='str', choices=['attribute', 'text']),
input_type=dict(type='str', default='yaml', choices=['xml',
'yaml']),
backup=dict(type='bool', default=False),
),
supports_check_mode=True,
required_by=dict(
add_children=['xpath'],
attribute=['value', 'xpath'],
content=['xpath'],
set_children=['xpath'],
value=['xpath'],
),
required_if=[
['count', True, ['xpath']],
['print_match', True, ['xpath']],
],
required_one_of=[
['path', 'xmlstring'],
['add_children', 'content', 'count', 'pretty_print', 'print_match', 'set_children', 'value'],
],
mutually_exclusive=[
['add_children', 'content', 'count', 'print_match','set_children', 'value'],
['path', 'xmlstring'],
],
)
```
* Rebase and fix conflict
* Add modules that use required_by functionality
* Update required_by schema
* Fix rebase issue
2019-02-15 01:57:45 +01:00
|
|
|
),
|
|
|
|
# Check required_by options
|
|
|
|
({"foobar": {"foo": "required", "bar": "good", "baz": "good", "bam4": "required_by", "bam1": "ok", "bam3": "yes"}},
|
2019-02-19 13:05:20 +01:00
|
|
|
{'bar': 'good', 'baz': 'good', 'bam1': 'ok', 'bam2': 'test', 'bam3': True, 'bam4': 'required_by', 'bam': None,
|
|
|
|
'foo': 'required', 'bar1': None, 'bar3': None, 'bar2': None, 'bar4': None}
|
|
|
|
),
|
|
|
|
# Check for elements in sub-options
|
|
|
|
({"foobar": {"foo": "good", "bam": "required_one_of", "bar1": [1, "good", "yes"],
|
|
|
|
"bar2": ['1', 1], "bar3": ['1.3', 1.3, 1]}},
|
|
|
|
{'foo': 'good', 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': None,
|
|
|
|
'baz': None, 'bam': 'required_one_of',
|
|
|
|
'bar1': ["1", "good", "yes"], 'bar2': [1, 1], 'bar3': [1.3, 1.3, 1.0], 'bar4': None}
|
2017-12-18 19:17:13 +01:00
|
|
|
),
|
|
|
|
)
|
|
|
|
|
Introduce new 'required_by' argument_spec option (#28662)
* Introduce new "required_by' argument_spec option
This PR introduces a new **required_by** argument_spec option which allows you to say *"if parameter A is set, parameter B and C are required as well"*.
- The difference with **required_if** is that it can only add dependencies if a parameter is set to a specific value, not when it is just defined.
- The difference with **required_together** is that it has a commutative property, so: *"Parameter A and B are required together, if one of them has been defined"*.
As an example, we need this for the complex options that the xml module provides. One of the issues we often see is that users are not using the correct combination of options, and then are surprised that the module does not perform the requested action(s).
This would be solved by adding the correct dependencies, and mutual exclusives. For us this is important to get this shipped together with the new xml module in Ansible v2.4. (This is related to bugfix https://github.com/ansible/ansible/pull/28657)
```python
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', aliases=['dest', 'file']),
xmlstring=dict(type='str'),
xpath=dict(type='str'),
namespaces=dict(type='dict', default={}),
state=dict(type='str', default='present', choices=['absent',
'present'], aliases=['ensure']),
value=dict(type='raw'),
attribute=dict(type='raw'),
add_children=dict(type='list'),
set_children=dict(type='list'),
count=dict(type='bool', default=False),
print_match=dict(type='bool', default=False),
pretty_print=dict(type='bool', default=False),
content=dict(type='str', choices=['attribute', 'text']),
input_type=dict(type='str', default='yaml', choices=['xml',
'yaml']),
backup=dict(type='bool', default=False),
),
supports_check_mode=True,
required_by=dict(
add_children=['xpath'],
attribute=['value', 'xpath'],
content=['xpath'],
set_children=['xpath'],
value=['xpath'],
),
required_if=[
['count', True, ['xpath']],
['print_match', True, ['xpath']],
],
required_one_of=[
['path', 'xmlstring'],
['add_children', 'content', 'count', 'pretty_print', 'print_match', 'set_children', 'value'],
],
mutually_exclusive=[
['add_children', 'content', 'count', 'print_match','set_children', 'value'],
['path', 'xmlstring'],
],
)
```
* Rebase and fix conflict
* Add modules that use required_by functionality
* Update required_by schema
* Fix rebase issue
2019-02-15 01:57:45 +01:00
|
|
|
# (Parameters, failure message)
|
2017-12-18 19:17:13 +01:00
|
|
|
FAILING_PARAMS_LIST = (
|
|
|
|
# Missing required option
|
|
|
|
({'foobar': [{}]}, 'missing required arguments: foo found in foobar'),
|
|
|
|
# Invalid option
|
|
|
|
({'foobar': [{"foo": "hello", "bam": "good", "invalid": "bad"}]}, 'module: invalid found in foobar. Supported parameters include'),
|
|
|
|
# Mutually exclusive options found
|
|
|
|
({'foobar': [{"foo": "test", "bam": "bad", "bam1": "bad", "baz": "req_to"}]},
|
|
|
|
'parameters are mutually exclusive: bam, bam1 found in foobar'),
|
|
|
|
# required_if fails
|
|
|
|
({'foobar': [{"foo": "hello", "bar": "bad"}]},
|
|
|
|
'foo is hello but all of the following are missing: bam found in foobar'),
|
|
|
|
# Missing required_one_of option
|
|
|
|
({'foobar': [{"foo": "test"}]},
|
|
|
|
'one of the following is required: bar, bam found in foobar'),
|
|
|
|
# Missing required_together option
|
|
|
|
({'foobar': [{"foo": "test", "bar": "required_one_of", "bam1": "bad"}]},
|
|
|
|
'parameters are required together: bam1, baz found in foobar'),
|
Introduce new 'required_by' argument_spec option (#28662)
* Introduce new "required_by' argument_spec option
This PR introduces a new **required_by** argument_spec option which allows you to say *"if parameter A is set, parameter B and C are required as well"*.
- The difference with **required_if** is that it can only add dependencies if a parameter is set to a specific value, not when it is just defined.
- The difference with **required_together** is that it has a commutative property, so: *"Parameter A and B are required together, if one of them has been defined"*.
As an example, we need this for the complex options that the xml module provides. One of the issues we often see is that users are not using the correct combination of options, and then are surprised that the module does not perform the requested action(s).
This would be solved by adding the correct dependencies, and mutual exclusives. For us this is important to get this shipped together with the new xml module in Ansible v2.4. (This is related to bugfix https://github.com/ansible/ansible/pull/28657)
```python
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', aliases=['dest', 'file']),
xmlstring=dict(type='str'),
xpath=dict(type='str'),
namespaces=dict(type='dict', default={}),
state=dict(type='str', default='present', choices=['absent',
'present'], aliases=['ensure']),
value=dict(type='raw'),
attribute=dict(type='raw'),
add_children=dict(type='list'),
set_children=dict(type='list'),
count=dict(type='bool', default=False),
print_match=dict(type='bool', default=False),
pretty_print=dict(type='bool', default=False),
content=dict(type='str', choices=['attribute', 'text']),
input_type=dict(type='str', default='yaml', choices=['xml',
'yaml']),
backup=dict(type='bool', default=False),
),
supports_check_mode=True,
required_by=dict(
add_children=['xpath'],
attribute=['value', 'xpath'],
content=['xpath'],
set_children=['xpath'],
value=['xpath'],
),
required_if=[
['count', True, ['xpath']],
['print_match', True, ['xpath']],
],
required_one_of=[
['path', 'xmlstring'],
['add_children', 'content', 'count', 'pretty_print', 'print_match', 'set_children', 'value'],
],
mutually_exclusive=[
['add_children', 'content', 'count', 'print_match','set_children', 'value'],
['path', 'xmlstring'],
],
)
```
* Rebase and fix conflict
* Add modules that use required_by functionality
* Update required_by schema
* Fix rebase issue
2019-02-15 01:57:45 +01:00
|
|
|
# Missing required_by options
|
|
|
|
({'foobar': [{"foo": "test", "bar": "required_one_of", "bam4": "required_by"}]},
|
|
|
|
"missing parameter(s) required by 'bam4': bam1, bam3"),
|
2017-12-18 19:17:13 +01:00
|
|
|
)
|
|
|
|
|
Introduce new 'required_by' argument_spec option (#28662)
* Introduce new "required_by' argument_spec option
This PR introduces a new **required_by** argument_spec option which allows you to say *"if parameter A is set, parameter B and C are required as well"*.
- The difference with **required_if** is that it can only add dependencies if a parameter is set to a specific value, not when it is just defined.
- The difference with **required_together** is that it has a commutative property, so: *"Parameter A and B are required together, if one of them has been defined"*.
As an example, we need this for the complex options that the xml module provides. One of the issues we often see is that users are not using the correct combination of options, and then are surprised that the module does not perform the requested action(s).
This would be solved by adding the correct dependencies, and mutual exclusives. For us this is important to get this shipped together with the new xml module in Ansible v2.4. (This is related to bugfix https://github.com/ansible/ansible/pull/28657)
```python
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', aliases=['dest', 'file']),
xmlstring=dict(type='str'),
xpath=dict(type='str'),
namespaces=dict(type='dict', default={}),
state=dict(type='str', default='present', choices=['absent',
'present'], aliases=['ensure']),
value=dict(type='raw'),
attribute=dict(type='raw'),
add_children=dict(type='list'),
set_children=dict(type='list'),
count=dict(type='bool', default=False),
print_match=dict(type='bool', default=False),
pretty_print=dict(type='bool', default=False),
content=dict(type='str', choices=['attribute', 'text']),
input_type=dict(type='str', default='yaml', choices=['xml',
'yaml']),
backup=dict(type='bool', default=False),
),
supports_check_mode=True,
required_by=dict(
add_children=['xpath'],
attribute=['value', 'xpath'],
content=['xpath'],
set_children=['xpath'],
value=['xpath'],
),
required_if=[
['count', True, ['xpath']],
['print_match', True, ['xpath']],
],
required_one_of=[
['path', 'xmlstring'],
['add_children', 'content', 'count', 'pretty_print', 'print_match', 'set_children', 'value'],
],
mutually_exclusive=[
['add_children', 'content', 'count', 'print_match','set_children', 'value'],
['path', 'xmlstring'],
],
)
```
* Rebase and fix conflict
* Add modules that use required_by functionality
* Update required_by schema
* Fix rebase issue
2019-02-15 01:57:45 +01:00
|
|
|
# (Parameters, failure message)
|
2017-12-18 19:17:13 +01:00
|
|
|
FAILING_PARAMS_DICT = (
|
|
|
|
# Missing required option
|
|
|
|
({'foobar': {}}, 'missing required arguments: foo found in foobar'),
|
|
|
|
# Invalid option
|
|
|
|
({'foobar': {"foo": "hello", "bam": "good", "invalid": "bad"}},
|
|
|
|
'module: invalid found in foobar. Supported parameters include'),
|
|
|
|
# Mutually exclusive options found
|
|
|
|
({'foobar': {"foo": "test", "bam": "bad", "bam1": "bad", "baz": "req_to"}},
|
|
|
|
'parameters are mutually exclusive: bam, bam1 found in foobar'),
|
|
|
|
# required_if fails
|
|
|
|
({'foobar': {"foo": "hello", "bar": "bad"}},
|
|
|
|
'foo is hello but all of the following are missing: bam found in foobar'),
|
|
|
|
# Missing required_one_of option
|
|
|
|
({'foobar': {"foo": "test"}},
|
|
|
|
'one of the following is required: bar, bam found in foobar'),
|
|
|
|
# Missing required_together option
|
|
|
|
({'foobar': {"foo": "test", "bar": "required_one_of", "bam1": "bad"}},
|
|
|
|
'parameters are required together: bam1, baz found in foobar'),
|
Introduce new 'required_by' argument_spec option (#28662)
* Introduce new "required_by' argument_spec option
This PR introduces a new **required_by** argument_spec option which allows you to say *"if parameter A is set, parameter B and C are required as well"*.
- The difference with **required_if** is that it can only add dependencies if a parameter is set to a specific value, not when it is just defined.
- The difference with **required_together** is that it has a commutative property, so: *"Parameter A and B are required together, if one of them has been defined"*.
As an example, we need this for the complex options that the xml module provides. One of the issues we often see is that users are not using the correct combination of options, and then are surprised that the module does not perform the requested action(s).
This would be solved by adding the correct dependencies, and mutual exclusives. For us this is important to get this shipped together with the new xml module in Ansible v2.4. (This is related to bugfix https://github.com/ansible/ansible/pull/28657)
```python
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', aliases=['dest', 'file']),
xmlstring=dict(type='str'),
xpath=dict(type='str'),
namespaces=dict(type='dict', default={}),
state=dict(type='str', default='present', choices=['absent',
'present'], aliases=['ensure']),
value=dict(type='raw'),
attribute=dict(type='raw'),
add_children=dict(type='list'),
set_children=dict(type='list'),
count=dict(type='bool', default=False),
print_match=dict(type='bool', default=False),
pretty_print=dict(type='bool', default=False),
content=dict(type='str', choices=['attribute', 'text']),
input_type=dict(type='str', default='yaml', choices=['xml',
'yaml']),
backup=dict(type='bool', default=False),
),
supports_check_mode=True,
required_by=dict(
add_children=['xpath'],
attribute=['value', 'xpath'],
content=['xpath'],
set_children=['xpath'],
value=['xpath'],
),
required_if=[
['count', True, ['xpath']],
['print_match', True, ['xpath']],
],
required_one_of=[
['path', 'xmlstring'],
['add_children', 'content', 'count', 'pretty_print', 'print_match', 'set_children', 'value'],
],
mutually_exclusive=[
['add_children', 'content', 'count', 'print_match','set_children', 'value'],
['path', 'xmlstring'],
],
)
```
* Rebase and fix conflict
* Add modules that use required_by functionality
* Update required_by schema
* Fix rebase issue
2019-02-15 01:57:45 +01:00
|
|
|
# Missing required_by options
|
|
|
|
({'foobar': {"foo": "test", "bar": "required_one_of", "bam4": "required_by"}},
|
|
|
|
"missing parameter(s) required by 'bam4': bam1, bam3"),
|
2017-12-18 19:17:13 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('stdin, expected', OPTIONS_PARAMS_DICT, indirect=['stdin'])
|
|
|
|
def test_options_type_dict(self, stdin, options_argspec_dict, expected):
|
|
|
|
"""Test that a basic creation with required and required_if works"""
|
|
|
|
# should test ok, tests basic foo requirement and required_if
|
|
|
|
am = basic.AnsibleModule(**options_argspec_dict)
|
|
|
|
|
|
|
|
assert isinstance(am.params['foobar'], dict)
|
|
|
|
assert am.params['foobar'] == expected
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('stdin, expected', OPTIONS_PARAMS_LIST, indirect=['stdin'])
|
|
|
|
def test_options_type_list(self, stdin, options_argspec_list, expected):
|
|
|
|
"""Test that a basic creation with required and required_if works"""
|
|
|
|
# should test ok, tests basic foo requirement and required_if
|
|
|
|
am = basic.AnsibleModule(**options_argspec_list)
|
|
|
|
|
|
|
|
assert isinstance(am.params['foobar'], list)
|
|
|
|
assert am.params['foobar'] == expected
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('stdin, expected', FAILING_PARAMS_DICT, indirect=['stdin'])
|
|
|
|
def test_fail_validate_options_dict(self, capfd, stdin, options_argspec_dict, expected):
|
|
|
|
"""Fail because one of a required_together pair of parameters has a default and the other was not specified"""
|
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
am = basic.AnsibleModule(**options_argspec_dict)
|
|
|
|
|
|
|
|
out, err = capfd.readouterr()
|
|
|
|
results = json.loads(out)
|
|
|
|
|
|
|
|
assert results['failed']
|
|
|
|
assert expected in results['msg']
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('stdin, expected', FAILING_PARAMS_LIST, indirect=['stdin'])
|
|
|
|
def test_fail_validate_options_list(self, capfd, stdin, options_argspec_list, expected):
|
|
|
|
"""Fail because one of a required_together pair of parameters has a default and the other was not specified"""
|
|
|
|
with pytest.raises(SystemExit):
|
|
|
|
am = basic.AnsibleModule(**options_argspec_list)
|
|
|
|
|
|
|
|
out, err = capfd.readouterr()
|
|
|
|
results = json.loads(out)
|
|
|
|
|
|
|
|
assert results['failed']
|
|
|
|
assert expected in results['msg']
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('stdin', [{'foobar': {'foo': 'required', 'bam1': 'test', 'bar': 'case'}}], indirect=['stdin'])
|
|
|
|
def test_fallback_in_option(self, mocker, stdin, options_argspec_dict):
|
|
|
|
"""Test that the complex argspec works if we get a required parameter via fallback"""
|
|
|
|
environ = os.environ.copy()
|
|
|
|
environ['BAZ'] = 'test data'
|
|
|
|
mocker.patch('ansible.module_utils.basic.os.environ', environ)
|
|
|
|
|
|
|
|
am = basic.AnsibleModule(**options_argspec_dict)
|
|
|
|
|
|
|
|
assert isinstance(am.params['foobar']['baz'], str)
|
|
|
|
assert am.params['foobar']['baz'] == 'test data'
|
|
|
|
|
2019-02-19 13:05:20 +01:00
|
|
|
@pytest.mark.parametrize('stdin',
|
|
|
|
[{'foobar': {'foo': 'required', 'bam1': 'test', 'baz': 'data', 'bar': 'case', 'bar4': '~/test'}}],
|
|
|
|
indirect=['stdin'])
|
|
|
|
def test_elements_path_in_option(self, mocker, stdin, options_argspec_dict):
|
|
|
|
"""Test that the complex argspec works with elements path type"""
|
|
|
|
|
|
|
|
am = basic.AnsibleModule(**options_argspec_dict)
|
|
|
|
|
|
|
|
assert isinstance(am.params['foobar']['bar4'][0], str)
|
|
|
|
assert am.params['foobar']['bar4'][0].startswith('/')
|
|
|
|
|
2018-05-07 18:23:13 +02:00
|
|
|
@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
|
|
|
|
|
2017-12-18 19:17:13 +01:00
|
|
|
|
|
|
|
class TestLoadFileCommonArguments:
|
|
|
|
@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
|
|
|
|
def test_smoketest_load_file_common_args(self, am):
|
|
|
|
"""With no file arguments, an empty dict is returned"""
|
|
|
|
am.selinux_mls_enabled = MagicMock()
|
|
|
|
am.selinux_mls_enabled.return_value = True
|
|
|
|
am.selinux_default_context = MagicMock()
|
|
|
|
am.selinux_default_context.return_value = 'unconfined_u:object_r:default_t:s0'.split(':', 3)
|
|
|
|
|
|
|
|
assert am.load_file_common_arguments(params={}) == {}
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
|
|
|
|
def test_load_file_common_args(self, am, mocker):
|
|
|
|
am.selinux_mls_enabled = MagicMock()
|
|
|
|
am.selinux_mls_enabled.return_value = True
|
|
|
|
am.selinux_default_context = MagicMock()
|
|
|
|
am.selinux_default_context.return_value = 'unconfined_u:object_r:default_t:s0'.split(':', 3)
|
|
|
|
|
|
|
|
base_params = dict(
|
|
|
|
path='/path/to/file',
|
|
|
|
mode=0o600,
|
|
|
|
owner='root',
|
|
|
|
group='root',
|
|
|
|
seuser='_default',
|
|
|
|
serole='_default',
|
|
|
|
setype='_default',
|
|
|
|
selevel='_default',
|
|
|
|
)
|
|
|
|
|
|
|
|
extended_params = base_params.copy()
|
|
|
|
extended_params.update(dict(
|
|
|
|
follow=True,
|
|
|
|
foo='bar',
|
|
|
|
))
|
|
|
|
|
|
|
|
final_params = base_params.copy()
|
|
|
|
final_params.update(dict(
|
|
|
|
path='/path/to/real_file',
|
|
|
|
secontext=['unconfined_u', 'object_r', 'default_t', 's0'],
|
|
|
|
attributes=None,
|
|
|
|
))
|
|
|
|
|
|
|
|
# with the proper params specified, the returned dictionary should represent
|
|
|
|
# only those params which have something to do with the file arguments, excluding
|
|
|
|
# other params and updated as required with proper values which may have been
|
|
|
|
# massaged by the method
|
|
|
|
mocker.patch('os.path.islink', return_value=True)
|
|
|
|
mocker.patch('os.path.realpath', return_value='/path/to/real_file')
|
|
|
|
|
|
|
|
res = am.load_file_common_arguments(params=extended_params)
|
|
|
|
|
|
|
|
assert res == final_params
|