diff --git a/README.md b/README.md index 504e84bfd..da89549a1 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Name | Description [cisco.ios.ios_user](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_user_module.rst)|Module to manage the aggregates of local users. [cisco.ios.ios_vlans](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_vlans_module.rst)|Resource module to configure VLANs. [cisco.ios.ios_vrf](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_vrf_module.rst)|Module to configure VRF definitions. +[cisco.ios.ios_vxlan_vtep](https://github.com/ansible-collections/cisco.ios/blob/main/docs/cisco.ios.ios_vxlan_vtep_module.rst)|Resource module to configure VXLAN VTEP interface. diff --git a/changelogs/fragments/add_ios_vxlan_vtep.yaml b/changelogs/fragments/add_ios_vxlan_vtep.yaml new file mode 100644 index 000000000..a2613b7f8 --- /dev/null +++ b/changelogs/fragments/add_ios_vxlan_vtep.yaml @@ -0,0 +1,2 @@ +minor_changes: + - Added ios_vxlan_vtep resource module. diff --git a/docs/cisco.ios.ios_vxlan_vtep_module.rst b/docs/cisco.ios.ios_vxlan_vtep_module.rst new file mode 100644 index 000000000..3f1abc5f5 --- /dev/null +++ b/docs/cisco.ios.ios_vxlan_vtep_module.rst @@ -0,0 +1,701 @@ +.. _cisco.ios.ios_vxlan_vtep_module: + + +************************ +cisco.ios.ios_vxlan_vtep +************************ + +**Resource module to configure VXLAN VTEP interface.** + + +Version added: 5.3.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This module provides declarative management of VXLAN VTEP interface on Cisco IOS network devices. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ config + +
+ list + / elements=dictionary +
+
+ +
A dictionary of VXLAN VTEP interface option
+
+
+ host_reachability_bgp + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes
  • +
+
+
Host reachability using EVPN protocol
+
+
+ interface + +
+ string + / required +
+
+ +
VXLAN VTEP interface
+
+
+ member + +
+ dictionary +
+
+ +
Configure VNI member
+
+
+ vni + +
+ dictionary +
+
+ +
Configure VNI information
+
+
+ l2vni + +
+ list + / elements=dictionary +
+
+ +
Associates L2VNI with the VXLAN VTEP interface
+
+
+ replication + +
+ dictionary +
+
+ +
Replication type for the L2VNI
+
+
+ mcast_group + +
+ dictionary +
+
+ +
Configure multicast group for VNs
+
+
+ ipv4 + +
+ string +
+
+ +
IPv4 multicast group
+
+
+ ipv6 + +
+ string +
+
+ +
IPv6 multicast group
+
+
+ type + +
+ string +
+
+
    Choices: +
  • ingress
  • +
  • static
  • +
+
+
Replication type
+
+
+ vni + +
+ integer +
+
+ +
VNI number
+
+
+ l3vni + +
+ list + / elements=dictionary +
+
+ +
Associates L3VNI with the VXLAN VTEP interface
+
+
+ vni + +
+ integer +
+
+ +
VNI number
+
+
+ vrf + +
+ string +
+
+ +
VRF name of the L3VNI
+
+
+ source_interface + +
+ string +
+
+ +
Source interface for the VXLAN VTEP interface
+
+
+ running_config + +
+ string +
+
+ +
This option is used only with state parsed.
+
The value of this option should be the output received from the IOS device by executing the command show running-config | section ^interface nve.
+
The state parsed reads the configuration from running_config option and transforms it into Ansible structured data as per the resource module's argspec and the value is then returned in the parsed key within the result.
+
+
+ state + +
+ string +
+
+
    Choices: +
  • merged ←
  • +
  • replaced
  • +
  • overridden
  • +
  • deleted
  • +
  • rendered
  • +
  • gathered
  • +
  • parsed
  • +
+
+
The state the configuration should be left in
+
+
+ + +Notes +----- + +.. note:: + - Tested against Cisco IOS device with Version 17.13.01 on Cat9k on CML. + - This module works with connection ``network_cli``. See https://docs.ansible.com/ansible/latest/network/user_guide/platform_ios.html + + + +Examples +-------- + +.. code-block:: yaml + + # Using state merged + + # Before state: + # ------------- + # interface nve1 + # no ip address + # source-interface Loopback1 + # host-reachability protocol bgp + # member vni 10101 mcast-group 225.0.0.101 + # member vni 10102 ingress-replication + # member vni 50901 vrf green + # member vni 10201 mcast-group 225.0.0.101 + # member vni 10202 ingress-replication + # member vni 50902 vrf blue + + # - name: Merge the provided configuration with the device configuration + # cisco.ios.ios_vxlan_vtep: + # config: + # - interface: nve1 + # source_interface: loopback2 + # member: + # vni: + # l2vni: + # - vni: 10101 + # replication: + # type: ingress + # - vni: 10201 + # replication: + # type: static + # mcast_group: + # ipv4: 225.0.0.101 + # ipv6: FF0E:225::101 + # l3vni: + # - vni: 50901 + # vrf: blue + # state: merged + + # Commands Fired: + # --------------- + # "commands": [ + # "interface nve1", + # "source-interface loopback2", + # "no member vni 10101 mcast-group 225.0.0.101", + # "member vni 10101 ingress-replication", + # "no member vni 10201 mcast-group 225.0.0.101", + # "member vni 10201 mcast-group 225.0.0.101 FF0E:225::101", + # "no member vni 50901 vrf green", + # "no member vni 50902 vrf blue", + # "member vni 50901 vrf blue" + # ], + + # After state: + # ------------ + # interface nve1 + # no ip address + # source-interface Loopback2 + # host-reachability protocol bgp + # member vni 10102 ingress-replication + # member vni 10202 ingress-replication + # member vni 10101 ingress-replication + # member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + # member vni 50901 vrf blue + + # Using state replaced + + # Before state: + # ------------- + # interface nve1 + # no ip address + # source-interface Loopback2 + # host-reachability protocol bgp + # member vni 10102 ingress-replication + # member vni 10202 ingress-replication + # member vni 10101 ingress-replication + # member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + # member vni 50901 vrf blue + + # - name: Replaces the device configuration with the provided configuration + # cisco.ios.ios_vxlan_vtep: + # config: + # - interface: nve1 + # source_interface: Loopback2 + # member: + # vni: + # l2vni: + # - vni: 10101 + # replication: + # type: static + # mcast_group: + # ipv6: FF0E:225::101 + # - vni: 10201 + # replication: + # type: static + # mcast_group: + # ipv6: FF0E:225::102 + # state: replaced + + # Commands Fired: + # --------------- + # "commands": [ + # "interface nve1", + # "no member vni 10101 ingress-replication", + # "member vni 10101 mcast-group FF0E:225::101", + # "no member vni 10201 mcast-group 225.0.0.101 FF0E:225::101", + # "member vni 10201 mcast-group FF0E:225::102", + # "no member vni 10102 ingress-replication", + # "no member vni 10202 ingress-replication", + # "no member vni 50901 vrf blue" + # ], + + # After state: + # ------------ + # interface nve1 + # no ip address + # source-interface Loopback2 + # host-reachability protocol bgp + # member vni 10101 mcast-group FF0E:225::101 + # member vni 10201 mcast-group FF0E:225::102 + + # Using state Deleted + + # Before state: + # ------------- + # interface nve1 + # no ip address + # source-interface Loopback2 + # host-reachability protocol bgp + # member vni 10101 mcast-group FF0E:225::101 + # member vni 10201 mcast-group FF0E:225::102 + + # - name: "Delete VXLAN VTEP interface" + # cisco.ios.ios_vxlan_vtep: + # config: + # - interface: nve1 + # state: deleted + + # Commands Fired: + # --------------- + # "commands": [ + # "interface nve1", + # "no source-interface Loopback2", + # "no host-reachability protocol bgp", + # "no member vni 10101 mcast-group FF0E:225::101", + # "no member vni 10201 mcast-group FF0E:225::102" + # ], + + # After state: + # ------------- + # interface nve1 + # no ip address + + # Using state Deleted with member VNIs + + # Before state: + # ------------- + # interface nve1 + # no ip address + # source-interface Loopback2 + # host-reachability protocol bgp + # member vni 10101 mcast-group FF0E:225::101 + # member vni 10102 mcast-group 225.0.0.101 + # member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + + # - name: "Delete VXLAN VTEP interface with member VNIs" + # cisco.ios.ios_vxlan_vtep: + # config: + # - interface: nve1 + # source_interface: Loopback2 + # member: + # vni: + # l2vni: + # - vni: 10101 + # - vni: 10102 + # state: deleted + + # Commands Fired: + # --------------- + # "commands": [ + # "interface nve1", + # "no member vni 10101 mcast-group FF0E:225::101", + # "no member vni 10102 mcast-group 225.0.0.101" + # ], + + # After state: + # ------------- + # interface nve1 + # no ip address + # source-interface Loopback2 + # host-reachability protocol bgp + # member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + + # Using state Deleted with no config + + # Before state: + # ------------- + # interface nve1 + # no ip address + # source-interface Loopback2 + # host-reachability protocol bgp + # member vni 10101 mcast-group FF0E:225::101 + # member vni 10201 mcast-group FF0E:225::102 + + # - name: "Delete VXLAN VTEP interface with no config" + # cisco.ios.ios_vxlan_vtep: + # state: deleted + + # Commands Fired: + # --------------- + # "commands": [ + # "interface nve1", + # "no source-interface Loopback2", + # "no host-reachability protocol bgp", + # "no member vni 10101 mcast-group FF0E:225::101", + # "no member vni 10201 mcast-group FF0E:225::102" + # ], + + # After state: + # ------------- + # interface nve1 + # no ip address + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ after + +
+ dictionary +
+
when changed +
The resulting configuration after module execution.
+
+
Sample:
+
This output will always be in the same format as the module argspec.
+
+
+ before + +
+ dictionary +
+
when state is merged, replaced, overridden, deleted or purged +
The configuration prior to the module execution.
+
+
Sample:
+
This output will always be in the same format as the module argspec.
+
+
+ commands + +
+ list +
+
when state is merged, replaced, overridden, deleted or purged +
The set of commands pushed to the remote device.
+
+
Sample:
+
['interface nve1', 'source-interface Loopback1', 'host-reachability protocol bgp', 'member vni 10101 ingress-replication']
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Padmini Priyadarshini Sivaraj (@PadminiSivaraj) diff --git a/meta/runtime.yml b/meta/runtime.yml index 063903876..4a4bd852d 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -71,6 +71,8 @@ plugin_routing: redirect: cisco.ios.ios ios_system: redirect: cisco.ios.ios + ios_vxlan_vtep: + redirect: cisco.ios.ios l2_interfaces: redirect: cisco.ios.ios l3_interfaces: @@ -119,6 +121,8 @@ plugin_routing: redirect: cisco.ios.ios vrf: redirect: cisco.ios.ios + vxlan_vtep: + redirect: cisco.ios.ios modules: acl_interfaces: redirect: cisco.ios.ios_acl_interfaces @@ -218,4 +222,6 @@ plugin_routing: redirect: cisco.ios.ios_vlans vrf: redirect: cisco.ios.ios_vrf + vxlan_vtep: + redirect: cisco.ios.ios_vxlan_vtep requires_ansible: ">=2.9.10" diff --git a/plugins/module_utils/network/ios/argspec/vxlan_vtep/__init__.py b/plugins/module_utils/network/ios/argspec/vxlan_vtep/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/network/ios/argspec/vxlan_vtep/vxlan_vtep.py b/plugins/module_utils/network/ios/argspec/vxlan_vtep/vxlan_vtep.py new file mode 100644 index 000000000..b5eb35c07 --- /dev/null +++ b/plugins/module_utils/network/ios/argspec/vxlan_vtep/vxlan_vtep.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# ansible.content_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the documentation in the module file and re-run +# ansible.content_builder commenting out +# the path to external 'docstring' in build.yaml. +# +############################################## + +""" +The arg spec for the ios_vxlan_vtep module +""" + + +class Vxlan_vtepArgs(object): # pylint: disable=R0903 + """The arg spec for the ios_vxlan_vtep module""" + + argument_spec = { + "config": { + "type": "list", + "elements": "dict", + "options": { + "interface": {"type": "str", "required": True}, + "source_interface": {"type": "str"}, + "host_reachability_bgp": { + "type": "bool", + }, + "member": { + "type": "dict", + "options": { + "vni": { + "type": "dict", + "options": { + "l2vni": { + "type": "list", + "elements": "dict", + "options": { + "vni": {"type": "int"}, + "replication": { + "type": "dict", + "options": { + "type": { + "type": "str", + "choices": ["ingress", "static"], + }, + "mcast_group": { + "type": "dict", + "options": { + "ipv4": {"type": "str"}, + "ipv6": {"type": "str"}, + }, + }, + }, + }, + }, + }, + "l3vni": { + "type": "list", + "elements": "dict", + "options": { + "vni": {"type": "int"}, + "vrf": {"type": "str"}, + }, + }, + }, + }, + }, + }, + }, + }, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "rendered", + "gathered", + "parsed", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/ios/config/vxlan_vtep/__init__.py b/plugins/module_utils/network/ios/config/vxlan_vtep/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/network/ios/config/vxlan_vtep/vxlan_vtep.py b/plugins/module_utils/network/ios/config/vxlan_vtep/vxlan_vtep.py new file mode 100644 index 000000000..bb35b04e4 --- /dev/null +++ b/plugins/module_utils/network/ios/config/vxlan_vtep/vxlan_vtep.py @@ -0,0 +1,190 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The ios_vxlan_vtep config file. +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 its desired end-state is +created. +""" + +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_merge, + param_list_to_dict, +) + +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.facts import Facts +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.rm_templates.vxlan_vtep import ( + Vxlan_vtepTemplate, +) + + +class Vxlan_vtep(ResourceModule): + """ + The ios_vxlan_vtep config class + """ + + def __init__(self, module): + super(Vxlan_vtep, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="vxlan_vtep", + tmplt=Vxlan_vtepTemplate(), + ) + self.parsers = [ + "source_interface", + "host_reachability_bgp", + ] + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + + wantd, haved = self._interface_list_to_dict(self.want, self.have) + + # if state is merged, merge want onto have and then compare + if self.state == "merged": + wantd = dict_merge(haved, wantd) + + # if state is deleted, empty out wantd and set haved to wantd + if self.state == "deleted": + haved = {k: v for k, v in iteritems(haved) if k in wantd or not wantd} + wantd_copy = wantd.copy() + wantd = {} + + # remove superfluous config for deleted + if self.state in ["overridden", "deleted"]: + for k, have in iteritems(haved): + if k not in wantd: + have = self._filtered_dict(wantd_copy.get(k), have) + self._compare(want={}, have=have) + + for k, want in iteritems(wantd): + self._compare(want=want, have=haved.pop(k, {})) + + def _compare(self, want, have): + """Leverages the base class `compare()` method and + populates the list of commands to be run by comparing + the `want` and `have` data with the `parsers` defined + for the Vxlan_vtep network resource. + """ + + begin = len(self.commands) + self.compare(parsers=self.parsers, want=want, have=have) + + self._compare_member_vnis( + want.pop("member", {}).get("vni", {}), + have.pop("member", {}).get("vni", {}), + ) + + if len(self.commands) != begin: + self.commands.insert( + begin, + self._tmplt.render(want or have, "interface", False), + ) + + def _compare_member_vnis(self, wantmv, havemv): + """Compare member VNIs dict""" + + PARSER_DICT = { + "l2vni": "replication", + "l3vni": "vrf", + } + + for vni_type in ["l2vni", "l3vni"]: + wantd = wantmv.get(vni_type, {}) + haved = havemv.get(vni_type, {}) + undel_vnis = haved.copy() + + for wvni, want in wantd.items(): + have = haved.pop(wvni, {}) + if want != have: + # remove exiting config of the member VNI + self.addcmd(undel_vnis.pop(wvni, {}), PARSER_DICT[vni_type], True) + if vni_type == "l3vni": + undel_vnis = self._remove_existing_vnis_vrfs(want["vrf"], undel_vnis) + self.addcmd(want, PARSER_DICT[vni_type]) + + # remove remaining configs in have for replaced state + for hvni, have in haved.items(): + if hvni in undel_vnis: + self.addcmd(have, PARSER_DICT[vni_type], True) + + def _interface_list_to_dict(self, want, have): + """Convert all list of dicts to dicts of dicts""" + + wantd = {entry["interface"]: entry for entry in want} + haved = {entry["interface"]: entry for entry in have} + + for each in wantd, haved: + if each: + for nvi, nvid in each.items(): + member_vni = nvid.get("member", {}).get("vni") + if member_vni: + for vni_type in member_vni: + member_vni[vni_type] = param_list_to_dict( + member_vni[vni_type], + unique_key="vni", + remove_key=False, + ) + + return wantd, haved + + def _remove_existing_vnis_vrfs(self, want_vrf, haved): + """Remove member VNIs of corresponding VRF""" + + vrf_haved = next( + (h for h in haved.values() if h["vrf"] == want_vrf), + None, + ) + if vrf_haved: + self.addcmd(haved.pop(vrf_haved["vni"]), "vrf", True) + return haved + + def _filtered_dict(self, want, have): + """Remove other config from 'have' if 'member' key is present""" + + if "member" in want: + have_member = {} + want_vni_dict = want.get("member", {}).get("vni", {}) + have_vni_dict = have.get("member", {}).get("vni", {}) + + for vni_type, have_vnis in have_vni_dict.items(): + want_vnis = want_vni_dict.get(vni_type, {}) + have_member[vni_type] = { + vni: have_vni_dict[vni_type].get(vni) for vni in have_vnis if vni in want_vnis + } + have = { + "interface": have["interface"], + "member": {"vni": have_member}, + } + + return have diff --git a/plugins/module_utils/network/ios/facts/facts.py b/plugins/module_utils/network/ios/facts/facts.py index e123108fe..7718b474d 100644 --- a/plugins/module_utils/network/ios/facts/facts.py +++ b/plugins/module_utils/network/ios/facts/facts.py @@ -99,6 +99,9 @@ from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.vlans.vlans import ( VlansFacts, ) +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.vxlan_vtep.vxlan_vtep import ( + Vxlan_vtepFacts, +) FACT_LEGACY_SUBSETS = dict(default=Default, hardware=Hardware, interfaces=Interfaces, config=Config) @@ -128,6 +131,7 @@ service=ServiceFacts, snmp_server=Snmp_serverFacts, hostname=HostnameFacts, + vxlan_vtep=Vxlan_vtepFacts, evpn_global=Evpn_globalFacts, evpn_evi=Evpn_eviFacts, ) diff --git a/plugins/module_utils/network/ios/facts/vxlan_vtep/__init__.py b/plugins/module_utils/network/ios/facts/vxlan_vtep/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/network/ios/facts/vxlan_vtep/vxlan_vtep.py b/plugins/module_utils/network/ios/facts/vxlan_vtep/vxlan_vtep.py new file mode 100644 index 000000000..0027504e4 --- /dev/null +++ b/plugins/module_utils/network/ios/facts/vxlan_vtep/vxlan_vtep.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The ios vxlan_vtep 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 ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils + +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.argspec.vxlan_vtep.vxlan_vtep import ( + Vxlan_vtepArgs, +) +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.rm_templates.vxlan_vtep import ( + Vxlan_vtepTemplate, +) + + +class Vxlan_vtepFacts(object): + """The ios vxlan_vtep facts class""" + + def __init__(self, module): + self._module = module + self.argument_spec = Vxlan_vtepArgs.argument_spec + + def get_vxlan_vtep_data(self, connection): + return connection.get("show running-config | section ^interface nve") + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Vxlan_vtep network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + objs = [] + + if not data: + data = self.get_vxlan_vtep_data(connection) + + # parse native config using the Vxlan_vtep template + vxlan_vtep_parser = Vxlan_vtepTemplate(lines=data.splitlines(), module=self._module) + objs = list(vxlan_vtep_parser.parse().values()) + + ansible_facts["ansible_network_resources"].pop("vxlan_vtep", None) + + params = utils.remove_empties( + vxlan_vtep_parser.validate_config(self.argument_spec, {"config": objs}, redact=True), + ) + + facts["vxlan_vtep"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/plugins/module_utils/network/ios/rm_templates/vxlan_vtep.py b/plugins/module_utils/network/ios/rm_templates/vxlan_vtep.py new file mode 100644 index 000000000..ed9517b17 --- /dev/null +++ b/plugins/module_utils/network/ios/rm_templates/vxlan_vtep.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The Vxlan_vtep parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +class Vxlan_vtepTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + super(Vxlan_vtepTemplate, self).__init__(lines=lines, tmplt=self, module=module) + + # fmt: off + PARSERS = [ + { + "name": "interface", + "getval": re.compile( + r"""^interface\s(?P\S+)$""", + re.VERBOSE, + ), + "setval": "interface {{ interface }}", + "result": {"{{ interface }}": {"interface": "{{ interface }}"}}, + "shared": True, + }, + { + "name": "source_interface", + "getval": re.compile( + r"""\s+source-interface + \s(?P\S+) + $""", + re.VERBOSE, + ), + "setval": "source-interface {{ source_interface }}", + "result": {"{{ interface }}": {"source_interface": "{{ source_interface }}"}}, + }, + { + "name": "host_reachability_bgp", + "getval": re.compile( + r"""\s+host-reachability\sprotocol\sbgp$""", + re.VERBOSE, + ), + "setval": "host-reachability protocol bgp", + "result": { + "{{ interface }}": { + "host_reachability_bgp": True, + }, + }, + }, + # member vni starts + { + "name": "replication", + "getval": re.compile( + r""" + \s+member\svni\s(?P\d+)\s(?Pmcast-group|ingress-replication) + (\s(?P[\d.]+))? + (\s(?P[\da-fA-F:]+))? + $""", + re.VERBOSE, + ), + "setval": "member vni {{ vni }}" + "{{ (' ' + 'ingress-replication') if replication.type == 'ingress' else (' ' + 'mcast-group') }}" + "{{ (' ' + replication.mcast_group.ipv4) if replication.mcast_group is defined and " + "replication.mcast_group.ipv4 is defined and replication.type == 'static' else '' }}" + "{{ (' ' + replication.mcast_group.ipv6) if replication.mcast_group is defined and " + "replication.mcast_group.ipv6 is defined and replication.type == 'static' else '' }}", + "result": { + "{{ interface }}": { + "member": { + "vni": { + "l2vni": [ + { + "vni": "{{ vni }}", + "replication": { + "type": "{{ 'ingress' if type == 'ingress-replication' else 'static' }}", + "mcast_group": { + "ipv4": "{{ ipv4_mcast_group }}", + "ipv6": "{{ ipv6_mcast_group }}", + }, + }, + }, + ], + }, + }, + }, + }, + }, + { + "name": "vrf", + "getval": re.compile( + r""" + \s+member\svni + \s(?P\d+) + \svrf\s(?P\S+) + $""", + re.VERBOSE, + ), + "setval": "member vni {{ vni }} vrf {{ vrf }}", + "result": { + "{{ interface }}": { + "member": { + "vni": { + "l3vni": [ + { + "vni": "{{ vni }}", + "vrf": "{{ vrf }}", + }, + ], + }, + }, + }, + }, + }, + # member vni ends + ] + # fmt: on diff --git a/plugins/modules/ios_vxlan_vtep.py b/plugins/modules/ios_vxlan_vtep.py new file mode 100644 index 000000000..b2c5933e7 --- /dev/null +++ b/plugins/modules/ios_vxlan_vtep.py @@ -0,0 +1,404 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for ios_vxlan_vtep +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: ios_vxlan_vtep +short_description: Resource module to configure VXLAN VTEP interface. +description: This module provides declarative management of VXLAN VTEP interface on Cisco IOS network + devices. +version_added: 5.3.0 +author: Padmini Priyadarshini Sivaraj (@PadminiSivaraj) +notes: + - Tested against Cisco IOS device with Version 17.13.01 on Cat9k on CML. + - This module works with connection C(network_cli). + See U(https://docs.ansible.com/ansible/latest/network/user_guide/platform_ios.html) +options: + config: + description: A dictionary of VXLAN VTEP interface option + type: list + elements: dict + suboptions: + interface: + description: + - VXLAN VTEP interface + type: str + required: true + source_interface: + description: + - Source interface for the VXLAN VTEP interface + type: str + host_reachability_bgp: + description: + - Host reachability using EVPN protocol + type: bool + member: + description: + - Configure VNI member + type: dict + suboptions: + vni: + description: + - Configure VNI information + type: dict + suboptions: + l2vni: + description: + - Associates L2VNI with the VXLAN VTEP interface + type: list + elements: dict + suboptions: + vni: + description: VNI number + type: int + replication: + description: Replication type for the L2VNI + type: dict + suboptions: + type: + description: Replication type + type: str + choices: ['ingress', 'static'] + mcast_group: + description: Configure multicast group for VNI(s) + type: dict + suboptions: + ipv4: + description: IPv4 multicast group + type: str + ipv6: + description: IPv6 multicast group + type: str + l3vni: + description: + - Associates L3VNI with the VXLAN VTEP interface + type: list + elements: dict + suboptions: + vni: + description: VNI number + type: int + vrf: + description: VRF name of the L3VNI + type: str + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the IOS device by + executing the command B(show running-config | section ^interface nve). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + + state: + description: + - The state the configuration should be left in + type: str + choices: + - merged + - replaced + - overridden + - deleted + - rendered + - gathered + - parsed + default: merged +""" + +EXAMPLES = """ + +# Using state merged + +# Before state: +# ------------- +# interface nve1 +# no ip address +# source-interface Loopback1 +# host-reachability protocol bgp +# member vni 10101 mcast-group 225.0.0.101 +# member vni 10102 ingress-replication +# member vni 50901 vrf green +# member vni 10201 mcast-group 225.0.0.101 +# member vni 10202 ingress-replication +# member vni 50902 vrf blue + +# - name: Merge the provided configuration with the device configuration +# cisco.ios.ios_vxlan_vtep: +# config: +# - interface: nve1 +# source_interface: loopback2 +# member: +# vni: +# l2vni: +# - vni: 10101 +# replication: +# type: ingress +# - vni: 10201 +# replication: +# type: static +# mcast_group: +# ipv4: 225.0.0.101 +# ipv6: FF0E:225::101 +# l3vni: +# - vni: 50901 +# vrf: blue +# state: merged + +# Commands Fired: +# --------------- +# "commands": [ +# "interface nve1", +# "source-interface loopback2", +# "no member vni 10101 mcast-group 225.0.0.101", +# "member vni 10101 ingress-replication", +# "no member vni 10201 mcast-group 225.0.0.101", +# "member vni 10201 mcast-group 225.0.0.101 FF0E:225::101", +# "no member vni 50901 vrf green", +# "no member vni 50902 vrf blue", +# "member vni 50901 vrf blue" +# ], + +# After state: +# ------------ +# interface nve1 +# no ip address +# source-interface Loopback2 +# host-reachability protocol bgp +# member vni 10102 ingress-replication +# member vni 10202 ingress-replication +# member vni 10101 ingress-replication +# member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 +# member vni 50901 vrf blue + +# Using state replaced + +# Before state: +# ------------- +# interface nve1 +# no ip address +# source-interface Loopback2 +# host-reachability protocol bgp +# member vni 10102 ingress-replication +# member vni 10202 ingress-replication +# member vni 10101 ingress-replication +# member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 +# member vni 50901 vrf blue + +# - name: Replaces the device configuration with the provided configuration +# cisco.ios.ios_vxlan_vtep: +# config: +# - interface: nve1 +# source_interface: Loopback2 +# member: +# vni: +# l2vni: +# - vni: 10101 +# replication: +# type: static +# mcast_group: +# ipv6: FF0E:225::101 +# - vni: 10201 +# replication: +# type: static +# mcast_group: +# ipv6: FF0E:225::102 +# state: replaced + +# Commands Fired: +# --------------- +# "commands": [ +# "interface nve1", +# "no member vni 10101 ingress-replication", +# "member vni 10101 mcast-group FF0E:225::101", +# "no member vni 10201 mcast-group 225.0.0.101 FF0E:225::101", +# "member vni 10201 mcast-group FF0E:225::102", +# "no member vni 10102 ingress-replication", +# "no member vni 10202 ingress-replication", +# "no member vni 50901 vrf blue" +# ], + +# After state: +# ------------ +# interface nve1 +# no ip address +# source-interface Loopback2 +# host-reachability protocol bgp +# member vni 10101 mcast-group FF0E:225::101 +# member vni 10201 mcast-group FF0E:225::102 + +# Using state Deleted + +# Before state: +# ------------- +# interface nve1 +# no ip address +# source-interface Loopback2 +# host-reachability protocol bgp +# member vni 10101 mcast-group FF0E:225::101 +# member vni 10201 mcast-group FF0E:225::102 + +# - name: "Delete VXLAN VTEP interface" +# cisco.ios.ios_vxlan_vtep: +# config: +# - interface: nve1 +# state: deleted + +# Commands Fired: +# --------------- +# "commands": [ +# "interface nve1", +# "no source-interface Loopback2", +# "no host-reachability protocol bgp", +# "no member vni 10101 mcast-group FF0E:225::101", +# "no member vni 10201 mcast-group FF0E:225::102" +# ], + +# After state: +# ------------- +# interface nve1 +# no ip address + +# Using state Deleted with member VNIs + +# Before state: +# ------------- +# interface nve1 +# no ip address +# source-interface Loopback2 +# host-reachability protocol bgp +# member vni 10101 mcast-group FF0E:225::101 +# member vni 10102 mcast-group 225.0.0.101 +# member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + +# - name: "Delete VXLAN VTEP interface with member VNIs" +# cisco.ios.ios_vxlan_vtep: +# config: +# - interface: nve1 +# source_interface: Loopback2 +# member: +# vni: +# l2vni: +# - vni: 10101 +# - vni: 10102 +# state: deleted + +# Commands Fired: +# --------------- +# "commands": [ +# "interface nve1", +# "no member vni 10101 mcast-group FF0E:225::101", +# "no member vni 10102 mcast-group 225.0.0.101" +# ], + +# After state: +# ------------- +# interface nve1 +# no ip address +# source-interface Loopback2 +# host-reachability protocol bgp +# member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + +# Using state Deleted with no config + +# Before state: +# ------------- +# interface nve1 +# no ip address +# source-interface Loopback2 +# host-reachability protocol bgp +# member vni 10101 mcast-group FF0E:225::101 +# member vni 10201 mcast-group FF0E:225::102 + +# - name: "Delete VXLAN VTEP interface with no config" +# cisco.ios.ios_vxlan_vtep: +# state: deleted + +# Commands Fired: +# --------------- +# "commands": [ +# "interface nve1", +# "no source-interface Loopback2", +# "no host-reachability protocol bgp", +# "no member vni 10101 mcast-group FF0E:225::101", +# "no member vni 10201 mcast-group FF0E:225::102" +# ], + +# After state: +# ------------- +# interface nve1 +# no ip address +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +after: + description: The resulting configuration after module execution. + returned: when changed + type: dict + sample: > + This output will always be in the same format as the + module argspec. +commands: + description: The set of commands pushed to the remote device. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: list + sample: + - 'interface nve1' + - 'source-interface Loopback1' + - 'host-reachability protocol bgp' + - 'member vni 10101 ingress-replication' +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.argspec.vxlan_vtep.vxlan_vtep import ( + Vxlan_vtepArgs, +) +from ansible_collections.cisco.ios.plugins.module_utils.network.ios.config.vxlan_vtep.vxlan_vtep import ( + Vxlan_vtep, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Vxlan_vtepArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Vxlan_vtep(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/ios_vxlan_vtep/defaults/main.yaml b/tests/integration/targets/ios_vxlan_vtep/defaults/main.yaml new file mode 100644 index 000000000..164afead2 --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/ios_vxlan_vtep/meta/main.yaml b/tests/integration/targets/ios_vxlan_vtep/meta/main.yaml new file mode 100644 index 000000000..23d65c7ef --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/meta/main.yaml @@ -0,0 +1,2 @@ +--- +dependencies: [] diff --git a/tests/integration/targets/ios_vxlan_vtep/tasks/cli.yaml b/tests/integration/targets/ios_vxlan_vtep/tasks/cli.yaml new file mode 100644 index 000000000..6f505600c --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tasks/cli.yaml @@ -0,0 +1,21 @@ +--- +- name: Collect all CLI test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + use_regex: true + register: test_cases + delegate_to: localhost + +- name: Set test_items + ansible.builtin.set_fact: + test_items: "{{ test_cases.files | map(attribute='path') | list }}" + delegate_to: localhost + +- name: Run test case (connection=ansible.netcommon.network_cli) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + vars: + ansible_connection: ansible.netcommon.network_cli + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/ios_vxlan_vtep/tasks/main.yaml b/tests/integration/targets/ios_vxlan_vtep/tasks/main.yaml new file mode 100644 index 000000000..d80ee1ae2 --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Main task for VXLAN VTEP module + ansible.builtin.include_tasks: cli.yaml + tags: + - network_cli diff --git a/tests/integration/targets/ios_vxlan_vtep/tests/cli/_parsed.cfg b/tests/integration/targets/ios_vxlan_vtep/tests/cli/_parsed.cfg new file mode 100644 index 000000000..46ecf2e1d --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tests/cli/_parsed.cfg @@ -0,0 +1,7 @@ +interface nve1 + source-interface Loopback1 + host-reachability protocol bgp + member vni 10101 ingress-replication + member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + member vni 50901 vrf green + member vni 50902 vrf blue diff --git a/tests/integration/targets/ios_vxlan_vtep/tests/cli/_populate_config.yaml b/tests/integration/targets/ios_vxlan_vtep/tests/cli/_populate_config.yaml new file mode 100644 index 000000000..1587cb9bc --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tests/cli/_populate_config.yaml @@ -0,0 +1,34 @@ +--- +- name: Populate VXLAN VTEP configuration + cisco.ios.ios_vxlan_vtep: + config: + - interface: nve1 + source_interface: Loopback1 + host_reachability_bgp: true + member: + vni: + l2vni: + - vni: 10101 + replication: + type: ingress + - vni: 10102 + replication: + type: ingress + - vni: 10201 + replication: + type: static + mcast_group: + ipv4: 225.0.0.101 + ipv6: FF0E:225::101 + - vni: 10202 + replication: + type: static + mcast_group: + ipv4: 225.0.0.102 + ipv6: FF0E:225::102 + l3vni: + - vni: 50901 + vrf: green + - vni: 50902 + vrf: blue + state: merged diff --git a/tests/integration/targets/ios_vxlan_vtep/tests/cli/_populate_vlan_vrf_config.yaml b/tests/integration/targets/ios_vxlan_vtep/tests/cli/_populate_vlan_vrf_config.yaml new file mode 100644 index 000000000..c6d5dee3e --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tests/cli/_populate_vlan_vrf_config.yaml @@ -0,0 +1,61 @@ +--- +- name: Configure VLANs and VRFs + ansible.netcommon.cli_config: + config: | + vlan 101 + name Access_VLAN_101 + vlan 102 + name Access_VLAN_102 + vlan 201 + name Access_VLAN_201 + vlan 202 + name Access_VLAN_202 + vlan 901 + name Core_VLAN_VRF_green + vlan 902 + name Core_VLAN_VRF_blue + vlan configuration 101 + member evpn-instance 101 vni 10101 + vlan configuration 102 + member evpn-instance 102 vni 10102 + vlan configuration 201 + member evpn-instance 201 vni 10201 + vlan configuration 202 + member evpn-instance 202 vni 10202 + vlan configuration 901 + member vni 50901 + vlan configuration 902 + member vni 50902 + vrf definition blue + description blue VRF defn + rd 2:2 + ! + address-family ipv4 + route-target export 2:2 + route-target import 2:2 + route-target export 2:2 stitching + route-target import 2:2 stitching + exit-address-family + ! + address-family ipv6 + route-target export 2:2 + route-target import 2:2 + route-target import 2:2 stitching + exit-address-family + vrf definition green + description green VRF defn + rd 1:1 + ! + address-family ipv4 + route-target export 1:1 + route-target import 1:1 + route-target export 1:1 stitching + route-target import 1:1 stitching + exit-address-family + ! + address-family ipv6 + route-target export 1:1 + route-target import 1:1 + route-target export 1:1 stitching + route-target import 1:1 stitching + exit-address-family diff --git a/tests/integration/targets/ios_vxlan_vtep/tests/cli/_remove_config.yaml b/tests/integration/targets/ios_vxlan_vtep/tests/cli/_remove_config.yaml new file mode 100644 index 000000000..52c43242f --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tests/cli/_remove_config.yaml @@ -0,0 +1,6 @@ +--- +- name: Remove VXLAN VTEP configuration + cisco.ios.ios_vxlan_vtep: + config: + - interface: nve1 + state: deleted diff --git a/tests/integration/targets/ios_vxlan_vtep/tests/cli/deleted.yaml b/tests/integration/targets/ios_vxlan_vtep/tests/cli/deleted.yaml new file mode 100644 index 000000000..75d18ddec --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tests/cli/deleted.yaml @@ -0,0 +1,33 @@ +--- +- ansible.builtin.debug: + msg: Start Deleted integration state for ios_vxlan_vtep ansible_connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _remove_config.yaml +- ansible.builtin.include_tasks: _populate_vlan_vrf_config.yaml +- ansible.builtin.include_tasks: _populate_config.yaml + +- block: + - name: Delete provided VXLAN VTEP interface + register: result + cisco.ios.ios_vxlan_vtep: &id001 + config: + - interface: nve1 + state: deleted + + - name: Assert that correct set of commands were generated + ansible.builtin.assert: + that: + - "{{ deleted['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + + - name: Assert that before dicts are correctly generated + ansible.builtin.assert: + that: + - "{{ merged['after'] | symmetric_difference(result['before']) | length == 0 }}" + + - name: Assert that after dict is correctly generated + ansible.builtin.assert: + that: + - "{{ deleted['after'] | symmetric_difference(result['after']) | length == 0 }}" + + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/ios_vxlan_vtep/tests/cli/empty_config.yaml b/tests/integration/targets/ios_vxlan_vtep/tests/cli/empty_config.yaml new file mode 100644 index 000000000..4034115e5 --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tests/cli/empty_config.yaml @@ -0,0 +1,58 @@ +--- +- ansible.builtin.debug: + msg: START ios_vxlan_vtep empty_config.yaml integration tests on connection={{ ansible_connection }} + +- name: Merged with empty configuration should give appropriate error message + register: result + ignore_errors: true + cisco.ios.ios_vxlan_vtep: + config: + state: merged + +- ansible.builtin.assert: + that: + - result.msg == 'value of config parameter must not be empty for state merged' + +- name: Replaced with empty configuration should give appropriate error message + register: result + ignore_errors: true + cisco.ios.ios_vxlan_vtep: + config: + state: replaced + +- ansible.builtin.assert: + that: + - result.msg == 'value of config parameter must not be empty for state replaced' + +- name: Overridden with empty configuration should give appropriate error message + register: result + ignore_errors: true + cisco.ios.ios_vxlan_vtep: + config: + state: overridden + +- ansible.builtin.assert: + that: + - result.msg == 'value of config parameter must not be empty for state overridden' + +- name: Rendered with empty configuration should give appropriate error message + register: result + ignore_errors: true + cisco.ios.ios_vxlan_vtep: + config: + state: rendered + +- ansible.builtin.assert: + that: + - result.msg == 'value of config parameter must not be empty for state rendered' + +- name: Parsed with empty configuration should give appropriate error message + register: result + ignore_errors: true + cisco.ios.ios_vxlan_vtep: + running_config: + state: parsed + +- ansible.builtin.assert: + that: + - result.msg == 'value of running_config parameter must not be empty for state parsed' diff --git a/tests/integration/targets/ios_vxlan_vtep/tests/cli/gathered.yaml b/tests/integration/targets/ios_vxlan_vtep/tests/cli/gathered.yaml new file mode 100644 index 000000000..2f334a79f --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tests/cli/gathered.yaml @@ -0,0 +1,23 @@ +--- +- ansible.builtin.debug: + msg: START ios_vxlan_vtep gathered integration tests on connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _remove_config.yaml +- ansible.builtin.include_tasks: _populate_vlan_vrf_config.yaml +- ansible.builtin.include_tasks: _populate_config.yaml + +- block: + - name: Ios_vxlan_vtep gather - play + register: result + cisco.ios.ios_vxlan_vtep: + config: + state: gathered + + - name: Ios_vxlan_vtep gather - assert + ansible.builtin.assert: + that: + - result.changed == false + - gathered['config'] == result['gathered'] + + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/ios_vxlan_vtep/tests/cli/merged.yaml b/tests/integration/targets/ios_vxlan_vtep/tests/cli/merged.yaml new file mode 100644 index 000000000..ba004e9db --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tests/cli/merged.yaml @@ -0,0 +1,67 @@ +--- +- ansible.builtin.debug: + msg: START Merged ios_vxlan_vtep state for integration tests on connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _remove_config.yaml +- ansible.builtin.include_tasks: _populate_vlan_vrf_config.yaml + +- block: + - name: Merge provided configuration with device configuration + register: result + cisco.ios.ios_vxlan_vtep: &id001 + config: + - interface: nve1 + source_interface: Loopback1 + host_reachability_bgp: true + member: + vni: + l2vni: + - vni: 10101 + replication: + type: ingress + - vni: 10102 + replication: + type: ingress + - vni: 10201 + replication: + type: static + mcast_group: + ipv4: 225.0.0.101 + ipv6: FF0E:225::101 + - vni: 10202 + replication: + type: static + mcast_group: + ipv4: 225.0.0.102 + ipv6: FF0E:225::102 + l3vni: + - vni: 50901 + vrf: green + - vni: 50902 + vrf: blue + state: merged + + - name: Assert that correct set of commands were generated + ansible.builtin.assert: + that: + - "{{ merged['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + + - name: Assert that before dicts are correctly generated + ansible.builtin.assert: + that: + - merged['before'] == result['before'] + + - name: Assert that after dict is correctly generated + ansible.builtin.assert: + that: + - merged['after'] == result['after'] + + - name: Merge provided configuration with device configuration (idempotent) + register: result + cisco.ios.ios_vxlan_vtep: *id001 + - name: Assert that the previous task was idempotent + ansible.builtin.assert: + that: + - result['changed'] == false + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/ios_vxlan_vtep/tests/cli/overridden.yaml b/tests/integration/targets/ios_vxlan_vtep/tests/cli/overridden.yaml new file mode 100644 index 000000000..187d71eae --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tests/cli/overridden.yaml @@ -0,0 +1,59 @@ +--- +- ansible.builtin.debug: + msg: START Overridden ios_vxlan_vtep state for integration tests on connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _remove_config.yaml +- ansible.builtin.include_tasks: _populate_vlan_vrf_config.yaml +- ansible.builtin.include_tasks: _populate_config.yaml + +- block: + - name: Override device configuration of all VTEP interfaces with provided configuration + register: result + cisco.ios.ios_vxlan_vtep: &id001 + config: + - interface: nve1 + source_interface: Loopback1 + host_reachability_bgp: true + member: + vni: + l2vni: + - vni: 10101 + replication: + type: static + mcast_group: + ipv6: FF0E:225::101 + - vni: 10201 + replication: + type: static + mcast_group: + ipv6: FF0E:225::102 + l3vni: + - vni: 50901 + vrf: blue + state: overridden + + - name: Assert that correct set of commands were generated + ansible.builtin.assert: + that: + - "{{ overridden['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + + - name: Assert that before dicts are correctly generated + ansible.builtin.assert: + that: + - overridden['before'] == result['before'] + + - name: Assert that after dict is correctly generated + ansible.builtin.assert: + that: + - overridden['after'] == result['after'] + + - name: Override device configuration of all VTEP interfaces with provided configuration (idempotent) + register: result + cisco.ios.ios_vxlan_vtep: *id001 + - name: Assert that task was idempotent + ansible.builtin.assert: + that: + - result['changed'] == false + + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/ios_vxlan_vtep/tests/cli/parsed.yaml b/tests/integration/targets/ios_vxlan_vtep/tests/cli/parsed.yaml new file mode 100644 index 000000000..82d84b037 --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tests/cli/parsed.yaml @@ -0,0 +1,19 @@ +--- +- ansible.builtin.debug: + msg: START Parsed ios_vxlan_vtep state for integration tests on connection={{ ansible_connection }} + +- block: + - name: Ios_vxlan_vtep parsed - play + register: result + cisco.ios.ios_vxlan_vtep: + running_config: "{{ lookup('file', '_parsed.cfg') }}" + state: parsed + + - name: Ios_vxlan_vtep parsed - assert config + ansible.builtin.assert: + that: + - result.changed == false + - parsed['config'] == result['parsed'] + + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/ios_vxlan_vtep/tests/cli/rendered.yaml b/tests/integration/targets/ios_vxlan_vtep/tests/cli/rendered.yaml new file mode 100644 index 000000000..9979d6f30 --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tests/cli/rendered.yaml @@ -0,0 +1,34 @@ +--- +- ansible.builtin.debug: + msg: START Rendered ios_vxlan_vtep state for integration tests on connection={{ ansible_connection }} + +- block: + - name: Ios_vxlan_vtep rendered - play + register: result + cisco.ios.ios_vxlan_vtep: &id001 + config: + - interface: nve1 + source_interface: Loopback1 + member: + vni: + l2vni: + - vni: 10101 + replication: + type: static + mcast_group: + ipv6: FF0E:225::101 + - vni: 10201 + replication: + type: static + mcast_group: + ipv6: FF0E:225::102 + l3vni: + - vni: 50901 + vrf: blue + state: rendered + + - name: Ios_vxlan_vtep rendered - assert commands + ansible.builtin.assert: + that: + - "{{ rendered['commands'] | symmetric_difference(result['rendered']) | length == 0 }}" + - result['changed'] == false diff --git a/tests/integration/targets/ios_vxlan_vtep/tests/cli/replaced.yaml b/tests/integration/targets/ios_vxlan_vtep/tests/cli/replaced.yaml new file mode 100644 index 000000000..5e24f4336 --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/tests/cli/replaced.yaml @@ -0,0 +1,56 @@ +--- +- ansible.builtin.debug: + msg: START Replaced ios_vxlan_vtep state for integration tests on connection={{ ansible_connection }} + +- ansible.builtin.include_tasks: _remove_config.yaml +- ansible.builtin.include_tasks: _populate_vlan_vrf_config.yaml +- ansible.builtin.include_tasks: _populate_config.yaml + +- block: + - name: Replaces device configuration of listed VTEP interfaces with provided configuration + register: result + cisco.ios.ios_vxlan_vtep: &id001 + config: + - interface: nve1 + source_interface: Loopback1 + host_reachability_bgp: true + member: + vni: + l2vni: + - vni: 10101 + replication: + type: static + mcast_group: + ipv6: FF0E:225::101 + - vni: 10201 + replication: + type: static + mcast_group: + ipv6: FF0E:225::102 + state: replaced + + - name: Assert that correct set of commands were generated + ansible.builtin.assert: + that: + - "{{ replaced['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + + - name: Assert that before dicts are correctly generated + ansible.builtin.assert: + that: + - replaced['before'] == result['before'] + + - name: Assert that after dict is correctly generated + ansible.builtin.assert: + that: + - replaced['after'] == result['after'] + + - name: Replaces device configuration of listed VTEP interfaces with provided configuration (idempotent) + register: result + cisco.ios.ios_vxlan_vtep: *id001 + - name: Assert that task was idempotent + ansible.builtin.assert: + that: + - result['changed'] == false + + always: + - ansible.builtin.include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/ios_vxlan_vtep/vars/main.yaml b/tests/integration/targets/ios_vxlan_vtep/vars/main.yaml new file mode 100644 index 000000000..9ee70d3f7 --- /dev/null +++ b/tests/integration/targets/ios_vxlan_vtep/vars/main.yaml @@ -0,0 +1,274 @@ +--- +merged: + before: + - interface: nve1 + commands: + - interface nve1 + - source-interface Loopback1 + - host-reachability protocol bgp + - member vni 10101 ingress-replication + - member vni 10102 ingress-replication + - member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + - member vni 10202 mcast-group 225.0.0.102 FF0E:225::102 + - member vni 50901 vrf green + - member vni 50902 vrf blue + after: + - interface: nve1 + source_interface: Loopback1 + host_reachability_bgp: true + member: + vni: + l2vni: + - vni: 10101 + replication: + type: ingress + - vni: 10102 + replication: + type: ingress + - vni: 10201 + replication: + type: static + mcast_group: + ipv4: 225.0.0.101 + ipv6: FF0E:225::101 + - vni: 10202 + replication: + type: static + mcast_group: + ipv4: 225.0.0.102 + ipv6: FF0E:225::102 + l3vni: + - vni: 50901 + vrf: green + - vni: 50902 + vrf: blue +replaced: + before: + - interface: nve1 + source_interface: Loopback1 + host_reachability_bgp: true + member: + vni: + l2vni: + - vni: 10101 + replication: + type: ingress + - vni: 10102 + replication: + type: ingress + - vni: 10201 + replication: + type: static + mcast_group: + ipv4: 225.0.0.101 + ipv6: FF0E:225::101 + - vni: 10202 + replication: + type: static + mcast_group: + ipv4: 225.0.0.102 + ipv6: FF0E:225::102 + l3vni: + - vni: 50901 + vrf: green + - vni: 50902 + vrf: blue + commands: + - interface nve1 + - no member vni 10101 ingress-replication + - member vni 10101 mcast-group FF0E:225::101 + - no member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + - member vni 10201 mcast-group FF0E:225::102 + - no member vni 10102 ingress-replication + - no member vni 10202 mcast-group 225.0.0.102 FF0E:225::102 + - no member vni 50901 vrf green + - no member vni 50902 vrf blue + after: + - interface: nve1 + source_interface: Loopback1 + host_reachability_bgp: true + member: + vni: + l2vni: + - vni: 10101 + replication: + type: static + mcast_group: + ipv6: FF0E:225::101 + - vni: 10201 + replication: + type: static + mcast_group: + ipv6: FF0E:225::102 + +overridden: + before: + - interface: nve1 + source_interface: Loopback1 + host_reachability_bgp: true + member: + vni: + l2vni: + - vni: 10101 + replication: + type: ingress + - vni: 10102 + replication: + type: ingress + - vni: 10201 + replication: + type: static + mcast_group: + ipv4: 225.0.0.101 + ipv6: FF0E:225::101 + - vni: 10202 + replication: + type: static + mcast_group: + ipv4: 225.0.0.102 + ipv6: FF0E:225::102 + l3vni: + - vni: 50901 + vrf: green + - vni: 50902 + vrf: blue + commands: + - interface nve1 + - no member vni 10101 ingress-replication + - member vni 10101 mcast-group FF0E:225::101 + - no member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + - member vni 10201 mcast-group FF0E:225::102 + - no member vni 10102 ingress-replication + - no member vni 10202 mcast-group 225.0.0.102 FF0E:225::102 + - no member vni 50901 vrf green + - no member vni 50902 vrf blue + - member vni 50901 vrf blue + after: + - interface: nve1 + source_interface: Loopback1 + host_reachability_bgp: true + member: + vni: + l2vni: + - vni: 10101 + replication: + type: static + mcast_group: + ipv6: FF0E:225::101 + - vni: 10201 + replication: + type: static + mcast_group: + ipv6: FF0E:225::102 + l3vni: + - vni: 50901 + vrf: blue + +deleted: + before: + - interface: nve1 + source_interface: Loopback1 + host_reachability_bgp: true + member: + vni: + l2vni: + - vni: 10101 + replication: + type: ingress + - vni: 10102 + replication: + type: ingress + - vni: 10201 + replication: + type: static + mcast_group: + ipv4: 225.0.0.101 + ipv6: FF0E:225::101 + - vni: 10202 + replication: + type: static + mcast_group: + ipv4: 225.0.0.102 + ipv6: FF0E:225::102 + l3vni: + - vni: 50901 + vrf: green + - vni: 50902 + vrf: blue + commands: + - interface nve1 + - no source-interface Loopback1 + - no host-reachability protocol bgp + - no member vni 10101 ingress-replication + - no member vni 10102 ingress-replication + - no member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + - no member vni 10202 mcast-group 225.0.0.102 FF0E:225::102 + - no member vni 50901 vrf green + - no member vni 50902 vrf blue + + after: + - interface: nve1 + host_reachability_bgp: true + +gathered: + config: + - interface: nve1 + source_interface: Loopback1 + host_reachability_bgp: true + member: + vni: + l2vni: + - vni: 10101 + replication: + type: ingress + - vni: 10102 + replication: + type: ingress + - vni: 10201 + replication: + type: static + mcast_group: + ipv4: 225.0.0.101 + ipv6: FF0E:225::101 + - vni: 10202 + replication: + type: static + mcast_group: + ipv4: 225.0.0.102 + ipv6: FF0E:225::102 + l3vni: + - vni: 50901 + vrf: green + - vni: 50902 + vrf: blue + +parsed: + config: + - interface: nve1 + source_interface: Loopback1 + host_reachability_bgp: true + member: + vni: + l2vni: + - vni: 10101 + replication: + type: ingress + - vni: 10201 + replication: + type: static + mcast_group: + ipv4: 225.0.0.101 + ipv6: FF0E:225::101 + l3vni: + - vni: 50901 + vrf: green + - vni: 50902 + vrf: blue + +rendered: + commands: + - interface nve1 + - source-interface Loopback1 + - member vni 10101 mcast-group FF0E:225::101 + - member vni 10201 mcast-group FF0E:225::102 + - member vni 50901 vrf blue diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 8c07fd37d..9dadf1b54 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -19,5 +19,6 @@ plugins/module_utils/network/ios/config/route_maps/route_maps.py import-2.6!skip plugins/module_utils/network/ios/config/logging_global/logging_global.py import-2.6!skip plugins/module_utils/network/ios/config/prefix_lists/prefix_lists.py import-2.6!skip plugins/module_utils/network/ios/config/ntp_global/ntp_global.py import-2.6!skip +plugins/module_utils/network/ios/config/vxlan_vtep/vxlan_vtep.py import-2.6!skip plugins/module_utils/network/ios/config/evpn_global/evpn_global.py import-2.6!skip plugins/module_utils/network/ios/config/evpn_evi/evpn_evi.py import-2.6!skip diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index 155ab8bce..2847c9344 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -19,5 +19,6 @@ plugins/module_utils/network/ios/config/route_maps/route_maps.py import-2.6!skip plugins/module_utils/network/ios/config/logging_global/logging_global.py import-2.6!skip plugins/module_utils/network/ios/config/prefix_lists/prefix_lists.py import-2.6!skip plugins/module_utils/network/ios/config/ntp_global/ntp_global.py import-2.6!skip +plugins/module_utils/network/ios/config/vxlan_vtep/vxlan_vtep.py import-2.6!skip plugins/module_utils/network/ios/config/evpn_global/evpn_global.py import-2.6!skip plugins/module_utils/network/ios/config/evpn_evi/evpn_evi.py import-2.6!skip diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 5075a284a..11aba94e9 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -27,5 +27,6 @@ plugins/module_utils/network/ios/config/route_maps/route_maps.py import-2.6!skip plugins/module_utils/network/ios/config/logging_global/logging_global.py import-2.6!skip plugins/module_utils/network/ios/config/prefix_lists/prefix_lists.py import-2.6!skip plugins/module_utils/network/ios/config/ntp_global/ntp_global.py import-2.6!skip +plugins/module_utils/network/ios/config/vxlan_vtep/vxlan_vtep.py import-2.6!skip plugins/module_utils/network/ios/config/evpn_global/evpn_global.py import-2.6!skip plugins/module_utils/network/ios/config/evpn_evi/evpn_evi.py import-2.6!skip diff --git a/tests/unit/modules/network/ios/test_ios_vxlan_vtep.py b/tests/unit/modules/network/ios/test_ios_vxlan_vtep.py new file mode 100644 index 000000000..25b0bc8a6 --- /dev/null +++ b/tests/unit/modules/network/ios/test_ios_vxlan_vtep.py @@ -0,0 +1,443 @@ +# +# (c) 2023, Red Hat +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from textwrap import dedent + +from ansible_collections.cisco.ios.plugins.modules import ios_vxlan_vtep +from ansible_collections.cisco.ios.tests.unit.compat.mock import patch +from ansible_collections.cisco.ios.tests.unit.modules.utils import set_module_args + +from .ios_module import TestIosModule + + +class TestIosVxlanVtepModule(TestIosModule): + module = ios_vxlan_vtep + + def setUp(self): + super(TestIosVxlanVtepModule, self).setUp() + + self.mock_get_resource_connection_facts = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module_base." + "get_resource_connection", + ) + self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start() + + self.mock_execute_show_command = patch( + "ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.vxlan_vtep.vxlan_vtep." + "Vxlan_vtepFacts.get_vxlan_vtep_data", + ) + self.execute_show_command = self.mock_execute_show_command.start() + + def tearDown(self): + super(TestIosVxlanVtepModule, self).tearDown() + self.mock_get_resource_connection_facts.stop() + self.mock_execute_show_command.stop() + + def test_ios_vxlan_vtep_merged(self): + self.execute_show_command.return_value = dedent( + """\ + interface nve1 + no ip address + source-interface Loopback1 + host-reachability protocol bgp + member vni 10101 mcast-group 225.0.0.101 + member vni 10102 ingress-replication + member vni 50901 vrf green + member vni 10201 mcast-group 225.0.0.101 + member vni 10202 ingress-replication + member vni 50902 vrf blue + """, + ) + set_module_args( + dict( + config=[ + { + "interface": "nve1", + "source_interface": "Loopback2", + "member": { + "vni": { + "l2vni": [ + { + "vni": "10101", + "replication": {"type": "ingress"}, + }, + { + "vni": "10201", + "replication": { + "type": "static", + "mcast_group": { + "ipv4": "225.0.0.101", + "ipv6": "FF0E:225::101", + }, + }, + }, + ], + "l3vni": [ + { + "vni": "50901", + "vrf": "blue", + }, + ], + }, + }, + }, + ], + state="merged", + ), + ) + commands = [ + "interface nve1", + "source-interface Loopback2", + "no member vni 10101 mcast-group 225.0.0.101", + "member vni 10101 ingress-replication", + "no member vni 10201 mcast-group 225.0.0.101", + "member vni 10201 mcast-group 225.0.0.101 FF0E:225::101", + "no member vni 50901 vrf green", + "no member vni 50902 vrf blue", + "member vni 50901 vrf blue", + ] + result = self.execute_module(changed=True) + self.assertEqual(result["commands"], commands) + + def test_ios_vxlan_vtep_merged_idempotent(self): + self.execute_show_command.return_value = dedent( + """\ + interface nve1 + no ip address + source-interface Loopback2 + host-reachability protocol bgp + member vni 10101 ingress-replication + member vni 10102 ingress-replication + member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + member vni 10202 ingress-replication + member vni 50901 vrf blue + """, + ) + set_module_args( + dict( + config=[ + { + "interface": "nve1", + "source_interface": "Loopback2", + "member": { + "vni": { + "l2vni": [ + { + "vni": "10101", + "replication": {"type": "ingress"}, + }, + { + "vni": "10201", + "replication": { + "type": "static", + "mcast_group": { + "ipv4": "225.0.0.101", + "ipv6": "FF0E:225::101", + }, + }, + }, + ], + "l3vni": [ + { + "vni": "50901", + "vrf": "blue", + }, + ], + }, + }, + }, + ], + state="merged", + ), + ) + self.execute_module(changed=False, commands=[]) + + def test_ios_vxlan_vtep_replaced(self): + self.execute_show_command.return_value = dedent( + """\ + interface nve1 + no ip address + source-interface Loopback2 + host-reachability protocol bgp + member vni 10101 ingress-replication + member vni 10102 ingress-replication + member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + member vni 10202 ingress-replication + member vni 50901 vrf blue + """, + ) + set_module_args( + dict( + config=[ + { + "interface": "nve1", + "source_interface": "Loopback2", + "host_reachability_bgp": True, + "member": { + "vni": { + "l2vni": [ + { + "vni": "10101", + "replication": { + "type": "static", + "mcast_group": { + "ipv6": "FF0E:225::101", + }, + }, + }, + { + "vni": "10201", + "replication": { + "type": "static", + "mcast_group": { + "ipv6": "FF0E:225::102", + }, + }, + }, + ], + }, + }, + }, + ], + state="replaced", + ), + ) + commands = [ + "interface nve1", + "no member vni 10101 ingress-replication", + "member vni 10101 mcast-group FF0E:225::101", + "no member vni 10201 mcast-group 225.0.0.101 FF0E:225::101", + "member vni 10201 mcast-group FF0E:225::102", + "no member vni 10102 ingress-replication", + "no member vni 10202 ingress-replication", + "no member vni 50901 vrf blue", + ] + result = self.execute_module(changed=True) + self.assertEqual(result["commands"], commands) + + def test_ios_vxlan_vtep_replaced_idempotent(self): + self.execute_show_command.return_value = dedent( + """\ + interface nve1 + no ip address + source-interface Loopback2 + host-reachability protocol bgp + member vni 10101 mcast-group FF0E:225::101 + member vni 10201 mcast-group FF0E:225::102 + """, + ) + set_module_args( + dict( + config=[ + { + "interface": "nve1", + "source_interface": "Loopback2", + "host_reachability_bgp": True, + "member": { + "vni": { + "l2vni": [ + { + "vni": "10101", + "replication": { + "type": "static", + "mcast_group": { + "ipv6": "FF0E:225::101", + }, + }, + }, + { + "vni": "10201", + "replication": { + "type": "static", + "mcast_group": { + "ipv6": "FF0E:225::102", + }, + }, + }, + ], + }, + }, + }, + ], + state="replaced", + ), + ) + self.execute_module(changed=False, commands=[]) + + def test_ios_vxlan_vtep_overridden(self): + self.execute_show_command.return_value = dedent( + """\ + interface nve1 + no ip address + source-interface Loopback2 + host-reachability protocol bgp + member vni 10102 ingress-replication + member vni 10202 ingress-replication + member vni 10101 ingress-replication + member vni 10201 mcast-group 225.0.0.101 FF0E:225::101 + member vni 50901 vrf blue + """, + ) + set_module_args( + dict( + config=[ + { + "interface": "nve1", + "source_interface": "Loopback2", + "host_reachability_bgp": True, + "member": { + "vni": { + "l2vni": [ + { + "vni": "10101", + "replication": { + "type": "static", + "mcast_group": { + "ipv6": "FF0E:225::101", + }, + }, + }, + { + "vni": "10201", + "replication": { + "type": "static", + "mcast_group": { + "ipv6": "FF0E:225::102", + }, + }, + }, + ], + }, + }, + }, + ], + state="overridden", + ), + ) + commands = [ + "interface nve1", + "no member vni 10101 ingress-replication", + "member vni 10101 mcast-group FF0E:225::101", + "no member vni 10201 mcast-group 225.0.0.101 FF0E:225::101", + "member vni 10201 mcast-group FF0E:225::102", + "no member vni 10102 ingress-replication", + "no member vni 10202 ingress-replication", + "no member vni 50901 vrf blue", + ] + result = self.execute_module(changed=True) + self.assertEqual(result["commands"], commands) + + def test_ios_vxlan_vtep_overridden_idempotent(self): + self.execute_show_command.return_value = dedent( + """\ + interface nve1 + no ip address + source-interface Loopback2 + host-reachability protocol bgp + member vni 10101 mcast-group FF0E:225::101 + member vni 10201 mcast-group FF0E:225::102 + """, + ) + set_module_args( + dict( + config=[ + { + "interface": "nve1", + "source_interface": "Loopback2", + "host_reachability_bgp": True, + "member": { + "vni": { + "l2vni": [ + { + "vni": "10101", + "replication": { + "type": "static", + "mcast_group": { + "ipv6": "FF0E:225::101", + }, + }, + }, + { + "vni": "10201", + "replication": { + "type": "static", + "mcast_group": { + "ipv6": "FF0E:225::102", + }, + }, + }, + ], + }, + }, + }, + ], + state="overridden", + ), + ) + self.execute_module(changed=False, commands=[]) + + def test_ios_vxlan_vtep_deleted(self): + self.execute_show_command.return_value = dedent( + """\ + interface nve1 + no ip address + source-interface Loopback2 + host-reachability protocol bgp + member vni 10101 mcast-group FF0E:225::101 + member vni 10201 mcast-group FF0E:225::102 + """, + ) + set_module_args(dict(config=[dict(interface="nve1")], state="deleted")) + commands = [ + "interface nve1", + "no source-interface Loopback2", + "no host-reachability protocol bgp", + "no member vni 10101 mcast-group FF0E:225::101", + "no member vni 10201 mcast-group FF0E:225::102", + ] + result = self.execute_module(changed=True) + self.assertEqual(result["commands"], commands) + + def test_ios_vxlan_vtep_deleted_member_vni(self): + self.execute_show_command.return_value = dedent( + """\ + interface nve1 + no ip address + source-interface Loopback2 + host-reachability protocol bgp + member vni 10101 mcast-group FF0E:225::101 + member vni 10201 mcast-group FF0E:225::102 + """, + ) + set_module_args( + dict( + config=[ + { + "interface": "nve1", + "member": { + "vni": { + "l2vni": [ + {"vni": "10101"}, + {"vni": "10201"}, + ], + }, + }, + }, + ], + state="deleted", + ), + ) + commands = [ + "interface nve1", + "no member vni 10101 mcast-group FF0E:225::101", + "no member vni 10201 mcast-group FF0E:225::102", + ] + result = self.execute_module(changed=True) + self.assertEqual(sorted(result["commands"]), sorted(commands))