diff --git a/library/__init__.py b/library/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library/ios_facts.py b/library/ios_facts.py new file mode 100644 index 0000000..b87568a --- /dev/null +++ b/library/ios_facts.py @@ -0,0 +1,112 @@ +#!/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) +""" +The module file for ios_facts +""" + +from __future__ import absolute_import, division, print_function + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + +NETWORK_OS = "ios" +RESOURCE = "facts" +COPYRIGHT = "Copyright 2019 Red Hat" + +DOCUMENTATION = """ +--- +module: ios_facts +version_added: 2.9 +short_description: Get facts about Cisco ios devices. +description: + - Collects facts from network devices running the ios operating + system. This module places the facts gathered in the fact tree keyed by the + respective resource name. The facts module will always collect a + base set of facts from the device and can enable or disable + collection of additional facts. +author: [u'Sumit Jaiswal (@justjais)'] +notes: + - Tested against iosv Version 6.1.3 on VIRL +options: + gather_subset: + description: + - When supplied, this argument will restrict the facts collected + to a given subset. Possible values for this argument include + all, min, hardware, config, legacy, and interfaces. Can specify a + list of values to include a larger subset. Values can also be used + with an initial C(M(!)) to specify that a specific subset should + not be collected. + required: false + default: 'all' + version_added: "2.2" + gather_network_resources: + description: + - When supplied, this argument will restrict the facts collected + 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. Values + can also be used with an initial C(M(!)) to specify that a + specific subset should not be collected. + required: false + version_added: "2.9" +""" + +EXAMPLES = """ +# Gather all facts +- ios_facts: + gather_subset: all + gather_network_resources: all +# Collect only the ios facts +- ios_facts: + gather_subset: + - !all + - !min + gather_network_resources: + - ios +# Do not collect ios facts +- ios_facts: + gather_network_resources: + - "!ios" +# Collect ios and minimal default facts +- ios_facts: + gather_subset: min + gather_network_resources: ios +""" + +RETURN = """ +See the respective resource module parameters for the tree. +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import Connection +from ansible.module_utils.ios.facts.facts import Facts + + +def main(): + """ + Main entry point for module execution + :returns: ansible_facts + """ + module = AnsibleModule(argument_spec=Facts.argument_spec, + supports_check_mode=True) + warnings = ['default value for `gather_subset` will be changed to `min` from `!config` v2.11 onwards'] + + connection = Connection(module._socket_path) + gather_subset = module.params['gather_subset'] + gather_network_resources = module.params['gather_network_resources'] + result = Facts().get_facts(module, connection, gather_subset, gather_network_resources) + + try: + ansible_facts, warning = result + warnings.extend(warning) + except (TypeError, KeyError): + ansible_facts = result + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + + +if __name__ == '__main__': + main() diff --git a/library/ios_l3_interfaces.py b/library/ios_l3_interfaces.py new file mode 100644 index 0000000..6d35686 --- /dev/null +++ b/library/ios_l3_interfaces.py @@ -0,0 +1,430 @@ +#!/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 /, + the mask is number in range 0-32 eg. 192.168.0.1/24. + 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: str + 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 /, + the mask is number in range 0-128 eg. fd5d:12c9:2201:1::1/64 + 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 config +# +# 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', 'command 2', 'command 3'] +""" + +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() diff --git a/module_utils/__init__.py b/module_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/__init__.py b/module_utils/network/ios/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/argspec/__init__.py b/module_utils/network/ios/argspec/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/argspec/facts/__init__.py b/module_utils/network/ios/argspec/facts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/argspec/facts/facts.py b/module_utils/network/ios/argspec/facts/facts.py new file mode 100644 index 0000000..3163064 --- /dev/null +++ b/module_utils/network/ios/argspec/facts/facts.py @@ -0,0 +1,35 @@ +# +# -*- 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 arg spec for the ios facts module. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class FactsArgs(object): + """ The arg spec for the ios facts module + """ + + def __init__(self, **kwargs): + pass + + choices = [ + 'all', + '!all', + 'interfaces', + '!interfaces' + 'l2_interfaces', + '!l2_interfaces', + 'l3_interfaces', + '!l3_interfaces' + ] + + argument_spec = { + 'gather_subset': dict(default=['!config'], type='list'), + 'gather_network_resources': dict(default=['all'], choices=choices, type='list'), + } diff --git a/module_utils/network/ios/argspec/l3_interfaces/__init__.py b/module_utils/network/ios/argspec/l3_interfaces/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/argspec/l3_interfaces/l3_interfaces.py b/module_utils/network/ios/argspec/l3_interfaces/l3_interfaces.py new file mode 100644 index 0000000..58e204c --- /dev/null +++ b/module_utils/network/ios/argspec/l3_interfaces/l3_interfaces.py @@ -0,0 +1,55 @@ +# +# -*- 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'}, + 'autoconfig': {'type': 'bool'}, + 'dhcp': {'type': 'str'}}} + }, + 'type': 'list'}, + 'state': {'choices': ['merged', 'replaced', 'overridden', 'deleted'], + 'default': 'merged', + 'type': 'str'}} diff --git a/module_utils/network/ios/config/__init__.py b/module_utils/network/ios/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/config/l3_interfaces/__init__.py b/module_utils/network/ios/config/l3_interfaces/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/config/l3_interfaces/l3_interfaces.py b/module_utils/network/ios/config/l3_interfaces/l3_interfaces.py new file mode 100644 index 0000000..330e50a --- /dev/null +++ b/module_utils/network/ios/config/l3_interfaces/l3_interfaces.py @@ -0,0 +1,278 @@ +# -*- 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.six import iteritems +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 get_interface_type, dict_diff +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 +import q + +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, each, module)) + continue + have_dict = filter_dict_having_none_value(interface, each) + q(have_dict) + 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) + q(commands) + 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, each, 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 + + # Get the diff b/w want and have + want_dict = dict_diff(want) + have_dict = dict_diff(have) + diff = want_dict - have_dict + + # To handle L3 IPV4 configuration + 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': + 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 {}".format(ipv4_dict.get('dhcp_client')) + add_command_to_config_list(interface, cmd, commands) + + # To handle L3 IPV6 configuration + 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 {} 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 diff --git a/module_utils/network/ios/facts/__init__.py b/module_utils/network/ios/facts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/facts/facts.py b/module_utils/network/ios/facts/facts.py new file mode 100644 index 0000000..ff12676 --- /dev/null +++ b/module_utils/network/ios/facts/facts.py @@ -0,0 +1,61 @@ +# +# -*- 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 facts class for ios +this file validates each subset of facts and selectively +calls the appropriate facts gathering function +""" + +from __future__ import absolute_import, division, print_function +__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.l3_interfaces.l3_interfaces import L3_InterfacesFacts +from ansible.module_utils.network.ios.facts.legacy.base import Default, Hardware, Interfaces, Config + + +FACT_LEGACY_SUBSETS = dict( + default=Default, + hardware=Hardware, + interfaces=Interfaces, + config=Config +) + +FACT_RESOURCE_SUBSETS = dict( + interfaces=InterfacesFacts, + l3_interfaces=L3_InterfacesFacts, +) + + +class Facts(FactsBase): + """ The fact class for ios + """ + + VALID_LEGACY_GATHER_SUBSETS = frozenset(FACT_LEGACY_SUBSETS.keys()) + VALID_RESOURCE_SUBSETS = frozenset(FACT_RESOURCE_SUBSETS.keys()) + + def __init__(self, module): + super(Facts, self).__init__(module) + + def get_facts(self, legacy_facts_type=None, resource_facts_type=None, data=None): + """ Collect the facts for ios + :param legacy_facts_type: List of legacy facts types + :param resource_facts_type: List of resource fact types + :param data: previously collected conf + :rtype: dict + :return: the facts gathered + """ + netres_choices = FactsArgs.argument_spec['gather_network_resources'].get('choices', []) + if self.VALID_RESOURCE_SUBSETS: + self.get_network_resources_facts(netres_choices, FACT_RESOURCE_SUBSETS, resource_facts_type, data) + + if self.VALID_LEGACY_GATHER_SUBSETS: + self.get_network_legacy_facts(FACT_LEGACY_SUBSETS, legacy_facts_type) + + return self.ansible_facts, self._warnings \ No newline at end of file diff --git a/module_utils/network/ios/facts/l3_interfaces/__init__.py b/module_utils/network/ios/facts/l3_interfaces/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/facts/l3_interfaces/l3_interfaces.py b/module_utils/network/ios/facts/l3_interfaces/l3_interfaces.py new file mode 100644 index 0000000..ffe2124 --- /dev/null +++ b/module_utils/network/ios/facts/l3_interfaces/l3_interfaces.py @@ -0,0 +1,128 @@ +# +# -*- 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.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 'hostname' in each: + each_ipv4["dhcp_hostname"] = each.split(' hostname ')[-1] + if 'client-id' in each: + each_ipv4['dhcp_client'] = int(each.split(' hostname ')[0].split('/')[-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 + + # encapsulation = re.search(r"encapsulation (\S+)", conf) + # if encapsulation: + # config['encapsulation'] = True + + return utils.remove_empties(config) diff --git a/module_utils/network/ios/utils/__init__.py b/module_utils/network/ios/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/module_utils/network/ios/utils/utils.py b/module_utils/network/ios/utils/utils.py new file mode 100644 index 0000000..ebf11ab --- /dev/null +++ b/module_utils/network/ios/utils/utils.py @@ -0,0 +1,224 @@ +#!/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) + +# utils + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.six import iteritems +from ansible.module_utils.network.common.utils import is_masklen, to_netmask + + +def remove_command_from_config_list(interface, cmd, commands): + # To delete the passed config + if interface not in commands: + commands.insert(0, interface) + commands.append('no %s' % cmd) + return commands + + +def add_command_to_config_list(interface, cmd, commands): + # To set the passed config + if interface not in commands: + commands.insert(0, interface) + commands.append(cmd) + + +def dict_diff(sample_dict): + # Generate a set with passed dictionary for comparison + 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.append(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 + + +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') + diff_ip = False + want_ip = '' + for k, v in want.items(): + if isinstance(v, dict): + for key, value in v.items(): + 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 isinstance(v, list): + for key, value in v[0].items(): + if value is None: + dict_val = have.get(k).get(key) + test_key_dict.update({key: dict_val}) + # below conditions 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 == True and diff_ip == True: + test_key_dict.update({key: value}) + test_dict.update({k: test_key_dict}) + if v is None: + val = have.get(k) + test_dict.update({k: val}) + return test_dict + + +def remove_duplicate_interface(commands): + # Remove duplicate interface from commands + set_cmd = [] + for each in commands: + if 'interface' in each: + if each not in set_cmd: + set_cmd.append(each) + else: + set_cmd.append(each) + + return set_cmd + + +def validate_ipv4(value, module): + if value: + address = value.split('/') + if len(address) != 2: + module.fail_json(msg='address format is /, got invalid format {}'.format(value)) + + if not is_masklen(address[1]): + module.fail_json(msg='invalid value for mask: {}, mask should be in range 0-32'.format(address[1])) + + +def validate_ipv6(value, module): + if value: + address = value.split('/') + if len(address) != 2: + module.fail_json(msg='address format is /, got invalid format {}'.format(value)) + else: + if not 0 <= int(address[1]) <= 128: + module.fail_json(msg='invalid value for mask: {}, mask should be in range 0-128'.format(address[1])) + + +def validate_n_expand_ipv4(module, want): + # Check if input IPV4 is valid IP and expand IPV4 with its subnet mask + ip_addr_want = want.get('address') + validate_ipv4(ip_addr_want, module) + ip = ip_addr_want.split('/') + if len(ip) == 2: + ip_addr_want = '{0} {1}'.format(ip[0], to_netmask(ip[1])) + + return ip_addr_want + + +def normalize_interface(name): + """Return the normalized interface name + """ + if not name: + return + + def _get_number(name): + digits = '' + for char in name: + if char.isdigit() or char in '/.': + digits += char + return digits + + if name.lower().startswith('gi'): + if_type = 'GigabitEthernet' + elif name.lower().startswith('te'): + if_type = 'TenGigabitEthernet' + elif name.lower().startswith('fa'): + if_type = 'FastEthernet' + elif name.lower().startswith('fo'): + if_type = 'FortyGigabitEthernet' + elif name.lower().startswith('long'): + if_type = 'LongReachEthernet' + elif name.lower().startswith('et'): + if_type = 'Ethernet' + elif name.lower().startswith('vl'): + if_type = 'Vlan' + elif name.lower().startswith('lo'): + if_type = 'loopback' + elif name.lower().startswith('po'): + if_type = 'port-channel' + elif name.lower().startswith('nv'): + if_type = 'nve' + elif name.lower().startswith('twe'): + if_type = 'TwentyFiveGigE' + elif name.lower().startswith('hu'): + if_type = 'HundredGigE' + else: + if_type = None + + number_list = name.split(' ') + if len(number_list) == 2: + number = number_list[-1].strip() + else: + number = _get_number(name) + + if if_type: + proper_interface = if_type + number + else: + proper_interface = name + + return proper_interface + + +def get_interface_type(interface): + """Gets the type of interface + """ + + if interface.upper().startswith('GI'): + return 'GigabitEthernet' + elif interface.upper().startswith('TE'): + return 'TenGigabitEthernet' + elif interface.upper().startswith('FA'): + return 'FastEthernet' + elif interface.upper().startswith('FO'): + return 'FortyGigabitEthernet' + elif interface.upper().startswith('LON'): + return 'LongReachEthernet' + elif interface.upper().startswith('ET'): + return 'Ethernet' + elif interface.upper().startswith('VL'): + return 'Vlan' + elif interface.upper().startswith('LO'): + return 'loopback' + elif interface.upper().startswith('PO'): + return 'port-channel' + elif interface.upper().startswith('NV'): + return 'nve' + elif interface.upper().startswith('TWE'): + return 'TwentyFiveGigE' + elif interface.upper().startswith('HU'): + return 'HundredGigE' + else: + return 'unknown' diff --git a/tests/integration/targets/ios_l3_interface/default/main.yaml b/tests/integration/targets/ios_l3_interface/default/main.yaml new file mode 100644 index 0000000..5f709c5 --- /dev/null +++ b/tests/integration/targets/ios_l3_interface/default/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/tests/integration/targets/ios_l3_interface/meta/main.yaml b/tests/integration/targets/ios_l3_interface/meta/main.yaml new file mode 100644 index 0000000..159cea8 --- /dev/null +++ b/tests/integration/targets/ios_l3_interface/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: + - prepare_ios_tests diff --git a/tests/integration/targets/ios_l3_interface/tasks/cli.yaml b/tests/integration/targets/ios_l3_interface/tasks/cli.yaml new file mode 100644 index 0000000..ea5c8c3 --- /dev/null +++ b/tests/integration/targets/ios_l3_interface/tasks/cli.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=network_cli) + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/ios_l3_interface/tasks/main.yaml b/tests/integration/targets/ios_l3_interface/tasks/main.yaml new file mode 100644 index 0000000..415c99d --- /dev/null +++ b/tests/integration/targets/ios_l3_interface/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/tests/integration/targets/ios_l3_interface/tests/cli/deleted.yaml b/tests/integration/targets/ios_l3_interface/tests/cli/deleted.yaml new file mode 100644 index 0000000..4e835b9 --- /dev/null +++ b/tests/integration/targets/ios_l3_interface/tests/cli/deleted.yaml @@ -0,0 +1,29 @@ +--- +- debug: + msg: "START ios_l3_interfaces Deleted integration tests on connection={{ ansible_connection }}" + +- include_tasks: reset_config.yaml + +- name: Delete attributes of given interfaces (Note: This won't delete the interface itself) + ios_l3_interfaces: + config: + - name: GigabitEthernet0/1 + - name: GigabitEthernet0/2 + - name: GigabitEthernet0/3 + - name: GigabitEthernet0/3.100 + state: deleted + +- ios_facts: + gather_subset: net_configuration_interfaces + +- set_fact: + expected_output: + - name: GigabitEthernet0/1 + - name: GigabitEthernet0/2 + - name: GigabitEthernet0/3 + - name: GigabitEthernet0/3.100 +- assert: + that: + - "ansible_facts.net_configuration.l3_interfaces == expected_output" + +- include_tasks: reset_config.yaml diff --git a/tests/integration/targets/ios_l3_interface/tests/cli/merged.yaml b/tests/integration/targets/ios_l3_interface/tests/cli/merged.yaml new file mode 100644 index 0000000..0a79437 --- /dev/null +++ b/tests/integration/targets/ios_l3_interface/tests/cli/merged.yaml @@ -0,0 +1,48 @@ +--- +- debug: + msg: "START ios_l3_interfaces Merged integration tests on connection={{ ansible_connection }}" + +- include_tasks: reset_config.yaml + +- name: Merge provided configuration with device configuration + ios_l3_interfaces: + config: + - name: GigabitEthernet0/1 + ipv4: + - address: 192.168.0.1/24 + - name: GigabitEthernet0/2 + ipv4: + - address: 192.168.2.1/24 + secondary: True + - name: GigabitEthernet0/3 + ipv6: + - address: fd5d:12c9:2201:1::1/64 + - name: GigabitEthernet0/3.100 + ipv6: + - address: dhcp + state: merged + +- ios_facts: + gather_subset: net_configuration_interfaces + +- set_fact: + expected_output: + - name: GigabitEthernet0/1 + ipv4: + - address: 192.168.0.1 255.255.255.0 + - name: GigabitEthernet0/2 + ipv4: + - address: 192.168.2.1 255.255.255.0 + secondary: true + - name: GigabitEthernet0/3 + ipv6: + - address: fd5d:12c9:2201:1::1/64 + - name: GigabitEthernet0/3.100 + ipv6: + - address: dhcp + +- assert: + that: + - "ansible_facts.net_configuration.l3_interfaces == expected_output" + +- include_tasks: reset_config.yaml diff --git a/tests/integration/targets/ios_l3_interface/tests/cli/overridden.yaml b/tests/integration/targets/ios_l3_interface/tests/cli/overridden.yaml new file mode 100644 index 0000000..c37318a --- /dev/null +++ b/tests/integration/targets/ios_l3_interface/tests/cli/overridden.yaml @@ -0,0 +1,44 @@ +--- +- debug: + msg: "START ios_l3_interfaces Overridden integration tests on connection={{ ansible_connection }}" + +- include_tasks: reset_config.yaml + +- name: Override device configuration of all interfaces with provided configuration + ios_l3_interfaces: + config: + - name: GigabitEthernet0/1 + ipv4: + - address: 192.168.0.1/24 + - name: GigabitEthernet0/2 + ipv4: + - address: dhcp + dhcp_client: 2 + dhcp_hostname: test.com + - name: GigabitEthernet0/3 + ipv6: + - address: autoconfig + state: overridden + +- ios_facts: + gather_subset: net_configuration_interfaces + +- set_fact: + expected_output: + - name: GigabitEthernet0/1 + ipv4: + - address: 192.168.0.1 255.255.255.0 + - name: GigabitEthernet0/2 + ipv4: + - address: dhcp + dhcp_client: 2 + dhcp_hostname: test.com + - name: GigabitEthernet0/3 + ipv6: + - address: autoconfig + +- assert: + that: + - "ansible_facts.net_configuration.l3_interfaces == expected_output" + +- include_tasks: reset_config.yaml diff --git a/tests/integration/targets/ios_l3_interface/tests/cli/replaced.yaml b/tests/integration/targets/ios_l3_interface/tests/cli/replaced.yaml new file mode 100644 index 0000000..4242b65 --- /dev/null +++ b/tests/integration/targets/ios_l3_interface/tests/cli/replaced.yaml @@ -0,0 +1,44 @@ +--- +- debug: + msg: "START ios_l3_interfaces Replaced integration tests on connection={{ ansible_connection }}" + +- include_tasks: reset_config.yaml + +- name: Replaces device configuration of listed interfaces with provided configuration + ios_l3_interfaces: + config: + - name: GigabitEthernet0/1 + ipv4: + - address: 192.168.3.1/24 + secondary: True + - name: GigabitEthernet0/3.100 + ipv6: + - address: autoconfig + state: replaced + +- ios_facts: + gather_subset: net_configuration_interfaces + +- set_fact: + expected_output: + - name: GigabitEthernet0/1 + ipv4: + - address: 192.168.0.1 255.255.255.0 + - address: 192.168.3.1 255.255.255.0 + secondary: True + - name: GigabitEthernet0/2 + ipv4: + - address: 192.168.2.1 255.255.255.0 + secondary: true + - name: GigabitEthernet0/3 + ipv6: + - address: fd5d:12c9:2201:1::1/64 + - name: GigabitEthernet0/3.100 + ipv6: + - address: autoconfig + +- assert: + that: + - "ansible_facts.net_configuration.l3_interfaces == expected_output" + +- include_tasks: reset_config.yaml diff --git a/tests/integration/targets/ios_l3_interface/tests/cli/reset_config.yaml b/tests/integration/targets/ios_l3_interface/tests/cli/reset_config.yaml new file mode 100644 index 0000000..ad796ad --- /dev/null +++ b/tests/integration/targets/ios_l3_interface/tests/cli/reset_config.yaml @@ -0,0 +1,37 @@ +--- +- name: Reset initial config + cli_config: + config: | + interface GigabitEthernet0/1 + ip address 192.168.0.1 255.255.255.0 + interface GigabitEthernet0/2 + ip address 192.168.2.1 255.255.255.0 + ip address 192.168.3.1 255.255.255.0 secondary + interface GigabitEthernet0/3 + ipv6 address fd5d:12c9:2201:1::1/64 + interface GigabitEthernet0/3.100 + ipv6 address dhcp + +- ios_facts: + gather_subset: net_configuration_interfaces + +- set_fact: + expected_output: + - name: GigabitEthernet0/1 + ipv4: + - address: 192.168.0.1/24 + - name: GigabitEthernet0/2 + ipv4: + - address: 192.168.2.1/24 + - address: 192.168.3.1/24 + secondary: True + - name: GigabitEthernet0/3 + ipv6: + - address: fd5d:12c9:2201:1::1/64 + - name: GigabitEthernet0/3.100 + ipv6: + - address: dhcp + +- assert: + that: + - "ansible_facts.net_configuration.l3_interfaces == expected_output" diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..0637ba7 --- /dev/null +++ b/tox.ini @@ -0,0 +1,7 @@ +[flake8] +# These are ignored intentionally; +# please don't submit patches that solely correct them or enable them. +ignore = E501,E125,E129,E402 +show-source = True +exclude = .venv,.tox,dist,doc,build,*.egg +max-line-length = 100