Resource module for ios_l3_interfaces (#60405)

* ios_l3_interfaces

* fix shippable

* fix tests

Signed-off-by: Sumit Jaiswal <sjaiswal@redhat.com>

* fix ignore txt file

Signed-off-by: Sumit Jaiswal <sjaiswal@redhat.com>

* fix doc

Signed-off-by: Sumit Jaiswal <sjaiswal@redhat.com>

* fix shippable n tests

* fix replace test

Signed-off-by: Sumit Jaiswal <sjaiswal@redhat.com>

* fix zuul failures

Signed-off-by: Sumit Jaiswal <sjaiswal@redhat.com>

* fix shippable n zuul

* fix zuul

* fix zuul

* fix zuul test

* fix zuul

Signed-off-by: Sumit Jaiswal <sjaiswal@redhat.com>

* code zuul fix

* fix merge

Signed-off-by: Sumit Jaiswal <sjaiswal@redhat.com>

* fix merge

Signed-off-by: Sumit Jaiswal <sjaiswal@redhat.com>
This commit is contained in:
Sumit Jaiswal 2019-08-23 22:09:20 +05:30 committed by GitHub
parent a275bf1625
commit c9f0b0fb42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1340 additions and 27 deletions

View file

@ -37,6 +37,8 @@ class FactsArgs(object):
'!lldp_global',
'lldp_interfaces',
'!lldp_interfaces',
'l3_interfaces',
'!l3_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_l3_interfaces module
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class L3_InterfacesArgs(object):
def __init__(self, **kwargs):
pass
argument_spec = {'config': {'elements': 'dict',
'options': {'name': {'type': 'str', 'required': True},
'ipv4': {'element': 'dict',
'type': 'list',
'options': {'address': {'type': 'str'},
'secondary': {'type': 'bool'},
'dhcp_client': {'type': 'int'},
'dhcp_hostname': {'type': 'str'}}},
'ipv6': {'element': 'dict',
'type': 'list',
'options': {'address': {'type': 'str'}}}
},
'type': 'list'},
'state': {'choices': ['merged', 'replaced', 'overridden', 'deleted'],
'default': 'merged',
'type': 'str'}}

View file

@ -0,0 +1,287 @@
# -*- 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_l3_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
from ansible.module_utils.network.ios.utils.utils import validate_n_expand_ipv4, validate_ipv6
class L3_Interfaces(ConfigBase):
"""
The ios_l3_interfaces class
"""
gather_subset = [
'!all',
'!min',
]
gather_network_resources = [
'l3_interfaces'
]
def get_l3_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)
l3_interfaces_facts = facts['ansible_network_resources'].get('l3_interfaces')
if not l3_interfaces_facts:
return []
return l3_interfaces_facts
def execute_module(self):
""" Execute the module
:rtype: A dictionary
:returns: The result from module execution
"""
result = {'changed': False}
commands = list()
warnings = list()
existing_l3_interfaces_facts = self.get_l3_interfaces_facts()
commands.extend(self.set_config(existing_l3_interfaces_facts))
if commands:
if not self._module.check_mode:
self._connection.edit_config(commands)
result['changed'] = True
result['commands'] = commands
changed_l3_interfaces_facts = self.get_l3_interfaces_facts()
result['before'] = existing_l3_interfaces_facts
if result['changed']:
result['after'] = changed_l3_interfaces_facts
result['warnings'] = warnings
return result
def set_config(self, existing_l3_interfaces_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 desired configuration
"""
want = self._module.params['config']
have = existing_l3_interfaces_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 desired 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
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
commands = []
for interface in want:
for each in have:
if each['name'] == interface['name']:
break
else:
if '.' in interface['name']:
commands.extend(self._set_config(interface, dict(), module))
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
: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
: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:
if '.' in interface['name']:
commands.extend(self._set_config(interface, dict(), module))
continue
commands.extend(self._set_config(interface, each, module))
return commands
def _state_deleted(self, want, have):
""" The command generator when state is deleted
: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
elif interface['name'] in each['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 _set_config(self, want, have, module):
# Set the interface config based on the want and have config
commands = []
interface = 'interface ' + want['name']
# To handle L3 IPV4 configuration
if want.get("ipv4"):
for each in want.get("ipv4"):
if each.get('address') != 'dhcp':
ip_addr_want = validate_n_expand_ipv4(module, each)
each['address'] = ip_addr_want
# Convert the want and have dict to set
want_dict = dict_to_set(want)
have_dict = dict_to_set(have)
# To handle L3 IPV4 configuration
if want.get('ipv4'):
# Get the diff b/w want and have IPV4
if have.get('ipv4'):
ipv4 = tuple(set(dict(want_dict).get('ipv4')) - set(dict(have_dict).get('ipv4')))
else:
diff = want_dict - have_dict
ipv4 = dict(diff).get('ipv4')
if ipv4:
for each in ipv4:
ipv4_dict = dict(each)
if ipv4_dict.get('address') != 'dhcp':
cmd = "ip address {0}".format(ipv4_dict['address'])
if ipv4_dict.get("secondary"):
cmd += " secondary"
elif ipv4_dict.get('address') == 'dhcp':
cmd = "ip address dhcp"
if ipv4_dict.get('dhcp_client') is not None and ipv4_dict.get('dhcp_hostname'):
cmd = "ip address dhcp client-id GigabitEthernet 0/{0} hostname {1}"\
.format(ipv4_dict.get('dhcp_client'), ipv4_dict.get('dhcp_hostname'))
elif ipv4_dict.get('dhcp_client') and not ipv4_dict.get('dhcp_hostname'):
cmd = "ip address dhcp client-id GigabitEthernet 0/{0}"\
.format(ipv4_dict.get('dhcp_client'))
elif not ipv4_dict.get('dhcp_client') and ipv4_dict.get('dhcp_hostname'):
cmd = "ip address dhcp hostname {0}".format(ipv4_dict.get('dhcp_client'))
add_command_to_config_list(interface, cmd, commands)
# To handle L3 IPV6 configuration
if want.get('ipv6'):
# Get the diff b/w want and have IPV6
if have.get('ipv6'):
ipv6 = tuple(set(dict(want_dict).get('ipv6')) - set(dict(have_dict).get('ipv6')))
else:
diff = want_dict - have_dict
ipv6 = dict(diff).get('ipv6')
if ipv6:
for each in ipv6:
ipv6_dict = dict(each)
validate_ipv6(ipv6_dict.get('address'), module)
cmd = "ipv6 address {0}".format(ipv6_dict.get('address'))
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
count = 0
commands = []
if want.get('name'):
interface = 'interface ' + want['name']
else:
interface = 'interface ' + have['name']
if have.get('ipv4') and want.get('ipv4'):
for each in have.get('ipv4'):
if each.get('secondary') and not (want.get('ipv4')[count].get('secondary')):
cmd = 'ipv4 address {0} secondary'.format(each.get('address'))
remove_command_from_config_list(interface, cmd, commands)
count += 1
if have.get('ipv4') and not want.get('ipv4'):
remove_command_from_config_list(interface, 'ip address', commands)
if have.get('ipv6') and not want.get('ipv6'):
remove_command_from_config_list(interface, 'ipv6 address', commands)
return commands

View file

@ -23,6 +23,7 @@ from ansible.module_utils.network.ios.facts.lacp.lacp import LacpFacts
from ansible.module_utils.network.ios.facts.lacp_interfaces.lacp_interfaces import Lacp_InterfacesFacts
from ansible.module_utils.network.ios.facts.lldp_global.lldp_global import Lldp_globalFacts
from ansible.module_utils.network.ios.facts.lldp_interfaces.lldp_interfaces import Lldp_InterfacesFacts
from ansible.module_utils.network.ios.facts.l3_interfaces.l3_interfaces import L3_InterfacesFacts
from ansible.module_utils.network.ios.facts.legacy.base import Default, Hardware, Interfaces, Config
@ -42,6 +43,7 @@ FACT_RESOURCE_SUBSETS = dict(
lacp_interfaces=Lacp_InterfacesFacts,
lldp_global=Lldp_globalFacts,
lldp_interfaces=Lldp_InterfacesFacts,
l3_interfaces=L3_InterfacesFacts,
)

View file

@ -0,0 +1,124 @@
#
# -*- 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_l3_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.l3_interfaces.l3_interfaces import L3_InterfacesArgs
class L3_InterfacesFacts(object):
""" The ios l3 interfaces fact class
"""
def __init__(self, module, subspec='config', options='options'):
self._module = module
self.argument_spec = L3_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 l3 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['l3_interfaces'] = []
params = utils.validate_config(self.argument_spec, {'config': objs})
for cfg in params['config']:
facts['l3_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 {}
# populate the facts from the configuration
config['name'] = normalize_interface(intf)
ipv4 = []
ipv4_all = re.findall(r"ip address (\S+.*)", conf)
for each in ipv4_all:
each_ipv4 = dict()
if 'secondary' not in each and 'dhcp' not in each:
each_ipv4['address'] = each
elif 'secondary' in each:
each_ipv4['address'] = each.split(' secondary')[0]
each_ipv4['secondary'] = True
elif 'dhcp' in each:
each_ipv4['address'] = 'dhcp'
if 'client-id' in each:
each_ipv4['dhcp_client'] = int(each.split(' hostname ')[0].split('/')[-1])
if 'hostname' in each:
each_ipv4["dhcp_hostname"] = each.split(' hostname ')[-1]
if 'client-id' in each and each_ipv4['dhcp_client'] is None:
each_ipv4['dhcp_client'] = int(each.split('/')[-1])
if 'hostname' in each and not each_ipv4["dhcp_hostname"]:
each_ipv4["dhcp_hostname"] = each.split(' hostname ')[-1]
ipv4.append(each_ipv4)
config['ipv4'] = ipv4
# Get the configured IPV6 details
ipv6 = []
ipv6_all = re.findall(r"ipv6 address (\S+)", conf)
for each in ipv6_all:
each_ipv6 = dict()
if 'autoconfig' in each:
each_ipv6['autoconfig'] = True
if 'dhcp' in each:
each_ipv6['dhcp'] = True
each_ipv6['address'] = each.lower()
ipv6.append(each_ipv6)
config['ipv6'] = ipv6
return utils.remove_empties(config)

View file

@ -80,20 +80,22 @@ def filter_dict_having_none_value(want, have):
if value is None:
dict_val = have.get(k).get(key)
test_key_dict.update({key: dict_val})
# below conditions checks are added to check if
# secondary IP is configured, if yes then delete
# the already configured IP if want and have IP
# is different else if it's same no need to delete
if isinstance(value, str):
want_ip = value.split('/')
have_ip = have.get('ipv4')
if len(want_ip) > 1 and have_ip:
have_ip = have_ip[0]['address'].split(' ')[0]
if have_ip != want_ip[0]:
diff_ip = True
if key == 'secondary' and value is True and diff_ip is True:
test_key_dict.update({key: value})
test_dict.update({k: test_key_dict})
# below conditions checks are added to check if
# secondary IP is configured, if yes then delete
# the already configured IP if want and have IP
# is different else if it's same no need to delete
for each in v:
if each.get('secondary'):
want_ip = each.get('address').split('/')
have_ip = have.get('ipv4')
if len(want_ip) > 1 and have_ip and have_ip[0].get('secondary'):
have_ip = have_ip[0]['address'].split(' ')[0]
if have_ip != want_ip[0]:
diff_ip = True
if each.get('secondary') and diff_ip is True:
test_key_dict.update({'secondary': True})
test_dict.update({'ipv4': test_key_dict})
if v is None:
val = have.get(k)
test_dict.update({k: val})

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-3 interfaces on Cisco IOS network devices.
description:
- This module provides declarative management of Layer-3 interfaces
on IOS network devices.
deprecated:
removed_in: '2.13'
alternative: ios_l3_interfaces
why: Newer and updated modules released with more functionality in Ansible 2.9
notes:
- Tested against IOS 15.2
options:
@ -55,34 +59,28 @@ EXAMPLES = """
ios_l3_interface:
name: GigabitEthernet0/3
state: absent
- name: Set GigabitEthernet0/3 IPv4 address
ios_l3_interface:
name: GigabitEthernet0/3
ipv4: 192.168.0.1/24
- name: Set GigabitEthernet0/3 IPv6 address
ios_l3_interface:
name: GigabitEthernet0/3
ipv6: "fd5d:12c9:2201:1::1/64"
- name: Set GigabitEthernet0/3 in dhcp
ios_l3_interface:
name: GigabitEthernet0/3
ipv4: dhcp
ipv6: dhcp
- name: Set interface Vlan1 (SVI) IPv4 address
ios_l3_interface:
name: Vlan1
ipv4: 192.168.0.5/24
- name: Set IP addresses on aggregate
ios_l3_interface:
aggregate:
- { name: GigabitEthernet0/3, ipv4: 192.168.2.10/24 }
- { name: GigabitEthernet0/3, ipv4: 192.168.3.10/24, ipv6: "fd5d:12c9:2201:1::1/64" }
- name: Remove IP addresses on aggregate
ios_l3_interface:
aggregate:

View file

@ -56,7 +56,7 @@ options:
Can specify a list of values to include a larger subset.
choices: ['all', '!all', 'interfaces', '!interfaces', 'l2_interfaces', '!l2_interfaces', 'vlans', '!vlans',
'lag_interfaces', '!lag_interfaces', 'lacp', '!lacp', 'lacp_interfaces', '!lacp_interfaces', 'lldp_global',
'!lldp_global', 'lldp_interfaces', '!lldp_interfaces']
'!lldp_global', 'lldp_interfaces', '!lldp_interfaces', 'l3_interfaces', '!l3_interfaces']
version_added: "2.9"
"""
@ -97,6 +97,12 @@ EXAMPLES = """
ios_facts:
gather_subset: min
gather_network_resources: l2_interfaces
- name: Gather L3 interfaces resource and minimal legacy facts
ios_facts:
gather_subset: min
gather_network_resources: l3_interfaces
"""
RETURN = """

View file

@ -0,0 +1,437 @@
#!/usr/bin/python
# -*- 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)
##############################################
# 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_l3_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_l3_interfaces
version_added: 2.9
short_description: Manage Layer-3 interface on Cisco IOS devices.
description:
- This module provides declarative management of Layer-3 interface
on Cisco IOS devices.
author: Sumit Jaiswal (@justjais)
options:
config:
description: A dictionary of Layer-3 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
ipv4:
description:
- IPv4 address to be set for the Layer-3 interface mentioned in
I(name) option. The address format is <ipv4 address>/<mask>,
the mask is number in range 0-32 eg. 192.168.0.1/24.
type: list
elements: dict
suboptions:
address:
description:
- Configures the IPv4 address for Interface.
type: str
secondary:
description:
- Configures the IP address as a secondary address.
type: bool
dhcp_client:
description:
- Configures and specifies client-id to use over DHCP ip.
Note, This option shall work only when dhcp is configured
as IP.
- GigabitEthernet interface number
type: int
dhcp_hostname:
description:
- Configures and specifies value for hostname option over
DHCP ip. Note, This option shall work only when dhcp is
configured as IP.
type: str
ipv6:
description:
- IPv6 address to be set for the Layer-3 interface mentioned in
I(name) option.
- The address format is <ipv6 address>/<mask>, the mask is number
in range 0-128 eg. fd5d:12c9:2201:1::1/64
type: list
elements: dict
suboptions:
address:
description:
- Configures the IPv6 address for Interface.
type: str
state:
choices:
- merged
- replaced
- overridden
- deleted
default: merged
description:
- The state the configuration should be left in
type: str
"""
EXAMPLES = """
---
# Using merged
#
# Before state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# description Configured by Ansible
# ip address 10.1.1.1 255.255.255.0
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description This is test
# no ip address
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# description Configured by Ansible Network
# no ip address
# interface GigabitEthernet0/3.100
# encapsulation dot1Q 20
- name: Merge provided configuration with device configuration
ios_l3_interfaces:
config:
- name: GigabitEthernet0/1
ipv4:
- address: 192.168.0.1/24
secondary: True
- name: GigabitEthernet0/2
ipv4:
- address: 192.168.0.2/24
- name: GigabitEthernet0/3
ipv6:
- address: fd5d:12c9:2201:1::1/64
- name: GigabitEthernet0/3.100
ipv4:
- address: 192.168.0.3/24
operation: merged
# After state:
# ------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# description Configured by Ansible
# ip address 10.1.1.1 255.255.255.0
# ip address 192.168.0.1 255.255.255.0 secondary
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description This is test
# ip address 192.168.0.2 255.255.255.0
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# description Configured by Ansible Network
# ipv6 address FD5D:12C9:2201:1::1/64
# interface GigabitEthernet0/3.100
# encapsulation dot1Q 20
# ip address 192.168.0.3 255.255.255.0
# Using replaced
#
# Before state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# description Configured by Ansible
# ip address 10.1.1.1 255.255.255.0
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description This is test
# no ip address
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# description Configured by Ansible Network
# ip address 192.168.2.0 255.255.255.0
# interface GigabitEthernet0/3.100
# encapsulation dot1Q 20
# ip address 192.168.0.2 255.255.255.0
- name: Replaces device configuration of listed interfaces with provided configuration
ios_l3_interfaces:
config:
- name: GigabitEthernet0/2
ipv4:
- address: 192.168.2.0/24
- name: GigabitEthernet0/3
ipv4:
- address: dhcp
dhcp_client: 2
dhcp_hostname: test.com
- name: GigabitEthernet0/3.100
ipv4:
- address: 192.168.0.3/24
secondary: True
operation: replaced
# After state:
# ------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# description Configured by Ansible
# ip address 10.1.1.1 255.255.255.0
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description This is test
# ip address 192.168.2.1 255.255.255.0
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# description Configured by Ansible Network
# ip address dhcp client-id GigabitEthernet0/2 hostname test.com
# interface GigabitEthernet0/3.100
# encapsulation dot1Q 20
# ip address 192.168.0.2 255.255.255.0
# ip address 192.168.0.3 255.255.255.0 secondary
# Using overridden
#
# Before state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# description Configured by Ansible
# ip address 10.1.1.1 255.255.255.0
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description This is test
# ip address 192.168.2.1 255.255.255.0
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# description Configured by Ansible Network
# ipv6 address FD5D:12C9:2201:1::1/64
# interface GigabitEthernet0/3.100
# encapsulation dot1Q 20
# ip address 192.168.0.2 255.255.255.0
- name: Override device configuration of all interfaces with provided configuration
ios_l3_interfaces:
config:
- name: GigabitEthernet0/2
ipv4:
- address: 192.168.0.1/24
- name: GigabitEthernet0/3.100
ipv6:
- address: autoconfig
operation: overridden
# After state:
# ------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# description Configured by Ansible
# no ip address
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description This is test
# ip address 192.168.0.1 255.255.255.0
# duplex auto
# speed 1000
# interface GigabitEthernet0/3
# description Configured by Ansible Network
# interface GigabitEthernet0/3.100
# encapsulation dot1Q 20
# ipv6 address autoconfig
# Using Deleted
#
# Before state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# ip address 192.0.2.10 255.255.255.0
# shutdown
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description Configured by Ansible Network
# ip address 192.168.1.0 255.255.255.0
# interface GigabitEthernet0/3
# description Configured by Ansible Network
# ip address 192.168.0.1 255.255.255.0
# shutdown
# duplex full
# speed 10
# ipv6 address FD5D:12C9:2201:1::1/64
# interface GigabitEthernet0/3.100
# encapsulation dot1Q 20
# ip address 192.168.0.2 255.255.255.0
- name: "Delete attributes of given interfaces (NOTE: This won't delete the interface itself)"
ios_l3_interfaces:
config:
- name: GigabitEthernet0/2
- name: GigabitEthernet0/3.100
operation: deleted
# After state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# no ip address
# shutdown
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description Configured by Ansible Network
# no ip address
# interface GigabitEthernet0/3
# description Configured by Ansible Network
# ip address 192.168.0.1 255.255.255.0
# shutdown
# duplex full
# speed 10
# ipv6 address FD5D:12C9:2201:1::1/64
# interface GigabitEthernet0/3.100
# encapsulation dot1Q 20
# Using Deleted without any config passed
#"(NOTE: This will delete all of configured L3 resource module attributes from each configured interface)"
#
# Before state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# ip address 192.0.2.10 255.255.255.0
# shutdown
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description Configured by Ansible Network
# ip address 192.168.1.0 255.255.255.0
# interface GigabitEthernet0/3
# description Configured by Ansible Network
# ip address 192.168.0.1 255.255.255.0
# shutdown
# duplex full
# speed 10
# ipv6 address FD5D:12C9:2201:1::1/64
# interface GigabitEthernet0/3.100
# encapsulation dot1Q 20
# ip address 192.168.0.2 255.255.255.0
- name: "Delete L3 attributes of ALL interfaces together (NOTE: This won't delete the interface itself)"
ios_l3_interfaces:
operation: deleted
# After state:
# -------------
#
# vios#show running-config | section ^interface
# interface GigabitEthernet0/1
# no ip address
# shutdown
# duplex auto
# speed auto
# interface GigabitEthernet0/2
# description Configured by Ansible Network
# no ip address
# interface GigabitEthernet0/3
# description Configured by Ansible Network
# shutdown
# duplex full
# speed 10
# interface GigabitEthernet0/3.100
# encapsulation dot1Q 20
"""
RETURN = """
before:
description: The configuration prior to the model invocation
returned: always
type: list
sample: The configuration returned will alwys 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 alwys 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', 'ip address 192.168.0.2 255.255.255.0']
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.ios.argspec.l3_interfaces.l3_interfaces import L3_InterfacesArgs
from ansible.module_utils.network.ios.config.l3_interfaces.l3_interfaces import L3_Interfaces
def main():
"""
Main entry point for module execution
:returns: the result form module invocation
"""
module = AnsibleModule(argument_spec=L3_InterfacesArgs.argument_spec,
supports_check_mode=True)
result = L3_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,20 @@
---
- 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: 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,12 @@
---
- name: Populate Config
cli_config:
config: "{{ lines }}"
vars:
lines: |
interface GigabitEthernet 0/1
ip address dhcp client-id GigabitEthernet 0/0 hostname test.com
interface GigabitEthernet 0/2
ip address 192.168.2.1 255.255.255.0 secondary
ip address 192.168.2.2 255.255.255.0
ipv6 address fd5d:12c9:2201:1::1/64

View file

@ -0,0 +1,12 @@
---
- name: Remove Config
cli_config:
config: "{{ lines }}"
vars:
lines: |
interface GigabitEthernet 0/1
no ip address
no ipv6 address
interface GigabitEthernet 0/2
no ip address
no ipv6 address

View file

@ -0,0 +1,43 @@
---
- debug:
msg: "Start Deleted integration state for ios_l3_interfaces ansible_connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate_config.yaml
- block:
- name: Delete attributes of provided configured interfaces
ios_l3_interfaces: &deleted
config:
- name: GigabitEthernet0/1
- name: GigabitEthernet0/2
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_l3_interfaces: *deleted
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result.changed == false"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,51 @@
---
- debug:
msg: "START Merged ios_l3_interfaces state for integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- block:
- name: Merge provided configuration with device configuration
ios_l3_interfaces: &merged
config:
- name: GigabitEthernet0/1
ipv4:
- address: dhcp
dhcp_client: 0
dhcp_hostname: test.com
- name: GigabitEthernet0/2
ipv4:
- address: 192.168.3.1/24
secondary: True
- address: 192.168.3.2/24
ipv6:
- address: fd5d:12c9:2201:1::1/64
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_l3_interfaces: *merged
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result['changed'] == false"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,49 @@
---
- debug:
msg: "START Overridden ios_l3_interfaces state for integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate_config.yaml
- block:
- name: Override device configuration of all interfaces with provided configuration
ios_l3_interfaces: &overridden
config:
- name: GigabitEthernet0/0
ipv4:
- address: dhcp
- name: GigabitEthernet0/2
ipv4:
- address: 192.168.4.1/24
- address: 192.168.4.2/24
secondary: True
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 configuration of all interfaces with provided configuration (IDEMPOTENT)
ios_l3_interfaces: *overridden
register: result
- name: Assert that task was idempotent
assert:
that:
- "result['changed'] == false"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,49 @@
---
- debug:
msg: "START Replaced ios_l3_interfaces state for integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate_config.yaml
- block:
- name: Replaces device configuration of listed interfaces with provided configuration
ios_l3_interfaces: &replaced
config:
- name: GigabitEthernet0/1
ipv4:
- address: 192.168.3.1/24
- name: GigabitEthernet0/2
ipv4:
- address: 192.168.4.1/24
secondary: True
- address: 192.168.4.2/24
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 configuration of listed interfaces with provided configuration (IDEMPOTENT)
ios_l3_interfaces: *replaced
register: result
- name: Assert that task was idempotent
assert:
that:
- "result['changed'] == false"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,160 @@
---
merged:
before:
- name: loopback888
- name: loopback999
- ipv4:
- address: dhcp
name: GigabitEthernet0/0
- name: GigabitEthernet0/1
- name: GigabitEthernet0/2
commands:
- "interface GigabitEthernet0/1"
- "ip address dhcp client-id GigabitEthernet 0/0 hostname test.com"
- "interface GigabitEthernet0/2"
- "ip address 192.168.3.1 255.255.255.0 secondary"
- "ip address 192.168.3.2 255.255.255.0"
- "ipv6 address fd5d:12c9:2201:1::1/64"
after:
- name: loopback888
- name: loopback999
- ipv4:
- address: dhcp
name: GigabitEthernet0/0
- ipv4:
- address: dhcp
dhcp_client: 0
dhcp_hostname: test.com
name: GigabitEthernet0/1
- ipv4:
- address: 192.168.3.1 255.255.255.0
secondary: true
- address: 192.168.3.2 255.255.255.0
ipv6:
- address: fd5d:12c9:2201:1::1/64
name: GigabitEthernet0/2
replaced:
before:
- name: loopback888
- name: loopback999
- ipv4:
- address: dhcp
name: GigabitEthernet0/0
- ipv4:
- address: dhcp
dhcp_client: 0
dhcp_hostname: test.com
name: GigabitEthernet0/1
- ipv4:
- address: 192.168.2.1 255.255.255.0
secondary: true
- address: 192.168.2.2 255.255.255.0
ipv6:
- address: fd5d:12c9:2201:1::1/64
name: GigabitEthernet0/2
commands:
- "interface GigabitEthernet0/1"
- "ip address 192.168.3.1 255.255.255.0"
- "interface GigabitEthernet0/2"
- "no ip address"
- "no ipv6 address"
- "ip address 192.168.4.1 255.255.255.0 secondary"
- "ip address 192.168.4.2 255.255.255.0"
after:
- name: loopback888
- name: loopback999
- ipv4:
- address: dhcp
name: GigabitEthernet0/0
- ipv4:
- address: 192.168.3.1 255.255.255.0
name: GigabitEthernet0/1
- ipv4:
- address: 192.168.4.1 255.255.255.0
secondary: true
- address: 192.168.4.2 255.255.255.0
name: GigabitEthernet0/2
overridden:
before:
- name: loopback888
- name: loopback999
- ipv4:
- address: dhcp
name: GigabitEthernet0/0
- ipv4:
- address: dhcp
dhcp_client: 0
dhcp_hostname: test.com
name: GigabitEthernet0/1
- ipv4:
- address: 192.168.2.1 255.255.255.0
secondary: true
- address: 192.168.2.2 255.255.255.0
ipv6:
- address: fd5d:12c9:2201:1::1/64
name: GigabitEthernet0/2
commands:
- "interface GigabitEthernet0/1"
- "no ip address"
- "interface GigabitEthernet0/2"
- "no ip address"
- "no ipv6 address"
- "ip address 192.168.4.1 255.255.255.0"
- "ip address 192.168.4.2 255.255.255.0 secondary"
after:
- name: loopback888
- name: loopback999
- ipv4:
- address: dhcp
name: GigabitEthernet0/0
- name: GigabitEthernet0/1
- ipv4:
- address: 192.168.4.2 255.255.255.0
secondary: true
- address: 192.168.4.1 255.255.255.0
name: GigabitEthernet0/2
deleted:
before:
- name: loopback888
- name: loopback999
- ipv4:
- address: dhcp
name: GigabitEthernet0/0
- ipv4:
- address: dhcp
dhcp_client: 0
dhcp_hostname: test.com
name: GigabitEthernet0/1
- ipv4:
- address: 192.168.2.1 255.255.255.0
secondary: true
- address: 192.168.2.2 255.255.255.0
ipv6:
- address: fd5d:12c9:2201:1::1/64
name: GigabitEthernet0/2
commands:
- "interface GigabitEthernet0/1"
- "no ip address"
- "interface GigabitEthernet0/2"
- "no ip address"
- "no ipv6 address"
after:
- name: loopback888
- name: loopback999
- ipv4:
- address: dhcp
name: GigabitEthernet0/0
- name: GigabitEthernet0/1
- name: GigabitEthernet0/2

View file

@ -3970,12 +3970,12 @@ 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
lib/ansible/modules/network/ios/ios_l3_interface.py validate-modules:E337
lib/ansible/modules/network/ios/ios_l3_interface.py validate-modules:E338
lib/ansible/modules/network/ios/ios_l3_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
lib/ansible/modules/network/ios/_ios_l3_interface.py validate-modules:E337
lib/ansible/modules/network/ios/_ios_l3_interface.py validate-modules:E338
lib/ansible/modules/network/ios/_ios_l3_interface.py validate-modules:E340
lib/ansible/modules/network/ios/ios_linkagg.py validate-modules:E322
lib/ansible/modules/network/ios/ios_linkagg.py validate-modules:E324
lib/ansible/modules/network/ios/ios_linkagg.py validate-modules:E326