Resource module for ios_l2_interfaces (#60344)

* ios_l2_interface checkin

Signed-off-by: Sumit Jaiswal <sjaiswal@redhat.com>
This commit is contained in:
Sumit Jaiswal 2019-08-17 23:02:36 +05:30 committed by GitHub
parent c11d144302
commit ab07c206aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1274 additions and 21 deletions

View file

@ -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):

View file

@ -22,7 +22,9 @@ class FactsArgs(object):
'all',
'!all',
'interfaces',
'!interfaces'
'!interfaces',
'l2_interfaces',
'!l2_interfaces'
]
argument_spec = {

View file

@ -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'}}

View file

@ -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

View file

@ -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,
)

View file

@ -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)

View file

@ -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
"""

View file

@ -9,7 +9,7 @@ __metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'status': ['deprecated'],
'supported_by': 'network'}
DOCUMENTATION = """
@ -21,6 +21,10 @@ 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:
@ -68,27 +72,23 @@ EXAMPLES = """
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
@ -112,7 +112,7 @@ 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 get_config, load_config, run_commands
from ansible.module_utils.network.ios.ios import ios_argument_spec

View file

@ -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 = """

View file

@ -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()

View file

@ -0,0 +1,3 @@
---
testcase: "[^_].*"
test_items: []

View file

@ -0,0 +1 @@
dependencies: []

View file

@ -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

View file

@ -0,0 +1,2 @@
---
- { include: cli.yaml, tags: ['cli'] }

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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