From ab07c206aa647dce1bc1c6a03a9b799fe5580d5f Mon Sep 17 00:00:00 2001 From: Sumit Jaiswal Date: Sat, 17 Aug 2019 23:02:36 +0530 Subject: Resource module for ios_l2_interfaces (#60344) * ios_l2_interface checkin Signed-off-by: Sumit Jaiswal --- lib/ansible/module_utils/network/common/utils.py | 7 + .../network/ios/argspec/facts/facts.py | 4 +- .../network/ios/argspec/l2_interfaces/__init__.py | 0 .../ios/argspec/l2_interfaces/l2_interfaces.py | 53 +++ .../network/ios/config/l2_interfaces/__init__.py | 0 .../ios/config/l2_interfaces/l2_interfaces.py | 312 +++++++++++++ .../module_utils/network/ios/facts/facts.py | 2 + .../network/ios/facts/l2_interfaces/__init__.py | 0 .../ios/facts/l2_interfaces/l2_interfaces.py | 108 +++++ .../module_utils/network/ios/utils/utils.py | 32 +- .../modules/network/ios/_ios_l2_interface.py | 499 +++++++++++++++++++++ lib/ansible/modules/network/ios/ios_facts.py | 7 +- .../modules/network/ios/ios_l2_interface.py | 499 --------------------- .../modules/network/ios/ios_l2_interfaces.py | 359 +++++++++++++++ .../targets/ios_l2_interfaces/defaults/main.yaml | 3 + .../targets/ios_l2_interfaces/meta/main.yaml | 1 + .../targets/ios_l2_interfaces/tasks/cli.yaml | 24 + .../targets/ios_l2_interfaces/tasks/main.yaml | 2 + .../tests/cli/_populate_config.yaml | 14 + .../tests/cli/_remove_config.yaml | 19 + .../ios_l2_interfaces/tests/cli/deleted.yaml | 41 ++ .../ios_l2_interfaces/tests/cli/merged.yaml | 49 ++ .../ios_l2_interfaces/tests/cli/overridden.yaml | 47 ++ .../ios_l2_interfaces/tests/cli/replaced.yaml | 50 +++ .../targets/ios_l2_interfaces/vars/main.yaml | 137 ++++++ test/sanity/ignore.txt | 12 +- 26 files changed, 1767 insertions(+), 514 deletions(-) create mode 100644 lib/ansible/module_utils/network/ios/argspec/l2_interfaces/__init__.py create mode 100644 lib/ansible/module_utils/network/ios/argspec/l2_interfaces/l2_interfaces.py create mode 100644 lib/ansible/module_utils/network/ios/config/l2_interfaces/__init__.py create mode 100644 lib/ansible/module_utils/network/ios/config/l2_interfaces/l2_interfaces.py create mode 100644 lib/ansible/module_utils/network/ios/facts/l2_interfaces/__init__.py create mode 100644 lib/ansible/module_utils/network/ios/facts/l2_interfaces/l2_interfaces.py create mode 100644 lib/ansible/modules/network/ios/_ios_l2_interface.py delete mode 100644 lib/ansible/modules/network/ios/ios_l2_interface.py create mode 100644 lib/ansible/modules/network/ios/ios_l2_interfaces.py create mode 100644 test/integration/targets/ios_l2_interfaces/defaults/main.yaml create mode 100644 test/integration/targets/ios_l2_interfaces/meta/main.yaml create mode 100644 test/integration/targets/ios_l2_interfaces/tasks/cli.yaml create mode 100644 test/integration/targets/ios_l2_interfaces/tasks/main.yaml create mode 100644 test/integration/targets/ios_l2_interfaces/tests/cli/_populate_config.yaml create mode 100644 test/integration/targets/ios_l2_interfaces/tests/cli/_remove_config.yaml create mode 100644 test/integration/targets/ios_l2_interfaces/tests/cli/deleted.yaml create mode 100644 test/integration/targets/ios_l2_interfaces/tests/cli/merged.yaml create mode 100644 test/integration/targets/ios_l2_interfaces/tests/cli/overridden.yaml create mode 100644 test/integration/targets/ios_l2_interfaces/tests/cli/replaced.yaml create mode 100644 test/integration/targets/ios_l2_interfaces/vars/main.yaml diff --git a/lib/ansible/module_utils/network/common/utils.py b/lib/ansible/module_utils/network/common/utils.py index 074821daac..afe419aa89 100644 --- a/lib/ansible/module_utils/network/common/utils.py +++ b/lib/ansible/module_utils/network/common/utils.py @@ -594,6 +594,13 @@ def validate_config(spec, data): return validated_data +def search_obj_in_list(name, lst, key='name'): + for item in lst: + if item[key] == name: + return item + return None + + class Template: def __init__(self): diff --git a/lib/ansible/module_utils/network/ios/argspec/facts/facts.py b/lib/ansible/module_utils/network/ios/argspec/facts/facts.py index 6d09ec4892..00bc062b0f 100644 --- a/lib/ansible/module_utils/network/ios/argspec/facts/facts.py +++ b/lib/ansible/module_utils/network/ios/argspec/facts/facts.py @@ -22,7 +22,9 @@ class FactsArgs(object): 'all', '!all', 'interfaces', - '!interfaces' + '!interfaces', + 'l2_interfaces', + '!l2_interfaces' ] argument_spec = { diff --git a/lib/ansible/module_utils/network/ios/argspec/l2_interfaces/__init__.py b/lib/ansible/module_utils/network/ios/argspec/l2_interfaces/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/ansible/module_utils/network/ios/argspec/l2_interfaces/l2_interfaces.py b/lib/ansible/module_utils/network/ios/argspec/l2_interfaces/l2_interfaces.py new file mode 100644 index 0000000000..6d7e406107 --- /dev/null +++ b/lib/ansible/module_utils/network/ios/argspec/l2_interfaces/l2_interfaces.py @@ -0,0 +1,53 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# +""" +The arg spec for the ios_l2_interfaces module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class L2_InterfacesArgs(object): + + def __init__(self, **kwargs): + pass + + argument_spec = {'config': {'elements': 'dict', + 'options': {'name': {'type': 'str', 'required': True}, + 'access': {'type': 'dict', + 'options': {'vlan': {'type': 'int'}} + }, + 'trunk': {'type': 'dict', + 'options': {'allowed_vlans': {'type': 'list'}, + 'encapsulation': {'type': 'str', + 'choices': + ['dot1q', 'isl', 'negotiate']}, + 'native_vlan': {'type': 'int'}, + 'pruning_vlans': {'type': 'list'}} + }}, + 'type': 'list'}, + 'state': {'choices': ['merged', 'replaced', 'overridden', 'deleted'], + 'default': 'merged', + 'type': 'str'}} diff --git a/lib/ansible/module_utils/network/ios/config/l2_interfaces/__init__.py b/lib/ansible/module_utils/network/ios/config/l2_interfaces/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/ansible/module_utils/network/ios/config/l2_interfaces/l2_interfaces.py b/lib/ansible/module_utils/network/ios/config/l2_interfaces/l2_interfaces.py new file mode 100644 index 0000000000..703cf399fb --- /dev/null +++ b/lib/ansible/module_utils/network/ios/config/l2_interfaces/l2_interfaces.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The ios_l2_interfaces class +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to it's desired end-state is +created +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ansible.module_utils.network.common.cfg.base import ConfigBase +from ansible.module_utils.network.common.utils import to_list +from ansible.module_utils.network.ios.facts.facts import Facts +from ansible.module_utils.network.ios.utils.utils import dict_to_set +from ansible.module_utils.network.ios.utils.utils import remove_command_from_config_list, add_command_to_config_list +from ansible.module_utils.network.ios.utils.utils import filter_dict_having_none_value, remove_duplicate_interface + + +class L2_Interfaces(ConfigBase): + """ + The ios_l2_interfaces class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'l2_interfaces', + ] + + access_cmds = {'access_vlan': 'switchport access vlan'} + trunk_cmds = {'encapsulation': 'switchport trunk encapsulation', 'pruning_vlans': 'switchport trunk pruning vlan', + 'native_vlan': 'switchport trunk native vlan', 'allowed_vlans': 'switchport trunk allowed vlan'} + + def get_interfaces_facts(self): + """ Get the 'facts' (the current configuration) + :rtype: A dictionary + :returns: The current configuration as a dictionary + """ + facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources) + interfaces_facts = facts['ansible_network_resources'].get('l2_interfaces') + if not interfaces_facts: + return [] + + return interfaces_facts + + def execute_module(self): + """ Execute the module + :rtype: A dictionary + :returns: The result from moduel execution + """ + result = {'changed': False} + commands = [] + warnings = [] + existing_facts = self.get_interfaces_facts() + commands.extend(self.set_config(existing_facts)) + result['before'] = existing_facts + if commands: + if not self._module.check_mode: + self._connection.edit_config(commands) + result['changed'] = True + result['commands'] = commands + + interfaces_facts = self.get_interfaces_facts() + + if result['changed']: + result['after'] = interfaces_facts + result['warnings'] = warnings + return result + + def set_config(self, existing_facts): + """ Collect the configuration from the args passed to the module, + collect the current configuration (as a dict from facts) + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the deisred configuration + """ + + want = self._module.params['config'] + have = existing_facts + resp = self.set_state(want, have) + + return to_list(resp) + + def set_state(self, want, have): + """ Select the appropriate function based on the state provided + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the deisred configuration + """ + commands = [] + + state = self._module.params['state'] + if state == 'overridden': + commands = self._state_overridden(want, have, self._module) + elif state == 'deleted': + commands = self._state_deleted(want, have) + elif state == 'merged': + commands = self._state_merged(want, have, self._module) + elif state == 'replaced': + commands = self._state_replaced(want, have, self._module) + + return commands + + def _state_replaced(self, want, have, module): + """ The command generator when state is replaced + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param interface_type: interface type + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the deisred configuration + """ + commands = [] + + for interface in want: + for each in have: + if each['name'] == interface['name']: + break + else: + continue + have_dict = filter_dict_having_none_value(interface, each) + commands.extend(self._clear_config(dict(), have_dict)) + commands.extend(self._set_config(interface, each, module)) + # Remove the duplicate interface call + commands = remove_duplicate_interface(commands) + + return commands + + def _state_overridden(self, want, have, module): + """ The command generator when state is overridden + :param want: the desired configuration as a dictionary + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + + for each in have: + for interface in want: + if each['name'] == interface['name']: + break + else: + # We didn't find a matching desired state, which means we can + # pretend we recieved an empty desired state. + interface = dict(name=each['name']) + kwargs = {'want': interface, 'have': each} + commands.extend(self._clear_config(**kwargs)) + continue + have_dict = filter_dict_having_none_value(interface, each) + commands.extend(self._clear_config(dict(), have_dict)) + commands.extend(self._set_config(interface, each, module)) + # Remove the duplicate interface call + commands = remove_duplicate_interface(commands) + + return commands + + def _state_merged(self, want, have, module): + """ The command generator when state is merged + :param want: the additive configuration as a dictionary + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + + for interface in want: + for each in have: + if each['name'] == interface['name']: + break + else: + continue + commands.extend(self._set_config(interface, each, module)) + + return commands + + def _state_deleted(self, want, have): + """ The command generator when state is deleted + :param want: the objects from which the configuration should be removed + :param obj_in_have: the current configuration as a dictionary + :param interface_type: interface type + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = [] + + if want: + for interface in want: + for each in have: + if each['name'] == interface['name']: + break + else: + continue + interface = dict(name=interface['name']) + commands.extend(self._clear_config(interface, each)) + else: + for each in have: + want = dict() + commands.extend(self._clear_config(want, each)) + + return commands + + def _check_for_correct_vlan_range(self, vlan, module): + # Function to check if the VLAN range passed is Valid + for each in vlan: + vlan_range = each.split('-') + if len(vlan_range) > 1: + if vlan_range[0] < vlan_range[1]: + return True + else: + module.fail_json(msg='Command rejected: Bad VLAN list - end of range not larger than the' + ' start of range!') + else: + return True + + def _set_config(self, want, have, module): + # Set the interface config based on the want and have config + commands = [] + interface = 'interface ' + want['name'] + + # Get the diff b/w want and have + want_dict = dict_to_set(want) + have_dict = dict_to_set(have) + want_trunk = dict(want_dict).get('trunk') + have_trunk = dict(have_dict).get('trunk') + if want_trunk and have_trunk: + diff = set(tuple(dict(want_dict).get('trunk'))) - set(tuple(dict(have_dict).get('trunk'))) + else: + diff = want_dict - have_dict + + if diff: + diff = dict(diff) + + if diff.get('access'): + cmd = 'switchport access vlan {0}'.format(diff.get('access')[0][1]) + add_command_to_config_list(interface, cmd, commands) + + if want_trunk: + if diff.get('trunk'): + diff = dict(diff.get('trunk')) + if diff.get('encapsulation'): + cmd = self.trunk_cmds['encapsulation'] + ' {0}'.format(diff.get('encapsulation')) + add_command_to_config_list(interface, cmd, commands) + if diff.get('native_vlan'): + cmd = self.trunk_cmds['native_vlan'] + ' {0}'.format(diff.get('native_vlan')) + add_command_to_config_list(interface, cmd, commands) + allowed_vlans = diff.get('allowed_vlans') + pruning_vlans = diff.get('pruning_vlans') + + if allowed_vlans and self._check_for_correct_vlan_range(allowed_vlans, module): + allowed_vlans = ','.join(allowed_vlans) + cmd = self.trunk_cmds['allowed_vlans'] + ' {0}'.format(allowed_vlans) + add_command_to_config_list(interface, cmd, commands) + if pruning_vlans and self._check_for_correct_vlan_range(pruning_vlans, module): + pruning_vlans = ','.join(pruning_vlans) + cmd = self.trunk_cmds['pruning_vlans'] + ' {0}'.format(pruning_vlans) + add_command_to_config_list(interface, cmd, commands) + + return commands + + def _clear_config(self, want, have): + # Delete the interface config based on the want and have config + commands = [] + if want.get('name'): + interface = 'interface ' + want['name'] + else: + interface = 'interface ' + have['name'] + + if have.get('access') and want.get('access') is None: + remove_command_from_config_list(interface, L2_Interfaces.access_cmds['access_vlan'], commands) + elif have.get('access') and want.get('access'): + if have.get('access').get('vlan') != want.get('access').get('vlan'): + remove_command_from_config_list(interface, L2_Interfaces.access_cmds['access_vlan'], commands) + + if have.get('trunk') and want.get('trunk') is None: + # Check when no config is passed + if have.get('trunk').get('encapsulation'): + remove_command_from_config_list(interface, self.trunk_cmds['encapsulation'], commands) + if have.get('trunk').get('native_vlan'): + remove_command_from_config_list(interface, self.trunk_cmds['native_vlan'], commands) + if have.get('trunk').get('allowed_vlans'): + remove_command_from_config_list(interface, self.trunk_cmds['allowed_vlans'], commands) + if have.get('trunk').get('pruning_vlans'): + remove_command_from_config_list(interface, self.trunk_cmds['pruning_vlans'], commands) + elif have.get('trunk') and want.get('trunk'): + # Check when config is passed, also used in replaced and override state + if have.get('trunk').get('encapsulation')\ + and have.get('trunk').get('encapsulation') != want.get('trunk').get('encapsulation'): + remove_command_from_config_list(interface, self.trunk_cmds['encapsulation'], commands) + if have.get('trunk').get('native_vlan') \ + and have.get('trunk').get('native_vlan') != want.get('trunk').get('native_vlan'): + remove_command_from_config_list(interface, self.trunk_cmds['native_vlan'], commands) + if have.get('trunk').get('allowed_vlans') \ + and have.get('trunk').get('allowed_vlans') != want.get('trunk').get('allowed_vlans'): + remove_command_from_config_list(interface, self.trunk_cmds['allowed_vlans'], commands) + if have.get('trunk').get('pruning_vlans') \ + and have.get('trunk').get('pruning_vlans') != want.get('trunk').get('pruning_vlans'): + remove_command_from_config_list(interface, self.trunk_cmds['pruning_vlans'], commands) + + return commands diff --git a/lib/ansible/module_utils/network/ios/facts/facts.py b/lib/ansible/module_utils/network/ios/facts/facts.py index e321d54419..a8a9eae793 100644 --- a/lib/ansible/module_utils/network/ios/facts/facts.py +++ b/lib/ansible/module_utils/network/ios/facts/facts.py @@ -16,6 +16,7 @@ __metaclass__ = type from ansible.module_utils.network.ios.argspec.facts.facts import FactsArgs from ansible.module_utils.network.common.facts.facts import FactsBase from ansible.module_utils.network.ios.facts.interfaces.interfaces import InterfacesFacts +from ansible.module_utils.network.ios.facts.l2_interfaces.l2_interfaces import L2_InterfacesFacts from ansible.module_utils.network.ios.facts.legacy.base import Default, Hardware, Interfaces, Config @@ -28,6 +29,7 @@ FACT_LEGACY_SUBSETS = dict( FACT_RESOURCE_SUBSETS = dict( interfaces=InterfacesFacts, + l2_interfaces=L2_InterfacesFacts, ) diff --git a/lib/ansible/module_utils/network/ios/facts/l2_interfaces/__init__.py b/lib/ansible/module_utils/network/ios/facts/l2_interfaces/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/ansible/module_utils/network/ios/facts/l2_interfaces/l2_interfaces.py b/lib/ansible/module_utils/network/ios/facts/l2_interfaces/l2_interfaces.py new file mode 100644 index 0000000000..d175f31386 --- /dev/null +++ b/lib/ansible/module_utils/network/ios/facts/l2_interfaces/l2_interfaces.py @@ -0,0 +1,108 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The ios interfaces fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from copy import deepcopy +import re +from ansible.module_utils.network.common import utils +from ansible.module_utils.network.ios.utils.utils import get_interface_type, normalize_interface +from ansible.module_utils.network.ios.argspec.l2_interfaces.l2_interfaces import L2_InterfacesArgs + + +class L2_InterfacesFacts(object): + """ The ios l2 interfaces fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = L2_InterfacesArgs.argument_spec + spec = deepcopy(self.argument_spec) + if subspec: + if options: + facts_argument_spec = spec[subspec][options] + else: + facts_argument_spec = spec[subspec] + else: + facts_argument_spec = spec + + self.generated_spec = utils.generate_dict(facts_argument_spec) + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for interfaces + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = [] + + if not data: + data = connection.get('show running-config | section ^interface') + # operate on a collection of resource x + config = data.split('interface ') + for conf in config: + if conf: + obj = self.render_config(self.generated_spec, conf) + if obj: + objs.append(obj) + + facts = {} + if objs: + facts['l2_interfaces'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + for cfg in params['config']: + facts['l2_interfaces'].append(utils.remove_empties(cfg)) + ansible_facts['ansible_network_resources'].update(facts) + + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys from spec for null values + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + config = deepcopy(spec) + match = re.search(r'^(\S+)', conf) + intf = match.group(1) + + if get_interface_type(intf) == 'unknown': + return {} + + if intf.lower().startswith('gi'): + # populate the facts from the configuration + config['name'] = normalize_interface(intf) + + has_access = utils.parse_conf_arg(conf, 'switchport access vlan') + if has_access: + config["access"] = {"vlan": int(has_access)} + + trunk = dict() + trunk["encapsulation"] = utils.parse_conf_arg(conf, 'encapsulation') + native_vlan = utils.parse_conf_arg(conf, 'native vlan') + if native_vlan: + trunk["native_vlan"] = int(native_vlan) + allowed_vlan = utils.parse_conf_arg(conf, 'allowed vlan') + if allowed_vlan: + trunk["allowed_vlans"] = allowed_vlan.split(',') + pruning_vlan = utils.parse_conf_arg(conf, 'pruning vlan') + if pruning_vlan: + trunk['pruning_vlans'] = pruning_vlan.split(',') + + config['trunk'] = trunk + + return utils.remove_empties(config) diff --git a/lib/ansible/module_utils/network/ios/utils/utils.py b/lib/ansible/module_utils/network/ios/utils/utils.py index 3b1272c456..16a05602e6 100644 --- a/lib/ansible/module_utils/network/ios/utils/utils.py +++ b/lib/ansible/module_utils/network/ios/utils/utils.py @@ -32,6 +32,24 @@ def dict_to_set(sample_dict): test_dict = {} for k, v in iteritems(sample_dict): if v is not None: + if isinstance(v, list): + if isinstance(v[0], dict): + li = [] + for each in v: + for key, value in iteritems(each): + if isinstance(value, list): + each[key] = tuple(value) + li.extend(tuple(each.items())) + v = tuple(li) + else: + v = tuple(v) + elif isinstance(v, dict): + li = [] + for key, value in iteritems(v): + if isinstance(value, list): + v[key] = tuple(value) + li.extend(tuple(v.items())) + v = tuple(li) test_dict.update({k: v}) return_set = set(tuple(test_dict.items())) return return_set @@ -40,8 +58,15 @@ def dict_to_set(sample_dict): def filter_dict_having_none_value(want, have): # Generate dict with have dict value which is None in want dict test_dict = dict() + test_key_dict = dict() test_dict['name'] = want.get('name') for k, v in iteritems(want): + if isinstance(v, dict): + for key, value in iteritems(v): + if value is None: + dict_val = have.get(k).get(key) + test_key_dict.update({key: dict_val}) + test_dict.update({k: test_key_dict}) if v is None: val = have.get(k) test_dict.update({k: val}) @@ -61,13 +86,6 @@ def remove_duplicate_interface(commands): return set_cmd -def search_obj_in_list(name, lst): - for o in lst: - if o['name'] == name: - return o - return None - - def normalize_interface(name): """Return the normalized interface name """ diff --git a/lib/ansible/modules/network/ios/_ios_l2_interface.py b/lib/ansible/modules/network/ios/_ios_l2_interface.py new file mode 100644 index 0000000000..bcfcb880ea --- /dev/null +++ b/lib/ansible/modules/network/ios/_ios_l2_interface.py @@ -0,0 +1,499 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['deprecated'], + 'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: ios_l2_interface +extends_documentation_fragment: ios +version_added: "2.5" +short_description: Manage Layer-2 interface on Cisco IOS devices. +description: + - This module provides declarative management of Layer-2 interfaces on + Cisco IOS devices. +deprecated: + removed_in: '2.13' + alternative: ios_l2_interfaces + why: Newer and updated modules released with more functionality in Ansible 2.9 +author: + - Nathaniel Case (@Qalthos) +options: + name: + description: + - Full name of the interface excluding any logical + unit number, i.e. GigabitEthernet0/1. + required: true + aliases: ['interface'] + mode: + description: + - Mode in which interface needs to be configured. + default: access + choices: ['access', 'trunk'] + access_vlan: + description: + - Configure given VLAN in access port. + If C(mode=access), used as the access VLAN ID. + trunk_vlans: + description: + - List of VLANs to be configured in trunk port. + If C(mode=trunk), used as the VLAN range to ADD or REMOVE + from the trunk. + native_vlan: + description: + - Native VLAN to be configured in trunk port. + If C(mode=trunk), used as the trunk native VLAN ID. + trunk_allowed_vlans: + description: + - List of allowed VLANs in a given trunk port. + If C(mode=trunk), these are the only VLANs that will be + configured on the trunk, i.e. "2-10,15". + aggregate: + description: + - List of Layer-2 interface definitions. + state: + description: + - Manage the state of the Layer-2 Interface configuration. + default: present + choices: ['present','absent', 'unconfigured'] +""" + +EXAMPLES = """ +- name: Ensure GigabitEthernet0/5 is in its default l2 interface state + ios_l2_interface: + name: GigabitEthernet0/5 + state: unconfigured +- name: Ensure GigabitEthernet0/5 is configured for access vlan 20 + ios_l2_interface: + name: GigabitEthernet0/5 + mode: access + access_vlan: 20 +- name: Ensure GigabitEthernet0/5 only has vlans 5-10 as trunk vlans + ios_l2_interface: + name: GigabitEthernet0/5 + mode: trunk + native_vlan: 10 + trunk_vlans: 5-10 +- name: Ensure GigabitEthernet0/5 is a trunk port and ensure 2-50 are being tagged (doesn't mean others aren't also being tagged) + ios_l2_interface: + name: GigabitEthernet0/5 + mode: trunk + native_vlan: 10 + trunk_vlans: 2-50 +- name: Ensure these VLANs are not being tagged on the trunk + ios_l2_interface: + name: GigabitEthernet0/5 + mode: trunk + trunk_vlans: 51-4094 + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always, except for the platforms that use Netconf transport to manage the device. + type: list + sample: + - interface GigabitEthernet0/5 + - switchport access vlan 20 +""" + +import re +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible.module_utils.network.ios.ios import get_config, load_config, run_commands +from ansible.module_utils.network.ios.ios import ios_argument_spec + + +def get_interface_type(interface): + intf_type = 'unknown' + if interface.upper()[:2] in ('ET', 'GI', 'FA', 'TE', 'FO', 'HU', 'TWE'): + intf_type = 'ethernet' + elif interface.upper().startswith('VL'): + intf_type = 'svi' + elif interface.upper().startswith('LO'): + intf_type = 'loopback' + elif interface.upper()[:2] in ('MG', 'MA'): + intf_type = 'management' + elif interface.upper().startswith('PO'): + intf_type = 'portchannel' + elif interface.upper().startswith('NV'): + intf_type = 'nve' + + return intf_type + + +def is_switchport(name, module): + intf_type = get_interface_type(name) + + if intf_type in ('ethernet', 'portchannel'): + config = run_commands(module, ['show interface {0} switchport'.format(name)])[0] + match = re.search(r'Switchport: Enabled', config) + return bool(match) + return False + + +def interface_is_portchannel(name, module): + if get_interface_type(name) == 'ethernet': + config = run_commands(module, ['show run interface {0}'.format(name)])[0] + if any(c in config for c in ['channel group', 'channel-group']): + return True + return False + + +def get_switchport(name, module): + config = run_commands(module, ['show interface {0} switchport'.format(name)])[0] + mode = re.search(r'Administrative Mode: (?:.* )?(\w+)$', config, re.M) + access = re.search(r'Access Mode VLAN: (\d+)', config) + native = re.search(r'Trunking Native Mode VLAN: (\d+)', config) + trunk = re.search(r'Trunking VLANs Enabled: (.+)$', config, re.M) + if mode: + mode = mode.group(1) + if access: + access = access.group(1) + if native: + native = native.group(1) + if trunk: + trunk = trunk.group(1) + if trunk == 'ALL': + trunk = '1-4094' + + switchport_config = { + "interface": name, + "mode": mode, + "access_vlan": access, + "native_vlan": native, + "trunk_vlans": trunk, + } + + return switchport_config + + +def remove_switchport_config_commands(name, existing, proposed, module): + mode = proposed.get('mode') + commands = [] + command = None + + if mode == 'access': + av_check = existing.get('access_vlan') == proposed.get('access_vlan') + if av_check: + command = 'no switchport access vlan {0}'.format(existing.get('access_vlan')) + commands.append(command) + + elif mode == 'trunk': + # Supported Remove Scenarios for trunk_vlans_list + # 1) Existing: 1,2,3 Proposed: 1,2,3 - Remove all + # 2) Existing: 1,2,3 Proposed: 1,2 - Remove 1,2 Leave 3 + # 3) Existing: 1,2,3 Proposed: 2,3 - Remove 2,3 Leave 1 + # 4) Existing: 1,2,3 Proposed: 4,5,6 - None removed. + # 5) Existing: None Proposed: 1,2,3 - None removed. + existing_vlans = existing.get('trunk_vlans_list') + proposed_vlans = proposed.get('trunk_vlans_list') + vlans_to_remove = set(proposed_vlans).intersection(existing_vlans) + + if vlans_to_remove: + proposed_allowed_vlans = proposed.get('trunk_allowed_vlans') + remove_trunk_allowed_vlans = proposed.get('trunk_vlans', proposed_allowed_vlans) + command = 'switchport trunk allowed vlan remove {0}'.format(remove_trunk_allowed_vlans) + commands.append(command) + + native_check = existing.get('native_vlan') == proposed.get('native_vlan') + if native_check and proposed.get('native_vlan'): + command = 'no switchport trunk native vlan {0}'.format(existing.get('native_vlan')) + commands.append(command) + + if commands: + commands.insert(0, 'interface ' + name) + return commands + + +def get_switchport_config_commands(name, existing, proposed, module): + """Gets commands required to config a given switchport interface + """ + + proposed_mode = proposed.get('mode') + existing_mode = existing.get('mode') + commands = [] + command = None + + if proposed_mode != existing_mode: + if proposed_mode == 'trunk': + command = 'switchport mode trunk' + elif proposed_mode == 'access': + command = 'switchport mode access' + + if command: + commands.append(command) + + if proposed_mode == 'access': + av_check = str(existing.get('access_vlan')) == str(proposed.get('access_vlan')) + if not av_check: + command = 'switchport access vlan {0}'.format(proposed.get('access_vlan')) + commands.append(command) + + elif proposed_mode == 'trunk': + tv_check = existing.get('trunk_vlans_list') == proposed.get('trunk_vlans_list') + + if not tv_check: + if proposed.get('allowed'): + command = 'switchport trunk allowed vlan {0}'.format(proposed.get('trunk_allowed_vlans')) + commands.append(command) + + else: + existing_vlans = existing.get('trunk_vlans_list') + proposed_vlans = proposed.get('trunk_vlans_list') + vlans_to_add = set(proposed_vlans).difference(existing_vlans) + if vlans_to_add: + command = 'switchport trunk allowed vlan add {0}'.format(proposed.get('trunk_vlans')) + commands.append(command) + + native_check = str(existing.get('native_vlan')) == str(proposed.get('native_vlan')) + if not native_check and proposed.get('native_vlan'): + command = 'switchport trunk native vlan {0}'.format(proposed.get('native_vlan')) + commands.append(command) + + if commands: + commands.insert(0, 'interface ' + name) + return commands + + +def is_switchport_default(existing): + """Determines if switchport has a default config based on mode + Args: + existing (dict): existing switchport configuration from Ansible mod + Returns: + boolean: True if switchport has OOB Layer 2 config, i.e. + vlan 1 and trunk all and mode is access + """ + + c1 = str(existing['access_vlan']) == '1' + c2 = str(existing['native_vlan']) == '1' + c3 = existing['trunk_vlans'] == '1-4094' + c4 = existing['mode'] == 'access' + + default = c1 and c2 and c3 and c4 + + return default + + +def default_switchport_config(name): + commands = [] + commands.append('interface ' + name) + commands.append('switchport mode access') + commands.append('switch access vlan 1') + commands.append('switchport trunk native vlan 1') + commands.append('switchport trunk allowed vlan all') + return commands + + +def vlan_range_to_list(vlans): + result = [] + if vlans: + for part in vlans.split(','): + if part.lower() == 'none': + break + if part: + if '-' in part: + start, stop = (int(i) for i in part.split('-')) + result.extend(range(start, stop + 1)) + else: + result.append(int(part)) + return sorted(result) + + +def get_list_of_vlans(module): + config = run_commands(module, ['show vlan'])[0] + vlans = set() + + lines = config.strip().splitlines() + for line in lines: + line_parts = line.split() + if line_parts: + try: + int(line_parts[0]) + except ValueError: + continue + vlans.add(line_parts[0]) + + return list(vlans) + + +def flatten_list(commands): + flat_list = [] + for command in commands: + if isinstance(command, list): + flat_list.extend(command) + else: + flat_list.append(command) + return flat_list + + +def map_params_to_obj(module): + obj = [] + + aggregate = module.params.get('aggregate') + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + obj.append(item.copy()) + else: + obj.append({ + 'name': module.params['name'], + 'mode': module.params['mode'], + 'access_vlan': module.params['access_vlan'], + 'native_vlan': module.params['native_vlan'], + 'trunk_vlans': module.params['trunk_vlans'], + 'trunk_allowed_vlans': module.params['trunk_allowed_vlans'], + 'state': module.params['state'] + }) + + return obj + + +def main(): + """ main entry point for module execution + """ + element_spec = dict( + name=dict(type='str', aliases=['interface']), + mode=dict(choices=['access', 'trunk']), + access_vlan=dict(type='str'), + native_vlan=dict(type='str'), + trunk_vlans=dict(type='str'), + trunk_allowed_vlans=dict(type='str'), + state=dict(choices=['absent', 'present', 'unconfigured'], default='present') + ) + + aggregate_spec = deepcopy(element_spec) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec), + ) + + argument_spec.update(element_spec) + argument_spec.update(ios_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + mutually_exclusive=[['access_vlan', 'trunk_vlans'], + ['access_vlan', 'native_vlan'], + ['access_vlan', 'trunk_allowed_vlans']], + supports_check_mode=True) + + warnings = list() + commands = [] + result = {'changed': False, 'warnings': warnings} + + want = map_params_to_obj(module) + for w in want: + name = w['name'] + mode = w['mode'] + access_vlan = w['access_vlan'] + state = w['state'] + trunk_vlans = w['trunk_vlans'] + native_vlan = w['native_vlan'] + trunk_allowed_vlans = w['trunk_allowed_vlans'] + + args = dict(name=name, mode=mode, access_vlan=access_vlan, + native_vlan=native_vlan, trunk_vlans=trunk_vlans, + trunk_allowed_vlans=trunk_allowed_vlans) + + proposed = dict((k, v) for k, v in args.items() if v is not None) + + name = name.lower() + + if mode == 'access' and state == 'present' and not access_vlan: + module.fail_json(msg='access_vlan param is required when mode=access && state=present') + + if mode == 'trunk' and access_vlan: + module.fail_json(msg='access_vlan param not supported when using mode=trunk') + + if not is_switchport(name, module): + module.fail_json(msg='Ensure interface is configured to be a L2' + '\nport first before using this module. You can use' + '\nthe ios_interface module for this.') + + if interface_is_portchannel(name, module): + module.fail_json(msg='Cannot change L2 config on physical ' + '\nport because it is in a portchannel. ' + '\nYou should update the portchannel config.') + + # existing will never be null for Eth intfs as there is always a default + existing = get_switchport(name, module) + + # Safeguard check + # If there isn't an existing, something is wrong per previous comment + if not existing: + module.fail_json(msg='Make sure you are using the FULL interface name') + + if trunk_vlans or trunk_allowed_vlans: + if trunk_vlans: + trunk_vlans_list = vlan_range_to_list(trunk_vlans) + elif trunk_allowed_vlans: + trunk_vlans_list = vlan_range_to_list(trunk_allowed_vlans) + proposed['allowed'] = True + + existing_trunks_list = vlan_range_to_list((existing['trunk_vlans'])) + + existing['trunk_vlans_list'] = existing_trunks_list + proposed['trunk_vlans_list'] = trunk_vlans_list + + current_vlans = get_list_of_vlans(module) + + if state == 'present': + if access_vlan and access_vlan not in current_vlans: + module.fail_json(msg='You are trying to configure a VLAN' + ' on an interface that\ndoes not exist on the ' + ' switch yet!', vlan=access_vlan) + elif native_vlan and native_vlan not in current_vlans: + module.fail_json(msg='You are trying to configure a VLAN' + ' on an interface that\ndoes not exist on the ' + ' switch yet!', vlan=native_vlan) + else: + command = get_switchport_config_commands(name, existing, proposed, module) + commands.append(command) + elif state == 'unconfigured': + is_default = is_switchport_default(existing) + if not is_default: + command = default_switchport_config(name) + commands.append(command) + elif state == 'absent': + command = remove_switchport_config_commands(name, existing, proposed, module) + commands.append(command) + + if trunk_vlans or trunk_allowed_vlans: + existing.pop('trunk_vlans_list') + proposed.pop('trunk_vlans_list') + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + result['changed'] = True + load_config(module, cmds) + if 'configure' in cmds: + cmds.pop(0) + + result['commands'] = cmds + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/ios/ios_facts.py b/lib/ansible/modules/network/ios/ios_facts.py index e4a66b9a0b..7bf72fdef2 100644 --- a/lib/ansible/modules/network/ios/ios_facts.py +++ b/lib/ansible/modules/network/ios/ios_facts.py @@ -54,7 +54,7 @@ options: to a given subset. Possible values for this argument include all and the resources like interfaces, vlans etc. Can specify a list of values to include a larger subset. - choices: ['all', '!all', 'interfaces', '!interfaces'] + choices: ['all', '!all', 'interfaces', '!interfaces', 'l2_interfaces', '!l2_interfaces'] version_added: "2.9" """ @@ -90,6 +90,11 @@ EXAMPLES = """ ios_facts: gather_subset: min gather_network_resources: interfaces + +- name: Gather L2 interfaces resource and minimal legacy facts + ios_facts: + gather_subset: min + gather_network_resources: l2_interfaces """ RETURN = """ diff --git a/lib/ansible/modules/network/ios/ios_l2_interface.py b/lib/ansible/modules/network/ios/ios_l2_interface.py deleted file mode 100644 index 1de0b374bb..0000000000 --- a/lib/ansible/modules/network/ios/ios_l2_interface.py +++ /dev/null @@ -1,499 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2017, Ansible by Red Hat, inc -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'network'} - -DOCUMENTATION = """ ---- -module: ios_l2_interface -extends_documentation_fragment: ios -version_added: "2.5" -short_description: Manage Layer-2 interface on Cisco IOS devices. -description: - - This module provides declarative management of Layer-2 interfaces on - Cisco IOS devices. -author: - - Nathaniel Case (@Qalthos) -options: - name: - description: - - Full name of the interface excluding any logical - unit number, i.e. GigabitEthernet0/1. - required: true - aliases: ['interface'] - mode: - description: - - Mode in which interface needs to be configured. - default: access - choices: ['access', 'trunk'] - access_vlan: - description: - - Configure given VLAN in access port. - If C(mode=access), used as the access VLAN ID. - trunk_vlans: - description: - - List of VLANs to be configured in trunk port. - If C(mode=trunk), used as the VLAN range to ADD or REMOVE - from the trunk. - native_vlan: - description: - - Native VLAN to be configured in trunk port. - If C(mode=trunk), used as the trunk native VLAN ID. - trunk_allowed_vlans: - description: - - List of allowed VLANs in a given trunk port. - If C(mode=trunk), these are the only VLANs that will be - configured on the trunk, i.e. "2-10,15". - aggregate: - description: - - List of Layer-2 interface definitions. - state: - description: - - Manage the state of the Layer-2 Interface configuration. - default: present - choices: ['present','absent', 'unconfigured'] -""" - -EXAMPLES = """ -- name: Ensure GigabitEthernet0/5 is in its default l2 interface state - ios_l2_interface: - name: GigabitEthernet0/5 - state: unconfigured - -- name: Ensure GigabitEthernet0/5 is configured for access vlan 20 - ios_l2_interface: - name: GigabitEthernet0/5 - mode: access - access_vlan: 20 - -- name: Ensure GigabitEthernet0/5 only has vlans 5-10 as trunk vlans - ios_l2_interface: - name: GigabitEthernet0/5 - mode: trunk - native_vlan: 10 - trunk_vlans: 5-10 - -- name: Ensure GigabitEthernet0/5 is a trunk port and ensure 2-50 are being tagged (doesn't mean others aren't also being tagged) - ios_l2_interface: - name: GigabitEthernet0/5 - mode: trunk - native_vlan: 10 - trunk_vlans: 2-50 - -- name: Ensure these VLANs are not being tagged on the trunk - ios_l2_interface: - name: GigabitEthernet0/5 - mode: trunk - trunk_vlans: 51-4094 - state: absent -""" - -RETURN = """ -commands: - description: The list of configuration mode commands to send to the device - returned: always, except for the platforms that use Netconf transport to manage the device. - type: list - sample: - - interface GigabitEthernet0/5 - - switchport access vlan 20 -""" - -import re -from copy import deepcopy - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.network.common.utils import remove_default_spec -from ansible.module_utils.network.ios.ios import load_config, run_commands -from ansible.module_utils.network.ios.ios import ios_argument_spec - - -def get_interface_type(interface): - intf_type = 'unknown' - if interface.upper()[:2] in ('ET', 'GI', 'FA', 'TE', 'FO', 'HU', 'TWE'): - intf_type = 'ethernet' - elif interface.upper().startswith('VL'): - intf_type = 'svi' - elif interface.upper().startswith('LO'): - intf_type = 'loopback' - elif interface.upper()[:2] in ('MG', 'MA'): - intf_type = 'management' - elif interface.upper().startswith('PO'): - intf_type = 'portchannel' - elif interface.upper().startswith('NV'): - intf_type = 'nve' - - return intf_type - - -def is_switchport(name, module): - intf_type = get_interface_type(name) - - if intf_type in ('ethernet', 'portchannel'): - config = run_commands(module, ['show interface {0} switchport'.format(name)])[0] - match = re.search(r'Switchport: Enabled', config) - return bool(match) - return False - - -def interface_is_portchannel(name, module): - if get_interface_type(name) == 'ethernet': - config = run_commands(module, ['show run interface {0}'.format(name)])[0] - if any(c in config for c in ['channel group', 'channel-group']): - return True - return False - - -def get_switchport(name, module): - config = run_commands(module, ['show interface {0} switchport'.format(name)])[0] - mode = re.search(r'Administrative Mode: (?:.* )?(\w+)$', config, re.M) - access = re.search(r'Access Mode VLAN: (\d+)', config) - native = re.search(r'Trunking Native Mode VLAN: (\d+)', config) - trunk = re.search(r'Trunking VLANs Enabled: (.+)$', config, re.M) - if mode: - mode = mode.group(1) - if access: - access = access.group(1) - if native: - native = native.group(1) - if trunk: - trunk = trunk.group(1) - if trunk == 'ALL': - trunk = '1-4094' - - switchport_config = { - "interface": name, - "mode": mode, - "access_vlan": access, - "native_vlan": native, - "trunk_vlans": trunk, - } - - return switchport_config - - -def remove_switchport_config_commands(name, existing, proposed, module): - mode = proposed.get('mode') - commands = [] - command = None - - if mode == 'access': - av_check = existing.get('access_vlan') == proposed.get('access_vlan') - if av_check: - command = 'no switchport access vlan {0}'.format(existing.get('access_vlan')) - commands.append(command) - - elif mode == 'trunk': - # Supported Remove Scenarios for trunk_vlans_list - # 1) Existing: 1,2,3 Proposed: 1,2,3 - Remove all - # 2) Existing: 1,2,3 Proposed: 1,2 - Remove 1,2 Leave 3 - # 3) Existing: 1,2,3 Proposed: 2,3 - Remove 2,3 Leave 1 - # 4) Existing: 1,2,3 Proposed: 4,5,6 - None removed. - # 5) Existing: None Proposed: 1,2,3 - None removed. - existing_vlans = existing.get('trunk_vlans_list') - proposed_vlans = proposed.get('trunk_vlans_list') - vlans_to_remove = set(proposed_vlans).intersection(existing_vlans) - - if vlans_to_remove: - proposed_allowed_vlans = proposed.get('trunk_allowed_vlans') - remove_trunk_allowed_vlans = proposed.get('trunk_vlans', proposed_allowed_vlans) - command = 'switchport trunk allowed vlan remove {0}'.format(remove_trunk_allowed_vlans) - commands.append(command) - - native_check = existing.get('native_vlan') == proposed.get('native_vlan') - if native_check and proposed.get('native_vlan'): - command = 'no switchport trunk native vlan {0}'.format(existing.get('native_vlan')) - commands.append(command) - - if commands: - commands.insert(0, 'interface ' + name) - return commands - - -def get_switchport_config_commands(name, existing, proposed, module): - """Gets commands required to config a given switchport interface - """ - - proposed_mode = proposed.get('mode') - existing_mode = existing.get('mode') - commands = [] - command = None - - if proposed_mode != existing_mode: - if proposed_mode == 'trunk': - command = 'switchport mode trunk' - elif proposed_mode == 'access': - command = 'switchport mode access' - - if command: - commands.append(command) - - if proposed_mode == 'access': - av_check = str(existing.get('access_vlan')) == str(proposed.get('access_vlan')) - if not av_check: - command = 'switchport access vlan {0}'.format(proposed.get('access_vlan')) - commands.append(command) - - elif proposed_mode == 'trunk': - tv_check = existing.get('trunk_vlans_list') == proposed.get('trunk_vlans_list') - - if not tv_check: - if proposed.get('allowed'): - command = 'switchport trunk allowed vlan {0}'.format(proposed.get('trunk_allowed_vlans')) - commands.append(command) - - else: - existing_vlans = existing.get('trunk_vlans_list') - proposed_vlans = proposed.get('trunk_vlans_list') - vlans_to_add = set(proposed_vlans).difference(existing_vlans) - if vlans_to_add: - command = 'switchport trunk allowed vlan add {0}'.format(proposed.get('trunk_vlans')) - commands.append(command) - - native_check = str(existing.get('native_vlan')) == str(proposed.get('native_vlan')) - if not native_check and proposed.get('native_vlan'): - command = 'switchport trunk native vlan {0}'.format(proposed.get('native_vlan')) - commands.append(command) - - if commands: - commands.insert(0, 'interface ' + name) - return commands - - -def is_switchport_default(existing): - """Determines if switchport has a default config based on mode - Args: - existing (dict): existing switchport configuration from Ansible mod - Returns: - boolean: True if switchport has OOB Layer 2 config, i.e. - vlan 1 and trunk all and mode is access - """ - - c1 = str(existing['access_vlan']) == '1' - c2 = str(existing['native_vlan']) == '1' - c3 = existing['trunk_vlans'] == '1-4094' - c4 = existing['mode'] == 'access' - - default = c1 and c2 and c3 and c4 - - return default - - -def default_switchport_config(name): - commands = [] - commands.append('interface ' + name) - commands.append('switchport mode access') - commands.append('switch access vlan 1') - commands.append('switchport trunk native vlan 1') - commands.append('switchport trunk allowed vlan all') - return commands - - -def vlan_range_to_list(vlans): - result = [] - if vlans: - for part in vlans.split(','): - if part.lower() == 'none': - break - if part: - if '-' in part: - start, stop = (int(i) for i in part.split('-')) - result.extend(range(start, stop + 1)) - else: - result.append(int(part)) - return sorted(result) - - -def get_list_of_vlans(module): - config = run_commands(module, ['show vlan'])[0] - vlans = set() - - lines = config.strip().splitlines() - for line in lines: - line_parts = line.split() - if line_parts: - try: - int(line_parts[0]) - except ValueError: - continue - vlans.add(line_parts[0]) - - return list(vlans) - - -def flatten_list(commands): - flat_list = [] - for command in commands: - if isinstance(command, list): - flat_list.extend(command) - else: - flat_list.append(command) - return flat_list - - -def map_params_to_obj(module): - obj = [] - - aggregate = module.params.get('aggregate') - if aggregate: - for item in aggregate: - for key in item: - if item.get(key) is None: - item[key] = module.params[key] - - obj.append(item.copy()) - else: - obj.append({ - 'name': module.params['name'], - 'mode': module.params['mode'], - 'access_vlan': module.params['access_vlan'], - 'native_vlan': module.params['native_vlan'], - 'trunk_vlans': module.params['trunk_vlans'], - 'trunk_allowed_vlans': module.params['trunk_allowed_vlans'], - 'state': module.params['state'] - }) - - return obj - - -def main(): - """ main entry point for module execution - """ - element_spec = dict( - name=dict(type='str', aliases=['interface']), - mode=dict(choices=['access', 'trunk']), - access_vlan=dict(type='str'), - native_vlan=dict(type='str'), - trunk_vlans=dict(type='str'), - trunk_allowed_vlans=dict(type='str'), - state=dict(choices=['absent', 'present', 'unconfigured'], default='present') - ) - - aggregate_spec = deepcopy(element_spec) - - # remove default in aggregate spec, to handle common arguments - remove_default_spec(aggregate_spec) - - argument_spec = dict( - aggregate=dict(type='list', elements='dict', options=aggregate_spec), - ) - - argument_spec.update(element_spec) - argument_spec.update(ios_argument_spec) - - module = AnsibleModule(argument_spec=argument_spec, - mutually_exclusive=[['access_vlan', 'trunk_vlans'], - ['access_vlan', 'native_vlan'], - ['access_vlan', 'trunk_allowed_vlans']], - supports_check_mode=True) - - warnings = list() - commands = [] - result = {'changed': False, 'warnings': warnings} - - want = map_params_to_obj(module) - for w in want: - name = w['name'] - mode = w['mode'] - access_vlan = w['access_vlan'] - state = w['state'] - trunk_vlans = w['trunk_vlans'] - native_vlan = w['native_vlan'] - trunk_allowed_vlans = w['trunk_allowed_vlans'] - - args = dict(name=name, mode=mode, access_vlan=access_vlan, - native_vlan=native_vlan, trunk_vlans=trunk_vlans, - trunk_allowed_vlans=trunk_allowed_vlans) - - proposed = dict((k, v) for k, v in args.items() if v is not None) - - name = name.lower() - - if mode == 'access' and state == 'present' and not access_vlan: - module.fail_json(msg='access_vlan param is required when mode=access && state=present') - - if mode == 'trunk' and access_vlan: - module.fail_json(msg='access_vlan param not supported when using mode=trunk') - - if not is_switchport(name, module): - module.fail_json(msg='Ensure interface is configured to be a L2' - '\nport first before using this module. You can use' - '\nthe ios_interface module for this.') - - if interface_is_portchannel(name, module): - module.fail_json(msg='Cannot change L2 config on physical ' - '\nport because it is in a portchannel. ' - '\nYou should update the portchannel config.') - - # existing will never be null for Eth intfs as there is always a default - existing = get_switchport(name, module) - - # Safeguard check - # If there isn't an existing, something is wrong per previous comment - if not existing: - module.fail_json(msg='Make sure you are using the FULL interface name') - - if trunk_vlans or trunk_allowed_vlans: - if trunk_vlans: - trunk_vlans_list = vlan_range_to_list(trunk_vlans) - elif trunk_allowed_vlans: - trunk_vlans_list = vlan_range_to_list(trunk_allowed_vlans) - proposed['allowed'] = True - - existing_trunks_list = vlan_range_to_list((existing['trunk_vlans'])) - - existing['trunk_vlans_list'] = existing_trunks_list - proposed['trunk_vlans_list'] = trunk_vlans_list - - current_vlans = get_list_of_vlans(module) - - if state == 'present': - if access_vlan and access_vlan not in current_vlans: - module.fail_json(msg='You are trying to configure a VLAN' - ' on an interface that\ndoes not exist on the ' - ' switch yet!', vlan=access_vlan) - elif native_vlan and native_vlan not in current_vlans: - module.fail_json(msg='You are trying to configure a VLAN' - ' on an interface that\ndoes not exist on the ' - ' switch yet!', vlan=native_vlan) - else: - command = get_switchport_config_commands(name, existing, proposed, module) - commands.append(command) - elif state == 'unconfigured': - is_default = is_switchport_default(existing) - if not is_default: - command = default_switchport_config(name) - commands.append(command) - elif state == 'absent': - command = remove_switchport_config_commands(name, existing, proposed, module) - commands.append(command) - - if trunk_vlans or trunk_allowed_vlans: - existing.pop('trunk_vlans_list') - proposed.pop('trunk_vlans_list') - - cmds = flatten_list(commands) - if cmds: - if module.check_mode: - module.exit_json(changed=True, commands=cmds) - else: - result['changed'] = True - load_config(module, cmds) - if 'configure' in cmds: - cmds.pop(0) - - result['commands'] = cmds - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/network/ios/ios_l2_interfaces.py b/lib/ansible/modules/network/ios/ios_l2_interfaces.py new file mode 100644 index 0000000000..8c9f6b838d --- /dev/null +++ b/lib/ansible/modules/network/ios/ios_l2_interfaces.py @@ -0,0 +1,359 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for ios_l2_interfaces +""" + + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + +DOCUMENTATION = """ +--- +module: ios_l2_interfaces +version_added: 2.9 +short_description: Manage Layer-2 interface on Cisco IOS devices. +description: This module provides declarative management of Layer-2 interface on Cisco IOS devices. +author: Sumit Jaiswal (@justjais) +notes: + - Tested against Cisco IOSv Version 15.2 on VIRL + - This module works with connection C(network_cli). + See L(IOS Platform Options,../network/user_guide/platform_ios.html). +options: + config: + description: A dictionary of Layer-2 interface options + type: list + elements: dict + suboptions: + name: + description: + - Full name of the interface excluding any logical unit number, i.e. GigabitEthernet0/1. + type: str + required: True + access: + description: + - Switchport mode access command to configure the interface as a layer 2 access. + type: dict + suboptions: + vlan: + description: + - Configure given VLAN in access port. It's used as the access VLAN ID. + type: int + trunk: + description: + - Switchport mode trunk command to configure the interface as a Layer 2 trunk. + Note The encapsulation is always set to dot1q. + type: dict + suboptions: + allowed_vlans: + description: + - List of allowed VLANs in a given trunk port. These are the only VLANs that will be + configured on the trunk. + type: list + native_vlan: + description: + - Native VLAN to be configured in trunk port. It's used as the trunk native VLAN ID. + type: int + encapsulation: + description: + - Trunking encapsulation when interface is in trunking mode. + choices: ['dot1q','isl','negotiate'] + type: str + pruning_vlans: + description: + - Pruning VLAN to be configured in trunk port. It's used as the trunk pruning VLAN ID. + type: list + state: + choices: + - merged + - replaced + - overridden + - deleted + default: merged + description: + - The state the configuration should be left in + type: str +""" + +EXAMPLES = """ +--- + +# Using merged + +# Before state: +# ------------- +# +# viosl2#show running-config | section ^interface +# interface GigabitEthernet0/1 +# description Configured by Ansible +# negotiation auto +# interface GigabitEthernet0/2 +# description This is test +# switchport access vlan 20 +# media-type rj45 +# negotiation auto + +- name: Merge provided configuration with device configuration + ios_l2_interfaces: + config: + - name: GigabitEthernet0/1 + access: + vlan: 10 + - name: GigabitEthernet0/2 + trunk: + allowed_vlan: 10-20, 40 + native_vlan: 20 + pruning_vlan: 10,20 + encapsulation: dot1q + state: merged + +# After state: +# ------------ +# +# viosl2#show running-config | section ^interface +# interface GigabitEthernet0/1 +# description Configured by Ansible +# switchport access vlan 10 +# negotiation auto +# interface GigabitEthernet0/2 +# description This is test +# switchport trunk allowed vlan 10-20,40 +# switchport trunk encapsulation dot1q +# switchport trunk native vlan 20 +# switchport trunk pruning vlan 10,20 +# media-type rj45 +# negotiation auto + +# Using replaced + +# Before state: +# ------------- +# +# viosl2#show running-config | section ^interface +# interface GigabitEthernet0/1 +# description Configured by Ansible +# switchport access vlan 20 +# negotiation auto +# interface GigabitEthernet0/2 +# description This is test +# switchport access vlan 20 +# media-type rj45 +# negotiation auto + +- name: Replaces device configuration of listed l2 interfaces with provided configuration + ios_l2_interfaces: + config: + - name: GigabitEthernet0/2 + trunk: + - allowed_vlan: 20-25,40 + native_vlan: 20 + pruning_vlan: 10 + encapsulation: isl + state: replaced + +# After state: +# ------------- +# +# viosl2#show running-config | section ^interface +# interface GigabitEthernet0/1 +# description Configured by Ansible +# switchport access vlan 20 +# negotiation auto +# interface GigabitEthernet0/2 +# description This is test +# switchport trunk allowed vlan 20-25,40 +# switchport trunk encapsulation isl +# switchport trunk native vlan 20 +# switchport trunk pruning vlan 10 +# media-type rj45 +# negotiation auto + +# Using overridden + +# Before state: +# ------------- +# +# viosl2#show running-config | section ^interface +# interface GigabitEthernet0/1 +# description Configured by Ansible +# switchport trunk encapsulation dot1q +# switchport trunk native vlan 20 +# negotiation auto +# interface GigabitEthernet0/2 +# description This is test +# switchport access vlan 20 +# switchport trunk encapsulation dot1q +# switchport trunk native vlan 20 +# media-type rj45 +# negotiation auto + +- name: Override device configuration of all l2 interfaces with provided configuration + ios_l2_interfaces: + config: + - name: GigabitEthernet0/2 + access: + vlan: 20 + state: overridden + +# After state: +# ------------- +# +# viosl2#show running-config | section ^interface +# interface GigabitEthernet0/1 +# description Configured by Ansible +# negotiation auto +# interface GigabitEthernet0/2 +# description This is test +# switchport access vlan 20 +# media-type rj45 +# negotiation auto + +# Using Deleted + +# Before state: +# ------------- +# +# viosl2#show running-config | section ^interface +# interface GigabitEthernet0/1 +# description Configured by Ansible +# switchport access vlan 20 +# negotiation auto +# interface GigabitEthernet0/2 +# description This is test +# switchport access vlan 20 +# switchport trunk allowed vlan 20-40,60,80 +# switchport trunk encapsulation dot1q +# switchport trunk native vlan 10 +# switchport trunk pruning vlan 10 +# media-type rj45 +# negotiation auto + +- name: Delete IOS L2 interfaces as in given arguments + ios_l2_interfaces: + config: + - name: GigabitEthernet0/1 + state: deleted + +# After state: +# ------------- +# +# viosl2#show running-config | section ^interface +# interface GigabitEthernet0/1 +# description Configured by Ansible +# negotiation auto +# interface GigabitEthernet0/2 +# description This is test +# switchport access vlan 20 +# switchport trunk allowed vlan 20-40,60,80 +# switchport trunk encapsulation dot1q +# switchport trunk native vlan 10 +# switchport trunk pruning vlan 10 +# media-type rj45 +# negotiation auto + + +# Using Deleted without any config passed +#"(NOTE: This will delete all of configured resource module attributes from each configured interface)" + +# Before state: +# ------------- +# +# viosl2#show running-config | section ^interface +# interface GigabitEthernet0/1 +# description Configured by Ansible +# switchport access vlan 20 +# negotiation auto +# interface GigabitEthernet0/2 +# description This is test +# switchport access vlan 20 +# switchport trunk allowed vlan 20-40,60,80 +# switchport trunk encapsulation dot1q +# switchport trunk native vlan 10 +# switchport trunk pruning vlan 10 +# media-type rj45 +# negotiation auto + +- name: Delete IOS L2 interfaces as in given arguments + ios_l2_interfaces: + state: deleted + +# After state: +# ------------- +# +# viosl2#show running-config | section ^interface +# interface GigabitEthernet0/1 +# description Configured by Ansible +# negotiation auto +# interface GigabitEthernet0/2 +# description This is test +# media-type rj45 +# negotiation auto + +""" + +RETURN = """ +before: + description: The configuration prior to the model invocation + returned: always + type: list + sample: The configuration returned will always be in the same format of the paramters above. +after: + description: The resulting configuration model invocation + returned: when changed + type: list + sample: The configuration returned will always be in the same format of the paramters above. +commands: + description: The set of commands pushed to the remote device + returned: always + type: list + sample: ['interface GigabitEthernet0/1', 'switchport access vlan 20'] +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.ios.argspec.l2_interfaces.l2_interfaces import L2_InterfacesArgs +from ansible.module_utils.network.ios.config.l2_interfaces.l2_interfaces import L2_Interfaces + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=L2_InterfacesArgs.argument_spec, + supports_check_mode=True) + + result = L2_Interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/ios_l2_interfaces/defaults/main.yaml b/test/integration/targets/ios_l2_interfaces/defaults/main.yaml new file mode 100644 index 0000000000..164afead28 --- /dev/null +++ b/test/integration/targets/ios_l2_interfaces/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/test/integration/targets/ios_l2_interfaces/meta/main.yaml b/test/integration/targets/ios_l2_interfaces/meta/main.yaml new file mode 100644 index 0000000000..32cf5dda7e --- /dev/null +++ b/test/integration/targets/ios_l2_interfaces/meta/main.yaml @@ -0,0 +1 @@ +dependencies: [] diff --git a/test/integration/targets/ios_l2_interfaces/tasks/cli.yaml b/test/integration/targets/ios_l2_interfaces/tasks/cli.yaml new file mode 100644 index 0000000000..55d0413040 --- /dev/null +++ b/test/integration/targets/ios_l2_interfaces/tasks/cli.yaml @@ -0,0 +1,24 @@ +--- +- name: Collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + use_regex: true + register: test_cases + delegate_to: localhost + +- name: Set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + delegate_to: localhost + +- name: Get the IOS version + ios_facts: + gather_subset: all + +- name: Run test case (connection=network_cli) + include: "{{ test_case_to_run }}" + vars: + ansible_connection: network_cli + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/ios_l2_interfaces/tasks/main.yaml b/test/integration/targets/ios_l2_interfaces/tasks/main.yaml new file mode 100644 index 0000000000..415c99d8b1 --- /dev/null +++ b/test/integration/targets/ios_l2_interfaces/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/ios_l2_interfaces/tests/cli/_populate_config.yaml b/test/integration/targets/ios_l2_interfaces/tests/cli/_populate_config.yaml new file mode 100644 index 0000000000..d6f07fe3bd --- /dev/null +++ b/test/integration/targets/ios_l2_interfaces/tests/cli/_populate_config.yaml @@ -0,0 +1,14 @@ +--- +- name: Populate Config + cli_config: + config: "{{ lines }}" + vars: + lines: | + interface GigabitEthernet 0/1 + switchport access vlan 10 + interface GigabitEthernet 0/2 + switchport trunk encapsulation dot1q + switchport trunk native vlan 10 + switchport trunk allowed vlan 10-20,40 + switchport trunk pruning vlan 10,20 + when: ansible_net_version != "15.6(2)T" diff --git a/test/integration/targets/ios_l2_interfaces/tests/cli/_remove_config.yaml b/test/integration/targets/ios_l2_interfaces/tests/cli/_remove_config.yaml new file mode 100644 index 0000000000..7072baa190 --- /dev/null +++ b/test/integration/targets/ios_l2_interfaces/tests/cli/_remove_config.yaml @@ -0,0 +1,19 @@ +--- +- name: Remove Config + cli_config: + config: "{{ lines }}" + vars: + lines: | + interface GigabitEthernet 0/1 + no switchport access vlan + no switchport trunk encapsulation + no switchport trunk native vlan + no switchport trunk allowed vlan + no switchport trunk pruning vlan + interface GigabitEthernet 0/2 + no switchport access vlan + no switchport trunk encapsulation + no switchport trunk native vlan + no switchport trunk allowed vlan + no switchport trunk pruning vlan + when: ansible_net_version != "15.6(2)T" diff --git a/test/integration/targets/ios_l2_interfaces/tests/cli/deleted.yaml b/test/integration/targets/ios_l2_interfaces/tests/cli/deleted.yaml new file mode 100644 index 0000000000..7f460e6b32 --- /dev/null +++ b/test/integration/targets/ios_l2_interfaces/tests/cli/deleted.yaml @@ -0,0 +1,41 @@ +--- +- debug: + msg: "Start Deleted integration state for ios_l2_interfaces ansible_connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _populate_config.yaml + +- block: + - name: Delete L2 attributes from all configured interfaces + ios_l2_interfaces: &deleted + state: deleted + register: result + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ deleted['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + + - name: Assert that before dicts are correctly generated + assert: + that: + - "{{ deleted['before'] | symmetric_difference(result['before']) | length == 0 }}" + + - name: Assert that after dict is correctly generated + assert: + that: + - "{{ deleted['after'] | symmetric_difference(result['after']) | length == 0 }}" + + - name: Delete attributes of all configured interfaces (IDEMPOTENT) + ios_l2_interfaces: *deleted + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result.changed == false" + when: ansible_net_version != "15.6(2)T" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_l2_interfaces/tests/cli/merged.yaml b/test/integration/targets/ios_l2_interfaces/tests/cli/merged.yaml new file mode 100644 index 0000000000..528bba52c9 --- /dev/null +++ b/test/integration/targets/ios_l2_interfaces/tests/cli/merged.yaml @@ -0,0 +1,49 @@ +--- +- debug: + msg: "START Merged ios_l2_interfaces state for integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge provided configuration with device configuration + ios_l2_interfaces: &merged + config: + - name: GigabitEthernet0/1 + access: + vlan: 30 + - name: GigabitEthernet0/2 + trunk: + allowed_vlans: 15-20,40 + native_vlan: 20 + pruning_vlans: 10,20 + encapsulation: dot1q + state: merged + register: result + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ merged['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + + - name: Assert that before dicts are correctly generated + assert: + that: + - "{{ merged['before'] | symmetric_difference(result['before']) | length == 0 }}" + + - name: Assert that after dict is correctly generated + assert: + that: + - "{{ merged['after'] | symmetric_difference(result['after']) | length == 0 }}" + + - name: Merge provided configuration with device configuration (IDEMPOTENT) + ios_l2_interfaces: *merged + register: result + + - name: Assert that the previous task was idempotent + assert: + that: + - "result['changed'] == false" + when: ansible_net_version != "15.6(2)T" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_l2_interfaces/tests/cli/overridden.yaml b/test/integration/targets/ios_l2_interfaces/tests/cli/overridden.yaml new file mode 100644 index 0000000000..66a60653d7 --- /dev/null +++ b/test/integration/targets/ios_l2_interfaces/tests/cli/overridden.yaml @@ -0,0 +1,47 @@ +--- +- debug: + msg: "START Overridden ios_l2_interfaces state for integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _populate_config.yaml + +- block: + - name: Override device L2 configuration from all interfaces with provided configuration + ios_l2_interfaces: &overridden + config: + - name: GigabitEthernet0/2 + trunk: + allowed_vlans: 30-35,40 + native_vlan: 30 + encapsulation: isl + state: overridden + register: result + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ overridden['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + + - name: Assert that before dicts are correctly generated + assert: + that: + - "{{ overridden['before'] | symmetric_difference(result['before']) | length == 0 }}" + + - name: Assert that after dict is correctly generated + assert: + that: + - "{{ overridden['after'] | symmetric_difference(result['after']) | length == 0 }}" + + - name: Override device L2 configuration from all interfaces with provided configuration (IDEMPOTENT) + ios_l2_interfaces: *overridden + register: result + + - name: Assert that task was idempotent + assert: + that: + - "result['changed'] == false" + when: ansible_net_version != "15.6(2)T" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_l2_interfaces/tests/cli/replaced.yaml b/test/integration/targets/ios_l2_interfaces/tests/cli/replaced.yaml new file mode 100644 index 0000000000..cbd7e95741 --- /dev/null +++ b/test/integration/targets/ios_l2_interfaces/tests/cli/replaced.yaml @@ -0,0 +1,50 @@ +--- +- debug: + msg: "START Replaced ios_l2_interfaces state for integration tests on connection={{ ansible_connection }}" + +- include_tasks: _remove_config.yaml + +- include_tasks: _populate_config.yaml + +- block: + - name: Replaces device L2 configuration from listed interfaces with provided configuration + ios_l2_interfaces: &replaced + config: + - name: GigabitEthernet0/1 + access: + vlan: 40 + - name: GigabitEthernet0/2 + trunk: + native_vlan: 20 + pruning_vlans: 10-20,30 + encapsulation: dot1q + state: replaced + register: result + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ replaced['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + + - name: Assert that before dicts are correctly generated + assert: + that: + - "{{ replaced['before'] | symmetric_difference(result['before']) | length == 0 }}" + + - name: Assert that after dict is correctly generated + assert: + that: + - "{{ replaced['after'] | symmetric_difference(result['after']) | length == 0 }}" + + - name: Replaces device L2 configuration from listed interfaces with provided configuration (IDEMPOTENT) + ios_l2_interfaces: *replaced + register: result + + - name: Assert that task was idempotent + assert: + that: + - "result['changed'] == false" + when: ansible_net_version != "15.6(2)T" + + always: + - include_tasks: _remove_config.yaml diff --git a/test/integration/targets/ios_l2_interfaces/vars/main.yaml b/test/integration/targets/ios_l2_interfaces/vars/main.yaml new file mode 100644 index 0000000000..6d195bb533 --- /dev/null +++ b/test/integration/targets/ios_l2_interfaces/vars/main.yaml @@ -0,0 +1,137 @@ +--- +merged: + before: + - name: GigabitEthernet0/0 + - name: GigabitEthernet0/1 + - name: GigabitEthernet0/2 + + commands: + - "interface GigabitEthernet0/1" + - "switchport access vlan 30" + - "interface GigabitEthernet0/2" + - "switchport trunk encapsulation dot1q" + - "switchport trunk native vlan 20" + - "switchport trunk allowed vlan 15-20,40" + - "switchport trunk pruning vlan 10,20" + + after: + - name: GigabitEthernet0/0 + - access: + vlan: 30 + name: GigabitEthernet0/1 + - name: GigabitEthernet0/2 + trunk: + allowed_vlans: + - 15-20 + - '40' + encapsulation: dot1q + native_vlan: 20 + pruning_vlans: + - '10' + - '20' + +replaced: + before: + - name: GigabitEthernet0/0 + - access: + vlan: 10 + name: GigabitEthernet0/1 + - name: GigabitEthernet0/2 + trunk: + allowed_vlans: + - 10-20 + - '40' + encapsulation: dot1q + native_vlan: 10 + pruning_vlans: + - '10' + - '20' + + commands: + - "interface GigabitEthernet0/1" + - "switchport access vlan 40" + - "interface GigabitEthernet0/2" + - "no switchport trunk allowed vlan" + - "switchport trunk native vlan 20" + - "switchport trunk pruning vlan 10-20,30" + + after: + - name: GigabitEthernet0/0 + - access: + vlan: 40 + name: GigabitEthernet0/1 + - name: GigabitEthernet0/2 + trunk: + encapsulation: dot1q + native_vlan: 20 + pruning_vlans: + - 10-20 + - '30' + +overridden: + before: + - name: GigabitEthernet0/0 + - access: + vlan: 10 + name: GigabitEthernet0/1 + - name: GigabitEthernet0/2 + trunk: + allowed_vlans: + - 10-20 + - '40' + encapsulation: dot1q + native_vlan: 10 + pruning_vlans: + - '10' + - '20' + + commands: + - "interface GigabitEthernet0/1" + - "no switchport access vlan" + - "interface GigabitEthernet0/2" + - "no switchport trunk pruning vlan" + - "switchport trunk encapsulation isl" + - "switchport trunk native vlan 30" + - "switchport trunk allowed vlan 30-35,40" + + after: + - name: GigabitEthernet0/0 + - name: GigabitEthernet0/1 + - name: GigabitEthernet0/2 + trunk: + allowed_vlans: + - 30-35 + - '40' + encapsulation: isl + native_vlan: 30 + +deleted: + before: + - name: GigabitEthernet0/0 + - access: + vlan: 10 + name: GigabitEthernet0/1 + - name: GigabitEthernet0/2 + trunk: + allowed_vlans: + - 10-20 + - '40' + encapsulation: dot1q + native_vlan: 10 + pruning_vlans: + - '10' + - '20' + + commands: + - "interface GigabitEthernet0/1" + - "no switchport access vlan" + - "interface GigabitEthernet0/2" + - "no switchport trunk encapsulation" + - "no switchport trunk native vlan" + - "no switchport trunk allowed vlan" + - "no switchport trunk pruning vlan" + + after: + - name: GigabitEthernet0/0 + - name: GigabitEthernet0/1 + - name: GigabitEthernet0/2 diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index 0415dc38f1..b59a1d6184 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -4110,12 +4110,12 @@ lib/ansible/modules/network/ios/_ios_interface.py validate-modules:E326 lib/ansible/modules/network/ios/_ios_interface.py validate-modules:E337 lib/ansible/modules/network/ios/_ios_interface.py validate-modules:E338 lib/ansible/modules/network/ios/_ios_interface.py validate-modules:E340 -lib/ansible/modules/network/ios/ios_l2_interface.py validate-modules:E322 -lib/ansible/modules/network/ios/ios_l2_interface.py validate-modules:E324 -lib/ansible/modules/network/ios/ios_l2_interface.py validate-modules:E326 -lib/ansible/modules/network/ios/ios_l2_interface.py validate-modules:E337 -lib/ansible/modules/network/ios/ios_l2_interface.py validate-modules:E338 -lib/ansible/modules/network/ios/ios_l2_interface.py validate-modules:E340 +lib/ansible/modules/network/ios/_ios_l2_interface.py validate-modules:E322 +lib/ansible/modules/network/ios/_ios_l2_interface.py validate-modules:E324 +lib/ansible/modules/network/ios/_ios_l2_interface.py validate-modules:E326 +lib/ansible/modules/network/ios/_ios_l2_interface.py validate-modules:E337 +lib/ansible/modules/network/ios/_ios_l2_interface.py validate-modules:E338 +lib/ansible/modules/network/ios/_ios_l2_interface.py validate-modules:E340 lib/ansible/modules/network/ios/ios_l3_interface.py validate-modules:E322 lib/ansible/modules/network/ios/ios_l3_interface.py validate-modules:E324 lib/ansible/modules/network/ios/ios_l3_interface.py validate-modules:E326 -- cgit v1.2.1