Deprecate Entity, EntityCollection and use subspec in network modules (#33575)

* Deprecate Entity, EntityCollection and use subspec in network modules

*  As per proposal https://github.com/ansible/proposals/issues/76
   deprecate use of Entity, EntityCollection, ComplexDict, ComplexList
   and use subspec instead.
*  Refactor ios modules
*  Refactor eos modules
*  Refactor vyos modules
*  Refactor nxos modules
*  Refactor iosxr modules
*  Add support for key in suboptions handling

* Fix CI issues
This commit is contained in:
Ganesh Nalawade 2017-12-11 20:31:25 +05:30 committed by GitHub
parent a23da23491
commit 4349b56643
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 195 additions and 200 deletions

View file

@ -1929,14 +1929,23 @@ class AnsibleModule(object):
self._options_context.append(k)
key = None
for (name, value) in spec.items():
# specifies how to map a single value to spec
if value.get('key'):
key = name
break
if isinstance(params[k], dict):
elements = [params[k]]
else:
elements = params[k]
for param in elements:
for index, param in enumerate(elements):
if not isinstance(param, dict):
self.fail_json(msg="value of %s must be of type dict or list of dict" % k)
if key is None:
self.fail_json(msg="options spec require a key argument to map it to a single value '%s'" % param)
elements[index] = param = {key: param}
self._set_fallbacks(spec, param)
options_aliases = self._handle_aliases(spec, param)

View file

@ -65,6 +65,10 @@ def sort_list(val):
class Entity(object):
"""Transforms a dict to with an argument spec
This class has been deprecated as of Ansible 2.5 and will be
removed from the code in future release.
Please use the suboptions in module argument spec instead.
This class will take a dict and apply an Ansible argument spec to the
values. The resulting dict will contain all of the keys in the param
with appropriate values set.
@ -183,7 +187,12 @@ class Entity(object):
class EntityCollection(Entity):
"""Extends ```Entity``` to handle a list of dicts """
"""Extends ```Entity``` to handle a list of dicts
This class has been deprecated as of Ansible 2.5 and will be
removed from the code in future release.
Please use the suboptions in module argument spec instead.
"""
def __call__(self, iterable, strict=True):
if iterable is None:
@ -198,11 +207,21 @@ class EntityCollection(Entity):
# these two are for backwards compatibility and can be removed once all of the
# modules that use them are updated
class ComplexDict(Entity):
"""
This class has been deprecated as of Ansible 2.5 and will be
removed from the code in future release.
Please use the suboptions in module argument spec instead.
"""
def __init__(self, attrs, module, *args, **kwargs):
super(ComplexDict, self).__init__(module, attrs, *args, **kwargs)
class ComplexList(EntityCollection):
"""
This class has been deprecated as of Ansible 2.5 and will be
removed from the code in future release.
Please use the suboptions in module argument spec instead.
"""
def __init__(self, attrs, module, *args, **kwargs):
super(ComplexList, self).__init__(module, attrs, *args, **kwargs)

View file

@ -33,8 +33,8 @@ import time
from ansible.module_utils._text import to_text, to_native
from ansible.module_utils.basic import env_fallback, return_values
from ansible.module_utils.connection import exec_command
from ansible.module_utils.network.common.utils import to_list, ComplexList
from ansible.module_utils.six import iteritems
from ansible.module_utils.network.common.utils import to_list
from ansible.module_utils.six import iteritems, string_types
from ansible.module_utils.urls import fetch_url
_DEVICE_CONNECTION = None
@ -150,7 +150,6 @@ class Cli:
"""Run list of commands on remote device and return results
"""
responses = list()
for cmd in to_list(commands):
rc, out, err = self.exec_command(cmd)
out = to_text(out, errors='surrogate_then_replace')
@ -429,22 +428,6 @@ def is_eapi(module):
return 'eapi' in (transport, provider_transport)
def to_command(module, commands):
if is_eapi(module):
default_output = 'json'
else:
default_output = 'text'
transform = ComplexList(dict(
command=dict(key=True),
output=dict(default=default_output),
prompt=dict(),
answer=dict()
), module)
return transform(to_list(commands))
def get_config(module, flags=None):
flags = None if flags is None else flags
@ -454,7 +437,15 @@ def get_config(module, flags=None):
def run_commands(module, commands):
conn = get_connection(module)
return conn.run_commands(to_command(module, commands))
if is_eapi(module):
default_output = 'json'
else:
default_output = 'text'
for index, cmd in enumerate(to_list(commands)):
if isinstance(cmd, string_types):
commands[index] = {'command': cmd, 'output': default_output}
return conn.run_commands(to_list(commands))
def load_config(module, config, commit=False, replace=False):

View file

@ -27,8 +27,9 @@
#
from ansible.module_utils._text import to_text
from ansible.module_utils.basic import env_fallback, return_values
from ansible.module_utils.network.common.utils import to_list, ComplexList
from ansible.module_utils.connection import exec_command
from ansible.module_utils.network.common.utils import to_list
from ansible.module_utils.six import string_types
_DEVICE_CONFIGS = {}
@ -100,20 +101,12 @@ def get_config(module, flags=None):
return cfg
def to_commands(module, commands):
spec = {
'command': dict(key=True),
'prompt': dict(),
'answer': dict()
}
transform = ComplexList(spec, module)
return transform(commands)
def run_commands(module, commands, check_rc=True):
responses = list()
commands = to_commands(module, to_list(commands))
for cmd in commands:
for cmd in to_list(commands):
if isinstance(cmd, string_types):
cmd = {'command': cmd}
cmd = module.jsonify(cmd)
rc, out, err = exec_command(module, cmd)
if check_rc and rc != 0:

View file

@ -79,12 +79,6 @@ iosxr_argument_spec = {
'provider': dict(type='dict', options=iosxr_provider_spec)
}
command_spec = {
'command': dict(),
'prompt': dict(default=None),
'answer': dict(default=None)
}
iosxr_top_spec = {
'host': dict(removed_in_version=2.9),
'port': dict(removed_in_version=2.9, type='int'),

View file

@ -32,7 +32,7 @@ import collections
from ansible.module_utils._text import to_text
from ansible.module_utils.basic import env_fallback, return_values
from ansible.module_utils.network.common.utils import to_list, ComplexList
from ansible.module_utils.network.common.utils import to_list
from ansible.module_utils.connection import exec_command
from ansible.module_utils.six import iteritems, string_types
from ansible.module_utils.urls import fetch_url
@ -399,28 +399,6 @@ def is_nxapi(module):
return 'nxapi' in (transport, provider_transport)
def to_command(module, commands):
if is_nxapi(module):
default_output = 'json'
else:
default_output = 'text'
transform = ComplexList(dict(
command=dict(key=True),
output=dict(default=default_output),
prompt=dict(),
answer=dict()
), module)
commands = transform(to_list(commands))
for item in commands:
if is_json(item['command']):
item['output'] = 'json'
return commands
def get_config(module, flags=None):
flags = [] if flags is None else flags
@ -430,7 +408,20 @@ def get_config(module, flags=None):
def run_commands(module, commands, check_rc=True):
conn = get_connection(module)
return conn.run_commands(to_command(module, commands), check_rc)
if is_nxapi(module):
default_output = 'json'
else:
default_output = 'text'
for index, cmd in enumerate(to_list(commands)):
if isinstance(cmd, string_types):
commands[index] = cmd = {'command': cmd, 'output': default_output}
if is_json(cmd['command']):
cmd['output'] = 'json'
return conn.run_commands(to_list(commands), check_rc)
def load_config(module, config, return_error=False, opts=None):

View file

@ -140,9 +140,8 @@ from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.pycompat24 import get_exception
from ansible.module_utils.six import string_types
from ansible.module_utils.network.common.parsing import Conditional
from ansible.module_utils.network.common.utils import ComplexList
from ansible.module_utils.network.eos.eos import run_commands
from ansible.module_utils.network.eos.eos import eos_argument_spec, check_args
from ansible.module_utils.network.eos.eos import eos_argument_spec
VALID_KEYS = ['command', 'output', 'prompt', 'response']
@ -157,16 +156,7 @@ def to_lines(stdout):
def parse_commands(module, warnings):
spec = dict(
command=dict(key=True),
output=dict(),
prompt=dict(),
answer=dict()
)
transform = ComplexList(spec, module)
commands = transform(module.params['commands'])
commands = module.params['commands']
if module.check_mode:
for item in list(commands):
if not item['command'].startswith('show'):
@ -189,8 +179,15 @@ def to_cli(obj):
def main():
"""entry point for module execution
"""
command_spec = dict(
command=dict(key=True),
output=dict(),
prompt=dict(),
answer=dict()
)
argument_spec = dict(
commands=dict(type='list', required=True),
commands=dict(type='list', elements='dict', options=command_spec, required=True),
wait_for=dict(type='list', aliases=['waitfor']),
match=dict(default='all', choices=['all', 'any']),
@ -207,7 +204,6 @@ def main():
result = {'changed': False}
warnings = list()
check_args(module, warnings)
commands = parse_commands(module, warnings)
if warnings:
result['warnings'] = warnings

View file

@ -129,7 +129,6 @@ session_name:
import re
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.common.utils import ComplexList
from ansible.module_utils.network.eos.eos import load_config, get_config
from ansible.module_utils.network.eos.eos import eos_argument_spec
@ -270,31 +269,27 @@ def map_params_to_obj(module):
obj = {
'hostname': module.params['hostname'],
'domain_name': module.params['domain_name'],
'domain_list': module.params['domain_list']
'domain_list': module.params['domain_list'],
'lookup_source': module.params['lookup_source'],
'name_servers': module.params['name_servers'],
}
lookup_source = ComplexList(dict(
interface=dict(key=True),
vrf=dict()
), module)
name_servers = ComplexList(dict(
server=dict(key=True),
vrf=dict(default='default')
), module)
for arg, cast in [('lookup_source', lookup_source), ('name_servers', name_servers)]:
if module.params[arg] is not None:
obj[arg] = cast(module.params[arg])
else:
obj[arg] = None
return obj
def main():
""" main entry point for module execution
"""
lookup_source_spec = dict(
interface=dict(key=True),
vrf=dict()
)
name_servers_spec = dict(
server=dict(key=True),
vrf=dict()
)
argument_spec = dict(
hostname=dict(),
@ -302,10 +297,10 @@ def main():
domain_list=dict(type='list', aliases=['domain_search']),
# { interface: <str>, vrf: <str> }
lookup_source=dict(type='list'),
lookup_source=dict(type='list', elements='dict', options=lookup_source_spec),
# { server: <str>; vrf: <str> }
name_servers=dict(type='list'),
name_servers=dict(type='list', elements='dict', options=name_servers_spec),
state=dict(default='present', choices=['present', 'absent'])
)
@ -333,5 +328,6 @@ def main():
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -134,9 +134,8 @@ failed_conditions:
import time
from ansible.module_utils.network.ios.ios import run_commands
from ansible.module_utils.network.ios.ios import ios_argument_spec, check_args
from ansible.module_utils.network.ios.ios import ios_argument_spec
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.common.utils import ComplexList
from ansible.module_utils.network.common.parsing import Conditional
from ansible.module_utils.six import string_types
@ -149,13 +148,8 @@ def to_lines(stdout):
def parse_commands(module, warnings):
command = ComplexList(dict(
command=dict(key=True),
prompt=dict(),
answer=dict()
), module)
commands = command(module.params['commands'])
for item in list(commands):
commands = module.params['commands']
for item in commands:
if module.check_mode and not item['command'].startswith('show'):
warnings.append(
'only show commands are supported when using check mode, not '
@ -173,8 +167,14 @@ def parse_commands(module, warnings):
def main():
"""main entry point for module execution
"""
command_spec = dict(
command=dict(key=True),
prompt=dict(),
answer=dict()
)
argument_spec = dict(
commands=dict(type='list', required=True),
commands=dict(type='list', elements='dict', options=command_spec, required=True),
wait_for=dict(type='list', aliases=['waitfor']),
match=dict(default='all', choices=['all', 'any']),
@ -191,7 +191,6 @@ def main():
result = {'changed': False}
warnings = list()
check_args(module, warnings)
commands = parse_commands(module, warnings)
result['warnings'] = warnings

View file

@ -120,8 +120,7 @@ import re
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.ios.ios import get_config, load_config
from ansible.module_utils.network.ios.ios import ios_argument_spec, check_args
from ansible.module_utils.network.common.utils import ComplexList
from ansible.module_utils.network.ios.ios import ios_argument_spec
_CONFIGURED_VRFS = None
@ -307,44 +306,43 @@ def map_params_to_obj(module):
'hostname': module.params['hostname'],
'lookup_source': module.params['lookup_source'],
'lookup_enabled': module.params['lookup_enabled'],
'domain_name': module.params['domain_name'],
'domain_search': module.params['domain_search'],
'name_servers': module.params['name_servers']
}
domain_name = ComplexList(dict(
name=dict(key=True),
vrf=dict()
), module)
domain_search = ComplexList(dict(
name=dict(key=True),
vrf=dict()
), module)
name_servers = ComplexList(dict(
server=dict(key=True),
vrf=dict()
), module)
for arg, cast in [('domain_name', domain_name),
('domain_search', domain_search),
('name_servers', name_servers)]:
if module.params[arg]:
obj[arg] = cast(module.params[arg])
else:
obj[arg] = None
return obj
def main():
""" Main entry point for Ansible module execution
"""
domain_name_spec = dict(
name=dict(key=True),
vrf=dict()
)
domain_search_spec = dict(
name=dict(key=True),
vrf=dict()
)
name_servers_spec = dict(
server=dict(key=True),
vrf=dict()
)
argument_spec = dict(
hostname=dict(),
domain_name=dict(type='list'),
domain_search=dict(type='list'),
name_servers=dict(type='list'),
# { name: <str>, vrf: <str> }
domain_name=dict(type='list', elements='dict', options=domain_name_spec),
# {name: <str>, vrf: <str> }
domain_search=dict(type='list', elements='dict', options=domain_search_spec),
# { server: <str>; vrf: <str> }
name_servers=dict(type='list', elements='dict', options=name_servers_spec),
lookup_source=dict(),
lookup_enabled=dict(type='bool'),
@ -360,7 +358,6 @@ def main():
result = {'changed': False}
warnings = list()
check_args(module, warnings)
result['warnings'] = warnings
want = map_params_to_obj(module)
@ -376,5 +373,6 @@ def main():
module.exit_json(**result)
if __name__ == "__main__":
main()

View file

@ -127,8 +127,8 @@ import time
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.iosxr.iosxr import run_command, iosxr_argument_spec
from ansible.module_utils.network.iosxr.iosxr import command_spec
from ansible.module_utils.network.common.parsing import Conditional
from ansible.module_utils.network.common.utils import to_list
from ansible.module_utils.six import string_types
from ansible.module_utils._text import to_native
@ -142,11 +142,8 @@ def to_lines(stdout):
def parse_commands(module, warnings):
commands = module.params['commands']
for item in list(commands):
try:
command = item['command']
except Exception:
command = item
for item in to_list(commands):
command = item['command']
if module.check_mode and not command.startswith('show'):
warnings.append(
'only show commands are supported when using check mode, not '
@ -163,8 +160,14 @@ def parse_commands(module, warnings):
def main():
command_spec = dict(
command=dict(key=True),
prompt=dict(),
answer=dict()
)
spec = dict(
commands=dict(type='list', required=True),
commands=dict(type='list', elements='dict', options=command_spec, required=True),
wait_for=dict(type='list', aliases=['waitfor']),
match=dict(default='all', choices=['all', 'any']),
@ -175,13 +178,10 @@ def main():
spec.update(iosxr_argument_spec)
spec.update(command_spec)
module = AnsibleModule(argument_spec=spec,
supports_check_mode=True)
warnings = list()
commands = parse_commands(module, warnings)
wait_for = module.params['wait_for'] or list()

View file

@ -147,8 +147,7 @@ import time
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.common.parsing import Conditional, FailedConditionalError
from ansible.module_utils.network.common.utils import ComplexList
from ansible.module_utils.network.nxos.nxos import check_args, nxos_argument_spec, run_commands
from ansible.module_utils.network.nxos.nxos import nxos_argument_spec, run_commands
from ansible.module_utils.six import string_types
from ansible.module_utils._text import to_native
@ -163,14 +162,7 @@ def to_lines(stdout):
def parse_commands(module, warnings):
transform = ComplexList(dict(
command=dict(key=True),
output=dict(),
prompt=dict(),
answer=dict()
), module)
commands = transform(module.params['commands'])
commands = module.params['commands']
if module.check_mode:
for item in list(commands):
@ -194,9 +186,16 @@ def to_cli(obj):
def main():
"""entry point for module execution
"""
command_spec = dict(
command=dict(key=True),
output=dict(),
prompt=dict(),
answer=dict()
)
argument_spec = dict(
# { command: <str>, output: <str>, prompt: <str>, response: <str> }
commands=dict(type='list', required=True),
commands=dict(type='list', elements='dict', options=command_spec, required=True),
wait_for=dict(type='list', aliases=['waitfor']),
match=dict(default='all', choices=['any', 'all']),
@ -213,7 +212,6 @@ def main():
result = {'changed': False}
warnings = list()
check_args(module, warnings)
commands = parse_commands(module, warnings)
result['warnings'] = warnings

View file

@ -112,11 +112,10 @@ commands:
import re
from ansible.module_utils.network.nxos.nxos import get_config, load_config
from ansible.module_utils.network.nxos.nxos import nxos_argument_spec, check_args
from ansible.module_utils.network.nxos.nxos import nxos_argument_spec
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible.module_utils.network.common.config import NetworkConfig
from ansible.module_utils.network.common.utils import ComplexList
_CONFIGURED_VRFS = None
@ -302,49 +301,45 @@ def map_params_to_obj(module):
obj = {
'hostname': module.params['hostname'],
'domain_lookup': module.params['domain_lookup'],
'system_mtu': module.params['system_mtu']
'system_mtu': module.params['system_mtu'],
'domain_name': module.params['domain_name'],
'domain_search': module.params['domain_search'],
'name_servers': module.params['name_servers']
}
domain_name = ComplexList(dict(
name=dict(key=True),
vrf=dict()
), module)
domain_search = ComplexList(dict(
name=dict(key=True),
vrf=dict()
), module)
name_servers = ComplexList(dict(
server=dict(key=True),
vrf=dict()
), module)
for arg, cast in [('domain_name', domain_name), ('domain_search', domain_search),
('name_servers', name_servers)]:
if module.params[arg] is not None:
obj[arg] = cast(module.params[arg])
else:
obj[arg] = None
return obj
def main():
""" main entry point for module execution
"""
domain_name_spec = dict(
name=dict(key=True),
vrf=dict()
)
domain_search_spec = dict(
name=dict(key=True),
vrf=dict()
)
name_servers_spec = dict(
server=dict(key=True),
vrf=dict()
)
argument_spec = dict(
hostname=dict(),
domain_lookup=dict(type='bool'),
# { name: <str>, vrf: <str> }
domain_name=dict(type='list'),
domain_name=dict(type='list', elements='dict', options=domain_name_spec),
# {name: <str>, vrf: <str> }
domain_search=dict(type='list'),
domain_search=dict(type='list', elements='dict', options=domain_search_spec),
# { server: <str>; vrf: <str> }
name_servers=dict(type='list'),
name_servers=dict(type='list', elements='dict', options=name_servers_spec),
system_mtu=dict(type='int'),
lookup_source=dict(),
@ -357,7 +352,6 @@ def main():
supports_check_mode=True)
warnings = list()
check_args(module, warnings)
result = {'changed': False}
if warnings:
@ -376,5 +370,6 @@ def main():
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -137,7 +137,6 @@ import time
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.pycompat24 import get_exception
from ansible.module_utils.network.common.parsing import Conditional
from ansible.module_utils.network.common.utils import ComplexList
from ansible.module_utils.six import string_types
from ansible.module_utils.network.vyos.vyos import run_commands
from ansible.module_utils.network.vyos.vyos import vyos_argument_spec
@ -151,12 +150,7 @@ def to_lines(stdout):
def parse_commands(module, warnings):
command = ComplexList(dict(
command=dict(key=True),
prompt=dict(),
answer=dict(),
), module)
commands = command(module.params['commands'])
commands = module.params['commands']
items = []
for item in commands:
@ -170,8 +164,15 @@ def parse_commands(module, warnings):
def main():
command_spec = dict(
command=dict(key=True),
prompt=dict(),
answer=dict()
)
spec = dict(
commands=dict(type='list', required=True),
commands=dict(type='list', elements='dict', options=command_spec, required=True),
wait_for=dict(type='list', aliases=['waitfor']),
match=dict(default='all', choices=['all', 'any']),

View file

@ -575,6 +575,21 @@ class TestModuleUtilsBasic(ModuleTestCase):
supports_check_mode=True,
)
# should test ok, handles key argument
key_spec = dict(foo=dict(key=True), bar=dict())
args = json.dumps(dict(ANSIBLE_MODULE_ARGS={'foobar': ['test-1', 'test-2']}))
with swap_stdin_and_argv(stdin_data=args):
basic._ANSIBLE_ARGS = None
am = basic.AnsibleModule(
argument_spec=dict(foobar=dict(type='list', elements='dict', options=key_spec, required=True)),
no_log=True,
check_invalid_arguments=False,
add_file_common_args=True,
supports_check_mode=True
)
self.assertEqual(am.params['foobar'][0]['foo'], 'test-1')
self.assertEqual(am.params['foobar'][1]['foo'], 'test-2')
def test_module_utils_basic_ansible_module_type_check(self):
from ansible.module_utils import basic