diff --git a/lib/ansible/modules/network/nxos/nxos_evpn_vni.py b/lib/ansible/modules/network/nxos/nxos_evpn_vni.py index a3e415d888..f5e2f465a0 100644 --- a/lib/ansible/modules/network/nxos/nxos_evpn_vni.py +++ b/lib/ansible/modules/network/nxos/nxos_evpn_vni.py @@ -16,9 +16,11 @@ # along with Ansible. If not, see . # -ANSIBLE_METADATA = {'metadata_version': '1.0', - 'status': ['preview'], - 'supported_by': 'community'} +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community' +} DOCUMENTATION = ''' @@ -28,146 +30,116 @@ extends_documentation_fragment: nxos version_added: "2.2" short_description: Manages Cisco EVPN VXLAN Network Identifier (VNI). description: - - Manages Cisco Ethernet Virtual Private Network (EVPN) VXLAN Network - Identifier (VNI) configurations of a Nexus device. + - Manages Cisco Ethernet Virtual Private Network (EVPN) VXLAN Network + Identifier (VNI) configurations of a Nexus device. author: Gabriele Gerbino (@GGabriele) notes: - - default, where supported, restores params default value. - - RD override is not permitted. You should set it to the default values - first and then reconfigure it. - - C(route_target_both), C(route_target_import) and - C(route_target_export valid) values are a list of extended communities, - (i.e. ['1.2.3.4:5', '33:55']) or the keywords 'auto' or 'default'. - - The C(route_target_both) property is discouraged due to the inconsistent - behavior of the property across Nexus platforms and image versions. - For this reason it is recommended to use explicit C(route_target_export) - and C(route_target_import) properties instead of C(route_target_both). - - RD valid values are a string in one of the route-distinguisher formats, - the keyword 'auto', or the keyword 'default'. + - default, where supported, restores params default value. + - RD override is not permitted. You should set it to the default values + first and then reconfigure it. + - C(route_target_both), C(route_target_import) and + C(route_target_export valid) values are a list of extended communities, + (i.e. ['1.2.3.4:5', '33:55']) or the keywords 'auto' or 'default'. + - The C(route_target_both) property is discouraged due to the inconsistent + behavior of the property across Nexus platforms and image versions. + For this reason it is recommended to use explicit C(route_target_export) + and C(route_target_import) properties instead of C(route_target_both). + - RD valid values are a string in one of the route-distinguisher formats, + the keyword 'auto', or the keyword 'default'. options: - vni: - description: - - The EVPN VXLAN Network Identifier. - required: true - default: null - route_distinguisher: - description: - - The VPN Route Distinguisher (RD). The RD is combined with - the IPv4 or IPv6 prefix learned by the PE router to create a - globally unique address. - required: true - default: null - route_target_both: - description: - - Enables/Disables route-target settings for both import and - export target communities using a single property. - required: false - default: null - route_target_import: - description: - - Sets the route-target 'import' extended communities. - required: false - default: null - route_target_export: - description: - - Sets the route-target 'import' extended communities. - required: false - default: null - state: - description: - - Determines whether the config should be present or not - on the device. - required: false - default: present - choices: ['present','absent'] + vni: + description: + - The EVPN VXLAN Network Identifier. + required: true + default: null + route_distinguisher: + description: + - The VPN Route Distinguisher (RD). The RD is combined with + the IPv4 or IPv6 prefix learned by the PE router to create a + globally unique address. + required: true + default: null + route_target_both: + description: + - Enables/Disables route-target settings for both import and + export target communities using a single property. + required: false + default: null + route_target_import: + description: + - Sets the route-target 'import' extended communities. + required: false + default: null + route_target_export: + description: + - Sets the route-target 'import' extended communities. + required: false + default: null + state: + description: + - Determines whether the config should be present or not + on the device. + required: false + default: present + choices: ['present','absent'] ''' + EXAMPLES = ''' -- nxos_evpn_vni: +- name: vni configuration + nxos_evpn_vni: vni: 6000 route_distinguisher: "60:10" route_target_import: - - "5000:10" - - "4100:100" + - "5000:10" + - "4100:100" route_target_export: auto route_target_both: default - username: "{{ un }}" - password: "{{ pwd }}" - host: "{{ inventory_hostname }}" ''' RETURN = ''' -proposed: - description: k/v pairs of parameters passed into module - returned: verbose mode - type: dict - sample: {"route_target_import": ["5000:10", "4100:100", - "5001:10"],"vni": "6000"} -existing: - description: k/v pairs of existing EVPN VNI configuration - returned: verbose mode - type: dict - sample: {"route_distinguisher": "70:10", "route_target_both": [], - "route_target_export": [], "route_target_import": [ - "4100:100", "5000:10"], "vni": "6000"} -end_state: - description: k/v pairs of EVPN VNI configuration after module execution - returned: verbose mode - type: dict - sample: {"route_distinguisher": "70:10", "route_target_both": [], - "route_target_export": [], "route_target_import": [ - "4100:100", "5000:10", "5001:10"], "vni": "6000"} -updates: +commands: description: commands sent to the device returned: always type: list sample: ["evpn", "vni 6000 l2", "route-target import 5001:10"] -changed: - description: check to see if a change was made on the device - returned: always - type: boolean - sample: true ''' import re +import time from ansible.module_utils.nxos import get_config, load_config, run_commands from ansible.module_utils.nxos import nxos_argument_spec, check_args from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.netcfg import CustomNetworkConfig + PARAM_TO_COMMAND_KEYMAP = { 'vni': 'vni', + 'route_distinguisher': 'rd', 'route_target_both': 'route-target both', 'route_target_import': 'route-target import', - 'route_target_export': 'route-target export', - 'route_distinguisher': 'rd' + 'route_target_export': 'route-target export' } -WARNINGS = [] - -import time - -def invoke(name, *args, **kwargs): - func = globals().get(name) - if func: - return func(*args, **kwargs) def get_value(arg, config, module): - REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + command = PARAM_TO_COMMAND_KEYMAP.get(arg) + command_re = re.compile(r'(?:{0}\s)(?P.*)$'.format(command), re.M) value = '' - if PARAM_TO_COMMAND_KEYMAP[arg] in config: - value = REGEX.search(config).group('value') + if command in config: + value = command_re.search(config).group('value') return value def get_route_target_value(arg, config, module): splitted_config = config.splitlines() value_list = [] - REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + command = PARAM_TO_COMMAND_KEYMAP.get(arg) + command_re = re.compile(r'(?:{0}\s)(?P.*)$'.format(command), re.M) for line in splitted_config: value = '' - if PARAM_TO_COMMAND_KEYMAP[arg] in line.strip(): - value = REGEX.search(line).group('value') + if command in line.strip(): + value = command_re.search(line).group('value') value_list.append(value) return value_list @@ -197,14 +169,10 @@ def get_existing(module, args): def apply_key_map(key_map, table): new_dict = {} - for key, value in table.items(): + for key in table: new_key = key_map.get(key) if new_key: - value = table.get(key) - if value: - new_dict[new_key] = value - else: - new_dict[new_key] = value + new_dict[new_key] = table.get(key) return new_dict @@ -236,26 +204,25 @@ def state_present(module, existing, proposed): if existing_value: for target in existing_value: commands.append('no {0} {1}'.format(key, target)) - else: - if not isinstance(value, list): - value = [value] - for target in value: - if existing: - if target not in existing.get(key.replace('-', '_').replace(' ', '_')): - commands.append('{0} {1}'.format(key, target)) - else: + elif not isinstance(value, list): + value = [value] + for target in value: + if existing: + if target not in existing.get(key.replace('-', '_').replace(' ', '_')): commands.append('{0} {1}'.format(key, target)) + else: + commands.append('{0} {1}'.format(key, target)) + elif value == 'default': + existing_value = existing_commands.get(key) + if existing_value: + commands.append('no {0} {1}'.format(key, existing_value)) else: - if value == 'default': - existing_value = existing_commands.get(key) - if existing_value: - commands.append('no {0} {1}'.format(key, existing_value)) - else: - command = '{0} {1}'.format(key, value) - commands.append(command) + command = '{0} {1}'.format(key, value) + commands.append(command) - if commands: - parents = ['evpn', 'vni {0} l2'.format(module.params['vni'])] + else: + commands = ['vni {0} l2'.format(module.params['vni'])] + parents = ['evpn'] return commands, parents @@ -266,13 +233,6 @@ def state_absent(module, existing, proposed): return commands, parents -def execute_config(module, candidate): - result = {} - response = load_config(module, candidate) - result.update(response) - return result - - def main(): argument_spec = dict( vni=dict(required=True, type='str'), @@ -280,8 +240,7 @@ def main(): route_target_both=dict(required=False, type='list'), route_target_import=dict(required=False, type='list'), route_target_export=dict(required=False, type='list'), - state=dict(choices=['present', 'absent'], default='present', - required=False), + state=dict(choices=['present', 'absent'], default='present', required=False), include_defaults=dict(default=True), config=dict(), save=dict(type='bool', default=False) @@ -289,26 +248,19 @@ def main(): argument_spec.update(nxos_argument_spec) - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) warnings = list() check_args(module, warnings) - + results = dict(changed=False, warnings=warnings) state = module.params['state'] - args = [ - 'vni', - 'route_distinguisher', - 'route_target_both', - 'route_target_import', - 'route_target_export' - ] - - existing = invoke('get_existing', module, args) - end_state = existing + args = PARAM_TO_COMMAND_KEYMAP.keys() + existing = get_existing(module, args) proposed_args = dict((k, v) for k, v in module.params.items() - if v is not None and k in args) + if v is not None and k in args) + commands = [] + parents = [] proposed = {} for key, value in proposed_args.items(): @@ -317,48 +269,42 @@ def main(): value = True elif value == 'false': value = False - if existing.get(key) or (not existing.get(key) and value): + if existing.get(key) != value: proposed[key] = value - result = {} - if state == 'present' or (state == 'absent' and existing): - candidate = CustomNetworkConfig(indent=3) - commands, parents = invoke('state_%s' % state, module, existing, - proposed) - if commands: - if (existing.get('route_distinguisher') and - proposed.get('route_distinguisher')): - if (existing['route_distinguisher'] != proposed['route_distinguisher'] and - proposed['route_distinguisher'] != 'default'): - WARNINGS.append('EVPN RD {0} was automatically removed. ' - 'It is highly recommended to use a task ' - '(with default as value) to explicitly ' - 'unconfigure it.'.format( - existing['route_distinguisher'])) - remove_commands = ['no rd {0}'.format( - existing['route_distinguisher'])] - candidate.add(remove_commands, parents=parents) - result = execute_config(module, candidate) - time.sleep(30) + if state == 'present': + commands, parents = state_present(module, existing, proposed) + elif state == 'absent' and existing: + commands, parents = state_absent(module, existing, proposed) + if commands: + if (existing.get('route_distinguisher') and + proposed.get('route_distinguisher')): + if (existing['route_distinguisher'] != proposed['route_distinguisher'] and + proposed['route_distinguisher'] != 'default'): + warnings.append('EVPN RD {0} was automatically removed. ' + 'It is highly recommended to use a task ' + '(with default as value) to explicitly ' + 'unconfigure it.'.format(existing['route_distinguisher'])) + remove_commands = ['no rd {0}'.format(existing['route_distinguisher'])] + + candidate = CustomNetworkConfig(indent=3) + candidate.add(remove_commands, parents=parents) + load_config(module, candidate) + results['changed'] = True + results['commands'] = candidate.items_text() + time.sleep(30) + + else: candidate = CustomNetworkConfig(indent=3) candidate.add(commands, parents=parents) - result = execute_config(module, candidate) + load_config(module, candidate) + results['changed'] = True + results['commands'] = candidate.items_text() else: - result['updates'] = [] - - if module._verbosity > 0: - end_state = invoke('get_existing', module, args) - result['end_state'] = end_state - result['existing'] = existing - result['proposed'] = proposed_args - - if WARNINGS: - result['warnings'] = WARNINGS - - module.exit_json(**result) + results['commands'] = [] + module.exit_json(**results) if __name__ == '__main__': main() - diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt index a071a35d2b..35d7617648 100644 --- a/test/sanity/pep8/legacy-files.txt +++ b/test/sanity/pep8/legacy-files.txt @@ -494,7 +494,6 @@ lib/ansible/modules/network/nxos/nxos_bgp_neighbor_af.py lib/ansible/modules/network/nxos/nxos_command.py lib/ansible/modules/network/nxos/nxos_config.py lib/ansible/modules/network/nxos/nxos_evpn_global.py -lib/ansible/modules/network/nxos/nxos_evpn_vni.py lib/ansible/modules/network/nxos/nxos_facts.py lib/ansible/modules/network/nxos/nxos_feature.py lib/ansible/modules/network/nxos/nxos_gir.py diff --git a/test/units/modules/network/nxos/test_nxos_evpn_vni.py b/test/units/modules/network/nxos/test_nxos_evpn_vni.py new file mode 100644 index 0000000000..6f849fc7ae --- /dev/null +++ b/test/units/modules/network/nxos/test_nxos_evpn_vni.py @@ -0,0 +1,59 @@ +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json + +from ansible.compat.tests.mock import patch +from ansible.modules.network.nxos import nxos_evpn_vni +from .nxos_module import TestNxosModule, load_fixture, set_module_args + + +class TestNxosEvpnVniModule(TestNxosModule): + + module = nxos_evpn_vni + + def setUp(self): + self.mock_run_commands = patch('ansible.modules.network.nxos.nxos_evpn_vni.run_commands') + self.run_commands = self.mock_run_commands.start() + + self.mock_load_config = patch('ansible.modules.network.nxos.nxos_evpn_vni.load_config') + self.load_config = self.mock_load_config.start() + + self.mock_get_config = patch('ansible.modules.network.nxos.nxos_evpn_vni.get_config') + self.get_config = self.mock_get_config.start() + + def tearDown(self): + self.mock_run_commands.stop() + self.mock_load_config.stop() + self.mock_get_config.stop() + + def load_fixtures(self, commands=None): + self.load_config.return_value = None + + def test_nxos_evpn_vni_absent(self): + set_module_args(dict(vni='6000', state='absent')) + result = self.execute_module(changed=False) + self.assertEqual(result['commands'], []) + + def test_nxos_evpn_vni_present(self): + set_module_args(dict(vni='6000', state='present')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['evpn', 'vni 6000 l2'])